diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000000..7402f5bef1f9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,44 @@ +root = true + +[*] +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +indent_style = space + +[{*.sh,gradlew}] +end_of_line = lf + +[{*.bat,*.cmd}] +end_of_line = crlf + +[*.md] +trim_trailing_whitespace = false + +[*.java] +# Doc: https://youtrack.jetbrains.com/issue/IDEA-170643#focus=streamItem-27-3708697.0-0 +# $ means "static" +ij_java_imports_layout = $*,|,* +indent_size = 4 +tab_width = 4 +max_line_length = 100 +ij_continuation_indent_size = 8 +ij_any_spaces_around_additive_operators = true +ij_any_spaces_around_assignment_operators = true +ij_any_spaces_around_bitwise_operators = true +ij_any_spaces_around_equality_operators = true +ij_any_spaces_around_lambda_arrow = true +ij_any_spaces_around_logical_operators = true +ij_any_spaces_around_multiplicative_operators = true +ij_any_spaces_around_relational_operators = true +ij_any_spaces_around_shift_operators = true +ij_java_align_multiline_parameters = false +ij_java_if_brace_force = always +ij_java_indent_case_from_switch = true +ij_java_names_count_to_use_import_on_demand = 9999 +ij_java_class_count_to_use_import_on_demand = 9999 +ij_java_space_after_colon = true +ij_java_space_before_colon = true +ij_java_ternary_operation_signs_on_next_line = true +ij_java_use_single_class_imports = true +ij_java_wrap_long_lines = true diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE index 6bfd544cca3a..2ac9854a547c 100644 --- a/.github/ISSUE_TEMPLATE +++ b/.github/ISSUE_TEMPLATE @@ -1,10 +1,6 @@ Thanks for submitting an issue. -As explained in the instructions for submitting an issue at https://checkerframework.org/manual/#reporting-bugs, please include at least the following information. - -The commands you ran, so that we can paste them into a command terminal. Please don't omit any steps, including installing software or setting environment varibales. Please include `-version -verbose -AprintAllQualifiers` in the javac options. If you ran the Checker Framework from an IDE, reproduce the problem from the command line; if you cannot, report the bug to the IDE integration. - -A transcript of all the output (as text, not a screenshot). - -What you expected to happen instead. It can be helpful to explain your reasoning or reference a section of the manual. - -Files that are used by the commands you ran. If you cannot share the code, please create a test case that reproduces the problem. +As explained in the instructions for submitting an issue at https://eisop.github.io/cf/manual/#reporting-bugs, please include four pieces of information: + * commands (that can be cut-and-pasted into a command shell), + * inputs, + * outputs, and + * expectation. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..897a79e9e6db --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: +- package-ecosystem: "gradle" + directory: "/docs/examples/errorprone/" + schedule: + interval: "daily" + +- package-ecosystem: "gradle" + directory: "/docs/examples/lombok/" + schedule: + interval: "daily" + +- package-ecosystem: "maven" + directory: "/docs/examples/MavenExample/" + schedule: + interval: "daily" + +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 000000000000..8a198f693d51 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base", + ":automergeDisabled", + ":automergeRequireAllStatusChecks", + "schedule:nonOfficeHours", + ":disableDependencyDashboard" + ], + "timezone": "America/Los_Angeles", + "packageRules": [ + { + "matchPackageNames": ["com.amazonaws:aws-java-sdk-bom"], + "schedule": ["before 3am on the first day of the month"] + } + ] +} diff --git a/.github/renovate.json-README b/.github/renovate.json-README new file mode 100644 index 000000000000..ab895bbdf156 --- /dev/null +++ b/.github/renovate.json-README @@ -0,0 +1,15 @@ +This works: + + "packageRules": [ + { + "matchPackageNames": ["com.amazonaws:aws-java-sdk-bom"], + "schedule": ["before 3am on the first day of the month"] + } + ] + +=========================================================================== + +This schedule doesn't work: Renovate accepts it but updates the dependency every day. + "schedule": ["on the last day of the month"] +This schedule doesn't work: Renovate accepts it but updates the dependency every day. + "schedule": ["* 23 28 * *"] diff --git a/.github/workflows/gradle-wrapper-validation.yml-DISABLED b/.github/workflows/gradle-wrapper-validation.yml-DISABLED new file mode 100644 index 000000000000..a5070c937c6b --- /dev/null +++ b/.github/workflows/gradle-wrapper-validation.yml-DISABLED @@ -0,0 +1,13 @@ +name: "Validate Gradle Wrapper" +on: [push, pull_request] + +permissions: + contents: read + +jobs: + validation: + name: "Gradle wrapper validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 diff --git a/.gitignore b/.gitignore index c0cc801a50ab..bcde890ce0f5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ tags *.swp *.orig .DS_Store + # Eclipse files .metadata .classpath @@ -20,45 +21,49 @@ tags .externalToolBuilders .settings dot_files + # Intellij files .idea +# VS Code files +.vscode + Makefile.user local.properties -# ant tab completion cache file -.ant-targets-build.xml - # build files api build dist docs/tmpapi/ -stubparser/docs/api -# Don't want to ignore .jar files in checker/lib/ nor in gradle/wrapper -# *.jar -checker/jdk/jdk*.jar -checker/jdk/**/*.class *.pyc checker/build-temp +checker/bin/.do-like-javac +checker/bin/.scc/ +checker/bin-devel/.html-tools checker/bin-devel/.plume-scripts -checker/bin-devel/.run-google-java-format checker/bin-devel/dockerdir checker/src/main/resources/git.properties +# These tests are generated from those in checker/tests/calledmethods-lombok during the build. +# They must be in the tests/ directory rather than the build/ directory so that +# CheckerFrameworkPerDirectoryTest can be used. +checker/tests/calledmethods-delomboked # manual files -bib +plume-bib *.synctex.gz manual.aux manual.blg manual.dvi +manual.fdb_latexmk +manual.fls manual.haux manual.html manual.htoc +manual.image.log manual.image.out manual.image.tex manual.image.tex.new -manual.image.log manual.log manual.out manual.pdf @@ -72,29 +77,45 @@ manual003.gif manual003.png manual004.gif manual004.png -docs/manual/contributors.txt -docs/manual/figures/*.eps -docs/manual/figures/*.pdf -docs/manual/figures/*.png -docs/manual/figures/signature-types-with-canonicalname.dia -docs/manual/*.png -docs/manual/*.svg + +# dataflow manual +dataflow/manual/dataflow.aux +dataflow/manual/dataflow.dvi +dataflow/manual/dataflow.log +dataflow/manual/dataflow.out +dataflow/manual/dataflow.pdf + +docs/tutorial/src/personalblog-demo/bin/net/eyde/personalblog/service/PersonalBlogService.class +docs/tutorial/src/personalblog-demo/bin/net/eyde/personalblog/struts/action/ReadAction.class +docs/tutorial/sourcefiles.zip docs/examples/**/*.class docs/examples/MavenExample/Out.txt +docs/examples/MavenExample-framework-all/Out.txt docs/examples/errorprone/.gradle/ docs/examples/errorprone/Out.txt -docs/examples/lombok/Out.txt docs/examples/lombok/.gradle/ +docs/examples/lombok/Out.txt docs/examples/lombok/lombok.config +docs/manual/*.png +docs/manual/*.svg +docs/manual/contributors.txt +docs/manual/figures/*.eps +docs/manual/figures/*.pdf +docs/manual/figures/*.png +docs/manual/figures/signature-types-with-canonicalname.dia docs/manual/manual.html-e -docs/tmpapi/ -# dataflow manual -dataflow/manual/dataflow.aux -dataflow/manual/dataflow.dvi -dataflow/manual/dataflow.log -dataflow/manual/dataflow.out -dataflow/manual/dataflow.pdf +# release tmps +tmp + +# Maven files +target/ +*.ipr +*.iws +*.iml +/.gradle/ + +## Tests checker/tests/command-line/issue618/TwoCheckers.class checker/tests/command-line/issue618/out.txt @@ -102,41 +123,50 @@ checker/tests/nullness-extra/*.class checker/tests/nullness-extra/compat/Out.txt checker/tests/nullness-extra/compat/javax/annotation/Nullable.class checker/tests/nullness-extra/compat/lib/Lib.class -checker/tests/nullness-extra/multiple-errors/*.class -checker/tests/nullness-extra/multiple-errors/Out.txt -checker/tests/nullness-extra/package-anno/Out.txt -checker/tests/nullness-extra/package-anno/test/*.class -checker/tests/nullness-extra/shorthand/NullnessRegexWithErrors.class -checker/tests/nullness-extra/shorthand/Out.txt +checker/tests/nullness-extra/issue3597/testpkg/Issue3597A.class +checker/tests/nullness-extra/issue3597/testpkg/Issue3597B.class checker/tests/nullness-extra/issue502/Issue502.class checker/tests/nullness-extra/issue502/Out.txt +checker/tests/nullness-extra/issue5174/*.class +checker/tests/nullness-extra/issue5174/Out.txt checker/tests/nullness-extra/issue594/Out.txt checker/tests/nullness-extra/issue607/Issue607.class checker/tests/nullness-extra/issue607/Issue607Interface.class checker/tests/nullness-extra/issue607/Issue607SuperClass.class -checker/tests/nullness/generics/*.class -checker/tests/nullness-temp/*.java +checker/tests/nullness-extra/multiple-errors/*.class +checker/tests/nullness-extra/multiple-errors/Out.txt +checker/tests/nullness-extra/package-anno/Out.txt +checker/tests/nullness-extra/package-anno/test/*.class +checker/tests/nullness-extra/shorthand/NullnessRegexWithErrors.class +checker/tests/nullness-extra/shorthand/Out.txt checker/tests/nullness-temp/*.class +checker/tests/nullness-temp/*.java +checker/tests/nullness/generics/*.class + +# Some tests produce output into the tests/ directory instead of the build/ directory. +checker/tests/ainfer-index/annotated/ +checker/tests/ainfer-index/inference-output/ +checker/tests/ainfer-nullness/annotated/ +checker/tests/ainfer-nullness/inference-output/ +checker/tests/ainfer-testchecker/annotated/ +checker/tests/ainfer-testchecker/inference-output/ +checker/tests/ainfer-resourceleak/annotated/ +checker/tests/ainfer-resourceleak/inference-output/ +framework/tests/returnsreceiverdelomboked/ + +dataflow/tests/busyexpr/Out.txt +dataflow/tests/busyexpr/*.class +dataflow/tests/cfgconstruction/*.class +dataflow/tests/constant-propagation/Out.txt +dataflow/tests/constant-propagation/*.class +dataflow/tests/issue3447/Out.txt +dataflow/tests/issue3447/*.class +dataflow/tests/live-variable/Out.txt +dataflow/tests/live-variable/*.class +dataflow/tests/reachingdef/Out.txt +dataflow/tests/reachingdef/*.class checker/jtreg/multipleexecutions/Main.class # Source is copied from elsewhere. -checker-qual/src checker-qual-android/src - -framework/tests/whole-program-inference/annotated/ -/framework/tests/returnsreceiverdelomboked/ - -docs/tutorial/src/personalblog-demo/bin/net/eyde/personalblog/service/PersonalBlogService.class -docs/tutorial/src/personalblog-demo/bin/net/eyde/personalblog/struts/action/ReadAction.class -docs/tutorial/sourcefiles.zip - -# release tmps -tmp - -# Maven files -target/ -*.ipr -*.iws -*.iml -/.gradle/ diff --git a/.lgtm.yml b/.lgtm.yml index 809a1c80a1cf..cbb457b16759 100644 --- a/.lgtm.yml +++ b/.lgtm.yml @@ -16,4 +16,4 @@ extraction: - (cd ${LGTM_WORKSPACE}/annotation-tools/annotation-file-utilities; ./gradlew assemble) index: build_command: - - ./gradlew assemble --console=plain --warning-mode=all --no-daemon + - ./gradlew assemble --console=plain --warning-mode=all diff --git a/.travis-build-without-test.sh b/.travis-build-without-test.sh deleted file mode 100755 index c60a4ed5d1c4..000000000000 --- a/.travis-build-without-test.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -## TODO: Eliminate this script, which exists for backward compatibility. - -echo "Don't use .travis-build-without-test.sh; use checker/bin-devel/build.sh instead." - -source checker/bin-devel/build.sh diff --git a/.travis-build.sh b/.travis-build.sh index 3ebac35f96fc..881268436007 100755 --- a/.travis-build.sh +++ b/.travis-build.sh @@ -29,22 +29,18 @@ fi ### Build the Checker Framework ### -if [ -d "/tmp/plume-scripts" ] ; then - (cd /tmp/plume-scripts && git pull -q) -else - (cd /tmp && git clone --depth 1 -q https://github.com/plume-lib/plume-scripts.git) -fi -# For debugging -/tmp/plume-scripts/ci-info typetools -eval $(/tmp/plume-scripts/ci-info typetools) - export CHECKERFRAMEWORK="${CHECKERFRAMEWORK:-$(pwd -P)}" echo "CHECKERFRAMEWORK=$CHECKERFRAMEWORK" ROOTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" SCRIPTDIR=$ROOTDIR/checker/bin-devel/ -source "$SCRIPTDIR/build.sh" +# For debugging +(cd "$CHECKERFRAMEWORK" && ./gradlew getPlumeScripts) +"${SCRIPTDIR}/plume-scripts/ci-info" eisop +eval $("${SCRIPTDIR}/plume-scripts/ci-info" eisop) + +source "$SCRIPTDIR/clone-related.sh" ### ### Run the test @@ -56,11 +52,13 @@ echo "In checker-framework/.travis-build.sh GROUP=$GROUP" case $GROUP in all) - # Run cftests-junit and cftests-nonjunit separately, because cftests-all it takes too long to run on Travis under JDK 11. + # Run cftests-junit, cftests-nonjunit, and cftests-inference separately, + # because cftests-all takes too long to run on Travis. "$SCRIPTDIR/test-cftests-junit.sh" "$SCRIPTDIR/test-cftests-nonjunit.sh" + "$SCRIPTDIR/test-cftests-inference.sh" "$SCRIPTDIR/test-misc.sh" - "$SCRIPTDIR/test-cf-inference.sh" + "$SCRIPTDIR/test-typecheck.sh" "$SCRIPTDIR/test-plume-lib.sh" "$SCRIPTDIR/test-daikon.sh" "$SCRIPTDIR/test-guava.sh" diff --git a/.travis.yml b/.travis.yml index 4f0cdd1c9026..2e85e62c11bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,34 +20,52 @@ env: matrix: - GROUP=cftests-junit JDKVER=jdk8 - GROUP=cftests-junit JDKVER=jdk11 + - GROUP=cftests-junit JDKVER=jdk17 - GROUP=cftests-nonjunit JDKVER=jdk8 - GROUP=cftests-nonjunit JDKVER=jdk11 + - GROUP=cftests-nonjunit JDKVER=jdk17 + - GROUP=cftests-inference JDKVER=jdk8 + - GROUP=cftests-inference JDKVER=jdk11 + - GROUP=cftests-inference JDKVER=jdk17 - GROUP=misc JDKVER=jdk8-plus - GROUP=misc JDKVER=jdk11-plus - - GROUP=cf-inference JDKVER=jdk8 - - GROUP=cf-inference JDKVER=jdk11 + - GROUP=misc JDKVER=jdk17-plus +# - GROUP=cf-inference JDKVER=jdk8 +# - GROUP=cf-inference JDKVER=jdk11 +# - GROUP=cf-inference JDKVER=jdk17 - GROUP=daikon-part1 JDKVER=jdk8 - GROUP=daikon-part1 JDKVER=jdk11 + - GROUP=daikon-part1 JDKVER=jdk17 - GROUP=daikon-part2 JDKVER=jdk8 - GROUP=daikon-part2 JDKVER=jdk11 + - GROUP=daikon-part2 JDKVER=jdk17 - GROUP=guava-formatter JDKVER=jdk8 - GROUP=guava-formatter JDKVER=jdk11 + - GROUP=guava-formatter JDKVER=jdk17 - GROUP=guava-index JDKVER=jdk8 - GROUP=guava-index JDKVER=jdk11 + - GROUP=guava-index JDKVER=jdk17 - GROUP=guava-interning JDKVER=jdk8 - GROUP=guava-interning JDKVER=jdk11 + - GROUP=guava-interning JDKVER=jdk17 - GROUP=guava-lock JDKVER=jdk8 - GROUP=guava-lock JDKVER=jdk11 + - GROUP=guava-lock JDKVER=jdk17 - GROUP=guava-nullness JDKVER=jdk8 - GROUP=guava-nullness JDKVER=jdk11 + - GROUP=guava-nullness JDKVER=jdk17 - GROUP=guava-regex JDKVER=jdk8 - GROUP=guava-regex JDKVER=jdk11 + - GROUP=guava-regex JDKVER=jdk17 - GROUP=guava-signature JDKVER=jdk8 - GROUP=guava-signature JDKVER=jdk11 + - GROUP=guava-signature JDKVER=jdk17 - GROUP=plume-lib JDKVER=jdk8 - GROUP=plume-lib JDKVER=jdk11 + - GROUP=plume-lib JDKVER=jdk17 - GROUP=downstream JDKVER=jdk8 - GROUP=downstream JDKVER=jdk11 + - GROUP=downstream JDKVER=jdk17 # The "docker run" command will pull if needed. # Running this first gives two tries in case of network lossage. @@ -60,7 +78,7 @@ before_script: # The fcntl line works around a bug where Travis truncates logs and fails. script: - python -c "import fcntl; fcntl.fcntl(1, fcntl.F_SETFL, 0)" -- REMOTE_ORIGIN_URL=`git config --get remote.origin.url` +- REMOTE_ORIGIN_URL=$(git config --get remote.origin.url) - echo "THIS_REPO=${THIS_REPO}" - echo "JDKVER=${JDKVER}" - echo "GROUP=${GROUP}" @@ -79,16 +97,16 @@ script: echo "Using docker image: " $DOCKERIMAGE && docker run -v $HOME/.gradle:/root/.gradle -v $HOME/.m2:/root/.m2 $DOCKERIMAGE /bin/bash -c "true && if [ $TRAVIS_EVENT_TYPE = pull_request ] ; then - git clone --quiet $REMOTE_ORIGIN_URL $THIS_REPO || git clone --quiet $REMOTE_ORIGIN_URL $THIS_REPO + git clone --quiet $REMOTE_ORIGIN_URL $THIS_REPO || (sleep 1m && git clone --quiet $REMOTE_ORIGIN_URL $THIS_REPO) cd $THIS_REPO git fetch origin +refs/pull/$TRAVIS_PULL_REQUEST/merge git checkout -qf $TRAVIS_PULL_REQUEST_SHA git config user.email noone@cares.com git config user.name Noone Cares git remote add theupstream https://github.com/$TRAVIS_REPO_SLUG.git - git pull theupstream $TRAVIS_BRANCH || git pull theupstream $TRAVIS_BRANCH + git pull theupstream $TRAVIS_BRANCH || (sleep 1m && git pull theupstream $TRAVIS_BRANCH) else - git clone --quiet -b $TRAVIS_BRANCH $REMOTE_ORIGIN_URL $THIS_REPO || git clone --quiet -b $TRAVIS_BRANCH $REMOTE_ORIGIN_URL $THIS_REPO + git clone --quiet -b $TRAVIS_BRANCH $REMOTE_ORIGIN_URL $THIS_REPO || (sleep 1m && git clone --quiet -b $TRAVIS_BRANCH $REMOTE_ORIGIN_URL $THIS_REPO) cd $THIS_REPO git checkout -qf $TRAVIS_COMMIT fi && diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d0b8acbf71ac..01687a508479 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,8 @@ # Contributing to the Checker Framework Thank you for contributing to the Checker Framework! This project is a -community effort of [more than 90 -developers](https://checkerframework.org/manual/#credits), plus countless +community effort of [more than 110 +developers](https://eisop.github.io/cf/manual/#credits), plus countless more people who have contributed bug reports and feature suggestions. We couldn't do it without your help. @@ -10,7 +10,7 @@ couldn't do it without your help. ## Reporting bugs Please see the [bug -reporting](https://checkerframework.org/manual/#reporting-bugs) section of +reporting](https://eisop.github.io/cf/manual/#reporting-bugs) section of the Checker Framework manual. If the documentation is incorrect, incomplete, or confusing, that is a @@ -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://htmlpreview.github.io/?https://github.com/eisop/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/eisop/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/eisop/, 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/eisop/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/1/code_of_conduct). diff --git a/LICENSE.txt b/LICENSE.txt index 8aa899e4c293..827585f1d41a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -6,34 +6,38 @@ 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. - - * javaparser is dual licensed under the LGPL or the Apache license -- you - may use it under whichever one you want. (The javaparser source code +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 + checker-qual*.jar and checker-util.jar and all the files that appear in + them, which is all files in checker-qual and checker-util directories. + It also applies to the cleanroom implementations of + third-party annotations (in checker/src/testannotations/, + framework/src/main/java/org/jmlspecs/, and + framework/src/main/java/com/google/). + +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/README.md b/README.md index fe11c52c8e26..fc604af5f38f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -Please see the Checker Framework manual -([HTML](https://checkerframework.org/manual/), -[PDF](https://checkerframework.org/manual/checker-framework-manual.pdf)). +# The EISOP Checker Framework: pluggable type-checking for Java + +Please see the EISOP Checker Framework manual +([HTML](https://eisop.github.io/cf/manual/), +[PDF](https://eisop.github.io/cf/manual/checker-framework-manual.pdf)). The history of releases and changes is in file -[changelog.txt](changelog.txt). +[docs/CHANGELOG.md](docs/CHANGELOG.md). Documentation for Checker Framework developers -is in directory `docs/developer/`. +is in directory [docs/developer/](docs/developer/). diff --git a/SKIP-REQUIRE-JAVADOC b/SKIP-REQUIRE-JAVADOC new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f3cec21eb701..3ab18c219c6e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,133 +8,896 @@ pr: include: - '*' - jobs: -- job: all_tests_jdk8 + +# Tests run on +# * "LTS" releases (8, 11, 17, 21), +# * jdk_latest, which is the latest JDK release. This is currently JDK 21, so not separate. +# (jdk_latest will move to JDK 22 once it is released.) +# * jdk_next, which is the next JDK version, which is not yet released and might still fail. +# This is currently JDK 22. +# +# The dependsOn clauses are: +# * Everything depends on the canary jobs (the 4 main jdk_21 jobs (the newest LTS release)), except those jobs themselves. +# * Anything *_jdk8, *_jdk11, *_jdk17, *_jdk_latest, or *_jdk_next depends on *_jdk21. +# * Anything daikon_* and guava_* depends on the framework jobs (all core CF tests for JDK 8, 11, 17, and 21). +# (This avoids running these long-running jobs if something already failed in the framework.) + +# Use inference/typecheck part1/part2 to reduce latency. +# Include misc_jdk_latest because JDK 20+ adds more strict checking (e.g., Javadoc) +- job: canary_jobs + dependsOn: + - junit_jdk21 + - nonjunit_jdk21 + # - inference_part1_jdk21 + # - inference_part2_jdk21 + - typecheck_part1_jdk21 + - typecheck_part2_jdk21 + - misc_jdk21 + # - misc_jdk_latest + pool: + vmImage: 'ubuntu-latest' + steps: + - checkout: none + +- job: framework_jobs + dependsOn: + - canary_jobs + - junit_jdk8 + - junit_jdk11 + - junit_jdk17 + # - junit_jdk_latest + # - junit_jdk_next + - nonjunit_jdk8 + - nonjunit_jdk11 + - nonjunit_jdk17 + # - nonjunit_jdk_latest + # - nonjunit_jdk_next + # - inference_jdk8 + # - inference_jdk11 + # - inference_jdk17 + # - inference_jdk_latest + # - inference_jdk_next + - typecheck_jdk8 + - typecheck_jdk11 + - typecheck_jdk17 + # - typecheck_jdk_latest + # - typecheck_jdk_next + pool: + vmImage: 'ubuntu-latest' + steps: + - checkout: none + +- job: required_jobs + dependsOn: + - framework_jobs + # - daikon_jdk8 + # - daikon_jdk11 + - daikon_jdk17 + - daikon_jdk21 + # - daikon_jdk_latest + # Not daikon_jdk_next + # - guava_jdk8 + # - guava_jdk11 + - guava_jdk17 + - guava_jdk21 + # - guava_jdk_latest + # Not guava_jdk_next + # - plume_lib_jdk8 + # - plume_lib_jdk11 + - plume_lib_jdk17 + - plume_lib_jdk21 + # - plume_lib_jdk_latest + # Not plume_lib_jdk_next + pool: + vmImage: 'ubuntu-latest' + steps: + - checkout: none + +- job: junit_jdk8 + dependsOn: + - canary_jobs + - junit_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk8:latest + timeoutInMinutes: 70 + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cftests-junit.sh + displayName: test-cftests-junit.sh + - task: PublishPipelineArtifact@1 + inputs: + targetPath: ./checker/dist + artifactName: cf_jdk8 + artifactType: pipeline +- job: junit_jdk11 + dependsOn: + - canary_jobs + - junit_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk11:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cftests-junit.sh + displayName: test-cftests-junit.sh + - task: PublishPipelineArtifact@1 + inputs: + targetPath: ./checker/dist + artifactName: cf_jdk11 + artifactType: pipeline +- job: junit_jdk17 + dependsOn: + - canary_jobs + - junit_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk17:latest + timeoutInMinutes: 70 + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cftests-junit.sh + displayName: test-cftests-junit.sh + - task: PublishPipelineArtifact@1 + inputs: + targetPath: ./checker/dist + artifactName: cf_jdk17 + artifactType: pipeline +- job: junit_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk21:latest + timeoutInMinutes: 70 + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cftests-junit.sh + displayName: test-cftests-junit.sh + - task: PublishPipelineArtifact@1 + inputs: + targetPath: ./checker/dist + artifactName: cf_jdk_latest + artifactType: pipeline +# Disable until JDK 22 is stable +# - job: junit_jdk_latest +# dependsOn: +# - canary_jobs +# - junit_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk-latest:latest +# timeoutInMinutes: 70 +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-cftests-junit.sh +# displayName: test-cftests-junit.sh +# - task: PublishPipelineArtifact@1 +# inputs: +# targetPath: ./checker/dist +# artifactName: cf_jdk_latest +# artifactType: pipeline +- job: junit_jdk_next + dependsOn: + - canary_jobs + - junit_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk-next:latest + timeoutInMinutes: 70 + steps: + - checkout: self + fetchDepth: 25 + # Run test, but do not cause overall failure + - bash: ./checker/bin-devel/test-cftests-junit.sh + continueOnError: true + displayName: test-cftests-junit.sh + - task: PublishPipelineArtifact@1 + continueOnError: true + inputs: + targetPath: ./checker/dist + artifactName: cf_jdk_next + artifactType: pipeline + +- job: nonjunit_jdk8 + dependsOn: + - canary_jobs + - nonjunit_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk8:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cftests-nonjunit.sh + displayName: test-cftests-nonjunit.sh +- job: nonjunit_jdk11 + dependsOn: + - canary_jobs + - nonjunit_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk11:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cftests-nonjunit.sh + displayName: test-cftests-nonjunit.sh +- job: nonjunit_jdk17 + dependsOn: + - canary_jobs + - nonjunit_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk17:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cftests-nonjunit.sh + displayName: test-cftests-nonjunit.sh +- job: nonjunit_jdk21 pool: vmImage: 'ubuntu-latest' - container: xingweitian/ubuntu-for-cfi-jdk8:latest + container: wmdietl/cf-ubuntu-jdk21: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-nonjunit.sh + displayName: test-cftests-nonjunit.sh +# Disable until JDK 22 is stable +# - job: nonjunit_jdk_latest +# dependsOn: +# - canary_jobs +# - nonjunit_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk-latest:latest +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-cftests-nonjunit.sh +# displayName: test-cftests-nonjunit.sh +- job: nonjunit_jdk_next + dependsOn: + - canary_jobs + - nonjunit_jdk21 pool: vmImage: 'ubuntu-latest' - container: xingweitian/ubuntu-for-cfi-jdk11:latest + container: wmdietl/cf-ubuntu-jdk-next:latest steps: - checkout: self fetchDepth: 25 - - bash: ./checker/bin-devel/test-cftests-all.sh - displayName: test-cftests-all.sh + # Run test, but do not cause overall failure + - bash: ./checker/bin-devel/test-cftests-nonjunit.sh + continueOnError: true + displayName: test-cftests-nonjunit.sh + +# # Inference tests in EISOP do nothing, so disable the jobs here. +# # Sometimes one of the invocations of wpi-many in `./gradlew wpiManyTest` +# # takes much longer to complete than normal, and this Azure job times out. +# # When there is a timeout, one cannot examine wpi or wpi-many logs. +# # So use a timeout of 90 minutes, and hope that is enough. +# - job: inference_jdk8 +# dependsOn: +# - canary_jobs +# - inference_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk8:latest +# timeoutInMinutes: 90 +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-cftests-inference.sh +# displayName: test-cftests-inference.sh +# - job: inference_jdk11 +# dependsOn: +# - canary_jobs +# - inference_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk11:latest +# timeoutInMinutes: 90 +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-cftests-inference.sh +# displayName: test-cftests-inference.sh +# - job: inference_jdk17 +# dependsOn: +# - canary_jobs +# - inference_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk17:latest +# timeoutInMinutes: 90 +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-cftests-inference.sh +# displayName: test-cftests-inference.sh +# # Split into part1 and part2 only for the inference job that "canary_jobs" depends on. +# - job: inference_part1_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk21:latest +# timeoutInMinutes: 90 +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-cftests-inference-part1.sh +# displayName: test-cftests-inference-part1.sh +# - job: inference_part2_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk21:latest +# timeoutInMinutes: 90 +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-cftests-inference-part2.sh +# displayName: test-cftests-inference-part2.sh +# # Disable until JDK 22 is stable +# # - job: inference_jdk_latest +# # dependsOn: +# # - canary_jobs +# # - inference_part1_jdk21 +# # - inference_part2_jdk21 +# # pool: +# # vmImage: 'ubuntu-latest' +# # container: wmdietl/cf-ubuntu-jdk-latest:latest +# # timeoutInMinutes: 90 +# # steps: +# # - checkout: self +# # fetchDepth: 25 +# # - bash: ./checker/bin-devel/test-cftests-inference.sh +# # displayName: test-cftests-inference.sh +# - job: inference_jdk_next +# dependsOn: +# - canary_jobs +# - inference_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk-next:latest +# timeoutInMinutes: 70 +# steps: +# - checkout: self +# fetchDepth: 25 +# # Run test, but do not cause overall failure +# - bash: ./checker/bin-devel/test-cftests-inference.sh +# continueOnError: true +# displayName: test-cftests-inference.sh + +# Unlimited fetchDepth for misc_jobs, because of need to make contributors.tex - job: misc_jdk8 + dependsOn: + # - canary_jobs + - misc_jdk21 pool: vmImage: 'ubuntu-latest' - container: xingweitian/ubuntu-for-cfi-jdk8-plus:latest + container: wmdietl/cf-ubuntu-jdk8-plus:latest steps: - checkout: self - fetchDepth: 1000 - bash: ./checker/bin-devel/test-misc.sh displayName: test-misc.sh - job: misc_jdk11 + dependsOn: + # - canary_jobs + - misc_jdk21 pool: vmImage: 'ubuntu-latest' - container: xingweitian/ubuntu-for-cfi-jdk11-plus:latest + container: wmdietl/cf-ubuntu-jdk11-plus:latest + steps: + - checkout: self + - bash: ./checker/bin-devel/test-misc.sh + displayName: test-misc.sh +- job: misc_jdk17 + dependsOn: + # - canary_jobs + - misc_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk17-plus:latest steps: - checkout: self - fetchDepth: 1000 - bash: ./checker/bin-devel/test-misc.sh displayName: test-misc.sh -- job: cf_inference_jdk8 +- job: misc_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk21-plus:latest + steps: + - checkout: self + - bash: ./checker/bin-devel/test-misc.sh + displayName: test-misc.sh +# Disable until JDK 22 is stable +# - job: misc_jdk_latest +# dependsOn: +# # - canary_jobs +# - misc_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk-latest-plus:latest +# steps: +# - checkout: self +# - bash: ./checker/bin-devel/test-misc.sh +# displayName: test-misc.sh +- job: misc_jdk_next + dependsOn: + # - canary_jobs + - misc_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk-next-plus:latest + steps: + - checkout: self + # Run test, but do not cause overall failure + - bash: ./checker/bin-devel/test-misc.sh + continueOnError: true + displayName: test-misc.sh + +- job: typecheck_jdk8 + dependsOn: + - canary_jobs + - typecheck_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk8:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-typecheck.sh + displayName: test-typecheck.sh +- job: typecheck_jdk11 + dependsOn: + - canary_jobs + - typecheck_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk11:latest + steps: + - checkout: self + fetchDepth: 1000 + - bash: ./checker/bin-devel/test-typecheck.sh + displayName: test-typecheck.sh +- job: typecheck_jdk17 + dependsOn: + - canary_jobs + - typecheck_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk17-plus:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-typecheck.sh + displayName: test-typecheck.sh +# Split into part1 and part2 only for the type-checking job that "canary_jobs" depends on. +- job: typecheck_part1_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk21-plus:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-typecheck-part1.sh + displayName: test-typecheck-part1.sh +- job: typecheck_part2_jdk21 pool: vmImage: 'ubuntu-latest' - container: xingweitian/ubuntu-for-cfi-jdk8:latest + container: wmdietl/cf-ubuntu-jdk21-plus:latest steps: - checkout: self fetchDepth: 25 - - bash: ./checker/bin-devel/test-cf-inference.sh - displayName: test-cf-inference.sh -- job: cf_inference_jdk11 + - bash: ./checker/bin-devel/test-typecheck-part2.sh + displayName: test-typecheck-part2.sh +- job: typecheck_jdk21 + dependsOn: + - typecheck_part1_jdk21 + - typecheck_part2_jdk21 pool: vmImage: 'ubuntu-latest' - container: xingweitian/ubuntu-for-cfi-jdk11:latest + steps: + - checkout: none +# Disable until JDK 22 is stable +# - job: typecheck_jdk_latest +# dependsOn: +# - canary_jobs +# - typecheck_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk-latest-plus:latest +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-typecheck.sh +# displayName: test-typecheck.sh +- job: typecheck_jdk_next + dependsOn: + - canary_jobs + - typecheck_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk-next:latest steps: - checkout: self fetchDepth: 25 - - bash: ./checker/bin-devel/test-cf-inference.sh - displayName: test-cf-inference.sh + # Run test, but do not cause overall failure + - bash: ./checker/bin-devel/test-typecheck.sh + continueOnError: true + displayName: test-typecheck.sh + - job: daikon_jdk8 + # Disable test on older JDK + condition: false + dependsOn: + - framework_jobs + - daikon_jdk21 pool: vmImage: 'ubuntu-latest' - container: xingweitian/ubuntu-for-cfi-jdk8:latest + container: wmdietl/cf-ubuntu-jdk8:latest + timeoutInMinutes: 70 steps: - checkout: self fetchDepth: 25 - bash: ./checker/bin-devel/test-daikon.sh displayName: test-daikon.sh - job: daikon_jdk11 + # Disable test on older JDK + condition: false + dependsOn: + - framework_jobs + - daikon_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk11:latest + timeoutInMinutes: 80 + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-daikon.sh + displayName: test-daikon.sh +- job: daikon_jdk17 + dependsOn: + - canary_jobs + - daikon_jdk21 pool: vmImage: 'ubuntu-latest' - container: xingweitian/ubuntu-for-cfi-jdk11:latest + container: wmdietl/cf-ubuntu-jdk17:latest + timeoutInMinutes: 80 steps: - checkout: self fetchDepth: 25 - bash: ./checker/bin-devel/test-daikon.sh displayName: test-daikon.sh +- job: daikon_part1_jdk21 + dependsOn: + - canary_jobs + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk21:latest + timeoutInMinutes: 70 + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-daikon-part1.sh + displayName: test-daikon.sh +- job: daikon_part2_jdk21 + dependsOn: + - canary_jobs + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk21:latest + timeoutInMinutes: 80 + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-daikon-part2.sh + displayName: test-daikon-part2.sh +- job: daikon_jdk21 + dependsOn: + - daikon_part1_jdk21 + - daikon_part2_jdk21 + pool: + vmImage: 'ubuntu-latest' + steps: + - checkout: none +# Disable until JDK 22 is stable +# - job: daikon_jdk_latest +# dependsOn: +# - canary_jobs +# - daikon_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk-latest:latest +# timeoutInMinutes: 80 +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-daikon.sh +# displayName: test-daikon.sh +- job: daikon_jdk_next + dependsOn: + - framework_jobs + - daikon_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk-next:latest + timeoutInMinutes: 70 + steps: + - checkout: self + fetchDepth: 25 + # Run test, but do not cause overall failure + - bash: ./checker/bin-devel/test-daikon.sh + continueOnError: true + displayName: test-daikon.sh + - job: guava_jdk8 + # Disable test on older JDK + condition: false + dependsOn: + - framework_jobs + - guava_jdk21 pool: vmImage: 'ubuntu-latest' - container: xingweitian/ubuntu-for-cfi-jdk8:latest - timeoutInMinutes: 300 + container: wmdietl/cf-ubuntu-jdk8:latest steps: - checkout: self fetchDepth: 25 - bash: ./checker/bin-devel/test-guava.sh displayName: test-guava.sh - job: guava_jdk11 + # Disable test on older JDK + condition: false + dependsOn: + - framework_jobs + - guava_jdk21 pool: vmImage: 'ubuntu-latest' - container: xingweitian/ubuntu-for-cfi-jdk11:latest - timeoutInMinutes: 300 + container: wmdietl/cf-ubuntu-jdk11:latest + timeoutInMinutes: 60 steps: - checkout: self fetchDepth: 25 - bash: ./checker/bin-devel/test-guava.sh displayName: test-guava.sh +- job: guava_jdk17 + dependsOn: + - framework_jobs + - guava_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk17:latest + timeoutInMinutes: 60 + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-guava.sh + displayName: test-guava.sh +- job: guava_jdk21 + dependsOn: + - canary_jobs + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk21:latest + # The guava job sometimes times out, because the time between these lines can be 30 minutes! + # [INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugin-tools/maven-plugin-tools-generators/3.5.1/maven-plugin-tools-generators-3.5.1.pom + # [INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/plugin-tools/maven-plugin-tools-generators/3.5.1/maven-plugin-tools-generators-3.5.1.pom + # I tried to configure Maven to prevent that problem, but it is still ocurring. + # Maybe I need to use caching? https://learn.microsoft.com/en-us/azure/devops/pipelines/release/caching?view=azure-devops + timeoutInMinutes: 60 + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-guava.sh + displayName: test-guava.sh +# Disable until JDK 22 is stable +# - job: guava_jdk_latest +# dependsOn: +# - framework_jobs +# - guava_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk-latest:latest +# timeoutInMinutes: 60 +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-guava.sh +# displayName: test-guava.sh +- job: guava_jdk_next + dependsOn: + - framework_jobs + - guava_jdk21 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk-next:latest + steps: + - checkout: self + fetchDepth: 25 + # Run test, but do not cause overall failure + - bash: ./checker/bin-devel/test-guava.sh + continueOnError: true + displayName: test-guava.sh + - job: plume_lib_jdk8 + # Disable test on older JDK + condition: false + dependsOn: + - canary_jobs + - plume_lib_jdk21 pool: vmImage: 'ubuntu-latest' - container: xingweitian/ubuntu-for-cfi-jdk8:latest + container: wmdietl/cf-ubuntu-jdk8:latest steps: - checkout: self fetchDepth: 25 - bash: ./checker/bin-devel/test-plume-lib.sh displayName: test-plume-lib.sh - job: plume_lib_jdk11 + # Disable test on older JDK + condition: false + dependsOn: + - canary_jobs + - plume_lib_jdk21 pool: vmImage: 'ubuntu-latest' - container: xingweitian/ubuntu-for-cfi-jdk11:latest + container: wmdietl/cf-ubuntu-jdk11:latest steps: - checkout: self fetchDepth: 25 - bash: ./checker/bin-devel/test-plume-lib.sh displayName: test-plume-lib.sh -- job: downstream_jdk11 +- job: plume_lib_jdk17 + dependsOn: + - canary_jobs + - plume_lib_jdk21 pool: vmImage: 'ubuntu-latest' - container: xingweitian/ubuntu-for-cfi-jdk11:latest + container: wmdietl/cf-ubuntu-jdk17:latest steps: - checkout: self fetchDepth: 25 - - bash: ./checker/bin-devel/test-downstream.sh - displayName: test-downstream.sh -- job: downstream_jdk8 + - bash: ./checker/bin-devel/test-plume-lib.sh + displayName: test-plume-lib.sh +- job: plume_lib_jdk21 + dependsOn: + - canary_jobs pool: vmImage: 'ubuntu-latest' - container: xingweitian/ubuntu-for-cfi-jdk8:latest + container: wmdietl/cf-ubuntu-jdk21:latest steps: - checkout: self fetchDepth: 25 - - bash: ./checker/bin-devel/test-downstream.sh - displayName: test-downstream.sh + - bash: ./checker/bin-devel/test-plume-lib.sh + displayName: test-plume-lib.sh +# Disable until JDK 22 is stable +# - job: plume_lib_jdk_latest +# dependsOn: +# - canary_jobs +# - plume_lib_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk-latest:latest +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-plume-lib.sh +# displayName: test-plume-lib.sh +- job: plume_lib_jdk_next + dependsOn: + - canary_jobs + - plume_lib_jdk17 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk-next:latest + steps: + - checkout: self + fetchDepth: 25 + # Run test, but do not cause overall failure + - bash: ./checker/bin-devel/test-plume-lib.sh + continueOnError: true + displayName: test-plume-lib.sh + +## The downstream jobs are not currently needed because test-downstream.sh is empty. +# - job: downstream_jdk8 +# dependsOn: +# - canary_jobs +# - downstream_jdk17 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk8:latest +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-downstream.sh +# displayName: test-downstream.sh +# - job: downstream_jdk11 +# dependsOn: +# - canary_jobs +# - downstream_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk11:latest +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-downstream.sh +# displayName: test-downstream.sh +# - job: downstream_jdk17 +# dependsOn: +# - canary_jobs +# - downstream_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk17:latest +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-downstream.sh +# displayName: test-downstream.sh +# - job: downstream_jdk21 +# dependsOn: +# - canary_jobs +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk21:latest +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-downstream.sh +# displayName: test-downstream.sh +# - job: downstream_jdk20 +# dependsOn: +# - canary_jobs +# - downstream_jdk21 +# pool: +# vmImage: 'ubuntu-latest' +# container: wmdietl/cf-ubuntu-jdk-latest:latest +# steps: +# - checkout: self +# fetchDepth: 25 +# - bash: ./checker/bin-devel/test-downstream.sh +# displayName: test-downstream.sh + +- job: cfinference_jdk8 + dependsOn: + - junit_jdk11 + - nonjunit_jdk11 + - typecheck_jdk11 + - cfinference_jdk11 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk8:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cf-inference.sh + displayName: test-cf-inference.sh +- job: cfinference_jdk11 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk11:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cf-inference.sh + displayName: test-cf-inference.sh +- job: cfinference_jdk17 + dependsOn: + - junit_jdk11 + - nonjunit_jdk11 + - typecheck_jdk11 + - cfinference_jdk11 + pool: + vmImage: 'ubuntu-latest' + container: wmdietl/cf-ubuntu-jdk17:latest + steps: + - checkout: self + fetchDepth: 25 + - bash: ./checker/bin-devel/test-cf-inference.sh + displayName: test-cf-inference.sh diff --git a/build.gradle b/build.gradle index 9c25ac6ec203..a3f44b927852 100644 --- a/build.gradle +++ b/build.gradle @@ -1,168 +1,380 @@ import de.undercouch.gradle.tasks.download.Download +buildscript { + dependencies { + if (JavaVersion.current() >= JavaVersion.VERSION_11) { + // Code formatting; defines targets "spotlessApply" and "spotlessCheck". + // https://github.com/diffplug/spotless/tags ; see tags starting "gradle/" + // Only works on JDK 11+. + classpath 'com.diffplug.spotless:spotless-plugin-gradle:6.25.0' + } + } +} + 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/com.github.johnrengelman.shadow + id 'com.github.johnrengelman.shadow' version '8.1.1' // https://plugins.gradle.org/plugin/de.undercouch.download - id "de.undercouch.download" version "4.0.4" + id 'de.undercouch.download' version '5.6.0' 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 'net.ltgt.errorprone' version '3.1.0' + // https://docs.gradle.org/current/userguide/eclipse_plugin.html + id 'eclipse' + // To show task list as a tree, run: ./gradlew taskTree + id 'com.dorongold.task-tree' version '2.1.1' } -apply plugin: "de.undercouch.download" -import org.ajoberstar.grgit.Grgit +apply plugin: 'de.undercouch.download' +// There is another `repositories { ... }` block below; if you change this one, change that one as well. repositories { - jcenter() + maven { url 'https://oss.sonatype.org/content/repositories/snapshots/'} mavenCentral() } -gitProperties { - // logically belongs in framework, but only checker resources are copied to .jar file - gitPropertiesResourceDir = "${project.rootDir}/checker/src/main/resources" -} - ext { release = false // On a Java 8 JVM, use error-prone javac and source/target 8. // On a Java 9+ JVM, use the host javac, default source/target, and required module flags. isJava8 = JavaVersion.current() == JavaVersion.VERSION_1_8 - - errorproneJavacVersion = '9+181-r4173-1' + isJava14 = JavaVersion.current() == JavaVersion.VERSION_14 + isJava15 = JavaVersion.current() == JavaVersion.VERSION_15 + isJava16 = JavaVersion.current() == JavaVersion.VERSION_16 + isJava17 = JavaVersion.current() == JavaVersion.VERSION_17 + isJava18 = JavaVersion.current() == JavaVersion.VERSION_18 + isJava19 = JavaVersion.current() == JavaVersion.VERSION_19 + isJava20 = JavaVersion.current() == JavaVersion.VERSION_20 + isJava21 = JavaVersion.current() == JavaVersion.VERSION_21 + + isJava21plus = isJava21 + isJava20plus = isJava20 || isJava21plus + isJava19plus = isJava19 || isJava20plus + isJava18plus = isJava18 || isJava19plus + isJava17plus = isJava17 || isJava18plus + isJava16plus = isJava16 || isJava17plus + isJava15plus = isJava15 || isJava16plus + isJava14plus = isJava14 || isJava15plus + isJava11plus = JavaVersion.current() >= JavaVersion.VERSION_11 + + // As of 2023-09-23, delombok doesn't yet support JDK 22; see https://projectlombok.org/changelog . + skipDelombok = JavaVersion.current() > JavaVersion.VERSION_21 parentDir = file("${rootDir}/../").absolutePath - annotationTools = "${parentDir}/annotation-tools" - afu = "${annotationTools}/annotation-file-utilities" - - stubparser = "${parentDir}/stubparser" - stubparserJar = "${stubparser}/javaparser-core/target/stubparser.jar" + // NO-AFU + // annotationTools = "${parentDir}/annotation-tools" + // afu = "${annotationTools}/annotation-file-utilities" jtregHome = "${parentDir}/jtreg" - formatScriptsHome = "${project(':checker').projectDir}/bin-devel/.run-google-java-format" plumeScriptsHome = "${project(':checker').projectDir}/bin-devel/.plume-scripts" + htmlToolsHome = "${project(':checker').projectDir}/bin-devel/.html-tools" + doLikeJavacHome = "${project(':checker').projectDir}/bin/.do-like-javac" javadocMemberLevel = JavadocMemberLevel.PROTECTED // The local git repository, typically in the .git directory, but not for worktrees. // This value is always overwritten, but Gradle needs the variable to be initialized. - localRepo = ".git" - - // Location of pom files that are only used to add meta-data to the artifacts published to Maven. - pomFiles = "${rootDir}/docs/developer/release/maven-artifacts" + localRepo = '.git' + + versions = [ + autoValue : '1.10.4', + errorprone : '2.25.0', + hashmapUtil : '0.0.1', + junit : '4.13.2', + lombok : '1.18.30', + // plume-util includes a version of reflection-util. When updating ensure the versions are consistent. + plumeUtil : '1.9.0', + reflectionUtil : '1.1.3', + ] } -// Keep in sync with check in org.checkerframework.framework.source.SourceChecker.init -// and with text in #installation + +// Keep in sync with check in +// framework/src/main/java/org/checkerframework/framework/source/SourceChecker.java . switch (JavaVersion.current()) { - case JavaVersion.VERSION_1_9: - case JavaVersion.VERSION_1_10: - case JavaVersion.VERSION_12: - logger.warn("The Checker Framework has only been tested with JDK 8 and 11." + - " Found version " + JavaVersion.current().majorVersion); - break; case JavaVersion.VERSION_1_8: case JavaVersion.VERSION_11: + case JavaVersion.VERSION_17: + case JavaVersion.VERSION_21: break; // Supported versions default: - throw new GradleException("Build the Checker Framework with JDK 8 or JDK 11." + - " Found version " + JavaVersion.current().majorVersion); + logger.info('The Checker Framework has only been tested with JDK 8, 11, 17, and 21.' + + ' You are using JDK ' + JavaVersion.current().majorVersion + '.'); + break; } task setLocalRepo(type:Exec) { commandLine 'git', 'worktree', 'list' standardOutput = new ByteArrayOutputStream() doLast { - String worktreeList = standardOutput.toString() - localRepo = worktreeList.substring(0, worktreeList.indexOf(" ")) + "/.git" + String worktreeList = standardOutput.toString() + localRepo = worktreeList.substring(0, worktreeList.indexOf(' ')) + '/.git' } } +// No group so it does not show up in the output of `gradlew tasks` task installGitHooks(type: Copy, dependsOn: 'setLocalRepo') { description 'Copies git hooks to .git directory' - from files("checker/bin-devel/git.post-merge", "checker/bin-devel/git.pre-commit") + from files('checker/bin-devel/git.post-merge', 'checker/bin-devel/git.pre-commit') rename('git\\.(.*)', '$1') - into localRepo + "/hooks" + into localRepo + '/hooks' +} + +if (isJava11plus) { + apply plugin: 'com.diffplug.spotless' + spotless { + // Resolve the Spotless plugin dependencies from the buildscript repositories rather than the + // project repositories. That way the spotless plugin does not use the locally built version of + // checker-qual as a dependency. Without this, errors like the follow are issued when running + // a spotless task without a locally-built version of checker-qual.jar: + // Could not determine the dependencies of task ':checker-qual:spotlessCheck'. + // > Could not create task ':checker-qual:spotlessJavaCheck'. + // > Could not create task ':checker-qual:spotlessJava'. + // > File signature can only be created for existing regular files, given: + // .../checker-framework/checker-qual/build/libs/checker-qual-3.25.1-SNAPSHOT.jar + predeclareDepsFromBuildscript() + } + + spotlessPredeclare { + // Put all the formatters that have dependencies here. Without this, errors like the following + // will happen: + // Could not determine the dependencies of task ':spotlessCheck'. + // > Could not create task ':spotlessJavaCheck'. + // > Could not create task ':spotlessJava'. + // > Add a step with [com.google.googlejavaformat:google-java-format:1.15.0] into the `spotlessPredeclare` block in the root project. + java { + googleJavaFormat() + } + groovyGradle { + greclipse() + } + } } allprojects { + // Increment the minor version (second number) rather than just the patch + // level (third number) if: + // * any new checkers have been added, or + // * backward-incompatible changes have been made to APIs or elsewhere. + // To make a snapshot release: ./gradlew publish + version '3.42.0-eisop3' + + tasks.withType(JavaCompile).configureEach { + options.fork = true + } + apply plugin: 'java' + apply plugin: 'eclipse' apply plugin: 'com.github.johnrengelman.shadow' - apply plugin: "de.undercouch.download" + apply plugin: 'de.undercouch.download' apply plugin: 'net.ltgt.errorprone' - group 'org.checkerframework' - // Increment the minor version rather than just the patch level if: - // * 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' + group 'io.github.eisop' + // Keep in sync with "repositories { ... }" block above. repositories { + maven { url 'https://oss.sonatype.org/content/repositories/snapshots/'} mavenCentral() } configurations { + // This is required to run the Checker Framework on JDK 8. javacJar // Holds the combined classpath of all subprojects including the subprojects themselves. allProjects - // There's a bug in IntelliJ that adds the processor path to the classpath when building - // a Gradle project. Because ErrorProne uses old versions of some of our jars, IntelliJ uses - // those jars instead of the source code. The next 3 lines exclude Checker Framework from the - // from the processor path as a work around for this problem. - annotationProcessor.exclude group:'org.checkerframework', module:'javacutil' - annotationProcessor.exclude group:'org.checkerframework', module:'dataflow' + // Exclude checker-qual dependency added by Error Prone to avoid a circular dependency. annotationProcessor.exclude group:'org.checkerframework', module:'checker-qual' } dependencies { - if (isJava8) { - javacJar group: 'com.google.errorprone', name: 'javac', version: "$errorproneJavacVersion" - } + javacJar group: 'com.google.errorprone', name: 'javac', version: "9+181-r4173-1" - errorproneJavac("com.google.errorprone:javac:$errorproneJavacVersion") + errorproneJavac("com.google.errorprone:javac:9+181-r4173-1") allProjects subprojects } + eclipse.classpath { + defaultOutputDir = file("build/default") + file.whenMerged { cp -> + cp.entries.forEach { cpe -> + if (cpe instanceof org.gradle.plugins.ide.eclipse.model.SourceFolder) { + cpe.output = cpe.output.replace "bin/", "build/classes/java/" + } + if (cpe instanceof org.gradle.plugins.ide.eclipse.model.Output) { + cpe.path = cpe.path.replace "bin/", "build/" + } + } + } + } + + ext { + // A list of add-export and add-open arguments to be used when running the Checker Framework. + // Keep this list in sync with the lists in CheckerMain#getExecArguments, + // the sections with labels "javac-jdk11-non-modularized", "maven", and "sbt" in the manual + // and in the checker-framework-gradle-plugin, CheckerFrameworkPlugin#applyToProject + compilerArgsForRunningCF = [ + // These are required in Java 16+ because the --illegal-access option is set to deny + // by default. None of these packages are accessed via reflection, so the module + // only needs to be exported, but not opened. + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED', + // Required because the Checker Framework reflectively accesses private members in com.sun.tools.javac.comp. + '--add-opens', + 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', + ] + } + + if (isJava11plus) { + apply plugin: 'com.diffplug.spotless' + spotless { + // If you add any formatters to this block that require dependencies, then you must also + // add them to spotlessPredeclare block. + def doNotFormat = [ + 'checker/bin-devel/.plume-scripts/**', + 'checker/tests/ainfer-*/annotated/*', + 'dataflow/manual/examples/', + '**/nullness-javac-errors/*', + '**/calledmethods-delomboked/*', + '**/returnsreceiverdelomboked/*', + '**/build/**', + '*/dist/**', + ] + if (!isJava14plus) { + doNotFormat += ['**/*record*/'] + } + if (!isJava16plus) { + // TODO: directories should be renamed `-switchexpr` or some such, + // as they only contain examples for switch expressions, which were + // added in Java 14, not Java 17. + doNotFormat += ['**/java17/'] + } + if (!isJava21plus) { + doNotFormat += ['**/java21/'] + } + + + format 'misc', { + // define the files to apply `misc` to + target '*.md', '*.tex', '.gitignore', 'Makefile' + targetExclude doNotFormat + // define the steps to apply to those files + indentWithSpaces(2) + trimTrailingWhitespace() + // endWithNewline() // Don't want to end empty files with a newline + } + + java { + def targets = [ + // add target folders here + 'checker', + 'checker-qual', + 'checker-util', + 'dataflow', + 'docs/examples', + 'docs/tutorial', + 'framework', + 'framework-test', + 'javacutil', + ] + targets = targets.collectMany { + [ + // must call toString() to convert GString to String + "${it}/**/*.java".toString(), + "${it}/**/*.ajava".toString() + ] + } + target targets + targetExclude doNotFormat + + googleJavaFormat().aosp() + importOrder('com', 'jdk', 'lib', 'lombok', 'org', 'java', 'javax') + formatAnnotations().addTypeAnnotation("PolyInitialized") + } + + groovyGradle { + target '**/*.gradle' + targetExclude doNotFormat + greclipse() // which formatter Spotless should use to format .gradle files. + indentWithSpaces(4) + trimTrailingWhitespace() + // endWithNewline() // Don't want to end empty files with a newline + } + } + } + + test { + minHeapSize = "256m" // initial heap size + maxHeapSize = "4g" // maximum heap size + } + // After all the tasks have been created, modify some of them. afterEvaluate { + configurations { + checkerFatJar { + canBeConsumed = false + canBeResolved = true + } + } + + dependencies { + checkerFatJar(project(path: ':checker', configuration: 'fatJar')) + } + // Add the fat checker.jar to the classpath of every Javadoc task. This allows Javadoc in // any module to reference classes in any other module. // Also, build and use ManualTaglet as a taglet. tasks.withType(Javadoc) { - def tagletVersion = isJava8 ? 'taglet' : 'tagletJdk11' + // Similar test in framework-test/build.gradle + def tagletVersion = isJava8 ? 'tagletJdk8' : 'taglet' dependsOn(':checker:shadowJar') dependsOn(":framework-test:${tagletVersion}Classes") doFirst { options.encoding = 'UTF-8' - if (!name.equals("javadocDoclintAll")) { + if (!name.equals('javadocDoclintAll')) { options.memberLevel = javadocMemberLevel } - classpath += files(project(':checker').tasks.getByName('shadowJar').archivePath) + classpath += configurations.getByName('checkerFatJar').asFileTree if (isJava8) { classpath += configurations.javacJar } options.taglets 'org.checkerframework.taglet.ManualTaglet' options.tagletPath(project(':framework-test').sourceSets."${tagletVersion}".output.classesDirs.getFiles() as File[]) - // We want to link to Java 9 documentation of the compiler classes since we use Java 9 - // versions of those classes and Java 8 for everything else. Because the compiler classes are not - // a part of the main documentation of Java 8, javadoc links to the Java 9 versions. - // TODO, this creates broken links to the com.sun.tools.javac package. - options.links = ['https://docs.oracle.com/javase/8/docs/api/', 'https://docs.oracle.com/javase/9/docs/api/'] // This file is looked for by Javadoc. file("${destinationDir}/resources/fonts/").mkdirs() ant.touch(file: "${destinationDir}/resources/fonts/dejavu.css") - options.addStringOption('source', '8') + + if (!isJava8) { + options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', true) + options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', true) + options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', true) + options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', true) + options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', true) + options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', true) + options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', true) + options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED', true) + options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', true) + } + // "-Xwerror" requires Javadoc everywhere. Currently, CI jobs require Javadoc only // on changed lines. Enable -Xwerror in the future when all Javadoc exists. // options.addBooleanOption('Xwerror', true) @@ -171,76 +383,156 @@ allprojects { } // Add standard javac options - tasks.withType(JavaCompile) { + tasks.withType(JavaCompile) { compilationTask -> dependsOn(':installGitHooks') - sourceCompatibility = 8 - targetCompatibility = 8 - // Because the target is 8, all of the public compiler classes are accessible, so - // --add-exports are not required, (nor are they allowed with target 8). See - // https://openjdk.java.net/jeps/247 for details on compiling for older versions. - - // When sourceCompatibilty is changed to 11, then the following will be required. - // options.compilerArgs += [ - // "--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", - // "--add-exports", "jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", - // "--add-exports", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", - // "--add-exports", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", - // "--add-exports", "jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", - // "--add-exports", "jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED", - // "--add-exports", "jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", - // "--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", - // "--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", - // ] - // This is equivalent to writing "exports jdk.compiler/... to ALL-UNNAMED" in the - // module-info.java of jdk.compiler, so corresponding --add-opens are only required for - // reflective access to private members. - // - // From https://openjdk.java.net/jeps/261, Section titled: "Breaking encapsulation" - // "The effect of each instance [of --add-exports] is to add a qualified export of the - // named package from the source module to the target module. This is, essentially, a - // command-line form of an exports clause in a module declaration[...]. - // [...] - // The --add-exports option enables access to the public types of a specified package. - // It is sometimes necessary to go further and enable access to all non-public elements - // via the setAccessible method of the core reflection API. The --add-opens option can - // be used, at run time, to do this." + boolean jdk17Compiler = project.getProperties().getOrDefault('useJdk17Compiler', false) + if (!isJava8 && jdk17Compiler) { + // This uses the Java 17 compiler to compile all code. + // https://docs.gradle.org/current/userguide/toolchains.html + // This property is final on Java 8, so don't set it then. + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(17) + } + } + + // Sorting is commented out because it disables incremental compilation. + // Uncomment when needed. + // // Put source files in deterministic order, for debugging. + // compilationTask.source = compilationTask.source.sort() + + // This test is for whether the Checker Framework supports (runs under) Java 8. + // Currently, the Checker Framework does support Java 8. + if (true) { + // Using `options.release.set(8)` here leads to compilation + // errors such as "package com.sun.source.tree does not exist". + sourceCompatibility = 8 + targetCompatibility = 8 + // Because the target is 8, all of the public compiler classes are accessible, so + // --add-exports are not required (nor are they allowed with target 8). See + // https://openjdk.org/jeps/247 for details on compiling for older versions. + } else { + // This makes the class files Java 11, and then the Checker Framework would not run under Java 8. + options.release.set(11) + options.compilerArgs += [ + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', + '--add-exports', + 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED', + ] + // This is equivalent to writing "exports jdk.compiler/... to ALL-UNNAMED" in the + // module-info.java of jdk.compiler, so corresponding --add-opens are only required for + // reflective access to private members. + // + // From https://openjdk.org/jeps/261, Section titled: "Breaking encapsulation" + // "The effect of each instance [of --add-exports] is to add a qualified export of the + // named package from the source module to the target module. This is, essentially, a + // command-line form of an exports clause in a module declaration[...]. + // [...] + // The --add-exports option enables access to the public types of a specified package. + // It is sometimes necessary to go further and enable access to all non-public elements + // via the setAccessible method of the core reflection API. The --add-opens option can + // be used, at run time, to do this." + } options.failOnError = true options.deprecation = true + // -options: To not get a warning about missing bootstrap classpath (when using Java 9 and `-source 8`). + // -fallthrough: Don't check fallthroughs. Instead, use Error Prone. Its + // warnings are suppressible with a "// fall through" comment. + // -classfile: classgraph jar file and https://bugs.openjdk.org/browse/JDK-8190452 + String lint = '-Xlint:-options,-fallthrough,-classfile' + if (isJava21plus && !jdk17Compiler) { + // TODO: Ignore this-escape for now, we may want to review and suppress each one later. + lint +=',-this-escape' + } options.compilerArgs += [ - '-g', - '-Werror', - // To not get a warning about missing bootstrap classpath for Java 8 (once we use Java 9). - "-Xlint:-options", - "-Xlint", + '-g', + '-Werror', + lint, + '-Xlint', ] options.encoding = 'UTF-8' options.fork = true if (isJava8) { - options.forkOptions.jvmArgs += ["-Xbootclasspath/p:${configurations.javacJar.asPath}"] + options.forkOptions.jvmArgs += [ + "-Xbootclasspath/p:${configurations.javacJar.asPath}".toString() + ] } - // Don't use error-prone by default - options.errorprone.enabled = false - } - } -} -task cloneAndBuildDependencies(type: Exec) { - description 'Clones (or updates) and builds all dependencies' - executable 'checker/bin-devel/build.sh' -} -task maybeCloneAndBuildDependencies(type: Exec) { - description 'Clones (or updates) and builds all dependencies if they are not present.' - onlyIf { - !file(stubparserJar).exists() - // The jdk repository is cloned via the copyAndMinimizeAnnotatedJdkFiles task that is run if - // the repository does not exist when building checker.jar. - } - executable 'checker/bin-devel/build.sh' -} + // Error Prone depends on checker-qual.jar, so don't run it on that project to avoid a circular dependency. + // TODO: enable Error Prone on test classes. + if (compilationTask.name.equals('compileJava') && !project.name.startsWith('checker-qual')) { + // Error Prone must be available in the annotation processor path + options.annotationProcessorPath = configurations.errorprone + // Enable Error Prone + options.errorprone.enabled = !isJava8 + 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', + // Recommends writing @InlineMe which is an Error-Prone-specific annotation + '-Xep:InlineMeSuggester:OFF', + // Recommends writing @CanIgnoreReturnValue which is an Error-Prone-specific annotation. + // It would be great if Error Prone recognized the @This annotation. + '-Xep:CanIgnoreReturnValueSuggester:OFF', + // Should be turned off when using the Checker Framework. + '-Xep:ExtendsObject: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', + ] + if (!isJava8) { + // Options needed for Error Prone on Java 16+, but don't hurt on Java 9+ + options.forkOptions.jvmArgs += [ + '--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', + '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED', + '--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', + ] + } + } else { + options.errorprone.enabled = false + } + } + } // end afterEvaluate +} // end allProjects -task version() { +task version(group: 'Documentation') { description 'Print Checker Framework version' doLast { println version @@ -249,96 +541,65 @@ task version() { /** * Creates a task that runs the checker on the main source set of each subproject. The task is named - * "check${shortName}", for example "checkPurity" or "checkNullness". + * "check${taskName}", for example "checkPurity" or "checkNullness". + * * @param projectName name of the project - * @param checker full qualified name of the checker to run - * @param shortName shorter version of the checker to use to name the task. + * @param taskName short name (often the checker name) to use as part of the task name + * @param checker fully qualified name of the checker to run * @param args list of arguments to pass to the checker */ -def createCheckTypeTask(projectName, checker, shortName, args = []) { - project("${projectName}").tasks.create(name: "check${shortName}", type: JavaCompile, dependsOn: ':checker:shadowJar') { - description "Run the ${shortName} Checker on the main sources." +def createCheckTypeTask(projectName, taskName, checker, args = []) { + project("${projectName}").tasks.create(name: "check${taskName}", type: JavaCompile, dependsOn: ':checker:shadowJar') { + description "Run the ${taskName} Checker on the main sources." group 'Verification' // Always run the task. outputs.upToDateWhen { false } source = project("${projectName}").sourceSets.main.java classpath = files(project("${projectName}").compileJava.classpath,project(':checker-qual').sourceSets.main.output) - destinationDir = file("${buildDir}") + destinationDirectory = file("${buildDir}") - options.annotationProcessorPath = files(project(':checker').tasks.shadowJar.archivePath) + options.annotationProcessorPath = files(project(':checker').tasks.shadowJar.archiveFile) options.compilerArgs += [ - '-processor', "${checker}", - '-proc:only', - '-Xlint:-processing', - ] + '-processor', + "${checker}", + '-proc:only', + '-Xlint:-processing', + '-Xmaxerrs', + '10000', + '-Xmaxwarns', + '10000', + '-ArequirePrefixInWarningSuppressions', + '-AwarnUnneededSuppressions', + '-AwarnRedundantAnnotations', + '-AnoJreVersionCheck', + ] options.compilerArgs += args + options.forkOptions.jvmArgs += ['-Xmx2g'] if (isJava8) { options.compilerArgs += [ - "-source", - "8", - "-target", - "8" - ] + '-source', + '8', + '-target', + '8' + ] } else { options.fork = true - options.forkOptions.jvmArgs += [ - "--add-opens", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", - ] - } - } -} - -/** - * Returns a list of all the Java files that should be formatted for the given project. These are: - * - * All java files in the main sourceSet. - * All java files in the tests directory that compile. - * - * @param projectName name of the project to format - * @return a list of all Java files that should be formatted for projectName - */ -List getJavaFilesToFormat(projectName) { - List javaFiles = new ArrayList<>(); - project(':' + projectName).sourceSets.forEach { set -> - javaFiles.addAll(set.java.files) - } - // 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')) { - javaFiles.add(details.file) - } - } - // Collect all java files in jtreg directory - fileTree("${project(projectName).projectDir}/jtreg").visit { details -> - if (!details.path.contains("nullness-javac-errors") && details.name.endsWith('java')) { - javaFiles.add(details.file) + options.forkOptions.jvmArgs += compilerArgsForRunningCF } } - - // Collect all java files in jtreg directory - fileTree("${project(projectName).projectDir}/jtregJdk11").visit { details -> - if (!details.path.contains("nullness-javac-errors") && details.name.endsWith('java')) { - javaFiles.add(details.file) - } - } - - List args = new ArrayList<>(); - for (File f : javaFiles) { - args += f.absolutePath - } - return args } task htmlValidate(type: Exec, group: 'Format') { description 'Validate that HTML files are well-formed' executable 'html5validator' args = [ - "--ignore", - "/api/", - "/build/", - "/docs/manual/manual.html", - "/checker/jdk/nullness/src/java/lang/ref/package.html" + '--ignore', + '/api/', + '/build/', + '/docs/manual/manual.html', + '/docs/manual/plume-bib/docs/index.html', + '/checker/jdk/nullness/src/java/lang/ref/package.html' ] } @@ -349,31 +610,44 @@ task htmlValidate(type: Exec, group: 'Format') { // It's needed to create the Javadoc jars that we release in Maven Central. // To make javadoc for only one subproject, run `./gradlew javadoc` // in the subproject or `./gradlew :checker:javadoc` at the top level. -task allJavadoc(type: Javadoc, group: "Documentation") { - description = 'Generates a global API documentation for all the modules' - dependsOn(':checker:shadowJar', 'getPlumeScripts') +task allJavadoc(type: Javadoc, group: 'Documentation') { + description = 'Generates API documentation that includes all the modules.' + dependsOn(':checker:shadowJar', 'getPlumeScripts', 'getHtmlTools') destinationDir = file("${rootDir}/docs/api") - source(project(':checker').sourceSets.main.allJava, project(':framework').sourceSets.main.allJava, - project(':dataflow').sourceSets.main.allJava, project(':javacutil').sourceSets.main.allJava) + source( + project(':checker-util').sourceSets.main.allJava, + project(':checker-qual').sourceSets.main.allJava, + project(':checker').sourceSets.main.allJava, + project(':framework').sourceSets.main.allJava, + project(':dataflow').sourceSets.main.allJava, + project(':javacutil').sourceSets.main.allJava, + project(':framework-test').sourceSets.main.allJava, + ) + + doFirst { + source( + project(':framework-test').sourceSets."${isJava8 ? 'tagletJdk8' : 'taglet'}".allJava + ) + } classpath = configurations.allProjects if (isJava8) { classpath += configurations.javacJar } doLast { - exec { - // Javadoc for to com.sun.tools.java.* is not publicly available, so these links are broken. - // This command removes those links. - workingDir "${rootDir}/docs/api" - executable "${plumeScriptsHome}/preplace" - args += [' checker-jar-contents.txt + // * Compare the files, and add relocate lines below. + // * Repeat until no new classes appear (all are under org/checkerframework/). + + // Note that string literals are also relocated. Therefore, when the original + // names should be used, e.g. to load the original classes, one needs to work + // around the relocation. When adding a new external dependency, make + // sure no existing string literals are accidentally relocated. + // For an example work-around see NullnessAnnotatedTypeFactory#NONNULL_ALIASES. + // Relocate packages that might conflict with user's classpath. + relocate 'org.apache', 'org.checkerframework.org.apache' + relocate 'org.relaxng', 'org.checkerframework.org.relaxng' + relocate 'org.plumelib', 'org.checkerframework.org.plumelib' + relocate 'org.codehaus', 'org.checkerframework.org.codehaus' + relocate 'org.objectweb.asm', 'org.checkerframework.org.objectweb.asm' + // Add the classgraph relocations if it is included in releases. + // relocate 'io.github.classgraph', 'org.checkerframework.io.github.classgraph' + // relocate 'nonapi.io.github.classgraph', 'org.checkerframework.nonapi.io.github.classgraph' + // relocate 'sun', 'org.checkerframework.sun' + relocate 'com.google', 'org.checkerframework.com.google' + + exclude '**/module-info.class' + doFirst { if (release) { // Only relocate JavaParser during a release: @@ -569,47 +885,28 @@ subprojects { } } - // These appear in annotation-file-utilities-all.jar: - relocate 'org.apache', 'org.checkerframework.org.apache' - relocate 'org.relaxng', 'org.checkerframework.org.relaxng' - relocate 'org.plumelib', 'org.checkerframework.org.plumelib' - relocate 'org.codehaus', 'org.checkerframework.org.codehaus' - // relocate 'sun', 'org.checkerframework.sun' - relocate 'org.objectweb.asm', 'org.checkerframework.org.objectweb.asm' - relocate 'com.google', 'org.checkerframework.com.google' - relocate 'plume', 'org.checkerframework.plume' + minimize() } - if (!project.name.startsWith('checker-qual')) { + if (!project.name.startsWith('checker-qual-android')) { task tags(type: Exec) { description 'Create Emacs TAGS table' - commandLine "bash", "-c", "find . \\( -name build \\) -prune -o -name '*.java' -print | sort-directory-order | xargs ctags -e -f TAGS" + commandLine 'bash', '-c', "find . \\( -name build -o -name jtreg -o -name tests \\) -prune -o -name '*.java' -print | sort-directory-order | xargs ctags -e -f TAGS" } } + java { + withJavadocJar() + withSourcesJar() + } + // Things in this block reference definitions in the subproject that do not exist, // until the project is evaluated. afterEvaluate { - // Create a sourcesJar task for each subproject - tasks.create(name: 'sourcesJar', type: Jar) { - description 'Creates sources jar.' - archiveClassifier = 'source' - archiveBaseName = jar.archiveBaseName - from sourceSets.main.java - } - - // Create a javadocJar task for each subproject - tasks.create(name: 'javadocJar', type: Jar, dependsOn: 'javadoc') { - description 'Creates javadoc jar.' - archiveClassifier = 'javadoc' - archiveBaseName = jar.archiveBaseName - from tasks.javadoc.destinationDir - } - // Adds manifest to all Jar files tasks.withType(Jar) { includeEmptyDirs = false - if (archiveFileName.get().startsWith("checker-qual")) { + if (archiveFileName.get().startsWith('checker-qual') || archiveFileName.get().startsWith('checker-util')) { metaInf { from './LICENSE.txt' } @@ -619,26 +916,64 @@ subprojects { } } manifest { - attributes("Implementation-Version": "${project.version}") - attributes("Implementation-URL": "https://checkerframework.org") - if (! archiveFileName.get().endsWith("source.jar")) { - attributes('Automatic-Module-Name': "org.checkerframework." + project.name.replaceAll('-', '.')) + attributes('Implementation-Version': "${project.version}") + attributes('Implementation-URL': 'https://eisop.github.io/') + if (! archiveFileName.get().endsWith('source.jar')) { + attributes('Automatic-Module-Name': 'org.checkerframework.' + project.name.replaceAll('-', '.')) } - if (archiveFileName.get().startsWith("checker-qual")) { - attributes("Bundle-License": "MIT") + if (archiveFileName.get().startsWith('checker-qual') || archiveFileName.get().startsWith('checker-util')) { + attributes('Bundle-License': 'MIT') } else { - attributes("Bundle-License": "(GPL-2.0-only WITH Classpath-exception-2.0)") + attributes('Bundle-License': '(GPL-2.0-only WITH Classpath-exception-2.0)') } } } - // 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']) - createCheckTypeTask(project.name, 'org.checkerframework.framework.util.PurityChecker', 'Purity') - createCheckTypeTask(project.name, 'org.checkerframework.checker.signature.SignatureChecker', 'Signature') + // Tasks such as `checkResourceLeak` to run various checkers on all the main source sets. + // These pass and are run by the `typecheck` task. + // When you add one here, also update a dependsOn item for the 'typecheck' task. + createCheckTypeTask(project.name, 'Formatter', + 'org.checkerframework.checker.formatter.FormatterChecker') + createCheckTypeTask(project.name, 'Interning', + 'org.checkerframework.checker.interning.InterningChecker', + [ + '-Astubs=javax-lang-model-element-name.astub' + ]) + createCheckTypeTask(project.name, 'Optional', + 'org.checkerframework.checker.optional.OptionalChecker', + [ + // to avoid having to annotate JavaParser + '-AassumePureGetters', + '-AassumeAssertionsAreEnabled', + ]) + createCheckTypeTask(project.name, 'Purity', + 'org.checkerframework.framework.util.PurityChecker') + createCheckTypeTask(project.name, 'ResourceLeak', + 'org.checkerframework.checker.resourceleak.ResourceLeakChecker') + createCheckTypeTask(project.name, 'Signature', + 'org.checkerframework.checker.signature.SignatureChecker') + + // The checkNullness task runs on all code, but it only *checks* the following code: + // * All files outside the 'framework' and 'checker' subprojects. + // * In the 'framework' and 'checker' subprojects, files with `@AnnotatedFor("nullness")`. + if (project.name.is('framework') || project.name.is('checker')) { + createCheckTypeTask(project.name, 'Nullness', + 'org.checkerframework.checker.nullness.NullnessChecker', + [ + '-AskipUses=com\\.sun\\.*', + // If a file does not contain @AnnotatedFor("nullness"), all its routines are assumed to return @Nullable. + '-AuseConservativeDefaultsForUncheckedCode=source', + '-AconservativeArgumentNullnessAfterInvocation=true', + ]) + } else { + createCheckTypeTask(project.name, 'Nullness', + 'org.checkerframework.checker.nullness.NullnessChecker', + [ + '-AskipUses=com\\.sun\\.*', + '-AconservativeArgumentNullnessAfterInvocation=true' + ]) + } + // Add jtregTests to framework and checker modules if (project.name.is('framework') || project.name.is('checker')) { @@ -652,83 +987,107 @@ subprojects { String name = 'all' String tests = '.' doLast { - exec { - executable "${jtregHome}/bin/jtreg" - args = [ + try { + exec { + executable "${jtregHome}/bin/jtreg" + args = [ "-dir:${projectDir}/jtreg", "-workDir:${jtregOutput}/${name}/work", "-reportDir:${jtregOutput}/${name}/report", - "-verbose:error,fail", - "-javacoptions:-g", - "-keywords:!ignore", - "-samevm", + '-verbose:error,fail,nopass', + // Don't add debugging information + // '-javacoptions:-g', + '-keywords:!ignore', + '-samevm', "-javacoptions:-classpath ${tasks.shadowJar.archiveFile.get()}:${sourceSets.test.output.asPath}", // Required for checker/jtreg/nullness/PersistUtil.java and other tests "-vmoptions:-classpath ${tasks.shadowJar.archiveFile.get()}:${sourceSets.test.output.asPath}", - ] - if (isJava8) { - // Use Error Prone javac and source/target 8 - args += [ - "-vmoptions:-Xbootclasspath/p:${configurations.javacJar.asPath}", - "-javacoptions:-Xbootclasspath/p:${configurations.javacJar.asPath}", - "-javacoptions:-source 8", - "-javacoptions:-target 8" + ] + if (isJava8) { + // Use Error Prone javac and source/target 8 + args += [ + "-vmoptions:-Xbootclasspath/p:${configurations.javacJar.asPath}", + "-javacoptions:-Xbootclasspath/p:${configurations.javacJar.asPath}", + '-javacoptions:-source 8', + '-javacoptions:-target 8' ] - } else { - args += [ + } else { + args += [ // checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java // uses the jdk.jdeps module. - "-javacoptions:--add-modules jdk.jdeps", - "-javacoptions:--add-exports=jdk.jdeps/com.sun.tools.classfile=ALL-UNNAMED", - "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", - ] - } - if (project.name.is('framework')) { - // Do not check for the annotated JDK - args += [ - "-javacoptions:-ApermitMissingJdk" - ] - } else if (project.name.is('checker')) { - args += [ + '-javacoptions:--add-modules jdk.jdeps', + '-javacoptions:--add-exports=jdk.jdeps/com.sun.tools.classfile=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.jdeps/com.sun.tools.classfile=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED', + ] + } + if (project.name.is('framework')) { + // Do not check for the annotated JDK + args += [ + '-javacoptions:-ApermitMissingJdk' + ] + } else if (project.name.is('checker')) { + args += [ "-javacoptions:-classpath ${sourceSets.testannotations.output.asPath}", + ] + } + + // Allow running on any JRE version. + args += [ + "-javacoptions:-AnoJreVersionCheck" ] - } - // Location of jtreg tests - args += "${tests}" + // Location of jtreg tests + args += "${tests}" + } + } catch (Exception ex) { + if (ex.getCause() != null && ex.getCause().getCause()!= null) { + String msg = ex.getCause().getLocalizedMessage() + ':\n' + msg += ex.getCause().getCause().getLocalizedMessage() + '\n' + msg += 'Have you installed jtreg?' + println msg + } + throw ex } } } } // Create a task for each JUnit test class whose name is the same as the JUnit class name. - sourceSets.test.allJava.filter { it.path.contains('src/test/java/tests') }.forEach { file -> - String junitClassName = file.name.replaceAll(".java", "") + sourceSets.test.allJava.filter { it.path.contains('test/junit') }.forEach { file -> + String junitClassName = file.name.replaceAll('.java', '') tasks.create(name: "${junitClassName}", type: Test) { description "Run ${junitClassName} tests." include "**/${name}.class" + testClassesDirs = testing.suites.test.sources.output.classesDirs + classpath = testing.suites.test.sources.runtimeClasspath } } // Configure JUnit tests tasks.withType(Test) { if (isJava8) { - jvmArgs "-Xbootclasspath/p:${configurations.javacJar.asPath}" + jvmArgs "-Xbootclasspath/p:${configurations.javacJar.asPath}".toString() } else { - jvmArgs += [ - "--illegal-access=warn", - "--add-opens", "jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", - ] + jvmArgs += compilerArgsForRunningCF } maxParallelForks = Integer.MAX_VALUE if (project.name.is('checker')) { - dependsOn('copyJarsToDist') + dependsOn('assembleForJavac') } if (project.hasProperty('emit.test.debug')) { - systemProperties += ["emit.test.debug": 'true'] + systemProperties += ['emit.test.debug': 'true'] } testLogging { @@ -737,8 +1096,11 @@ subprojects { outputs.upToDateWhen { false } // Show the found unexpected diagnostics and expected diagnostics not found. - exceptionFormat "full" - events "failed" + exceptionFormat 'full' + events 'failed' + + // Don't show the uninteresting stack traces from the exceptions. + showStackTraces = false } // After each test, print a summary. @@ -753,57 +1115,17 @@ subprojects { "Skipped: ${result.skippedTestCount}, " + "Time elapsed: ${seconds} sec\n" } - } } - // 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') + description 'Run all Checker Framework tests except for the JUnit tests and inference tests.' if (project.name.is('framework') || project.name.is('checker')) { - dependsOn('checkCompilerMessages', 'jtregTests') + dependsOn('jtregTests') } if (project.name.is('framework')) { - dependsOn('wholeProgramInferenceTests', 'loaderTests') + dependsOn('loaderTests') } if (project.name.is('checker')) { @@ -814,16 +1136,59 @@ subprojects { } if (project.name.is('dataflow')) { - dependsOn('liveVariableTest') + dependsOn('allDataflowTests') + } + } + + // Create an inferenceTests task per project + tasks.create(name: 'inferenceTests', group: 'Verification') { + description 'Run inference tests.' + if (project.name.is('checker')) { + // NO-AFU: Needs to depend on future AFU version of CF + // dependsOn('inferenceTests-part1', 'inferenceTests-part1') + } + } + tasks.create(name: 'inferenceTests-part1', group: 'Verification') { + description 'Run inference tests (part 1).' + if (project.name.is('checker')) { + // NO-AFU: Needs to depend on future AFU version of CF + // dependsOn('ainferTest', 'wpiManyTest') + } + } + tasks.create(name: 'inferenceTests-part2', group: 'Verification') { + description 'Run inference tests (part 2).' + if (project.name.is('checker')) { + // NO-AFU: Needs to depend on future AFU version of CF + // dependsOn('wpiPlumeLibTest') + } + } + + // 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 tasks are. + // Tasks such as 'checkInterning' are constructed by createCheckTypeTask. + tasks.create(name: 'typecheck', group: 'Verification') { + description 'Run the Checker Framework on itself' + dependsOn('typecheck-part1', 'typecheck-part2') + } + tasks.create(name: 'typecheck-part1', group: 'Verification') { + description 'Run the Checker Framework on itself (part 1)' + dependsOn('checkFormatter', 'checkInterning', 'checkOptional', 'checkPurity') + } + tasks.create(name: 'typecheck-part2', group: 'Verification') { + description 'Run the Checker Framework on itself (part 2)' + dependsOn('checkResourceLeak', 'checkSignature') + if (project.name.is('framework') || project.name.is('checker')) { + dependsOn('checkCompilerMessages') } + dependsOn('checkNullness') } // Create an allTests task per project. - // allTests = test + nonJunitTests + // allTests = test + nonJunitTests + inferenceTests + 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('test', 'nonJunitTests', 'inferenceTests', 'typecheck') } task javadocPrivate(dependsOn: javadoc) { @@ -837,110 +1202,16 @@ subprojects { } } -assemble.dependsOn(':checker:copyJarsToDist') - -task checkBasicStyle(group: 'Format') { - description 'Check basic style guidelines. Not related to Checkstyle tool.' - String[] ignoreDirectories = ['.git', - '.gradle', - '.idea', - '.plume-scripts', - 'annotated', - 'api', - 'bib', - 'bootstrap', - 'build', - 'jdk', - 'maven-artifacts'] - - String[] ignoreFilePatterns = [ - '*.aux', - '*.bib', - '*.class', - '*.dvi', - '*.expected', - '*.gif', - '*.jar', - '*.jtr', - '*.log', - '*.out', - '*.patch', - '*.pdf', - '*.png', - '*.sty', - '*.toc', - '*.xcf', - '*~', - '#*#', - 'CFLogo.ai', - 'logfile.log.rec.index', - 'manual.html', - 'manual.html-e', - 'junit.*.properties', - 'framework/src/main/resources/git.properties'] - doLast { - FileTree tree = fileTree(dir: projectDir) - for (String dir : ignoreDirectories) { - tree.exclude "**/${dir}/**" - } - for (String file : ignoreFilePatterns) { - tree.exclude "**/${file}" - } - boolean failed = false - tree.visit { - if (!it.file.isDirectory()) { - int isBlankLine - it.file.eachLine { line -> - if (line.endsWith(' ')) { - println("Trailing whitespace: ${it.file.absolutePath}") - failed = true - } - if (!line.startsWith('\\') && - (line.matches('^.* (else|finally|try)\\{}.*$') - || line.matches('^.*}(catch|else|finally) .*$') - || line.matches('^.* (catch|for|if|while)\\('))) { - // This runs on non-java files, too. - println("Missing space: ${it.file.absolutePath}") - failed = true - } - if (line.isEmpty()) { - isBlankLine++; - } else { - isBlankLine = 0; - } - } - - if (isBlankLine > 1) { - println("Blank line at end of file: ${it.file.absolutePath}") - failed = true - } +// The `assembleForJavac` task is faster than the `assemble` task, if you only +// care about running `javac`. +// The assemble task also produces Javadoc jars, because its purpose is to build +// all artifacts produced by the Gradle project: +// https://docs.gradle.org/current/userguide/base_plugin.html#sec:base_tasks +assemble.dependsOn(':checker:assembleForJavac') - RandomAccessFile file - try { - file = new RandomAccessFile(it.file, 'r') - int end = file.length() - 1; - if (end > 0) { - file.seek(end) - byte last = file.readByte() - if (last != '\n') { - println("Missing newline at end of file: ${it.file.absolutePath}") - failed = true - } - } - } finally { - if (file != null) { - file.close() - } - } - } - } - if (failed) { - throw new GradleException("Files do not meet basic style guidelines.") - } - } -} assemble.mustRunAfter(clean) -task buildAll { + +task buildAll(group: 'Build') { description 'Build all jar files, including source and javadoc jars' dependsOn(allJavadoc) subprojects { Project subproject -> @@ -948,10 +1219,10 @@ task buildAll { dependsOn("${subproject.name}:javadocJar") dependsOn("${subproject.name}:sourcesJar") } - dependsOn('framework:allJavadocJar', 'framework:allSourcesJar', 'checker:allJavadocJar', 'checker:allSourcesJar') + dependsOn('framework:allJavadocJar', 'framework:allSourcesJar', 'checker:allJavadocJar', 'checker:allSourcesJar', 'checker-qual:jar', 'checker-util:jar') } -task releaseBuild { +task releaseBuild(group: 'Build') { description 'Build everything required for a release' dependsOn(clean) doFirst { @@ -959,8 +1230,10 @@ task releaseBuild { } // Use finalizedBy rather than dependsOn so that release is set to true before any of the tasks are run. finalizedBy(buildAll) + finalizedBy('checker:assembleForJavac') } +// No group so it does not show up in the output of `gradlew tasks` task releaseAndTest { description 'Build everything required for a release and run allTests' dependsOn(releaseBuild) @@ -973,99 +1246,47 @@ task releaseAndTest { jar.onlyIf {false} /** - * Copies the pom file and expands any properties in the file. For example, - * ${checkerframeworkversion} is changed to the current version. - * @param pomFile file name of pom - * @return file name of the copied and expanded pom file - */ -String copyAndExpandPomFile(String pomFile) { - mkdir "${buildDir}/maven" - copy { - from pomFile - into "${buildDir}/maven" - expand('checkerframeworkversion': version) - } - - new File("${buildDir}/maven").toPath().resolve(new File(pomFile).toPath().last()) -} - -/** - * Deploys the given jar file to the local Maven repository. - * @param jar fully qualified file name of a jar file - * @param pom filename of pom that describes the artifact + * Adds the shared pom information to the given publication. + * @param publication the MavenPublication */ -void mvnDeployToLocalRepo(String jar, String pom) { - String copiedFilename = copyAndExpandPomFile(pom) - exec { - executable 'mvn' - args += [ - 'install:install-file', - "-Dfile=${jar}", - "-DpomFile=${copiedFilename}", - '-DgeneratePom=true' - - ] - } -} - -/** - * Deploys the jar file produced by the "jar" task of the given project to the local Maven repository. - * @param project project whose jar file is deployed - * @param pom filename of pom that describes the artifact - */ -void mvnDeployToLocalRepo(Project project, String pomFile) { - String jarFile = project.tasks.getByName('jar').archiveFile.get().toString(); - mvnDeployToLocalRepo(jarFile, pomFile) -} +final sharedPublicationConfiguration(publication) { + publication.pom { + url = 'https://eisop.github.io/' + developers { + // These are the lead developers/maintainers, not all the developers or contributors. + + // For eisop, previously also typetools + developer { + id = 'wmdietl' + name = 'Werner M. Dietl' + email = 'wdietl@gmail.com' + url = 'https://ece.uwaterloo.ca/~wdietl/' + organization = 'University of Waterloo' + organizationUrl = 'https://uwaterloo.ca/' + } -/** - * Signs and deploys the given jar file to the Sonatypes Maven repository. - * @param jar fully qualified file name of a jar file - * @param pom filename of pom that describes the artifact - * @param classifier the kind of artifact; sources or javadoc; defaults to the empty string - */ -void mvnSignAndDeployToSonatype(String jar, String pom, String classifier = "") { - String copiedFilename = copyAndExpandPomFile(pom) - String passwordFile = "/projects/swlab1/checker-framework/hosting-info/release-private.password" - String passphrase = new File(passwordFile).readLines().get(0); + // For typetools + developer { + id = 'mernst' + name = 'Michael Ernst' + email = 'mernst@cs.washington.edu' + url = 'https://homes.cs.washington.edu/~mernst/' + organization = 'University of Washington' + organizationUrl = 'https://www.cs.washington.edu/' + } + developer { + id = 'smillst' + name = 'Suzanne Millstein' + email = 'smillst@cs.washington.edu' + organization = 'University of Washington' + organizationUrl = 'https://www.cs.washington.edu/' + } + } - exec { - executable 'mvn' - args += [ - 'gpg:sign-and-deploy-file', - '-Durl=https://oss.sonatype.org/service/local/staging/deploy/maven2/', - '-DrepositoryId=sonatype-nexus-staging', - "-DpomFile=${copiedFilename}", - "-Dfile=${jar}", - '-Dgpg.keyname=checker-framework-dev@googlegroups.com', - "-Dgpg.passphrase=${passphrase}" - ] - if (!classifier.isEmpty()) { - args += ["-Dclassifier=${classifier}"] + scm { + url = 'https://github.com/eisop/checker-framework.git' + connection = 'scm:git:https://github.com/eisop/checker-framework.git' + developerConnection = 'scm:git:ssh://git@github.com/eisop/checker-framework.git' } } } - -/** - * Signs and deploys the classes jar, sources jar, and javadoc jar files for a single artifact. - * @param classesJar class jar file - * @param sourceJar source jar file corresponding to the classesJar - * @param javadocJar javadoc jar file corresponding to the classesJar - * @param pom filename of pom that describes the artifact - */ -void mvnSignAndDeployMultipleToSonatype(String classesJar, String sourceJar, String javadocJar, String pom) { - mvnSignAndDeployToSonatype(classesJar, pom) - mvnSignAndDeployToSonatype(sourceJar, pom, "sources") - mvnSignAndDeployToSonatype(javadocJar, pom, "javadoc") -} - -/** - * Signs and deploys the classes jar, sources jar, and javadoc jar files for a single artifact. - * @param project with a jar, sourcesJar, and javadocJar tasks defined - * @param pom filename of pom that describes the artifact - */ -void mvnSignAndDeployMultipleToSonatype(Project project, String pom) { - mvnSignAndDeployMultipleToSonatype(project.tasks.getByName('jar').archiveFile.get().toString(), - project.tasks.getByName('sourcesJar').archiveFile.get().toString(), - project.tasks.getByName('javadocJar').archiveFile.get().toString(), pom) -} diff --git a/changelog.txt b/changelog.txt deleted file mode 100644 index a053d6d81214..000000000000 --- a/changelog.txt +++ /dev/null @@ -1,3551 +0,0 @@ -Version 3.5.0, July 1, 2020 - -Use "allcheckers:" instead of "all:" as a prefix in a warning suppression string. -Writing `@SuppressWarnings("allcheckers")` means the same thing as -`@SuppressWarnings("all")`, unless the `-ArequirePrefixInWarningSuppressions` -command-line argument is supplied. See the manual for details. - -It is no longer necessary to pass -Astubs=checker.jar/javadoc.astub when -compiling a program that uses Javadoc classes. - -Renamed command-line arguments: - * -AshowSuppressWarningKeys to -AshowSuppressWarningsStrings - -The Signature Checker no longer considers Java keywords to be identifiers. -Renamed Signature Checker annotations: - @BinaryNameInUnnamedPackage => @BinaryNameWithoutPackage - @FieldDescriptorForPrimitiveOrArrayInUnnamedPackage => @FieldDescriptorWithoutPackage - @IdentifierOrArray => @ArrayWithoutPackage -Added new Signature Checker annotations: - @BinaryNameOrPrimitiveType - @DotSeparatedIdentifiersOrPrimitiveType - #IdentifierOrPrimitiveType - -The Nullness Checker now treats `System.getProperty()` soundly. Use -`-Alint=permitClearProperty` to disable special treatment of -`System.getProperty()` and to permit undefining built-in system properties. - -Class qualifier parameters: When a generic class represents a collection, -a user can write a type qualifier on the type argument, as in -`List<@Tainted Character>` versus `List<@Untainted Character>`. When a -non-generic class represents a collection with a hard-coded type (as -`StringBuffer` hard-codes `Character`), you can use the new class qualifier -parameter feature to distinguish `StringBuffer`s that contain different -types of characters. - -The Dataflow Framework supports backward analysis. See its manual. - -Implementation details: - -Changed the types of some fields and methods from array to List: - * QualifierDefaults.validLocationsForUncheckedCodeDefaults() - * QualifierDefaults.STANDARD_CLIMB_DEFAULTS_TOP - * QualifierDefaults.STANDARD_CLIMB_DEFAULTS_BOTTOM - * QualifierDefaults.STANDARD_UNCHECKED_DEFAULTS_TOP - * QualifierDefaults.STANDARD_UNCHECKED_DEFAULTS_BOTTOM - -Dataflow Framework: Analysis is now an interface. Added AbstractAnalysis, -ForwardAnalysis, ForwardTransferFunction, ForwardAnalysisImpl, -BackwardAnalysis, BackwardTransferFunction, and BackwardAnalysisImpl. -To adapt existing code: - * `extends Analysis` => `extends ForwardAnalysisImpl` - * `implements TransferFunction` => `implements ForwardTransferFunction` - -In AbstractQualifierPolymorphism, use AnnotationMirrors instead of sets of -annotation mirrors. - -Renamed meta-annotation SuppressWarningsKeys to SuppressWarningsPrefix. -Renamed SourceChecker#getSuppressWarningsKeys(...) to getSuppressWarningsPrefixes. -Renamed SubtypingChecker#getSuppressWarningsKeys to getSuppressWarningsPrefixes. - -Added GenericAnnotatedTypeFactory#postAnalyze, changed signature of -GenericAnnotatedTypeFactory#handleCFGViz, and removed CFAbstractAnalysis#visualizeCFG. - -Removed methods and classes marked deprecated in release 3.3.0 or earlier. - -Closed issues: -#1362, #1727, #2632, #3249, #3296, #3300, #3356, #3357, #3358, #3359, #3380. - ---------------------------------------------------------------------------- - -Version 3.4.1, June 1, 2020 - --Ainfer now takes an argument: - * -Ainfer=jaifs uses .jaif files to store the results of whole-program inference. - * -Ainfer=stubs uses .astub files to store the results of whole-program inference. - * -Ainfer is deprecated but is the same as -Ainfer=jaifs, for backwards compatibility. - -New command-line option: - -AmergeStubsWithSource If both a stub file and a source file are available, use both. - -Closed issues: -#2893, #3021, #3128, #3160, #3232, #3277, #3285, #3289, #3295, #3302, #3305, -#3307, #3310, #3316, #3318, #3329. - ---------------------------------------------------------------------------- - -Version 3.4.0, May 3, 2020 - -The annotated jdk8.jar is no longer used. You should remove any occurrence of - -Xbootclasspath/p:.../jdk8.jar -from your build scripts. Annotations for JDK 8 are included in checker.jar. - -The Returns Receiver Checker enables documenting and checking that a method -returns its receiver (i.e., the `this` parameter). - -Closed issues: -#3267, #3263, #3217, #3212, #3201, #3111, #3010, #2943, #2930. - ---------------------------------------------------------------------------- - -Version 3.3.0, April 1, 2020 - -New command-line options: - -Alint=trustArrayLenZero trust @ArrayLen(0) annotations when determining - the type of Collections.toArray. - -Renamings: - -AuseDefaultsForUncheckedCode to -AuseConservativeDefaultsForUncheckedCode - The old name works temporarily but will be removed in a future release. - -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`. - -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 -substring worked, including the empty string which suppressed all warnings. -Use "all" to suppress all warnings.) - -All postcondition annotations are repeatable (e.g., `@EnsuresNonNull`, -`@EnsuresNonNullIf`, ...). - -Renamed wrapper annotations (which users should not write): - * `@DefaultQualifiers` => `@DefaultQualifier.List` - * `@EnsuresQualifiersIf` => `@EnsuresQualifierIf.List` - * `@EnsuresQualifiers` => `@EnsuresQualifier.List` - * `@RequiresQualifiers` => `@RequiresQualifier.List` - -Implementation details: - * Removed `@DefaultInUncheckedCodeFor` and - `@DefaultQualifierInHierarchyInUncheckedCode`. - * Renamings: - applyUncheckedCodeDefaults() to applyConservativeDefaults() - useUncheckedCodeDefault() to useConservativeDefault() - AnnotatedTypeReplacer to AnnotatedTypeCopierWithReplacement - AnnotatedTypeMerger to AnnotatedTypeReplacer - * Deprecated the `framework.source.Result` class; use `DiagMessage` or - `List` instead. If you were creating a `Result` just to - pass it to `report`, then call new methods `reportError` and - `reportWarning` instead. - * AbstractTypeProcessor#typeProcessingOver() always gets called. - -Closed issues: -#1307, #1881, #1929, #2432, #2793, #3040, #3046, #3050, #3056, #3083, #3124, -#3126, #3129, #3132, #3139, #3149, #3150, #3167, #3189. - ---------------------------------------------------------------------------- - -Version 3.2.0, March 2, 2020 - -@SuppressWarnings("initialization") suppresses only warnings whose key -contains "initialization". Previously, it suppressed all warnings issued -by the Nullness Checker or the Initialization Checker. - -Closed issues: -#2719, #3001, #3020, #3069, #3093, #3120. - ---------------------------------------------------------------------------- - -Version 3.1.1, February 3, 2020 - -New command-line options: - -AassumeDeterministic Unsoundly assume that every method is deterministic - -AassumePure Unsoundly assume that every method is pure - -Renamed -Anocheckjdk to -ApermitMissingJdk. -The old version still works, for backward compatibility. - -Renamed -Alint=forbidnonnullarraycomponents to --Alint=soundArrayCreationNullness. The old version still works, for -backward compatibility. - -Implementation details: - * Deprecated QualifierHierarchy#getTypeQualifiers. - * Deprecated Analysis#Analysis(ProcessingEnvironment) and Analysis#Analysis(T, - int, ProcessingEnvironment); use Analysis#Analysis(), Analysis#Analysis(int), - Analysis#Analysis(T), and Analysis#Analysis(T, int) instead. - * Renamed SourceChecker#getMessages to getMessagesProperties. - * Renamed one overload of SourceChecker.printMessages to printOrStoreMessage. - -Closed issues: -#2181, #2975, #3018, #3022, #3032, #3036, #3037, #3038, #3041, #3049, #3055, -#3076. - ---------------------------------------------------------------------------- - -Version 3.1.0, January 3, 2020 - -Command-line option -AprintGitProperties prints information about the git -repository from which the Checker Framework was compiled. - -Implementation details: - * Removed static cache in AnnotationUtils#areSameByClass and added - AnnotatedTypeFactory#areSameByClass that uses an instance cache. - * Removed static cache in AnnotationBuilder#fromName and #fromClass. - * ContractsUtils#getPreconditions takes an ExecutableElement as an argument. - * ContractsUtils#getContracts returns a Set. - * Moved ContractUtils.Contract to outer level. - * Renamed ConditionalPostcondition#annoResult to ConditionalPostcondition#resultValue. - -Closed issues: -#2867, #2897, #2972. - ---------------------------------------------------------------------------- - -Version 3.0.1, December 2, 2019 - -New command-line option for the Constant Value Checker -`-AnoNullStringsConcatenation` unsoundly assumes that every operand of a String -concatenation is non-null. - -Implementation details: - * Moved AnnotatedTypes#hasTypeQualifierElementTypes to AnnotationUtils. - * Deprecated AnnotatedTypes#isTypeAnnotation and AnnotatedTypes#hasTypeQualifierElementTypes. - -Closed issues: -#945, #1224, #2024, #2744, #2809, #2815, #2818, #2830, #2840, #2853, #2854, -#2865, #2873, #2874, #2878, #2880, #2886, #2888, #2900, #2905, #2919, #2923. - ---------------------------------------------------------------------------- - -Version 3.0.0, November 1, 2019 - -The Checker Framework works on both JDK 8 and JDK 11. - * Type annotations for JDK 8 remain in jdk8.jar. - * Type annotations for JDK 11 appear in stub files in checker.jar. - -Removed the @PolyAll annotation. - -Implementation details: - * Removed all previously deprecated methods. - * AnnotatedTypeFactory#getFnInterfaceFromTree now returns an AnnotatedExecutableType. - * AnnotationUtils#areSame and #areSameByName now only accept non-null - AnnotationMirrors - -Closed issues: -#1169, #1654, #2081, #2703, #2739, #2749, #2779, #2781, #2798, #2820, #2824, -#2829, #2842, #2845, #2848. - ---------------------------------------------------------------------------- - -Version 2.11.1, October 1, 2019 - -The manual links to the Object Construction Checker. - -Closed issues: -#1635, #2718, #2767. - ---------------------------------------------------------------------------- - -Version 2.11.0, August 30, 2019 - -The Checker Framework now uses the Java 9 javac API. The manual describes -how to satisfy this dependency, in a way that works on a Java 8 JVM. -Running the Checker Framework on a Java 9 JVM is not yet supported. - ---------------------------------------------------------------------------- - -Version 2.10.1, August 22, 2019 - -Closed issues: -#1152, #1614, #2031, #2482, #2543, #2587, #2678, #2686, #2690, #2712, #2717, -#2713, #2721, #2725, #2729. - ---------------------------------------------------------------------------- - -Version 2.10.0, August 1, 2019 - -Removed the NullnessRawnessChecker. Use the NullnessChecker instead. - -Closed issues: -#435, #939, #1430, #1687, #1771, #1902, #2173, #2345, #2470, #2534, #2606, -#2613, #2619, #2633, #2638. - ---------------------------------------------------------------------------- - -Version 2.9.0, July 3, 2019 - -Renamed the Signedness Checker's @Constant annotation to @SignednessGlb. -Introduced an alias, @SignedPositive, for use by programmers. - -Annotated the first argument of Opt.get and Opt.orElseThrow as @NonNull. - -Removed meta-annotation @ImplicitFor: - * Use the new meta-annotation @QualifierForLiteral to replace - @ImplicitFor(literals, stringpatterns). - * Use the meta-annotation @DefaultFor to replace @ImplicitFor(typeKinds, - types). - * Use the new meta-annotation @UpperBoundFor to specify a qualifier upper - bound for certain types. - * You can completely remove - @ImplicitFor(typeNames = Void.class, literals = LiteralKind.NULL) - on bottom qualifiers. - @DefaultFor(types = Void.class) - and - @QualifierForLiterals(literals = LiteralKind.NULL) - are added to the bottom qualifier by default. - -Add @DefaultQualifierOnUse and @NoDefaultQualifierOnUse type declaration annotations - -New/changed error message keys: - * initialization.static.fields.uninitialized for uninitialized static fields - * unary.increment.type.incompatible and unary.decrement.type.incompatible - replace some occurrences of compound.assignment.type.incompatible - -Implementation details: - * Renamed QualifierPolymorphism#annotate methods to resolve - * Renamed ImplicitsTreeAnnotator to LiteralTreeAnnotator - * Renamed ImplicitsTypeAnnotator to DefaultForTypeAnnotator - * Removed TypeUseLocation.TYPE_DECLARATION - * Removed InheritedFromClassAnnotator, replace with DefaultQualifierForUseTypeAnnotator - * Rename TreeUtils.isSuperCall and TreeUtils.isThisCall to - isSuperConstructorCall and isThisConstructorCall - -Closed issues: -#2247, #2391, #2409, #2434, #2451, #2457, #2468, #2484, #2485, #2493, #2505, -#2536, #2537, #2540, #2541, #2564, #2565, #2585. - ---------------------------------------------------------------------------- - -Version 2.8.2, June 3, 2019 - -The Signature Checker supports a new type, @FqBinaryName. - -Added a template for a repository that you can use to write a custom checker. - -Linked to the Checker Framework Gradle plugin, which makes it easy to run -a checker on a project that is built using the Gradle build tool. - -Implementation detail: deprecated TreeUtils.skipParens in favor of -TreeUtils.withoutParens which has the same specification. - -Closed issues: -#2291, #2406, #2469, #2477, #2479, #2480, #2494, #2499. - ---------------------------------------------------------------------------- - -Version 2.8.1, May 1, 2019 - -Moved text about the Purity Checker into its own chapter in the manual. - -Closed issues: -#660, #2030, #2223, #2240, #2244, #2375, #2407, #2410, #2415, #2420, #2421, -#2446, #2447, #2460, #2462. - ---------------------------------------------------------------------------- - -Version 2.8.0, April 3, 2019 - -Support `androidx.annotation.RecentlyNonNull` and `RecentlyNullable` (as of -2.6.0, but not previously documented). - -The following qualifiers are now repeatable: `@DefaultQualifier` -`@EnsuresQualifierIf` `@EnsuresQualifier` `@RequiresQualifier`. Therefore, -users generally do not need to write the following wrapper annotations: -`@DefaultQualifiers` `@EnsuresQualifiersIf` `@EnsuresQualifiers` -`@RequiresQualifiers`. - -New command-line option `-ArequirePrefixInWarningSuppressions` makes -`@SuppressWarnings` recognize warning keys of the form -"checkername:key.about.problem" but ignore warning keys of the form -"key.about.problem" without the checker name as a prefix. - -New CONSTRUCTOR_RESULT enum constant in TypeUseLocation makes it possible to -set default annotations for constructor results. - -Clarified the semantics of annotations on class and constructor declarations. -See Section 25.5 "Annotations on classes and constructors" in the manual. - -Interface changes: - * Added protected methods to BaseTypeVisitor so that checkers can change the - checks for annotations on classes, constructor declarations, and constructor - invocations. - * Removed BaseTypeVisitor#checkAssignability and BaseTypeVisitor#isAssignable - methods. - * Renamed AnnotatedTypeFactory#getEnclosingMethod to - AnnotatedTypeFactory#getEnclosingElementForArtificialTree - -Closed issues: -#2159, #2230, #2318, #2324, #2330, #2334, #2343, #2344, #2353, #2366, #2367, -#2370, #2371, #2385. - ---------------------------------------------------------------------------- - -Version 2.7.0, March 1, 2019 - -The manual links to the AWS crypto policy compliance checker, which enforces -that no weak cipher algorithms are used with the Java crypto API. - -The Nullness Checker supports RxJava annotations -io.reactivex.annotations.NonNull and io.reactivex.annotations.Nullable. - -The checker-qual artifact (jar file) contains an OSGi manifest. - -New TYPE_DECLARATION enum constant in TypeUseLocation makes it possible to -(for example) set defaults annotations for class/interface definitions. - -Interface changes: - * Renamed the "value" element of the @HasSubsequence annotation to - "subsequence". - * Renamed @PolySignedness to @PolySigned. - * Renamed AnnotatedTypeFactory.ParameterizedMethodType to - ParameterizedExecutableType. - -Added missing checks regarding annotations on classes, constructor -declarations, and constructor invocations. You may see new warnings. - -Closed issues: -#788, #1751, #2147, #2163, #2186, #2235, #2243, #2263, #2264, #2286, #2302, -#2326, #2327. - ---------------------------------------------------------------------------- - -Version 2.6.0, February 3, 2019 - -The manual includes a section about how to use Lombok and the Checker -Framework simultaneously. - -Commons CSV has been added to the annotated libraries on Maven Central. - -Some error messages have been changed to improve comprehensibility, -such as by adjusting wording or adding additional information. - -Relevant to type system implementers: -Renamed method areSameIgnoringValues to areSameByName. - -Closed issues: #2008, #2166, #2185, #2187, #2221, #2224, #2229, #2234, #2248. -Also fixed false negatives in handling of Map.get(). - ---------------------------------------------------------------------------- - -Version 2.5.8, December 5, 2018 - -The manual now links to the AWS KMS compliance checker, which enforces -that calls to AWS KMS only generate 256-bit keys. - -Closed issues: #372, #1678, #2207, #2212, #2217. - ---------------------------------------------------------------------------- - -Version 2.5.7, November 4, 2018 - -New @EnsuresKeyFor and @EnsuresKeyForIf method annotations permit -specifying the postcondition that a method gives some value a @KeyFor type. - -The manual links to the Rx Thread & Effect Checker, which enforces -UI Thread safety properties for stream-based Android applications. - -Closed issues: -#1014, #2151, #2178, #2180, #2183, #2188, #2190, #2195, #2196, #2198, #2199. - ---------------------------------------------------------------------------- - -Version 2.5.6, October 3, 2018 - -Introduce checker-qual-android artifact that is just like the checker-qual -artifact, but the qualifiers have classfile retention. This is useful for -Android projects. - -Remove the checker-compat-qual artifact, which was only useful for Java 7, -which the Checker Framework no longer supports. It remains available on -Maven Central, with versions 2.5.5 and earlier. - -Closed issues: -#2135, #2157, #2158, #2164, #2171. - ---------------------------------------------------------------------------- - -Version 2.5.5, August 30, 2018 - -Implicit imports (deprecated in November 2014) are no longer supported. - -Renamed the testlib Maven artifact to framework-test. - -Removed command-line option -AprintErrorStack, which is now the default. -Added -AnoPrintErrorStack to disable it (which should be rare). - -Replaced ErrorReporter class with BugInCF and UserError exceptions. - -Closed issues: -#1999, #2008, #2023, #2029, #2074, #2088, #2098, #2099, #2102, #2107. - ---------------------------------------------------------------------------- - -Version 2.5.4, August 1, 2018 - -Closed issues: -#2030, #2048, #2052, #2059, #2065, #2067, #2073, #2082. - ---------------------------------------------------------------------------- - -Version 2.5.3, July 2, 2018 - -Closed issues: -#266, #1248, #1678, #2010, #2011, #2018, #2020, #2046, #2047, #2054. - ---------------------------------------------------------------------------- - -Version 2.5.2, June 1, 2018 - -In the Map Key Checker, null is now @UnknownKeyFor. See the "Map Key Checker" -chapter in the manual for more details. - -Closed issues: -#370, #469, #1701, #1916, #1922, #1959, #1976, #1978, #1981, #1983, #1984, #1991, #1992. - ---------------------------------------------------------------------------- - -Version 2.5.1, May 1, 2018 - -Added a Maven artifact of the Checker Framework testing library, testlib. - -Closed issues: -#849, #1739, #1838, #1847, #1890, #1901, #1911, #1912, #1913, #1934, #1936, -#1941, #1942, #1945, #1946, #1948, #1949, #1952, #1953, #1956, #1958. - ---------------------------------------------------------------------------- - -Version 2.5.0, April 2, 2018 - -Declaration annotations that are aliases for type annotations are now treated -as if they apply to the top-level type. See "Declaration annotations" section -in the "Warnings" chapter in the manual for more details. - -Ended support for annotations in comments. See "Migrating away from -annotations in comments" section in the "Handling legacy code" chapter in the -manual for instructions on how to remove annotations from comments. - -Closed issues: -#515, #1667, #1739, #1776, #1819, #1863, #1864, #1865, #1866, #1867, #1870, -#1876, #1879, #1882, #1898, #1903, #1905, #1906, #1910, #1914, #1915, #1920. - ---------------------------------------------------------------------------- - -Version 2.4.0, March 1, 2018 - -Added the Index Checker, which eliminates ArrayIndexOutOfBoundsException. - -Added the Optional Checker, which verifies uses of Java 8's Optional class. - -Removed the Linear Checker, whose implementation was inconsistent with its -documentation. - -Added a @QualifierArgument annotation to be used on pre- and postcondition - annotations created by @PreconditionAnnotation, @PostconditionAnnotation, - and @ConditionalPostconditionAnnotation. This allows qualifiers with - arguments to be used in pre- and postconditions. - -Added new type @InternalFormForNonArray to the Signature Checker - -Moved annotated libraries from checker/lib/*.jar to the Central Repository: -https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.checkerframework.annotatedlib%22 - -Moved the Javadoc stub file from checker/lib/javadoc.astub to -checker/resources/javadoc.astub. - -Simplified the instructions for running the Checker Framework with Gradle. - -The Checker Framework Eclipse plugin is no longer released nor supported. - -Closed issues: -#65, #66, #100, #108, #175, #184, #190, #194, #209, #239, #260, #270, #274, -#293, #302, #303, #306, #321, #325, #341, #356, #360, #361, #371, #383, #385, -#391, #397, #398, #410, #423, #424, #431, #430, #432, #548, #1131, #1148, -#1213, #1455, #1504, #1642, #1685, #1770, #1796, #1797, #1801, #1809, #1810, -#1815, #1817, #1818, #1823, #1831, #1837, #1839, #1850, #1851, #1852, #1861. - ---------------------------------------------------------------------------- - -Version 2.3.2, February 1, 2018 - -Closed issues: -#946, #1133, #1232, #1319, #1625, #1633, #1696, #1709, #1712, #1734, #1738, -#1749, #1754, #1760, #1761, #1768, #1769, #1781. - ---------------------------------------------------------------------------- - -Version 2.3.1, January 2, 2018 - -Closed issues: -#1695, #1696, #1697, #1698, #1705, #1708, #1711, #1714, #1715, #1724. - ---------------------------------------------------------------------------- - -Version 2.3.0, December 1, 2017 - -Removed the deprecated @LazyNonNull type qualifier. -Deprecated most methods in InternalUtils and moved them to either -TreeUtils or TypesUtils. Adapted a few method names and parameter -orders for consistency. - -Closed issues: -#951, #1356, #1495, #1602, #1605, #1623, #1628, #1636, #1641, #1653, #1655, -#1664, #1665, #1681, #1684, #1688, #1690. - ---------------------------------------------------------------------------- - -Version 2.2.2, November 2, 2017 - -The Interning Checker supports a new annotation, @InternedDistinct, which -indicates that the value is not equals() to any other value. - -An annotated version of the Commons IO library appears in checker/lib/ . - -Closed issue #1586, which required re-opening issues 293 and 341 until -proper fixes for those are implemented. - -Closed issues: -#1386, #1389, #1423, #1520, #1529, #1530, #1531, #1546, #1553, #1555, #1565, -#1570, #1579, #1580, #1582, #1585, #1586, #1587, #1598, #1609, #1615, #1617. - ---------------------------------------------------------------------------- - -Version 2.2.1, September 29, 2017 - -Deprecated some methods in AnnotatedTypeMirror and AnnotationUtils, to -be removed after the 2.2.1 release. - -The qualifiers and utility classes in checker-qual.jar are compiled to Java 8 -byte code. A new jar, checker-qual7.jar, includes the qualifiers and utility -classes compiled to Java 7 byte code. - -Closed issues: -#724, #1431, #1442, #1459, #1464, #1482, #1496, #1499, #1500, #1506, #1507, -#1510, #1512, #1522, #1526, #1528, #1532, #1535, #1542, #1543. - ---------------------------------------------------------------------------- - -Version 2.2.0, September 5, 2017 - -A Java 8 JVM is required to run the Checker Framework. -You can still typecheck and compile Java 7 (or earlier) code. -With the "-target 7" flag, the resulting .class files still run with JDK 7. - -The stub file format has changed to be more similar to regular Java syntax. -Most notably, receiver annotations are written using standard Java 8 syntax -(a special first formal paramter named "this") and inner classes are written -using standard Java syntax (rather than at the top level using a name that -contains "$". You need to update your stub files to conform to the new syntax. - -Closed issues: -#220, #293, #297, #341, #375, #407, #536, #571, #798, #867, #1180, #1214, #1218, -#1371, #1411, #1427, #1428, #1435, #1438, #1450, #1456, #1460, #1466, #1473, -#1474. - ---------------------------------------------------------------------------- - -Version 2.1.14, 3 August 2017 - -Nullness Checker change to annotated JDK: The type argument to the Class, -Constructor, and Optional classes may now be annotated as @Nullable or -@NonNull. The nullness of the type argument doesn't matter, but this -enables easier integration with generic clients. - -Many crashes and false positives associated with uninferred method type -arguments have been correct. By default, uninferred method type arguments, -which can happen with Java 8 style target type contexts, are silently ignored. -Use the option -AconservativeUninferredTypeArguments to see warnings about -method calls where the Checker Framework fails to infer type arguments. - -Closed issues: -#753, #804, #961, #1032, #1062, #1066, #1098, #1209, #1280, #1316, #1329, #1355, -#1365, #1366, #1367, #1377, #1379, #1382, #1384, #1397, #1398, #1399, #1402, -#1404, #1406, #1407. - ---------------------------------------------------------------------------- - -Version 2.1.13, 3 July 2017 - -Verified that the Checker Framework builds from source on Windows Subsystem -for Linux, on Windows 10 Creators Edition. - -The manual explains how to configure Android projects that use Android Studio -3.0 and Android Gradle Plugin 3.0.0, which support type annotations. - -Closed issues: -#146, #1264, #1275, #1290, #1303, #1308, #1310, #1312, #1313, #1315, #1323, -#1324, #1331, #1332, #1333, #1334, #1347, #1357, #1372. - ---------------------------------------------------------------------------- - -Version 2.1.12, 1 June 2017 - -The manual links to Glacier, a class immutability checker. - -The stubparser license has been updated. You can now use stubparser under -either the LGPL or the Apache license, whichever you prefer. - -Closed issues: -#254, #1201, #1229, #1236, #1239, #1240, #1257, #1265, #1270, #1271, #1272, -#1274, #1288, #1291, #1299, #1304, #1305. - ---------------------------------------------------------------------------- - -Version 2.1.11, 1 May 2017 - -The manual contains new FAQ (frequently asked questions) sections about -false positive warnings and about inference for field types. - -Closed issues: -#989, #1096, #1136, #1228. - ---------------------------------------------------------------------------- - -Version 2.1.10, 3 April 2017 - -The Constant Value Checker, which performs constant propagation, has been -extended to perform interval analysis -- that is, it determines, for each -expression, a statically-known lower and upper bound. Use the new -@IntRange annotation to express this. Thanks to Jiasen (Jason) Xu for this -feature. - -Closed issues: -#134, #216, #227, #307, #334, #437, #445, #718, #1044, #1045, #1051, #1052, -#1054, #1055, #1059, #1077, #1087, #1102, #1108, #1110, #1111, #1120, #1124, -#1127, #1132. - ---------------------------------------------------------------------------- - -Version 2.1.9, 1 March 2017 - -By default, uninferred method type arguments, which can happen with Java 8 -style target type contexts, are silently ignored, removing many false -positives. The new option -AconservativeUninferredTypeArguments can be used to -get the conservative behavior. - -Closed issues: -#1006, #1011, #1015, #1027, #1035, #1036, #1037, #1039, #1043, #1046, #1049, -#1053, #1072, #1084. - ---------------------------------------------------------------------------- - -Version 2.1.8, 20 January 2017 - -The Checker Framework webpage has moved to https://checkerframework.org/. -Old URLs should redirect to the new one, but please update your links -and let us know if any old links are broken rather than redirecting. - -The documentation has been reorganized in the Checker Framework repository. -The manual, tutorial, and webpages now appear under checker-framework/docs/. - -Closed issues: -#770, #1003, #1012. - ---------------------------------------------------------------------------- - -Version 2.1.7, 3 January 2017 - -Manual improvements: - * Added a link to jOOQ's SQL checker. - * Documented the `-AprintVerboseGenerics` command-line option. - * Better explanation of relationship between Fake Enum and Subtyping Checkers. - -Closed issues: -#154, #322, #402, #404, #433, #531, #578, #720, #795, #916, #953, #973, #974, -#975, #976, #980, #988, #1000. - ---------------------------------------------------------------------------- - -Version 2.1.6, 1 December 2016 - -Closed issues: -#412, #475. - ---------------------------------------------------------------------------- - -Version 2.1.5, 2 November 2016 - -The new class org.checkerframework.checker.nullness.Opt provides every -method in Java 8's java.util.Optional class, but written for possibly-null -references rather than for the Optional type. This can shorten code that -manipulates possibly-null references. - -In bytecode, type variable upper bounds of type Object may or may not have -been explicitly written. The Checker Framework now assumes they were not -written explicitly in source code and defaults them as implicit upper bounds. - -The manual describes how to run a checker within the NetBeans IDE. - -The manual describes two approaches to creating a type alias or typedef. - -Closed issues: -#643, #775, #887, #906, #941. - ---------------------------------------------------------------------------- - -Version 2.1.4, 3 October 2016 - -Closed issues: -#885, #886, #919. - ---------------------------------------------------------------------------- - -Version 2.1.3, 16 September 2016 - -Closed issues: -#122, #488, #495, #580, #618, #647, #713, #764, #818, #872, #893, #894, #901, -#902, #903, #905, #913. - ---------------------------------------------------------------------------- - -Version 2.1.2, 1 September 2016 - -Closed issues: -#182, #367, #712, #811, #846, #857, #858, #863, #870, #871, #878, #883, #888. - ---------------------------------------------------------------------------- - -Version 2.1.1, 1 August 2016 - -The codebase conforms to a consistent coding style, which is enforced by -a git pre-commit hook. - -AnnotatedTypeFactory#createSupportedTypeQualifiers() must now return a mutable -list. Checkers that override this method will have to be changed. - -Closed issues: -#384, #590, #681, #790, #805, #809, #810, #820, #824, #826, #829, #838, #845, -#850, #856. - ---------------------------------------------------------------------------- - -Version 2.1.0, 1 July 2016 - -The new Signedness Checker prevents mixing of unsigned and signed -values and prevents meaningless operations on unsigned values. - -The Lock Checker expresses the annotated variable as ``; -previously it used `itself`, which may conflict with an identifier. - -Closed issues: -#166, #273, #358, #408, #471, #484, #594, #625, #692, #700, #701, #711, #717, -#752, #756, #759, #763, #767, #779, #783, #794, #807, #808. - ---------------------------------------------------------------------------- - -Version 2.0.1, 1 June 2016 - -We renamed method annotateImplicit to addComputedTypeAnnotations. If you -have implemented a checker, you need to change occurrences of -annotateImplicit to addComputedTypeAnnotations. - -The Checker Framework (checker.jar) is now placed on the processorpath -during compilation. Previously, it was placed on the classpath. The -qualifiers (checker-qual.jar) remain on the classpath. This change should -reduce conflicts between your code and the Checker Framework. If your code -depends on classes in the Checker Framework, then you should add those -classes to the classpath when you run the compiler. - -Closed issues: -#171, #250, #291, #523, #577, #672, #680, #688, #689, #690, #691, #695, #696, -#698, #702, #704, #705, #706, #707, #720, #721, #723, #728, #736, #738, #740. - ---------------------------------------------------------------------------- - -Version 2.0.0, 2 May 2016 - -Inference: - - * The infer-and-annotate.sh script infers annotations and inserts them in - your source code. This can reduce the burden of writing annotations and - let you get started using a type system more quickly. See the - "Whole-program inference" section in the manual for details. - -Type systems: - - * The Lock Checker has been replaced by a new implementation that provides - a stronger guarantee. The old Lock Checker prevented two threads from - simultaneously using a given variable, but race conditions were still - possible due to aliases. The new Lock Checker prevents two threads from - simultaneously dereferencing a given value, and thus prevents race - conditions. For details, see the "Lock Checker" chapter in the manual, - which has been rewritten to describe the new semantics. - - * The top type qualifier for the Signature String type system has been - renamed from @UnannotatedString to @SignatureUnknown. You shouldn't - ever write this annotation, but if you perform separate compilation (for - instance, if you do type-checking with the Signature String Checker - against a library that is annotated with Signature String annotations), - then you need to re-compile the library. - - * The IGJ, OIGJ, and Javari Checkers are no longer distributed with the - Checker Framework. If you wish to use them, install version 1.9.13 of - the Checker Framework. The implementations have been removed because - they were not being maintained. The type systems are valuable, but the - type-checkers should be rewritten from scratch. - -Documentation improvements: - - * New manual section "Tips for creating a checker" shows how to break down - the implementation of a type system into small, manageable pieces. - - * Improved instructions for using Maven and Gradle, including for Android - code. - -Tool changes: - - * The Checker Framework Live Demo webpage lets you try the Checker - Framework without installing it: http://eisop.uwaterloo.ca/live/ - - * New command-line arguments -Acfgviz and -Averbosecfg enable better - debugging of the control-flow-graph generation step of type-checking. - - * New command-line argument -Ainfer is used by the infer-and-annotate.sh - script that performs type inference. - -Closed issues: -#69, #86, #199, #299, #329, #421, #428, #557, #564, #573, #579, #665, #668, #669, -#670, #671. - ---------------------------------------------------------------------------- - -Version 1.9.13, 1 April 2016 - -Documentation: - * Clarified Maven documentation about use of annotations in comments. - * Added FAQ about annotating fully-qualified type names. - -Closed issues: #438, #572, #579, #607, #624, #631. - ---------------------------------------------------------------------------- - -Version 1.9.12, 1 March 2016 - -The Checker Framework distribution contains annotated versions -of libraries in directory checker-framework/checker/lib/. -During type-checking, you should put these versions first on your classpath, -to obtain more precise type-checking with fewer false positive warnings. - -tools.jar is no longer required to be on the classpath when using -checker-qual.jar - -The Signature String Checker supports two new string representations of a -Java type: @InternalForm and @ClassGetSimpleName. - -The manual documents how to run a pluggable type-checker in IntelliJ IDEA. - -The instructions on how to run a type-checker in Gradle have been updated to -use the artifacts in Maven Central. Examples using the instructions have been -added under checker-framework/docs/examples/GradleExamples/. - -Renamed enum DefaultLocation to TypeUseLocation. - -Closed issues: #130, #263, #345, #458, #559, #559, #574, #582, #596. - ---------------------------------------------------------------------------- - -Version 1.9.11, 1 February 2016 - -Renamed and merged -AuseSafeDefaultsForUnannotatedSourceCode and --AsafeDefaultsForUnannotatedBytecode command-line options to --AuseDefaultsForUncheckedCode that takes arguments source and bytecode. - -For type-system developers: - -* The previously deprecated - org.checkerframework.framework.qual.TypeQualifier{s} annotations - were removed. -* Every type system uses the CLIMB-to-top defaulting scheme, unless it - explicitly specifies a different one. Previously a type system needed - to explicitly request CLIMB-to-top, but now it is the default. - -Closed issues: #524, #563, #568. - ---------------------------------------------------------------------------- - -Version 1.9.10, 4 January 2016 - -The Checker Framework distribution files now contain a version number: -for example, checker-framework-1.9.9.zip rather than checker-framework.zip. - -The Nullness Checker supports the org.eclipse.jgit.annotations.Nullable and -NonNull annotations. - -Buildfiles do less unnecessary recomputation. - -Documentation: - * Documented how to initialize circular data structures in the - Initialization type system. - * Linked to David Bürgin's Nullness Checker tutorial at - https://github.com/glts/safer-spring-petclinic/wiki - * Acknowledged more contributors in the manual. - -For type-system developers: - * The org.checkerframework.framework.qual.TypeQualifier{s} annotations are - now deprecated. To indicate which annotations a checker supports, see - https://checkerframework.org/manual/#creating-indicating-supported-annotations . - Support for TypeQualifier{s} will be removed in the next release. - * Renamed - org.checkerframework.framework.qual.Default{,Qualifier}ForUnannotatedCode to - DefaultInUncheckedCodeFor and DefaultQualifierInHierarchyInUncheckedCode. - -Closed issues: #169, #363, #448, #478, #496, #516, #529. - ---------------------------------------------------------------------------- - -Version 1.9.9, 1 December 2015 - -Fixed issues: #511, #513, #514, #455, #527. - -Removed the javac_maven script and batch file, -which had been previously deprecated. - ---------------------------------------------------------------------------- - -Version 1.9.8, 9 November 2015 - -Field initialization warnings can now be suppressed for a single field at a -time, by placing @SuppressWarnings("initialization") on the field declaration. - -Updated Maven instructions to no longer require a script. -Added an example of how to use the instructions under -docs/examples/MavenExample. - -The javac_maven script (and batch file) are deprecated and will be -removed as of December 2015. - -Fixed issues: #487, #500, #502. - ---------------------------------------------------------------------------- - -Version 1.9.7, 24 October 2015 - -Fixed issues: #291, #474. - ----------------------------------------------------------------------- - -Version 1.9.6, 8 October 2015 - -Fixed issue: #460. - ----------------------------------------------------------------------- - -Version 1.9.5, 1 September 2015 - -Test Framework Updates: - * The test framework has been refactored to improve extensibility. - * Tests that previously extended ParameterizedCheckerTest or - CheckerTest should extend either CheckerFrameworkTest or nothing. - * If a test used methods that were previously found on - CheckerTest, you may find them in TestUtilities. - -Fixed issues: #438, #457, #459. - ----------------------------------------------------------------------- - -Version 1.9.4, 4 August 2015 - -Documented the notion of a compound checker, which depends on other checkers - and automatically runs them. - -Renamed -AuseConservativeDefaultsForUnannotatedSourceCode command-line - option to -AuseSafeDefaultsForUnannotatedSourceCode - -Moved the Checker Framework version control repository from Google Code to -GitHub, and from the Mercurial version control system to Git. If you have -cloned the old repository, then discard your old clone and create a new one -using this command: - git clone https://github.com/typetools/checker-framework.git - -Fixed issues: #427, #429, #434, #442, #450. - ----------------------------------------------------------------------- - -Version 1.9.3, 1 July 2015 - -New command-line options: - * -AsafeDefaultsForUnannotatedBytecode causes a checker to use conservative - defaults for .class files that were compiled without running the given - checker. Without this option, type-checking is unsound (that is, there - might be errors at run time even though the checker issues no warnings). - * -AuseConservativeDefaultsForUnannotatedSourceCode uses conservative - annotations for unannotated type uses. Use this when compiling a library in - which some but not all classes are annotated. - -Various bug fixes and documentation improvements. - -Fixed issues: #436. - ----------------------------------------------------------------------- - -Version 1.9.2, 1 June 2015 - -Internationalization Format String Checker: -This new type-checker prevents use of incorrect internationalization -format strings. - -Fixed issues: #434. - ----------------------------------------------------------------------- - -Version 1.9.1, 1 May 2015 - -New FAQ entry: - "How does the Checker Framework compare with Eclipse's null analysis?" - ----------------------------------------------------------------------- - -Version 1.9.0, 17 April 2015 - -Bug fixes for generics, especially type parameters: - * Manual chapter 21 "Generics and polymorphism" has been expanded, - and it gives more information on annotating type parameters. - * The qualifier on a type parameter (e.g. <@HERE T> ) only applies - to the lower bound of that type parameter. Previously it also - applied to the upper bound. - * Unannotated, unbounded wildcards are now qualified with the - annotations of the type parameter to which they are an argument. - See the new manual section 23.3.4 for more details. - * Warning "bound.type.incompatible" is issued if the lower bound of - a type parameter or wildcard is a supertype of its upper bound, - e.g. <@Nullable T extends @NonNull Object> - * Method type argument inference has been improved. Fewer warnings - should be issued when method invocations omit type arguments. - * Added command-line option -AprintVerboseGenerics to print more - information about type parameters and wildcards when they appear - in warning messages. - -Reflection resolution: -If you supply the -AresolveReflection command-line option, the Checker -Framework attempts to resolve reflection. This reduces the number of -false positive warnings caused by reflection. - -The documentation for the Map Key Checker has been moved into its own -chapter in the manual. - -Fixed issues: #221, #241, #313, #314, #328, #335, #337, #338, #339, #355, #369, - #376, #378, #386, #388, #389, #393, #403, #404, #413, #414, #415, - #417, #418, #420, #421, #422, #426. - ----------------------------------------------------------------------- - -Version 1.8.11, 2 March 2015 - -Fixed issues: #396, #400, #401. - ----------------------------------------------------------------------- - -Version 1.8.10, 30 January 2015 - -Fixed issues: #37, #127, #350, #364, #365, #387, #392, #395. - ----------------------------------------------------------------------- - -Version 1.8.9, 19 December 2014 - -Aliasing Checker: -This new type-checker ensures that an expression has no aliases. - -Fixed issues: #362, #380, #382. - ----------------------------------------------------------------------- - -Version 1.8.8, 26 November 2014 - -@SuppressWarnings("all") suppresses all Checker Framework warnings. - -Implicit imports are deprecated, including the jsr308_imports environment -variable and the -jsr308_imports ... and -Djsr308.imports=... command-line -options. - -For checkers bundled with the Checker Framework, package names may now -be omitted when running from the command line. -E.g. - javac -processor NullnessChecker MyFile.java - -The Nullness checker supports Android annotations -android.support.annotation.NonNull and android.support.annotation.Nullable. - -Fixed issues: #366, #379. - ----------------------------------------------------------------------- - -Version 1.8.7, 30 October 2014 - -Fix performance regression introduced in release 1.8.6. - -Nullness Checker: - * Updated Nullness annotations in the annotated JDK. - See issues: #336, #340, #374. - * String concatenations with null literals are now @NonNull - rather than @Nullable. See issue #357. - -Fixed issues: #200, #300, #332, #336, #340, #357, #359, #373, #374. - ----------------------------------------------------------------------- - -Version 1.8.6, 25 September 2014 - -Method Reference and Lambda Expression Support: -The Checker Framework now supports type-checking method references -and lambda expressions to ensure they are congruent with the -functional interface they are assigned to. The bodies of lambda expressions -are also now type-checked similarly to regular method bodies. - -Dataflow: - * Handling of the following language features has been improved: - boxed Booleans, finally blocks, switch statements, type casts, enhanced - for loops - * Performance improvements - -Annotations: -The checker-compat-qual.jar is now included with the Checker Framework -release. It can also be found in Maven Central at the coordinates: -org.checkerframework:checker-compat-qual -Annotations in checker-compat-qual.jar do not require Java 8 but -can only be placed in annotation locations valid in Java 7. - ----------------------------------------------------------------------- - -Version 1.8.5, 29 August 2014 - -Eclipse Plugin: -All checkers in the Checker Framework manual now appear in the -Eclipse plugin by default. Users no longer have to include -checker.jar on their classpath to run any of the built-in checkers. - -Improved Java 7 compatibility and introduced Java 7 compliant -annotations for the Nullness Checker. Please see the section on -"Class-file compatibility with Java 7" in the manual for more details. - -Fixed issue #347. - ----------------------------------------------------------------------- - -Version 1.8.4, 1 August 2014 - -The new Constant Value Checker is a constant propagation analysis: it -determines which variable values can be known at compile time. - -Overriding methods now inherit declaration annotations from methods they -override, if the declaration annotation is meta-annotate with -@InheritedAnnotation. In particular, the purity annotations @SideEffectFree, -@Deterministic, and @Pure are inherited. - -Command-line options: - * Renamed the -AenablePurity command-line flag to -AcheckPurityAnnotations. - * Added a command-line option -AoutputArgsToFile to output all command-line - options passed to the compiler to a file. This is especially useful when - debugging Maven compilation. - -Annotations: -These changes are relevant only to people who wish to use pluggable -type-checking with a standard Java 7 toolset. (If you are not having -trouble with your Java 7 JVM, then you don't care about them.) - * Made clean-room reimplementations of nullness-related annotations - compatible with Java 7 JVMs, by removing TYPE_USE as a target. - * Added a new set of Java 7 compatibility annotations for the Nullness Checker - in the org.checkerframework.checker.nullness.compatqual package. These - annotations do not require Java 8 but can only be placed in annotation - locations valid in Java 7. - -Java 8 support: -The Checker Framework no longer crashes when type-checking code with lambda -expressions, but it does issue a lambda.unsupported warning when -type-checking code containing lambda expressions. Full support for -type-checking lambda expressions will appear in a future release. - -Fixed issue #343. - ----------------------------------------------------------------------- - -Version 1.8.3, 1 July 2014 - -Updated the Initialization Checker section in the manual with -a new introduction paragraph. - -Removed the Maven plugin section from the manual as the plugin is -no longer maintained and the final release was on June 2, #2014. -The javac_maven script (and batch file) are available to use -the Checker Framework from Maven. - -Fixed issue #331. - ----------------------------------------------------------------------- - -Version 1.8.2, 2 Jun 2014 - -Converted from using rt.jar to ct.sym for creating the annotated jdk. -Using the annotated jdk on the bootclasspath of a VM will cause the -vm to crash immediately. - -The Lock Checker has been rewritten to support dataflow analysis. -It can now understand conditional expressions, for example, and -knows that "lock" is held in the body of statements like -"if (lock.tryLock()) { ... }" -The Lock Checker chapter in the manual has been updated accordingly -and describes the new Lock Checker features in detail. - -Provided a javac_maven script (and batch file) to make it simpler -to use the Checker Framework from Maven. The Maven plug-in is deprecated -and will be removed as of July 1, 2014. Added an explanation of how -to use the script in the Maven section of the manual. - -The Checker Framework installation instructions in the manual have -been updated. - -Fixed issues: #312, #315, #316, #318, #319, #324, #326, #327. - ----------------------------------------------------------------------- - -Version 1.8.1, 1 May 2014 - -Support to directly use the Java 8 javac in addition to jsr308-langtools. -Added docs/examples directory to checker-framework.zip. -New section in the manual describing the contents of checker-framework.zip. - -Fixed issues: #204, #304, #320. - ----------------------------------------------------------------------- - -Version 1.8.0, 2 April 2014 - -Added the GUI Effect Checker, which prevents "invalid thread access" errors -when a background thread in a GUI attempts to access the UI. - -Changed the Java package of all type-checkers and qualifiers. The package -"checkers" has been renamed to "org.checkerframeork.checker". This -requires you to change your import statements, such as from - import checkers.nullness.quals.*; -to - import org.checkerframework.checker.nullness.qual.*; -It also requires you to change command-line invocations of javac, such as from - javac -processor checkers.nullness.NullnessChecker ... -to - javac -processor org.checkerframework.checker.nullness.NullnessChecker ... - -Restructured the Checker Framework project and package layout, -using the org.checkerframework prefix. - ----------------------------------------------------------------------- - -Version 1.7.5, 5 March 2014 - -Minor improvements to documentation and demos. -Support a few new units in the UnitsChecker. - ----------------------------------------------------------------------- - -Version 1.7.4, 19 February 2014 - -Error messages now display the error key that can be used in -SuppressWarnings annotations. Use -AshowSuppressWarningKeys to -show additional keys. - -Defaulted type qualifiers are now stored in the Element and written -to the final bytecode. - -Reduce special treatment of checkers.quals.Unqualified. - -Fixed issues: #170, #240, #265, #281. - ----------------------------------------------------------------------- - -Version 1.7.3, 4 February 2014 - -Fixes for Issues #210, #253, #280, #288. - -Manual: - Improved discussion of checker guarantees. - -Maven Plugin: - Added option useJavacOutput to display exact compiler output. - -Eclipse Plugin: - Added the Format String Checker to the list of built-in checkers. - ----------------------------------------------------------------------- - -Version 1.7.2, 2 January 2014 - -Fixed issues: #289, #292, #295, #296, #298. - ----------------------------------------------------------------------- - -Version 1.7.1, 9 December 2013 - -Fixes for Issues #141, #145, #257, #261, #269, #267, #275, #278, #282, #283, #284, #285. - -Implementation details: - Renamed AbstractBasicAnnotatedTypeFactory to GenericAnnotatedTypeFactory - ----------------------------------------------------------------------- - -Version 1.7.0, 23 October 2013 - -Format String Checker: - This new type-checker ensures that format methods, such as - System.out.printf, are invoked with correct arguments. - -Renamed the Basic Checker to the Subtyping Checker. - -Reimplemented the dataflow analysis that performs flow-sensitive type - refinement. This fixes many bugs, improves precision, and adds features. - Many more Java expressions can be written as annotation arguments. - -Initialization Checker: - This new abstract type-checker verifies initialization properties. It - needs to be combined with another type system whose proper initialization - should be checked. This is the new default initialzation checker for the - Nullness Checker. It is based on the "Freedom Before Commitment" approach. - -Renamed method annotations used by the Nullness Checker: - @AssertNonNullAfter => @EnsuresNonNull - @NonNullOnEntry => @RequiresNonNull - @AssertNonNullIfTrue(...) => @IfMethodReturnsFalseEnsuresNonNull - @AssertNonNullIfFalse(...) => @IfMethodReturnsFalseEnsuresNonNull - @LazyNonNull => @MonotonicNonNull - @AssertParametersNonNull => [no replacement] -Removed annotations used by the Nullness Checker: - @AssertParametersNonNull -Renamed type annotations used by the Initialization Checker: - @NonRaw => @Initialized - @Raw => @UnknownInitialization - new annotation @UnderInitialization -The old Initialization Checker (that uses @Raw and @NonRaw) can be invoked - by invoking the NullnessRawnessChecker rather than the NullnessChecker. - -Purity (side effect) analysis uses new annotations @SideEffectFree, - @Deterministic, and @TerminatesExecution; @Pure means both @SideEffectFree - and @Deterministic. - -Pre- and postconditions about type qualifiers are available for any type system - through @RequiresQualifier, @EnsuresQualifier and @EnsuresQualifierIf. The - contract annotations for the Nullness Checker (e.g. @EnsuresNonNull) are now - only a special case of these general purpose annotations. - The meta-annotations @PreconditionAnnotation, @PostconditionAnnotation, and - @ConditionalPostconditionAnnotation can be used to create more special-case - annotations for other type systems. - -Renamed assertion comment string used by all checkers: - @SuppressWarnings => @AssumeAssertion - -To use an assert statement to suppress warnings, the assertion message must - include the string "@AssumeAssertion(warningkey)". Previously, just the - warning key sufficed, but the string @SuppressWarnings(warningkey) was - recommended. - -New command-line options: - -AonlyDefs and -AonlyUses complement existing -AskipDefs and -AskipUses - -AsuppressWarnings Suppress warnings matching the given key - -AassumeSideEffectFree Unsoundly assume that every method is side-effect-free - -AignoreRawTypeArguments Ignore subtype tests for type arguments that - were inferred for a raw type - -AenablePurity Check the bodies of methods marked as pure - (@SideEffectFree or @Deterministic) - -AsuggestPureMethods Suggest methods that could be marked as pure - -AassumeAssertionsAreEnabled, -AassumeAssertionsAreDisabled Whether to - assume that assertions are enabled or disabled - -AconcurrentSemantics Whether to assume concurrent semantics - -Anocheckjdk Don't err if no annotated JDK can be found - -Aflowdotdir Create an image of the control flow graph - -AinvariantArrays replaces -Alint=arrays:invariant - -AcheckCastElementType replaces -Alint=cast:strict - -Manual: - New manual section about array types. - New FAQ entries: "Which checker should I start with?", "How can I handle - typestate, or phases of my program with different data properties?", - "What is the meaning of a type qualifier at a class declaration?" - Reorganized FAQ chapter into sections. - Many other improvements. - ----------------------------------------------------------------------- - -Version 1.6.7, 28 August 2013 - -User-visible framework improvements: - Improve the error message produced by -Adetailedmsgtext - -Bug fixes: - Fix issue #245: anonymous classes were skipped by default - ----------------------------------------------------------------------- - -Version 1.6.6, 01 August 2013 - -Documentation: - The Checker Framework manual has been improved. Changes include: -more troubleshooting tips to the Checker Framework manual, an improved -discussion on qualifier bounds, more examples, improved formatting, and more. - An FAQ entry has been added to discuss JSR305. - Minor clarifications have been added to the Checker Framework tutorial. - ----------------------------------------------------------------------- - -Version 1.6.5, 01 July 2013 - -User-visible framework improvements: - Stub files now support static imports. - -Maven plugin: - Maven plugin will now issue a warning rather than quit when zero checkers are specified in a project's pom.xml. - -Documentation: - Improved the Maven plugin instructions in the Checker Framework manual. - Added documentation for the -XDTA:noannotationsincomments compiler flag. - -Internal framework improvements: - Improved Maven-plugin developer documentation. - ----------------------------------------------------------------------- - -Version 1.6.4, 01 June 2013 - -User-visible framework improvements: - StubGenerator now generates stubs that can be read by the StubParser. - -Maven plugin: - The Maven plugin no longer requires the Maven project's output directory to exist in order to run the Checker Framework. However, if you ask the Checker Framework to generate class files then the output directory will be created. - -Documentation: - Improved the Maven plugin instructions in the Checker Framework manual. - Improved the discussion of why to define both a bottom and a top qualifier in the Checker Framework manual. - Update FAQ to discuss that some other tools incorrectly interpret array declarations. - ----------------------------------------------------------------------- - -Version 1.6.3, 01 May 2013 - -Eclipse plugin bug fixes: - The javac argument files used by the Eclipse plugin now properly escape file paths. Windows users should no longer encounter errors about missing built-in checkers. - -Documentation: - Add FAQ "What is the meaning of an annotation after a type?" - ----------------------------------------------------------------------- - -Version 1.6.2, 04 Apr 2013 - -Eclipse plugin: - The "Additional compiler parameters" text field has now been replaced by a list. Parameters in this list may be activated/deactivated via checkbox. - -Eclipse plugin bug fixes: - Classpaths and source files should now be correctly quoted when they contain spaces. - -Internal framework improvements: - Update pom files to use the same update-version code as the Checker Framework "web" ant task. Remove pom specific update-version code. - Update build ant tasks to avoid re-running targets when executing tests from the release script. - ----------------------------------------------------------------------- - -Version 1.6.1, 01 Mar 2013 - -User-visible framework improvements: - A number of error messages have been clarified. - Stub file now supports type annotations in front and after method type variable declarations. - You may now specify custom paths to javac.jar and jdk7.jar on the command line for non-standard installations. - -Internal framework improvements: - Add shouldBeApplied method to avoid unnecessary scans in DefaultApplier and avoid annotating void types. - Add createQualifierDefaults and createQualifierPolymorphism factory methods. - -Maven plugin: - Put Checker Framework jars at the beginning of classpath. - Added option to compile code in order to support checking for multi-module projects. - The plugin no longer copies the various Checker Framework maven artifacts to one location but instead takes advantage of the new custom path options for javac.jar and jdk7.jar. - The maven plugin no longer attempts to resolve jdk6.jar - -Eclipse plugin: - Put Checker Framework jars at the beginning of classpath. - All files selected from a single project can now be checked. The previous behavior only checked the entire project or one file depending on the type of the first file selected. - -Documentation: - Fixed broken links and incomplete URLs in the Checker Framework Manual. - Update FAQ to discuss that some other tools incorrectly interpret array declarations. - -Bug fixes - ----------------------------------------------------------------------- - -Version 1.6.0, 1 Feb 2013 - -User-visible framework improvements: - It is possible to use enum constants in stub files without requiring the fully qualified name, as was previously necessary. - Support build on a stock Java 8 OpenJDK. - -Adapt to underlying jsr308-langtools changes. - The most visible change is syntax for fully-qualified types, from @A java.lang.Object to java.lang.@A Object. - JDK 7 is now required. The Checker Framework does not build or run on JDK 6. - -Documentation: - A new tutorial is available at https://checkerframework.org/tutorial/ - ----------------------------------------------------------------------- - -Version 1.5.0, 14 Jan 2013 - -User-visible framework improvements: - To invoke the Checker Framework, call the main method of class - CheckerMain, which is a drop-in replacement for javac. This replaces - all previous techniques for invoking the Checker Framework. Users - should no longer provide any Checker Framework jars on the classpath or - bootclasspath. jsr308-all.jar has been removed. - The Checker Framework now works with both JDK 6 and JDK 7, without need - for user customization. The Checker Framework determines the - appropriate annotated JDK to use. - All jar files now reside in checker-framework/checkers/binary/. - -Maven plugin: - Individual pom files (and artifacts in the Maven repository) for all - Checker Framework jar files. - Avoid too-long command lines on Windows. - See the Maven section of the manual for more details. - -Eclipse plugin: - Avoid too-long command lines on Windows. - Other bug fixes and interface improvements. - -Other framework improvements: - New -Adetailedmsgtext command-line option, intended for use by IDE plugins. - ----------------------------------------------------------------------- - -Version 1.4.4, 1 Dec 2012 - -Internal framework improvements: - Add shutdown hook mechanism and use it for -AresourceStats resource - statistics flag. - Add -AstubWarnIfNotFound and -AstubDebug options to improve - warnings and debug information from the stub file parsing. - Ignore case when comparing error suppression keys. - Support the bottom type as subtype of any wildcard type. - -Tool Integration Changes - The Maven plugin id has been changed to reflect standard Maven - naming conventions. - Eclipse and Maven plugin version numbers will now - track the Checker Framework version numbers. - -Bug fixes. - ----------------------------------------------------------------------- - -Version 1.4.3, 1 Nov 2012 - -Clarify license: - The Checker Framework is licensed under the GPL2. More permissive - licenses apply to annotations, tool plugins (Maven, Eclipse), - external libraries included with the Checker Framework, and examples in - the Checker Framework Manual. - Replaced all third-party annotations by cleanroom implementations, to - avoid any potential problems or confusion with licensing. - -Aliased annotations: - Clarified that there is no need to rewrite your program. The Checker - Framework recognizes dozens of annotations used by other tools. - -Improved documentation of Units Checker and Gradle Integration. -Improved developer documentation of Eclipse and Maven plugins. - -Bug fixes. - ----------------------------------------------------------------------- - -Version 1.4.2, 16 Oct 2012 - -External tool support: - Eclipse plug-in now works properly, due to many fixes - -Regex Checker: - New CheckedPatternSyntaxException added to RegexUtil - -Support new foreign annotations: - org.eclipse.jdt.annotation.Nullable - org.eclipse.jdt.annotation.NonNull - -New FAQ: "What is a receiver?" - -Make annotations use 1-based numbering for formal parameters: - Previously, due to a bug the annotations used 0-based numbering. - This change means that you need to rewrite annotations in the following ways: - @KeyFor("#3") => @KeyFor("#4") - @AssertNonNullIfTrue("#0") => @AssertNonNullIfTrue("#1") - @AssertNonNullIfTrue({"#0", "#1"}) => @AssertNonNullIfTrue({"#1", "#2"}) - @AssertNonNullAfter("get(#2)") => @AssertNonNullAfter("get(#3)") - This command: - find . -type f -print | xargs perl -pi -e 's/("#)([0-9])(")/$1.($2+1).$3/eg' - handles the first two cases, which account for most uses. You would need - to handle any annotations like the last two cases in a different way, - such as by running - grep -r -n -E '\("[^"]+#[0-9][^A-Za-z]|\("#[0-9][^"]' . - and making manual changes to the matching lines. (It is possible to - provide a command that handles all cases, but it would be more likely to - make undesired changes.) - Whenever making automated changes, it is wise to save a copy of your - codebase, then compare it to the modified version so you can undo any - undesired changes. Also, avoid running the automated command over version - control files such as your .hg, .git, .svn, or CVS directory. - ----------------------------------------------------------------------- - -Version 1.4.1, 29 Sep 2012 - -User-visible framework improvements: - Support stub files contained in .jar files. - Support aliasing for declaration annotations. - Updated the Maven plugin. - -Code refactoring: - Make AnnotationUtils and AnnotatedTypes into stateless utility classes. - Instead, provide the necessary parameters for particular methods. - Make class AnnotationBuilder independent of AnnotationUtils. - Remove the ProcessingEnvironment from AnnotatedTypeMirror, which was - hardly used and can be replaced easily. - Used more consistent naming for a few more fields. - Moved AnnotatedTypes from package checkers.types to checkers.utils. - this required making a few methods in AnnotatedTypeFactory public, - which might require changes in downstream code. - -Internal framework improvements: - Fixed Issues #136, #139, #142, #156. - Bug fixes and documentation improvements. - ----------------------------------------------------------------------- - -Version 1.4.0, 11 Sep 2012 - -User-visible framework improvements: - Defaulting: - @DefaultQualifier annotations now use a Class instead of a String, - preventing simple typo errors. - @DefaultLocation extended with more constants. - TreeAnnotator propagates the least-upper-bound of the operands of - binary/compound operations, instead of taking the default qualifier. - Stub files now ignore the return type, allowing for files automatically - generated from other formats. - Type factories and type hierarchies: - Simplify AnnotatedTypeFactory constructors. - Add a GeneralAnnotatedTypeFactory that supports multiple type systems. - Improvements to QualifierHierarchy construction. - Type-checking improvements: - Propagate annotations from the sub-expression of a cast to its result. - Better handling of assignment context and improved inference of - array creation expressions. - Optional stricter checking of casts to array and generic types using - the new -Alint=cast:strict flag. - This will become the default in the future. - Code reorganization: - SourceChecker.initChecker no longer has a ProcessingEnvironment - parameter. The environment can now be accessed using the standard - processingEnv field (instead of the previous env field). - Classes com.sun.source.util.AbstractTypeProcessor and - checkers.util.AggregateChecker are now in package checkers.source. - Move isAssignable from the BaseTypeChecker to the BaseTypeVisitor; now - the Checker only consists of factories and logic is contained in the - Visitor. - Warning and error messages: - Issue a warning if an unsupported -Alint option is provided. - Improved error messages. - Maven plugin now works. - -Nullness Checker: - Only allow creation of (implicitly) non-null objects. - Optionally forbid creation of arrays with @NonNull component type, - when flag -Alint=arrays:forbidnonnullcomponents is supplied. - This will become the default in the future. - -Internal framework improvements: - Enable assertion checking. - Improve handling of annotated type variables. - Assignment context is now a type, not a tree. - Fix all compiler warnings. - ----------------------------------------------------------------------- - -Version 1.3.1, 21 Jul 2012 - -Installation: - Clarify installation instructions for Windows. Remove javac.bat, which - worked for running distributed checkers but not for creating new checkers. - -User-visible framework improvements: - Implement @PolyAll qualifier to vary over multiple type systems. - The Checker Framework is unsound due to Java's covariant array subtyping. - You can enable invariant array subtyping (for qualifiers only, not for - base Java types) with the command-line option -Alint=arrays:invariant. - This will become the default in the future. - -Internal framework improvements: - Improve defaulting for multiple qualifier hierarchies. - Big refactoring of how qualifier hierarchies are built up. - Improvements to error handling output for unexpected exceptions. - Bug fixes and documentation improvements. - ----------------------------------------------------------------------- - -Version 1.3.0, 3 Jul 2012 - -Annotation syntax changes, as mandated by the latest Type Annotations -(JSR 308) specification. The most important ones are: -- New receiver syntax, using "this" as a formal parameter name: - ReturnType methodname(@ReceiverAnnotation MyClass this, ...) { ... } -- Changed @Target default to be the Java 1.5 values -- UW extension: in addition to annotations in comments, support - special /*>>> */ comments to hide multiple tokens. - This is useful for the new receiver syntax and for import statements. - -Framework improvements: - Adapt to annotation storage changes in jsr308-langtools 1.3.0. - Move type validation methods from the BaseTypeChecker to BaseTypeVisitor. - ----------------------------------------------------------------------- - -Version 1.2.7, 14 May 2012 - -Regex Checker: - Add basic support for the concatenation of two non-regular expressions - that produce a valid regular expression. - Support "isRegex" in flow inference. - -Framework improvements: - New @StubFiles annotation declaratively adds stub files to a checker. - -Internal bug fixes: - Respect skipDefs and skipUses in NullnessFlow. - Support package annotations in stub files. - Better support for enums in annotation attributes. - Cleanups to how implicit receivers are determined. - ----------------------------------------------------------------------- - -Version 1.2.6, 18 Mar 2012 - -Nullness Checker: - Correctly handle unboxing in more contexts (if, switch (Issue 129), - while loops, ...) - -Regex Checker: - Add capturing groups parameter to Regex qualifier. - Count groups in String literals and String concatenation. - Verify group number to method calls that take a capturing group - number. - Update RegexUtil methods to take optional groups parameter. - Modify regex qualifier hierarchy to support groups parameter. - Add special case for Pattern.compile when called with Pattern.LITERAL flag. - -Internal bug fixes: - Improve flow's support of annotations with parameters. - Fix generics corner cases (Issues #131, #132, #133, #135). - Support type annotations in annotations and type-check annotations. - Improve reflective look-up of visitors and factories. - Small cleanups. - ----------------------------------------------------------------------- - -Version 1.2.5.1, 06 Feb 2012 - -Nullness Checker: - Correct the annotations on ThreadLocal and InheritableThreadLocal. - -Internal bug fixes: - Expand release tests. - Compile release with JDK 6 to work on both JDK 6 and JDK 7. - ----------------------------------------------------------------------- - -Version 1.2.5, 3 Feb 2012 - -Don't put classpath on the bootclasspath when invoking javac. This -prevents problems if, for example, android.jar is on the classpath. - -New -jsr308_imports ... and -Djsr308.imports=... command-line options, for -specifying implicit imports from the command line. This is needed by Maven. - -New -Aignorejdkastub option makes the checker not load the jdk.astub -file. Files from the "stubs" option are still loaded. - -Regex Checker: - Support concatenation of PolyRegex strings. - Improve examples of use of RegexUtil methods. - -Signature Checker: - Add new @ClassGetName annotation, for a 4th string representation of a - class that is used by the JDK. Add supporting annotations to make the - type hierarchy a complete lattice. - Add PolySignature annotation. - -Internal bug fixes: - Improve method type argument inference. - Handle type variables whose upper bound is a type variable. - Fix bug in least upper bound computation for anonymous classes. - Improve handling of annotations inherited from superclasses. - Fix design problem with Nullness Checker and primitive types. - Ensure that overriding methods respect pre- and postconditions. - Correctly resolve references to an enclosing this. - Improve handling of Java source that contains compilation errors. - ----------------------------------------------------------------------- - -Version 1.2.4, 15 Dec 2011 - -All checkers: -- @Target(TYPE_USE) meta-annotation is properly handled. - -Nullness Checker: -- Do not allow nullness annotations on primitive types. -- Improvements to rawness (initialization) checks. -- Special-case known keys for System.getProperty. -- The -Alint=uninitialized command-line option now defaults to off, and - applies only to initialization of primitive and @Nullable fields. It is - not possible to disable, from the command line, the check that all - @NonNull fields are initialized. Such warnings must be suppressed - explicitly, for example by using @SuppressWarnings. - -Regex Checker: -- Improved RegexUtil class. - -Manual: -- Add FAQ item "Is the Checker Framework an official part of Java?" -- Trim down README.txt; users should read the manual instead. -- Improvements throughout, especially to Nullness and Regex Checker sections. - -Implementation details: -- Add a new @InvisibleQualifier meta-annotation for type qualifiers. - Instead of special-casing @Unqualified in the AnnotatedTypeMirror it - now looks for this meta-annotation. This also allows type systems to - hide type qualifiers it doesn't want visible, which we now use in the - Nullness Checker to hide the @Primitive annotation. -- Nullness Checker: Introduce a new internal qualifier @Primitive that is - used for primitive types. -- Be stricter about qualifiers being present on all types. If you get - errors about missing qualifiers, check your defaulting rules. - This helped in fixing small bugs in corner cases of the type - hierarchy and type factory. -- Unify decoding type annotations from trees and elements. -- Improve handling of annotations on type variables and upper bounds. -- Support checkers that use multiple, disjoint qualifier hierarchies. -- Many bug fixes. - ----------------------------------------------------------------------- - -Version 1.2.3, 1 Nov 2011 - -Regex Checker: -- Add @PolyRegex polymorphic annotation -- Add more stub library annotations - -Implementation details: -- Do not use "null" for unqualified types. Explicitly use @Unqualified - and be strict about correct usage. If this causes trouble for you, - check your @ImplicitFor and @DefaultQualifierInHierarchy - meta-annotations and ensure correct defaulting in your - AnnotatedTypeFactory. - -Bug fixes: -- Correctly handle f-bounded polymorphism. AnnotatedTypeMirror now has - methods to query the "effective" annotations on a type, which - handles type variable and wildcard bounds correctly. Also, terminate - recursions by not doing lazy-initialization of bounds during defaulting. -- Many other small bug fixes and documentation updates. - ----------------------------------------------------------------------- - -Version 1.2.2, 1 Oct 2011 - -Be less restrictive about when to start type processing when errors -already exist. -Add -AskipDefs command-line option to not type-check some class -definitions. -Documentation improvements. - ----------------------------------------------------------------------- - -Version 1.2.1, 20 Sep 2011 - -Fix issues #109, #110, #111 and various other cleanups. -Improvements to the release process. -Documentation improvements. - ----------------------------------------------------------------------- - -Version 1.2.0.1, 4 Sep 2011 - -New version number to stay in sync with JSR 308 compiler bugfix. -No significant changes. - ----------------------------------------------------------------------- - -Version 1.2.0, 2 Sep 2011 - -Updated to JDK 8. Use -source 8 (the new default) for type annotations. -Documentation improvements -Bug fixes all over - -Nullness Checker: -- Correct the upper bounds of all Collection subtypes - ----------------------------------------------------------------------- - -Version 1.1.5, 22 Jul 2011 - -Units Checker: - Instead of conversion routines, provide unit constants, with which - to multiply unqualified values. This is easier to type and the - multiplication gets optimized away by the compiler. - -Fenum Checker: - Ensure that the switch statement expression is a supertype of all - the case expressions. - -Implementation details: - -- Parse declaration annotations in stub files - -- Output error messages instead of raising exceptions. This change - required us to introduce method "initChecker" in class - SourceChecker, which should be used instead of "init". This allows - us to handle the calls to initChecker within the framework. - Use method "errorAbort" to output an error message and abort - processing. - ----------------------------------------------------------------------- - -Version 1.1.4, 8 Jul 2011 - -Units Checker (new): - Ensures operations are performed on variables of correct units of - measurement (e.g., miles vs. kilometers vs. kilograms). - -Changed -AskipClasses command-line option to -AskipUses - -Implementation details: - -- Improve support for type qualifiers with enum attributes - ----------------------------------------------------------------------- - -Version 1.1.3, 17 Jun 2011 - -Interning: -- Add @UsesObjectEquals annotation - -Manual: -- Signature Checker is now documented -- Fenum Checker documentation improved -- Small improvements to other sections - -Implementation details: - -- Updates to the web-site build process - -- The BaseTypeVisitor used to provide the same two type parameters as - class SourceVisitor. However, all subtypes of BaseTypeVisitor were - instantiated as . We decided to directly instantiate the - SourceVisitor as and removed this complexity. - Instead, the BaseTypeVisitor is now parameterized by the subtype of - BaseTypeChecker that should be used. This gives a more concrete type - to field "checker" and is similar to BasicAnnotatedTypeFactory. - -- Added method AnnotatedTypeFactory.typeVariablesFromUse to allow - type-checkers to adapt the upper bounds of a type variable depending on - the type instantiation. - -- Method type argument inference: - Changed AnnotatedTypeFactory.methodFromUse to return a Pair consisting - of the method and the inferred or explicit method type arguments. - If you override this method, you will need to update your version. - See this change set for a simple example: - https://github.com/typetools/checker-framework/source/detail?r=8381a213a4 - -- Testing framework: - Support for multiple expected errors using the "// :: A :: B :: C" syntax. - -Many small updates and fixes. - ----------------------------------------------------------------------- - -Version 1.1.2, 12 Jan 2011 - -Fake Enum Checker (new): - A "fake enumeration" is a set of integers rather than a proper Java enum. - They are used in legacy code and for efficiency (e.g., in Android). The - Fake Enum Checker gives them the same safety guarantees as a proper Java - enum. - -Property File Checker (new): - Ensures that valid keys are used for property files and resource bundles. - Also includes a checker that code is properly internationalized and a - checker for compiler message keys as used in the Checker Framework. - -Signature Checker (new): - Ensures that different string representations of a Java type (e.g., - "pakkage.Outer.Inner" vs. "pakkage.Outer$Inner" vs. "Lpakkage/Outer$Inner;") - are not misused. - -Interning Checker enhancements: - Issues fewer false positives for code like "a==b || a.equals(b)" - -Foreign annotations: - The Checker Framework supports more non-Checker-Framework annotations. - This means that it can check already-annotated code without requiring you - to rewrite your annotations. - Add as an alias for checkers.interning.quals.Interned: - com.sun.istack.Interned - Add as aliases for checkers.nullness.quals.NonNull: - com.sun.istack.NotNull - org.netbeans.api.annotations.common.NonNull - Add as aliases for checkers.nullness.quals.Nullable: - com.sun.istack.Nullable - javax.validation.constraints.NotNull - org.netbeans.api.annotations.common.CheckForNull - org.netbeans.api.annotations.common.NullAllowed - org.netbeans.api.annotations.common.NullUnknown - -Manual improvements: - Improve installation instructions - Rewrite section on generics (thanks to Bert Fernandez and David Cok) - Also refactor the generics section into its own chapter - Rewrite section on @Unused and @Dependent - New manual section: Writing Java expressions as annotation arguments - Better explanation of warning suppression - JSR 308 is planned for Java 8, not Java 7 - -Stub files: - Support nested classes by expressing them at top level in binary form: A$B - Improved error reporting when parsing stub files - -Annotated JDK: - New way of generating annotated JDK - jdk.jar file no longer appears in repository - Warning if you are not using the annotated JDK. - -Miscellaneous: - Warn if -source command-line argument does not support type annotations - -Many bug fixes - There are too many to list, but some notable ones are to local type - inference, generics, pre- and post-conditions (e.g., @NonNullOnEntry, - @AssertNonNull*), and map keys (@KeyFor). In particular, preconditions - and map key annotations are now checked, and if they cannot be verified, - an error is raised; previously, they were not verified, just unsoundly - trusted. - ----------------------------------------------------------------------- - -Version 1.1.1, 18 Sep 2010 - -Eclipse support: - Removed the obsolete Eclipse plug-in from repository. The new one uses a - different repository - (http://code.google.com/a/eclipselabs.org/p/checker-plugin/) but a user - obtains it from the same URL as before: - https://checkerframework.org/eclipse/ - -Property Key Checker: - The property key checker allows multiple resource bundles and the - simultaneous use of both resource bundles and property files. - -Javari Checker: - Added Javari stub classes for more JDK classes. - -Distribution: - Changed directory structure (top level is "checker-framework"; "checkers" - is a under that) for consistency with version control repository. - -Many documentation improvements and minor bugfixes. - ----------------------------------------------------------------------- - -Version 1.1.0b, 16 Jun 2010 - -Fixed a bug related to running binary release in JDK 6 - ----------------------------------------------------------------------- - -Version 1.1.0, 13 Jun 2010 - -Checkers - Introduced a new simple mechanism for running a checker - Added one annotated JDK for all checkers - -Nullness Checker - Fixed bugs related to map.get() and KeyFor annotation - Fixed bugs related to AssertNonNull* and parameters - Minor updates to the annotated JDK, especially to java.io.File - -Manual - Updated installation instructions - Clarified section regarding fields and type inference - ----------------------------------------------------------------------- - -Version 1.0.9, 25 May 2010 - -Nullness Checker: - Improved Javadocs and manual documentation - Added two new annotations: AssertNonNullAfter, KeyFor - Fixed a bug related to AssertNonNullIfFalse and assert statements - Renamed NonNullVariable to NonNullOnEntry - -Checkers: - Interning: Skipping equality check, if either operands should be skipped - Fixed a bug related to annotations targeting array fields found in classfile - Fixed a bug related to method invocation generic type inference - in static methods - -Manual - Added a section on nullness method annotations - Revised the Nullness Checker section - Updated Ant usage instructions - ----------------------------------------------------------------------- - -Version 1.0.8, 15 May 2010 - -Checkers - Changed behavior of flow type refinement when annotation is explicit - Handle array initializer trees (without explicit type) - Handle the case of Vector.copyInto - Include javax classes in the distributed jdk jar files - -Interning Checker - Handle interning inference of string concatenation - Add 20+ @Interned annotations to the JDK - Add an option, checkclass, to validate the interning - of specific classes only - -Bug fixes - Fix a bug related to array implicit types - Lock Checker: Treat null as a bottom type - -Manual - Added a new section about Flow inference and fields - ----------------------------------------------------------------------- - -Version 1.0.7, 12 Apr 2010 - -Checkers - Distributed a Maven repository - Updated stub parser project to latest version (javaparser 1.0.8) - Fixed bugs related to iterable wildcards and type parameter types - ----------------------------------------------------------------------- - -Version 1.0.6, 24 Feb 2009 - -Nullness Checker - Added support for new annotations: - Pure - indicates that the method, given the same parameters, return the - same values - AssertNonNullIfFalse - indicates that a field is NonNull if the method - returns false - Renamed AssertNonNull to AssertParametersNonNull - Updated the annotated jdk - -Javari Checker - Fixed many bugs: - handle implicit dereferencing of this (e.g. `field` in place of - `this.field`) - apply default annotations to method parameters - ----------------------------------------------------------------------- - -Version 1.0.5, 12 Jan 2009 - -Checkers - Added support for annotated jdk jars - Improved readability of some failure messages - Added AssertNonNullIfTrue support for method parameter references - Fixed a bug related to LazyNonNull and array fields - Fixed a bug related to inference and compound assignments (e.g. +=) - nullness: permit the type of @NonNull Void - -Manual - Updated annotating-libraries chapter regarding annotated jdk - ----------------------------------------------------------------------- - -Version 1.0.4, 19 Dec 2009 - -Bug Fixes - wildcards not recognized as subtypes of type variables - e.g. '? extends A' and 'A' - PolyNull methods not accepting null literal value arguments - spurious unexpected Raw warnings - -Manual - Clarified FAQ item regarding why List's type parameter is - "extends @NonNull Object" - ----------------------------------------------------------------------- - -Version 1.0.3, 5 Dec 2009 - -Checkers - New location UPPER_BOUND for DefaultQualifier permits setting the default - for upper bounds, such as Object in "? extends Object". - @DefaultQualifier accepts simple names, like @DefaultQualifier("Nullable"), - rather than requiring @DefaultQualifier("checkers.nullness.quals.Nullable"). - Local variable type inference has improved support for array accesses. - The repository contains Eclipse project and launch configuration files. - This is helpful too people who want to build a checker, not to people - who merely want to run a checker. - Many bug fixes, including: - handling wildcard subtyping rules - stub files and vararg methods being ignored - nullness and spurious rawness errors - uses of array clone method (e.g. String[].clone()) - multibound type parameters (e.g. ) - -Manual - Documented the behavior of annotations on type parameter declarations. - New FAQ item: - How to collect warnings from multiple files - Why a qualifier shouldn't apply to both types and declarations - ----------------------------------------------------------------------- - -Version 1.0.2, 16 Nov 2009 - -Checkers - Renamed Regex Checker's @ValidRegex annotation to @Regex - Improved Collection.toArray() heuristics to be more sound - -Bug fixes - Fixed the annotated JDK to match OpenJDK 6 - - Added missing methods and corrected class hierarchy - Fixed a crash related to intersection types - ----------------------------------------------------------------------- - -Version 1.0.1, 1 Nov 2009 - -Checkers - Added new checkers: - RegEx checker to detect invalid regular expression use - Internationalization (I18n) checker to detect internationalization errors - -Functionality - Added more performance optimizations - nullness: Added support for netbeans nullness annotations - nullness: better semantics for redundant nullness tests - related to redundant tests in assertions - lock: Added support for JCIP annotation in the Lock Checker - tainting: Added support for polymorphism - Lock Checker supports the JCIP GuardedBy annotation - -Bug fixes - Fixed a crashing bug related to interaction between - generic types and wildcards - Fixed a bug in stub file parser related to vararg annotations - Fixed few bugs in skeleton file generators - -Manual - Tweak installation instructions - Reference Units Checker - Added new sections for new checkers - RegEx checker (S 10) - Internationalization Checker (S 11) - ----------------------------------------------------------------------- - -Version 1.0.0, 30 Sep 2009 - -Functionality - Added Linear Checker to restrict aliasing - -Bug fixes - Fixed flow erros related to loop controls and break/continue - -Manual - Adopt new term, "Declaration Annotation" instead of non-type annotations - Added new sections: - Linear Checker (S 9) - Inexpressible types (S 14.3) - How to get started annotating legacy code (S 2.4.4) - Expanded Tainting Checker section - ----------------------------------------------------------------------- - -Version 0.9.9, 4 Sep 2009 - -Functionality - Added more optional lint checks (cast:unsafe, all) - Nullness Checker supports @SuppressWarnings("nullness:generic.argument"), - for suppressing warnings related to misuse of generic type arguments. - This was already supported and documented, but had not been mentioned - in the changelog. - -Bug fixes - Fixed many bugs related to Stub files causing parser to ignore - bodiless constructors - annotated arrays annotations - type parameter and wildcard bounds annotations - -Manual - Rewrote 'javac implementation survival guide' (S 13.9) - Restructured 'Using a checker' (S 2) - Added 'Integration with external tools' (S 14) - Added new questions to the FAQ (S 15) - ----------------------------------------------------------------------- - -Version 0.9.8, 21 Aug 2009 - -Functionality - Added a Tainting Checker - Added support for conditional nonnull checking - Added optional check for redundant nullness tests - Updated stub parser to latest libraries - -Bug fixes - Fixed a bug related to int[] treated as Object when passed to vararg T... - Fixed a crash related to intersection types - Fixed a bug related to -AskipClasses not being honored - Fixed a bug related to flow - -Manual - Added new sections - 8 Tainting Checker - 3.2.3 Conditional nullness - ----------------------------------------------------------------------- - -Version 0.9.7, 12 Aug 2009 - -Functionality - Changed swNonNull to castNonNull - nullness: Improved flow to infer nullness based on method invocations - locking: Permitted @Holding to appear on constructors - -Bug fixes - Fixed a bug related to typevar and wildcard extends clauses - ----------------------------------------------------------------------- - -Version 0.9.6, 29 Jul 2009 - -Functionality - Changed 'jsr308.skipClasses' property with '-AskipClasses' option - Locking checker - - Add subtype checking for Holding - - Treat constructors as synchronized methods - -Bug fixes - Added some missing nullness annotations in the jdk - Fixed some bugs related to reading stub files - -Manual - Added a new section - 2.10 Tips about writing annotations - Updated sections of - 2.6 Unused fields and dependent types - 3.1.1 Rawness annotation hierarchy - ----------------------------------------------------------------------- - -Version 0.9.5, 13 Jul 2009 - -Functionality - Added support for Findbugs, JSR305, and IntelliJ nullness annotations - Added an Aggregate Checker base-class - Added support for a form of field access control - -Bug fixes - Added check for arguments in super() calls in constructors - -Manual - Added new sections: - Fields access control - Other tools for nullness checking - Bundling multiple checkers - ----------------------------------------------------------------------- - -Version 0.9.4, 30 Jun 2009 - -Functionality - Added Lock Checker - -Bug fixes - Handle more patterns for determining Map.get() return type - -Manual Documentations - Improved installation instructions - Added the following sections - 2.6 Dependent types - 3.1 subsection for LazyNonNull - 10.9 When to use (and not to use) type qualifiers - ----------------------------------------------------------------------- - -Version 0.9.3, 23 Jun 2009 - -Functionality - Added support DefaultQualifier on packages - Added support for Dependent qualifier types - see checkers.quals.Dependent - Added an option to treat checker errors as warnings - Improved flow handling of boolean logic - -Manual Documentations - Improved installation instructions - Improved discussion of effective and implicit qualifiers and defaults - Added a discussion about the need for bottom qualifiers - Added sections for how-to - . suppress Basic Checker warnings - . troubleshoot skeleton files - ----------------------------------------------------------------------- - -Version 0.9.2, 2 Jun 2009 - -Functionality - Added pre-liminary support for lazy initialization in nullness - see LazyNonNull - -Bug fixes - Corrected method declarations in JDK skeleton files - - bug resulted in a runtime error - -Documentations - Updated qualifier javadoc documentations - Corrected a reference on passing qualifiers to javac - ----------------------------------------------------------------------- - -Version 0.9.1, 19 May 2009 - -Bug fixes - Eliminated unexpected compiler errors when using checkers - Fixed bug related to reading annotations in skeleton files - -API Changes - Renamed SourceChecker.process() to .typeProcess() - -Manual - Updated troubleshooting info - info for annotations in skeleton files - ----------------------------------------------------------------------- - -Version 0.9b, 22 Apr 2009 - -No visible changes - ----------------------------------------------------------------------- - -Version 0.9, 16 Apr 2009 - -Framework - More space and performance optimizations - Handle raw type with multiple type var level - e.g. class Pair { ... } - -Manual - Improve installation instructions - Update references to command line arguments - ----------------------------------------------------------------------- - -Version 0.8.9, 28 Mar 2009 - -Framework - Introduce Space (and minor performance) optimizations - Type-check constructor invocation receiver type - Fixed bug related to try-catch flow sensitivity analysis - Fixed bugs when type-checking annotations and enums - - bug results in null-pointer exception - ----------------------------------------------------------------------- - -Version 0.8.8, 13 Mar 2009 - -Nullness Checker - Support for custom nullness assertion via @AssertNonNull - Support for meta-annotation AssertNonNull - Support for Collection.toArray() method - Infer the nullness of the returned type - Corrected some JDK Collection API annotations - -Framework - Fixed bugs related to assignments expressions in Flow - Fixed bugs related to enum and annotation type hierarchy - Fixed bugs related to default annotations on wildcard bounds - ----------------------------------------------------------------------- - -Version 0.8.7, 27 Feb 2009 - -Framework - Support annotations on type parameters - Fixed bugs related to polymorphic types/annotations - Fixed bugs related to stub fixes - -Manual - Specify annotation defaults settings for IGJ - Update Known Problems section ----------------------------------------------------------------------- - -Version 0.8.6, 3 Feb 2009 - -Framework - Fixed bugs related to flow sensitivity analysis related to - . for loop and do while loops - . multiple iterations of a loop - . complement of logical conditions - Declarative syntax for string literal type introduction rules - Support for specifying stub file directories - ----------------------------------------------------------------------- - -Version 0.8.5, 17 Jan 2009 - -Framework - Fixed bugs related to flow sensitivity analysis - Fixed bugs related to annotations on type parameters - ----------------------------------------------------------------------- - -Version 0.8.4, 17 Dec 2008 - -Distribution - Included checkers-quals.jar which contains the qualifiers only - -Framework - Fixed bugs related to inner classes - Fixed a bug related to resolving polymorphic qualifiers - within static methods - -Manual - Added 'Distributing your annotated project' - ----------------------------------------------------------------------- - -Version 0.8.3, 7 Dec 2008 - -Framework - Fixed bugs related to inner classes - Changed cast semantics - Unqualified casts don't change cast away (or in) any qualifiers - Refactored AnnotationBuilder to ease building annotations - Added support for Object += String new behavior - Added a type validation check for method return types - -Nullness - Added inference of field initialization - Suppress false warnings due to method invocations within constructors - -IGJ - Added proper support for AssignsFields and inner classes interactions - -Manual - Updated 'Known Problems' section - ----------------------------------------------------------------------- - -Version 0.8.2, 14 Nov 2008 - -Framework - Included a binary distribution in the releases - Added support for annotations on type parameters - Fixed bugs related to casts - -Nullness - Improved error messages readability - Added partial support for Map.get() detection - -Manual - Improved installation instructions - ----------------------------------------------------------------------- - -Version 0.8.1, 1 Nov 2008 - -Framework - Added support for array initializers - Fixed many bugs related to generics and generic type inference - -Documentations - Added 'Getting Started' guide - ----------------------------------------------------------------------- - -Version 0.8, 27 Sep 2008 - -Framework - Added support for newly specified array syntax - Refactored code for annotating supertypes - Fixed AnnotationBuilder AnnotationMirror string representation - Fixed AnnotatedTypeMirror hashCode - -Manual - Reorganized 'Annotating Libraries' section - ----------------------------------------------------------------------- - -Version 0.7.9, 19 Sep 2008 - -Framework - Added support for stub files/classes - Fixed bugs related to anonymous classes - Fixed bugs related to qualifier polymorphism - -Manual - Updated 'Annotating Libraries' section to describe stub files - -Tests - Added support for Windows - Fixed a bug causing IGJ tests to fail on Windows - ----------------------------------------------------------------------- - -Version 0.7.8, 12 Sep 2008 - -Framework - Improved support for anonymous classes - Included refactorings to ease extensibility - Fixed some minor bugs - -Nullness - Fix some errors in annotated JDK - ----------------------------------------------------------------------- - -Version 0.7.7, 29 Aug 2008 - -Framework - Fixed bugs related to polymorphic qualifiers - Fixed bugs related to elements array convention - Add implicit type arguments to raw types - -Interning - Suppress cast warnings for interned classes - -Manual - Removed discussion of non-standard array syntax alternatives - ----------------------------------------------------------------------- - -Version 0.7.6, 12 Aug 2008 - -Framework - Changed default array syntax to ARRAYS-PRE, per the JSR 308 specification - Added an optional check for qualifier unsafe casts - Added support for running multiple checkers at once - Fixed bugs related array syntax - Fixed bugs related to accessing outer classes with-in inner classes - -Manual - Added a new subsection about Checker Auto-Discovery - 2.2.1 Checker Auto-discovery - ----------------------------------------------------------------------- - -Version 0.7.5, 2 Aug 2008 - -Framework - Added support for ARRAYS-PRE and ELTS-PRE array syntax - Added a check for unsafe casts - Some improvements to the AnnotationBuilder API - -Nullness Checker - Added a check for synchronized objects - Added a check for (un)boxing conversions - -Javari Checker - Fixed some JDK annotated classes - ----------------------------------------------------------------------- - -Version 0.7.4, 11 July 2008 - -Framework - Added support for annotations found in classfiles - Added support for the ARRAY-IN array syntax - Added AnnotationBuilder, to create AnotationMirrors with values - Improved the readability of recursive types string representation - -Nullness Checker - Added a check for thrown Throwable nullability - -IGJ Checker - Treat enums as mutable by default, like regular classes - -Manual - Added a new subsection about array syntax proposals: - 2.1.2 Annotating Arrays - ----------------------------------------------------------------------- - -Version 0.7.3, 4 July 2008 - -Javari Checker - Converted JDK files into stubs - -Nullness Checker - Fixed java.lang.Number declaration in the annotated jdk - -Framework - Fixed a bug causing crashes related to primitive type boxing - Renamed DAGQualifierHierarchy to GraphQualifierHierarchy - ----------------------------------------------------------------------- - -Version 0.7.2, 26 June 2008 - -IGJ Checker - Supports flow-sensitive type refinement - -Framework - Renamed Default annotation to DefaultQualifier - Added DefaultQualifiers annotation - Fixed bugs related to flow-sensitive type refinement - Fixed an error in the build script in Windows - -Manual - Added a new section - 9.2 javac implementation survival guide - Added hyperlinks to Javadocs of the referenced classes - ----------------------------------------------------------------------- - -Version 0.7.1, 20 June 2008 - -Nullness Checker - Made NNEL the default qualifier scheme - -Basic Checker - Moved to its own checkers.basic package - -Framework - Enhanced type-checking within qualifier-polymorphic method bodies - Fixed a bug causing StackOverflowError when type-checking wildcards - Fixed a bug causing a NullPointerException when type-checking - compound assignments, in the form of += - -Class Skeleton Generator - Distributed in compiled form (no more special installation instructions) - Added required asmx.jar library to lib/ - -Manual - Added new sections - 2.2.1 Ant tasks - 2.2.2 Eclipse plugin - 2.6 The effective qualifier on a type - Rewrote section 8 on annotating libraries - Added reference to the new Eclipse plug-in - Deleted installation instructions - -Javari Checker - Fixed bugs causing a NullPointerException when type-checking - primitive arrays - -IGJ Checker - Fixed bugs related to uses of raw types - -API Changes - Moved AnnotationFactory functionality to AnnotationUtils - Removed .root and .inConflict from DAGQualifierHierarchy - ----------------------------------------------------------------------- - -Version 0.7, 14 June 2008 - -Installation - New, very simple installation instructions for Linux. For other - operating systems, you should continue to use the old instructions. - -Nullness Checker - Renamed from "NonNull Checker" to "Nullness Checker". - Renamed package from checkers.nonnull to checkers.nullness. - The annotation names remain the same. - Added PolyNull, a polymorphic type qualifier for nullness. - -Interning Checker - Renamed from "Interned Checker" to "Interning Checker". - Renamed package from checkers.interned to checkers.interning. - The annotation names remain the same. - Added PolyInterned, a polymorphic type qualifier for Interning. - Added support for @Default annotation. - -Framework - Qualifiers - @PolymorphicQualifier was not previously documented in the manual. - Moved meta-qualifiers from checkers.metaquals package to checkers.quals. - Removed @VariableQualifier and @RootQualifier meta-qualifiers. - Added BasicAnnotatedTypeFactory, a factory that handles implicitFor, - defaults, flow-sensitive type inference. - Deprecated GraphQualifierHierarchy; DAGQualifierHierarchy replaces it. - Renamed methods in QualifierHierarchy. - -Manual - Rewrote several manual sections, most notably: - 2.1.1 Writing annotations in comments for backward compatibility - (note new -Xspacesincomments argument to javac) - 2.3 Checking partially-annotated programs: handling unannotated code - 2.6 Default qualifier for unannotated types - 2.7 Implicitly refined types (flow-sensitive type qualifier inference) - 8 Annotating libraries - 9 How to create a new checker plugin - Javadoc for the Checker Framework is included in its distribution and is - available online at https://checkerframework.org/api/ . - ----------------------------------------------------------------------- - -Version 0.6.4, 9 June 2008 - -All Framework - Updated the distributed JDK and examples to the new location of qualifiers - -Javari Checker - Improved documentation on polymorphism resolution - Removed redundant code now added to the framework from JavariVisitor, - JavariChecker and JavariAnnotatedTypeFactory - Refactored method polymorphism into JavariAnnotatedTypeFactory - Fixed bug on obtaining type from NewClassTree, annotations at constructor - invocation are not ignored now - Refactored polymorphism resolution, now all annotations on parameters and - receivers are replaced, not only on the return type - Refactored and renamed internal annotator classes in - JavariAnnotatedTypeFactory - Added more constructor tests - Moved Javari annotations to checkers.javari.quals package - ----------------------------------------------------------------------- - -Version 0.6.3, 6 June 2008 - -Checker Framework - Improved documentation and manual - Treat qualifiers on extends clauses of type variables and wildcard types as - if present on type variable itself - Renamed AnnotationRelations to QualifierHierarchy - Renamed GraphAnnotationRelations to GraphQualifierHierarchy - Renamed TypeRelations to TypeHierarchy - Added flow as a supported lint option for all checkers - Determined the suppress warning key reflectively - -Interned Checker - Moved @Interned annotation to checkers.interned.quals package - -NonNull Checker - Moved nonnull annotations to checkers.nonnull.quals package - -Miscellaneous - Included Javadocs in the release - Improved documentation for all checkers - ----------------------------------------------------------------------- - -Version 0.6.2, 30 May 2008 - -Checker Framework API - Added support for @Default annotation via TreeAnnotator - Added support for PolymorphicQualifier meta-annotation - Disallow the use of @SupportedAnnotationTypes on checkers - Fixed bugs related to wildcards with super clauses - Improved flow-sensitive analysis for fields - -Javari Checker - Moved Javari qualifiers from checkers.quals to checkers.javari.quals - Fixed bugs causing null pointer exceptions - -NonNull Checker - Fixed bugs related to nonnull flow - Added new tests to test suite - -Basic Checker - Renamed Custom Checker to Basic Checker - ----------------------------------------------------------------------- - -Version 0.6.1, 26 Apr 2008 - -Checker Framework API - Added support for @ImplicitFor meta-annotations via the new TypeAnnotator - and TreeAnnotator classes - Improved documentation and specifications - Fixed a bug related to getting supertypes of wildcards - Fixed a crash on class literals of primitive and array types - Framework ignores annotations that are not part of a type system - Fixed several minor bugs in the flow-sensitive inference implementation. - -IGJ Checker - Updated the checker to use AnnotationRelations and TypeRelations - -Javari Checker - Changing RoMaybe annotation to PolyRead - Updated checker to use AnnotationRelations and TypeRelations - Updated the JDK - Fixed bugs related to QReadOnly and type argument subtyping - Fixed bugs related to this-mutable fields in methods with @ReadOnly receiver - Fixed bugs related to primitive type casts - Added new tests to test suit - -NonNull Checker - Updated the annotated JDK - Fixed bugs in which default annotations were not correctly applied - Added @Raw types to handle partial object initialization. - Fixed several minor bugs in the checker implementation. - -Custom Checker - Updated checker to use hierarchy meta-annotations, via -Aquals argument - ----------------------------------------------------------------------- - -Version 0.6, 11 Apr 2008 - -Checker Framework API - Introduced AnnotationRelations and TypeRelations, more robust classes to - represent type and annotation hierarchies, and deprecated - SimpleSubtypeRelation - Add support for meta-annotations to declare type qualifiers subtype relations - Re-factored AnnotatedTypes and AnnotatedTypeFactory - Added a default implementation of SourceChecker.getSuppressWarningsKey() - that reads the @SuppressWarningsKey class annotation - Improved support for multidimensional arrays and new array expressions - Fixed a bug in which implicit annotations were not being applied to - parenthesized expressions - Framework ignores annotations on a type that do not have @TypeQualifier - Moved error/warning messages into "messages.properties" files in each - checker package - Fixed a bug in which annotations were inferred to liberally by - checkers.flow.Flow - -Interned Checker - Added heuristics that suppress warnings for certain comparisons (namely in - methods that override Comparator.compareTo and Object.equals) - The Interned checker uses flow-sensitive inference by default - -IGJ Checker - Fixed bugs related to resolving immutability variable in method invocation - Fixed a bug related to reassignability of fields - Add more tests - -Javari Checker - Added placeholder annotation for ThisMutable mutability - Re-factored JavariAnnotatedTypeFactory - Fixed self-type resolution for method receivers for readonly classes - Fixed annotations on parameters of readonly methods - Fixed type validation for arrays of primitives - Added more tests - Renamed @RoMaybe annotation to @PolyRead - -NonNull Checker - Removed deprecated checkers.nonnull.flow package - Fixed a bug in which default annotations were not applied correctly - -Miscellaneous - Improved Javadocs - Added FactoryTestChecker, a more modular tester for the annotated type - factory - Simplify error output for some types by stripping package names - ----------------------------------------------------------------------- - -Version 0.5.1, 21 Mar 2008 - -Checker Framework API - Added support for conditional expression - Added checks for type validity and assignability - Added support for per-checker customization of asMemberOf - Added support for type parameters in method invocation, - including type inference - Enhanced performance of AnnotatedTypeFactory - Checkers run only when no errors are found by Javac - Fixed bugs related AnnotationUtils.deepCopy() - Fixed support for annotated class type parameters - Fixed some support for annotated type variable bounds - Added enhancements to flow-sensitive qualifier inference - Added checks for type parameter bounds - -Interned Checker - Fixed some failing test cases - Fixed a bug related to autoboxing/unboxing - Added experimental flow-sensitive qualifier inference (use - "-Alint=flow" to enable) - Improved subtype testing, removing some spurious errors - -IGJ Checker - Deleted IGJVisitor! - Fixed some bugs related to immutability type variable resolution - -Javari Checker - Removed redundant methods from JavariVisitor in the new framework - Added support to constructor receivers - Added support to parenthesized expressions - Fixed a bug related to resolving RoMaybe constructors - Fixed a bug related to parsing conditional expressions - Added parsing of parenthesized expressions - Replaced checkers.javari.VisitorState with - checkers.types.VisitorState, present in BaseTypeVisitor - Modified JavariVisitor type parameters (it now extends - BaseTypeVisitor, not BaseTypeVisitor) - Modified JavariAnnotatedTypeFactory TreePreAnnotator to mutate a - AnnotatedTypeMirror parameter instead of returning a - List, in accordance with other parts of the - framework design - Modified test output format - Added tests to test suite - -NonNull Checker - Fixed a bug related to errors produced on package declarations - Exception parameters are now treated as NonNull by default - Added better support for complex conditionals in NonNull-specific - flow-sensitive inference - Fixed some failing test cases - Improved subtype testing, removing some spurious errors - -Custom Checker - Added a new type-checker for type systems with no special semantics, for - which annotations can be provided via the command line - -Miscellaneous - Made corrections and added more links to Javadocs - A platform-independent binary version of the checkers and framework - (checkers.jar) is now included in this release - ----------------------------------------------------------------------- - -Version 0.5, 7 Mar 2008 - -Checker Framework API - Enhanced the supertype finder to take annotations on extends and - implements clauses of a class type - Fixed a bug related to checking an empty array initializer ("{}") - Fixed a bug related to missing type information when multiple - top-level classes are defined in a single file - Fixed infinite recursion when checking expressions like "Enum>" - Fixed a crash in checkers.flow.Flow related to multiple top-level - classes in a single file - Added better support for annotated wildcard type bounds - Added AnnotatedTypeFactory.annotateImplicit() methods to replace - overriding the getAnnotatedType() methods directly - Fixed a bug in which constructor arguments were not checked - -Interned Checker - Fixed a bug related to auto-unboxing of classes for primitives - Added checks for calling methods with an @Interned receiver - -IGJ Checker - Implemented the immutability inference for self-type (type of - 'this') properly - Enhanced the implicit annotations to make an un-annotated code - type-check - Fixed bugs related to invoking methods based on a method's receiver - annotations - -Javari Checker - Restored in this version, after porting to the new framework - -NonNull Checker - Fixed a bug in which primitive types were considered possibly null - Improvements to support for @Default annotations - -Miscellaneous - Improved error message display for all checkers - ----------------------------------------------------------------------- - -Version 0.4.1, 22 Feb 2008 - -Checker Framework API - Introduced AnnotatedTypeFactory.directSupertypes() which finds the - supertypes as annotated types, which can be used by the framework. - Introduced default error messages analogous to javac's error messages. - Fixed bugs related to handling array access and enhanced-for-loop type - testing. - Fixed several bugs that are due AnnotationMirror not overriding .equals() - and .hashCode(). - Improved Javadocs for various classes and methods. - Fixed several bugs that caused crashes in the checkers. - Fixed a bug where varargs annotations were not handled correctly. - -IGJ Checker - Restored in this version, after porting the checker to the new framework. - -NonNull Checker - Fixed a bug where static field accesses were not handled correctly. - Improved error messages for the NonNull checker. - Added the NNEL (NonNull Except Locals) annotation default. - -Interned Checker - Fixed a bug where annotations on type parameter bounds were not handled - correctly. - Improved error messages for the Interned checker. - ----------------------------------------------------------------------- - -Version 0.4, 11 Feb 2008 - -Checker Framework API - Added checkers.flow, an improved and generalized flow-sensitive type - qualifier inference, and removed redundant parts from - checkers.nonnull.flow. - Fixed a bug that prevented AnnotatedTypeMirror.removeAnnotation from working - correctly. - Fixed incorrect behavior in checkers.util.SimpleSubtypeRelation. - -NonNull Checker - Adopted the new checkers.flow.Flow type qualifier inference. - Clarifications and improvements to Javadocs. - ----------------------------------------------------------------------- - -Version 0.3.99, 20 Nov 2007 - -Checker Framework API - Deprecated AnnotatedClassType, AnnotatedMethodType, and AnnotationLocation - in favor of AnnotatedTypeMirror (a new representation of annotated types - based on the javax.lang.model.type hierarchy). - Added checkers.basetype, which provides simple assignment and - pseudo-assignment checking. - Deprecated checkers.subtype in favor of checkers.basetype. - Added options for debugging output from checkers: -Afilenames, -Ashowchecks - -Interned Checker - Adopted the new Checker Framework API. - Fixed a bug in which "new" expressions had an incorrect type. - -NonNull Checker - Adopted the new Checker Framework API. - -Javari Checker -IGJ Checker - Removed in this version, to be restored in a future version pending - completion of updates to these checkers with respect to the new framework - API. - ----------------------------------------------------------------------- - -Version 0.3, 1 Oct 2007 - -Miscellaneous Changes - Consolidated HTML documentation into a single user manual (see the "manual" - directory in the distribution). - -IGJ Checker - New features: - Added a test suite. - Added annotations (skeleton files) for parts of java.util and java.lang. - -NonNull Checker - New features: - @SuppressWarnings("nonnull") annotation suppresses checker warnings. - @Default annotation can make NonNull (not Nullable) the default. - Added annotations (skeleton classes) for parts of java.util and java.lang. - NonNull checker skips no classes by default (previously skipped JDK). - Improved error messages: checker reports expected and found types. - - Bug fixes: - Fixed a null-pointer exception when checking certain array accesses. - Improved checking for field dereferences. - -Interned Checker - New features: - @SuppressWarnings("interned") annotation suppresses checker warnings. - The checker warns when two @Interned objects are compared with .equals - - Bug fixes: - The checker honors @Interned annotations on method receivers. - java.lang.Class types are treated as @Interned. - -Checker Framework API - New features: - Added support for default annotations and warning suppression in checkers - ----------------------------------------------------------------------- - -Version 0.2.3, 30 Aug 2007 - -IGJ Checker - New features: - changed @W(int) annotation to @I(String) to improve readability - improved readability of error messages - added a test for validity of types (testing @Mutable String) - - Bug fixes: - fixed resolving of @I on fields on receiver type - fixed assignment checking assignment validity for enhanced for loop - added check for constructor invocation parameters - -Interned Checker - added the Interned checker, for verifying the absence of equality testing - errors; see "interned-checker.html" for more information - -Javari Checker - New features: - added skeleton classes for parts of java.util and java.lang with Javari - annotations - - Bug fixes: - fixed readonly inner class bug on Javari Checker - -NonNull Checker - New features: - flow-sensitive analysis for assignments from a known @NonNull type (e.g., - when the right-hand of an assignment is @NonNull, the left-hand is - considered @NonNull from the assignment to the next possible - reassignment) - flow-sensitive analysis within conditional checks - - Bug fixes: - fixed several sources of null-pointer errors in the NonNull checker - fixed a bug in the flow-sensitive analysis when a variable was used on - both sides of the "=" operator - -Checker Framework API - New features: - added the TypesUtils.toString() method for pretty-printing annotated types - added AnnotationUtils, a utility class for working with annotations and - their values - added SourceChecker.getDefaultSkipPattern(), so that checkers can - individually specify which classes to skip by default - added preliminary support for suppressing checker warnings via - the @SuppressWarnings annotation - - Bug fixes: - fixed handling of annotations of field values - InternalAnnotation now correctly uses defaults for annotation values - improved support for annotations on class type parameter bounds - fixed an assertion violation when compiling certain uses of arrays - ----------------------------------------------------------------------- - -Version 0.2.2, 16 Aug 2007 - - -Code Changes - -* checkers.igj - some bug fixes and improved documentation - -* checkers.javari - fixed standard return value to be @Mutable - fixed generic and array handling of @ReadOnly - fixed @RoMaybe resolution of receivers at method invocation - fixed parsing of parenthesized trees and conditional trees - added initial support for for-enhanced loop - fixed constructor behavior on @ReadOnly classes - added checks for annotations on primitive types inside arrays - -* checkers.nonnull - flow sensitive analysis supports System.exit, new class/array creation - -* checkers.subtype - fixes for method overriding and other generics-related bugs - -* checkers.types - added AnnotatedTypeMirror, a new representation for annotated types that - might be moved to the compiler in later version - added AnnotatedTypeScanner and AnnotatedTypeVisitor, visitors for types - AnnotatedTypeFactory uses GenericsUtils for improved handing of annotated - generic types - -* checkers.util - added AnnotatedTypes, a utility class for AnnotatedTypeMirror - added GenericsUtils, a utility class for working with generic types - -* tests - modified output to print only missing and unexpected diagnostics - added new test cases for the Javari Checker - - -Documentation Changes - -* checkers/igj-checker.html - improvements to page - -* checkers/javari-checker.html - examples now point to test suit files - -Miscellaneous Changes - -* checkers/build.xml - Ant script fails if it doesn't find the correct JSR 308 javac version - ----------------------------------------------------------------------- - -Version 0.2.1, 1 Aug 2007 - - -Code Changes - -* checkers.igj & checkers.igj.quals - added an initial implementation for the IGJ language - -* checkers.javari - added a state parameter to the visitor methods - added tests and restructured the test suite - restructured and implemented RoMaybe - modified return type to be mutable by default - fixed mutability type handling for type casts and field access - fixed bug, ensuring no primitives can be ReadOnly - a method receiver type is now based on the correct annotation - fixed parameter type-checking for overriden methods - fixed bug on readonly field initialization - added handling for unary trees - -* checkers.nonnull - added a tests for the flow-senstive analysis and varargs methods - improved flow-sensitive analysis: else statements, asserts, - return/throw statements, instanceof checks, complex conditionals with && - fixed a bug in the flow-sensitive analysis that incorrectly inferred - @NonNull for some elements - removed NonnullAnnotatedClassType, moving its functionality into - NonnullAnnotatedTypeFactory - -* checkers.source - SourceChecker.getSupportedAnnotationTypes() returns ["*"], overriding - AbstractProcessor.getSupportedAnnotationTypes(). This enables all - checkers to run on unannotated code - -* checkers.subtypes - fixed a bug pertaining to method parameter checks for overriding methods - fixed a bug that caused crashes when checking varargs methods - -* checkers.types - AnnotatedTypeFactory.getClass(Element) and getMethod(Element) use the - tree of the passed Element if one exists - AnnotatedClassType.includeAt, .execludeAt, .getAnnotationData were - added and are public - added constructor() and skipParens() methods to InternalUtils - renamed getTypeArgumentLocations() to getAnnotatedTypeArgumentLocations() - in AnnotatedClassType - added AnnotationData to represent annotations instead of Class instances; - primarily allows querying annotation arguments - added switch for whether or not to use includes/excludes in - AnnotatedClassType.hasAnnotationAt() - -* checkers.util - added utility classes - added skeleton class generator utility for annotating external libraries - - -Documentation Changes - -* checkers/nonnull-checker.html - added a note about JML - added a caveat about variable initialization - -* checkers/README-checkers.html - improvements to instructions - ----------------------------------------------------------------------- - -Version 0.2, 2 Jul 2007 - - -Code Changes - -* checkers.subtype - subtype checker warns for annotated and redundant typecasts - SubtypeVisitor checks for invalid return and parameter types in overriding - methods - added checks for compound assignments (like '+=') - -* checkers.source - SourceChecker honors the "checkers.skipClasses" property as a regex for - suppressing warnings from unannotated code (property is "java.*" by - default) - SourceVisitor extends TreePathScanner instead of - TreeScanner - -* checkers.types - AnnotatedClassType.isAnnotatedWith removed - AnnotatedClassType.getInnerLocations renamed to getTypeArgumentLocations - AnnotatedClassType.include now removes from the exclude list (and - vice-versa) - AnnotatedClassType.setElement and setTree methods are now public - -* checkers.nonnull - added a flow-sensitive analysis for inferring @NonNull in "if (var != - null)"-style checks - added checks for prefix and postfix increment and decrement operations - -* checkers.javari - added initial implementation of a type-checker for the Javari language - ----------------------------------------------------------------------- - -Version 0.1.1, 7 Jun 2007 - - -Documentation Changes - -* checkers/nonnull-checker.html - created "Tiny examples" subsection - created "Annotated library" subsection - noted where to read @NonNull-annotated source - moved instructions for unannotated code to README-checkers.html - various minor corrections and clarifications - -* checkers/README-checkers.html - added cross-references to other Checker Framework documents - removed redundant text - moved instructions for unannotated code from nonnull-checker.html - various minor corrections and clarifications - -* checkers/creating-a-checker.html - added note about getSupportedSourceVersion - removed line numbers from @Interned example - added section on SubtypeChecker/SubtypeVisitor - various minor corrections and clarifications - - -Code Changes - -* checkers.subtype - removed deprecated getCheckedAnnotation() mechanism - added missing package Javadocs - package Javadocs reference relevant HTML documentation - various improvements to Javadocs - SubtypeVisitor and SubtypeChecker are now abstract classes - updated with respect to preferred usages of - AnnotatedClassType.hasAnnotationAt and AnnotatedClassType.annotateAt - -* checkers.source - added missing package Javadocs - package Javadocs reference relevant HTML documentation - -* checkers.types - added missing package Javadocs - package Javadocs reference relevant HTML documentation - AnnotatedClassType.annotateAt now correctly handles - AnnotationLocation.RAW argument - AnnotatedClassType.annotate deprecated in favor of - AnnotatedClassType.annotateAt with AnnotationLocation.RAW as an argument - AnnotatedClassType.isAnnotatedWith deprecated in favor of - AnnotatedClassType.hasAnnotationAt with AnnotationLocation.RAW as an - argument - Added fromArray and fromList methods to AnnotationLocation and made - corresponding constructors private. - -* checkers.quals - added Javadocs and meta-annotations on annotation declarations where - missing - package Javadocs reference relevant HTML documentation - -* checkers.nonnull - various improvements to Javadocs - package Javadocs reference relevant HTML documentation - - -Miscellaneous Changes - - improved documentation of ch examples - Checker Framework build file now only attempts to compile .java files - - ----------------------------------------------------------------------- - -Version 0.1.0, 1 May 2007 - -Initial release. diff --git a/checker-qual-android/build.gradle b/checker-qual-android/build.gradle index 022874e4e35f..b2cf1541edc5 100644 --- a/checker-qual-android/build.gradle +++ b/checker-qual-android/build.gradle @@ -1,54 +1,69 @@ -evaluationDependsOn(":checker-qual") +evaluationDependsOn(':checker-qual') -task copySources(type: Copy, dependsOn: ':checker-qual:copySources') { +task copySources(type: Copy) { description 'Copy checker-qual source to checker-qual-android' - includeEmptyDirs = false doFirst { // Delete the directory in case a previously copied file should no longer be in checker-qual - delete file('src') + delete file(layout.buildDirectory.dir("generated/sources/main/java")) } - from files('../checker-qual/src/main') - include "**/*.java" - into file('src/main') + from files(project(':checker-qual').sourceSets.main.java) + include '**/*.java' + exclude '**/SignednessUtilExtra.java' + + // Types annotated with runtime annotations are always kept in the main dex by the default Android Gradle plugin. + // Using the standard Checker Framework annotations can lead to main dex overflows; + // users of the Checker Framework may find themselves unable to build their Android apps. + // By contrast, class-retention annotations are stripped out before packaging by all build systems as a convention. + filter { line -> line.replaceAll('RetentionPolicy.RUNTIME', 'RetentionPolicy.CLASS') } - // Not read only because "replaceAnnotations" tasks writes to the files. - fileMode(0666) - dirMode(0777) + into file(layout.buildDirectory.dir("generated/sources/main/java")) + + fileMode(0444) } -/** -* Types annotated with runtime annotations are always kept in the main dex by the default Android Gradle plugin. -* Using the standard Checker Framework annotations can lead to main dex overflows; -* users of the Checker framework may find themselves unable to build their Android apps. -* By contrast, class-retention annotations are stripped out before packaging by all build systems as a convention. -*/ -task replaceAnnotations { - doLast { - fileTree(dir: 'src', include: "**/*.java").each { - it.text = it.text.replaceAll("RetentionPolicy.RUNTIME", "RetentionPolicy.CLASS") +sourceSets { + main { + java { + srcDir(copySources) } } } -replaceAnnotations.dependsOn copySources -compileJava.dependsOn replaceAnnotations +apply from: rootProject.file('gradle-mvn-push.gradle') -clean { - delete file('src') +/** Adds information to the publication for uploading to Maven repositories. */ +final checkerQualAndroidPom(publication) { + sharedPublicationConfiguration(publication) + publication.from components.java + publication.pom { + name = 'Checker Qual Android' + description = 'checker-qual-android contains annotations (type qualifiers) that a programmer\n' + + 'writes to specify Java code for type-checking by the Checker Framework.\n' + + '\n' + + 'The checker-qual-android artifact is identical to the checker-qual\n' + + 'artifact, except that in checker-qual-android annotations have classfile\n' + + 'retention. The default Android Gradle plugin retains types annotated with\n' + + 'runtime annotations in the main dex, but strips out class-retention\n' + + 'annotations.\n' + licenses { + license { + name = 'The MIT License' + url = 'http://opensource.org/licenses/MIT' + distribution = 'repo' + } + } + } } -task deployArtifactsToLocalRepo(dependsOn: jar) { - description "Deploys ${jar.archiveFileName.get()} to the local Maven repository" - doLast { - mvnDeployToLocalRepo(project, "${pomFiles}/checker-qual-android-pom.xml") +publishing { + publications { + checkerQualAndroid(MavenPublication) { + checkerQualAndroidPom it + } } } -task deployArtifactsToSonatype { - description "Deploys ${jar.archiveFileName.get()} to the Sonatype repository" - dependsOn ('jar', 'sourcesJar', 'javadocJar') - doLast { - mvnSignAndDeployMultipleToSonatype(project, "${pomFiles}/checker-qual-android-pom.xml") - } +signing { + sign publishing.publications.checkerQualAndroid } diff --git a/checker-qual/build.gradle b/checker-qual/build.gradle index 121e0c1067c6..6bd990d9865a 100644 --- a/checker-qual/build.gradle +++ b/checker-qual/build.gradle @@ -1,67 +1,55 @@ buildscript { - repositories { - mavenCentral() - } dependencies { - // Create OSGI bundles - classpath "biz.aQute.bnd:biz.aQute.bnd.gradle:5.1.1" + if (JavaVersion.current() >= JavaVersion.VERSION_17) { + // bnd builder depends on Java 17. + // As EISOP releases are made on Java 21, this shouldn't impact releases. + classpath('biz.aQute.bnd:biz.aQute.bnd.gradle:7.0.0') + } } } -task copySources(type: Copy) { - description 'Copy checker-qual source from other projects.' - includeEmptyDirs = false - doFirst { - // Delete the directory in case a previously copied file should no longer be in checker-qual - delete file('src/main/java') - } - - from files('../checker/src/main/java', '../dataflow/src/main/java', '../framework/src/main/java') - include "**/FormatUtil.java" - include "**/NullnessUtil.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' - - // Make files read only. - fileMode(0444) - - into file('src/main/java') +plugins { + id 'java-library' } -apply plugin: 'biz.aQute.bnd.builder' +if (JavaVersion.current() >= JavaVersion.VERSION_17) { + apply plugin: 'biz.aQute.bnd.builder' +} jar { - dependsOn copySources manifest { attributes('Export-Package': '*') } } -compileJava { - dependsOn copySources -} - -clean { - delete file('src/') +apply from: rootProject.file('gradle-mvn-push.gradle') + +/** Adds information to the publication for uploading to Maven repositories. */ +final checkerQualPom(publication) { + sharedPublicationConfiguration(publication) + publication.from components.java + publication.pom { + name = 'Checker Qual' + description = 'checker-qual contains annotations (type qualifiers) that a programmer\n' + + 'writes to specify Java code for type-checking by the Checker Framework.\n' + licenses { + license { + name = 'The MIT License' + url = 'http://opensource.org/licenses/MIT' + distribution = 'repo' + } + } + } } -task deployArtifactsToLocalRepo(dependsOn: jar) { - description "Deploys ${jar.archiveFileName.get()} to the local Maven repository" - doLast { - mvnDeployToLocalRepo(project, "${pomFiles}/checker-qual-pom.xml") +publishing { + publications { + checkerQual(MavenPublication) { + checkerQualPom it + } } } -task deployArtifactsToSonatype { - description "Deploys ${jar.archiveFileName.get()} to the Sonatype repository" - dependsOn ('jar', 'sourcesJar', 'javadocJar') - doLast { - mvnSignAndDeployMultipleToSonatype(project, "${pomFiles}/checker-qual-pom.xml") - } +signing { + sign publishing.publications.checkerQual } diff --git a/checker-qual/src/main/java/org/checkerframework/checker/builder/qual/CalledMethods.java b/checker-qual/src/main/java/org/checkerframework/checker/builder/qual/CalledMethods.java new file mode 100644 index 000000000000..9d27516f9de6 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/builder/qual/CalledMethods.java @@ -0,0 +1,27 @@ +package org.checkerframework.checker.builder.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; + +/** + * A deprecated variant of {@link org.checkerframework.checker.calledmethods.qual.CalledMethods}. + * + *

Lombok outputs this annotation. This annotation could be marked as deprecated, but that causes + * extra warnings when processing delombok'd code. + * + * @checker_framework.manual #called-methods-checker Called Methods Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface CalledMethods { + /** + * The names of methods that have definitely been called. + * + * @return the names of methods that have definetely been called + */ + String[] value(); +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/builder/qual/NotCalledMethods.java b/checker-qual/src/main/java/org/checkerframework/checker/builder/qual/NotCalledMethods.java new file mode 100644 index 000000000000..0e9bb18d7f63 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/builder/qual/NotCalledMethods.java @@ -0,0 +1,31 @@ +package org.checkerframework.checker.builder.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; + +/** + * An annotation speculatively used by Lombok's lombok.config checkerframework = true option. It has + * no meaning to the Called Methods Checker, which treats it as {@code @}{@link + * org.checkerframework.checker.calledmethods.qual.CalledMethods}{@code ()}. + * + *

A similar annotation might be supported in the future. + * + *

This annotation could be marked as deprecated, but that causes extra warnings when processing + * delombok'd code. + * + * @checker_framework.manual #called-methods-checker Called Methods Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface NotCalledMethods { + /** + * The names of the methods that have NOT been called. + * + * @return the names of the methods that have NOT been called + */ + String[] value(); +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/builder/qual/ReturnsReceiver.java b/checker-qual/src/main/java/org/checkerframework/checker/builder/qual/ReturnsReceiver.java new file mode 100644 index 000000000000..05eb7c218f22 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/builder/qual/ReturnsReceiver.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.builder.qual; + +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; + +/** + * A deprecated variant of {@code org.checkerframework.common.returnsreceiver.qual.This}. + * + *

Lombok outputs this annotation. It is retained only for backwards-compatibility with Lombok's + * {@code checkerframework = true} lombok.config flag. It should not be used in new code, because it + * is TRUSTED, NOT CHECKED. + * + *

This annotation could be marked as deprecated, but that causes extra warnings when processing + * delombok'd code. + * + * @checker_framework.manual #called-methods-checker Called Methods Checker + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Inherited +public @interface ReturnsReceiver {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/CalledMethods.java b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/CalledMethods.java new file mode 100644 index 000000000000..95377aa8894c --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/CalledMethods.java @@ -0,0 +1,34 @@ +package org.checkerframework.checker.calledmethods.qual; + +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * If an expression has type {@code @CalledMethods({"m1", "m2"})}, then methods {@code m1} and + * {@code m2} have definitely been called on its value. Other methods might or might not have been + * called. "Been called" is defined as having been invoked: a method has "been called" even if it + * might never return or might throw an exception. + * + *

The subtyping relationship is: + * + *

{@code @CalledMethods({"m1", "m2", "m3"}) <: @CalledMethods({"m1", "m2"})}
+ * + * @checker_framework.manual #called-methods-checker Called Methods Checker + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) +@DefaultQualifierInHierarchy +public @interface CalledMethods { + /** + * Methods that have definitely been called on the expression whose type is annotated. + * + * @return methods that have definitely been called + */ + public String[] value() default {}; +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/CalledMethodsBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/CalledMethodsBottom.java new file mode 100644 index 000000000000..ecc5d2546f2b --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/CalledMethodsBottom.java @@ -0,0 +1,23 @@ +package org.checkerframework.checker.calledmethods.qual; + +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The bottom type for the Called Methods type system. + * + *

It should rarely be written by a programmer. + * + * @checker_framework.manual #called-methods-checker Called Methods Checker + */ +@SubtypeOf({CalledMethods.class, CalledMethodsPredicate.class}) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) +public @interface CalledMethodsBottom {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/CalledMethodsPredicate.java b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/CalledMethodsPredicate.java new file mode 100644 index 000000000000..c0f5e8bf222a --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/CalledMethodsPredicate.java @@ -0,0 +1,32 @@ +package org.checkerframework.checker.calledmethods.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation represents a predicate on {@code @}{@link CalledMethods} annotations. If method + * {@code c()}'s receiver type is annotated with {@code @CalledMethodsPredicate("a || b")}, then it + * is acceptable to call either method {@code a()} or method {@code b()} before calling method + * {@code c()}. + * + * @checker_framework.manual #called-methods-checker Called Methods Checker + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({CalledMethods.class}) +public @interface CalledMethodsPredicate { + /** + * A boolean expression constructed from the following grammar: + * + *

S → method name | S && S | S || S | !S | (S) + * + *

The expression uses standard Java operator precedence: "!" then "&&" then "||". + * + * @return the boolean expression + */ + String value(); +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethods.java b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethods.java new file mode 100644 index 000000000000..d3728957fd00 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethods.java @@ -0,0 +1,87 @@ +package org.checkerframework.checker.calledmethods.qual; + +import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.PostconditionAnnotation; +import org.checkerframework.framework.qual.QualifierArgument; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the method, if it terminates successfully, always invokes the given methods on the + * given expressions. This annotation is repeatable, which means that users can write more than one + * instance of it on the same method (users should NOT manually write an + * {@code @EnsuresCalledMethods.List} annotation, which the checker will create from multiple copies + * of this annotation automatically). + * + *

Consider the following method: + * + *

+ * @EnsuresCalledMethods(value = "#1", methods = "m")
+ * public void callM(T t) { ... }
+ * 
+ * + *

This method guarantees that {@code t.m()} is always called before the method returns. + * + *

If a class has any {@code @}{@link org.checkerframework.checker.mustcall.qual.Owning Owning} + * fields, then one or more of its must-call methods should be annotated to indicate that the + * must-call obligations are satisfied. The must-call methods are those named by the {@code @}{@link + * org.checkerframework.checker.mustcall.qual.MustCall MustCall} or {@code @}{@link + * org.checkerframework.checker.mustcall.qual.InheritableMustCall InheritableMustCall} annotation on + * the class declaration, such as {@code close()}. Here is a common example: + * + *

+ * @EnsuresCalledMethods(value = {"owningField1", "owningField2"}, methods = "close")
+ * public void close() { ... }
+ * 
+ * + * @see EnsuresCalledMethodsIf + * @see EnsuresCalledMethodsOnException + * @checker_framework.manual #called-methods-checker Called Methods Checker + */ +@PostconditionAnnotation(qualifier = CalledMethods.class) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@Repeatable(EnsuresCalledMethods.List.class) +@InheritedAnnotation +public @interface EnsuresCalledMethods { + /** + * The Java expressions that will have methods called on them. + * + * @return the Java expressions that will have methods called on them + * @see org.checkerframework.framework.qual.EnsuresQualifier + */ + // Postconditions must use "value" as the name (conditional postconditions use "expression"). + String[] value(); + + /** + * The methods guaranteed to be invoked on the expressions. + * + * @return the methods guaranteed to be invoked on the expressions + */ + @QualifierArgument("value") + String[] methods(); + + /** + * A wrapper annotation that makes the {@link EnsuresCalledMethods} annotation repeatable. This + * annotation is an implementation detail: programmers generally do not need to write this. It + * is created automatically by Java when a programmer writes more than one {@link + * EnsuresCalledMethods} annotation at the same location. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) + @InheritedAnnotation + @PostconditionAnnotation(qualifier = CalledMethods.class) + public static @interface List { + /** + * Return the repeatable annotations. + * + * @return the repeatable annotations + */ + EnsuresCalledMethods[] value(); + } +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethodsIf.java b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethodsIf.java new file mode 100644 index 000000000000..621776705994 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethodsIf.java @@ -0,0 +1,74 @@ +package org.checkerframework.checker.calledmethods.qual; + +import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; +import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.QualifierArgument; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the method, if it terminates with the given result, invokes the given methods on + * the given expressions. + * + * @see EnsuresCalledMethods + * @see CalledMethods + * @checker_framework.manual #called-methods-checker Called Methods Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@ConditionalPostconditionAnnotation(qualifier = CalledMethods.class) +@InheritedAnnotation +@Repeatable(EnsuresCalledMethodsIf.List.class) +public @interface EnsuresCalledMethodsIf { + /** + * Returns the return value of the method under which the postcondition holds. + * + * @return the return value of the method under which the postcondition holds + */ + boolean result(); + + /** + * Returns Java expressions that have had the given methods called on them after the method + * returns {@link #result}. + * + * @return an array of Java expressions + * @checker_framework.manual #java-expressions-as-arguments Syntax of Java expressions + */ + String[] expression(); + + /** + * The methods guaranteed to be invoked on the expressions if the result of the method is {@link + * #result}. + * + * @return the methods guaranteed to be invoked on the expressions if the result of the method + * is {@link #result} + */ + @QualifierArgument("value") + String[] methods(); + + /** + * A wrapper annotation that makes the {@link EnsuresCalledMethodsIf} annotation repeatable. + * + *

Programmers generally do not need to write this. It is created by Java when a programmer + * writes more than one {@link EnsuresCalledMethodsIf} annotation at the same location. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) + @ConditionalPostconditionAnnotation(qualifier = CalledMethods.class) + @InheritedAnnotation + public static @interface List { + /** + * Return the repeatable annotations. + * + * @return the repeatable annotations + */ + EnsuresCalledMethodsIf[] value(); + } +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethodsOnException.java b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethodsOnException.java new file mode 100644 index 000000000000..5d2d7e6f26c8 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethodsOnException.java @@ -0,0 +1,98 @@ +package org.checkerframework.checker.calledmethods.qual; + +import org.checkerframework.framework.qual.InheritedAnnotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the method, if it terminates by throwing an {@link Exception}, always invokes the + * given methods on the given expressions. This annotation is repeatable, which means that users can + * write more than one instance of it on the same method (users should NOT manually write an + * {@code @EnsuresCalledMethodsOnException.List} annotation, which the checker will create from + * multiple copies of this annotation automatically). + * + *

Consider the following method: + * + *

+ * @EnsuresCalledMethodsOnException(value = "#1", methods = "m")
+ * public void callM(T t) { ... }
+ * 
+ * + *

The callM method promises to always call {@code t.m()} before throwing any kind + * of {@link Exception}. + * + *

Note that {@code EnsuresCalledMethodsOnException} only describes behavior for {@link + * Exception} (and by extension {@link RuntimeException}, {@link NullPointerException}, etc.) but + * not {@link Error} or other throwables. + * + * @see EnsuresCalledMethods + * @checker_framework.manual #called-methods-checker Called Methods Checker + */ +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@Repeatable(EnsuresCalledMethodsOnException.List.class) +@Retention(RetentionPolicy.RUNTIME) +@InheritedAnnotation +public @interface EnsuresCalledMethodsOnException { + + /** + * Returns Java expressions that have had the given methods called on them after the method + * throws an exception. + * + * @return an array of Java expressions + * @checker_framework.manual #java-expressions-as-arguments Syntax of Java expressions + */ + String[] value(); + + // NOTE 2023/10/6: There seems to be a fundamental limitation in the dataflow framework that + // prevent us from supporting a custom set of exceptions. Specifically, in the following code: + // + // try { + // m1(); + // } finally { + // m2(); + // } + // + // all exceptional edges out of the `m1()` call will flow to the same place: the start of the + // `m2()` call in the finally block. Any information about what `m1()` promised on specific + // exception types will be lost. + // + // /** + // * Returns the exception types under which the postcondition holds. + // * + // * @return the exception types under which the postcondition holds. + // */ + // Class[] exceptions(); + + /** + * The methods guaranteed to be invoked on the expressions if the result of the method throws an + * exception. + * + * @return the methods guaranteed to be invoked on the expressions if the method throws an + * exception + */ + String[] methods(); + + /** + * A wrapper annotation that makes the {@link EnsuresCalledMethodsOnException} annotation + * repeatable. This annotation is an implementation detail: programmers generally do not need to + * write this. It is created automatically by Java when a programmer writes more than one {@link + * EnsuresCalledMethodsOnException} annotation at the same location. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) + @InheritedAnnotation + public static @interface List { + /** + * Return the repeatable annotations. + * + * @return the repeatable annotations + */ + EnsuresCalledMethodsOnException[] value(); + } +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethodsVarArgs.java b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethodsVarArgs.java new file mode 100644 index 000000000000..553ebc5ebd48 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/EnsuresCalledMethodsVarArgs.java @@ -0,0 +1,31 @@ +package org.checkerframework.checker.calledmethods.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Indicates that the method, if it terminates successfully, always invokes the given methods on all + * of the arguments passed in the varargs position. + * + *

Consider the following method: + * + *

+ * @EnsuresCalledMethodsVarArgs("m")
+ * public void callMOnAll(S s, T t...) { ... }
+ * 
+ * + *

This method guarantees that {@code m()} is always called on every {@code T} object passed in + * the {@code t} varargs argument before the method returns. + * + *

This annotation is not checked. An error will always be issued when it is used. + */ +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +public @interface EnsuresCalledMethodsVarArgs { + + /** + * Returns the methods guaranteed to be invoked on the varargs parameters. + * + * @return the methods guaranteed to be invoked on the varargs parameters + */ + String[] value(); +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/RequiresCalledMethods.java b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/RequiresCalledMethods.java new file mode 100644 index 000000000000..3b645366bf63 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/calledmethods/qual/RequiresCalledMethods.java @@ -0,0 +1,64 @@ +package org.checkerframework.checker.calledmethods.qual; + +import org.checkerframework.framework.qual.PreconditionAnnotation; +import org.checkerframework.framework.qual.QualifierArgument; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates a method precondition: when the method is invoked, the specified expressions must have + * had the specified methods called on them. + * + *

Do not use this annotation for formal parameters; instead, give their type a {@code @}{@link + * CalledMethods} annotation. The {@code @RequiresCalledMethods} annotation is intended for other + * expressions, such as field accesses or method calls. + * + * @checker_framework.manual #called-methods-checker Called Methods Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@PreconditionAnnotation(qualifier = CalledMethods.class) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@Repeatable(RequiresCalledMethods.List.class) +public @interface RequiresCalledMethods { + /** + * The Java expressions that must have had methods called on them. + * + * @return the Java expressions that must have had methods called on them + * @see org.checkerframework.framework.qual.EnsuresQualifier + */ + // Preconditions must use "value" as the name (conditional preconditions use "expression"). + String[] value(); + + /** + * The methods guaranteed to be invoked on the expressions. + * + * @return the methods guaranteed to be invoked on the expressions + */ + @QualifierArgument("value") + String[] methods(); + + /** + * A wrapper annotation that makes the {@link RequiresCalledMethods} annotation repeatable. + * + *

Programmers generally do not need to write this. It is created by Java when a programmer + * writes more than one {@link RequiresCalledMethods} annotation at the same location. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @PreconditionAnnotation(qualifier = CalledMethods.class) + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) + public static @interface List { + /** + * Returns the repeatable annotations. + * + * @return the repeatable annotations + */ + RequiresCalledMethods[] value(); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/compilermsgs/qual/CompilerMessageKey.java b/checker-qual/src/main/java/org/checkerframework/checker/compilermsgs/qual/CompilerMessageKey.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/compilermsgs/qual/CompilerMessageKey.java rename to checker-qual/src/main/java/org/checkerframework/checker/compilermsgs/qual/CompilerMessageKey.java index cd9f177bf838..c842d05f1f64 100644 --- a/checker/src/main/java/org/checkerframework/checker/compilermsgs/qual/CompilerMessageKey.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/compilermsgs/qual/CompilerMessageKey.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.compilermsgs.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * A string that is definitely a compiler message key. diff --git a/checker/src/main/java/org/checkerframework/checker/compilermsgs/qual/CompilerMessageKeyBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/compilermsgs/qual/CompilerMessageKeyBottom.java similarity index 91% rename from checker/src/main/java/org/checkerframework/checker/compilermsgs/qual/CompilerMessageKeyBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/compilermsgs/qual/CompilerMessageKeyBottom.java index 911db2ca01b4..f730e685a021 100644 --- a/checker/src/main/java/org/checkerframework/checker/compilermsgs/qual/CompilerMessageKeyBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/compilermsgs/qual/CompilerMessageKeyBottom.java @@ -1,14 +1,15 @@ package org.checkerframework.checker.compilermsgs.qual; +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.DefaultFor; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the Compiler Message Key type system. Programmers should rarely write this @@ -21,6 +22,6 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf(CompilerMessageKey.class) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @DefaultFor(TypeUseLocation.LOWER_BOUND) public @interface CompilerMessageKeyBottom {} diff --git a/checker/src/main/java/org/checkerframework/checker/compilermsgs/qual/UnknownCompilerMessageKey.java b/checker-qual/src/main/java/org/checkerframework/checker/compilermsgs/qual/UnknownCompilerMessageKey.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/compilermsgs/qual/UnknownCompilerMessageKey.java rename to checker-qual/src/main/java/org/checkerframework/checker/compilermsgs/qual/UnknownCompilerMessageKey.java index 1e8916ee1065..8de3589889b6 100644 --- a/checker/src/main/java/org/checkerframework/checker/compilermsgs/qual/UnknownCompilerMessageKey.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/compilermsgs/qual/UnknownCompilerMessageKey.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.compilermsgs.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.InvisibleQualifier; -import org.checkerframework.framework.qual.SubtypeOf; /** * A {@code String} that might or might not be a compiler message key. diff --git a/checker/src/main/java/org/checkerframework/checker/compilermsgs/qual/package-info.java b/checker-qual/src/main/java/org/checkerframework/checker/compilermsgs/qual/package-info.java similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/compilermsgs/qual/package-info.java rename to checker-qual/src/main/java/org/checkerframework/checker/compilermsgs/qual/package-info.java diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/AwtAlphaCompositingRule.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/AwtAlphaCompositingRule.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/AwtAlphaCompositingRule.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/AwtAlphaCompositingRule.java index c6f28499e247..273f3678414c 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/AwtAlphaCompositingRule.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/AwtAlphaCompositingRule.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Basic alpha compositing rules for combining source and destination colors to achieve blending and diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/AwtColorSpace.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/AwtColorSpace.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/AwtColorSpace.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/AwtColorSpace.java index 0dbe2718055b..5488f8708924 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/AwtColorSpace.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/AwtColorSpace.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Color space tags to identify the specific color space of a Color object or, via a ColorModel diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/AwtCursorType.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/AwtCursorType.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/AwtCursorType.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/AwtCursorType.java index 7e78009279e2..7cfeb7681f5c 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/AwtCursorType.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/AwtCursorType.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * AwtCursorType. diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/AwtFlowLayout.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/AwtFlowLayout.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/AwtFlowLayout.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/AwtFlowLayout.java index a5bc81f1a5de..838cf66f6fc1 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/AwtFlowLayout.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/AwtFlowLayout.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Line alignments in a flow layout (see {@link java.awt.FlowLayout} for more details). diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/Fenum.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/Fenum.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/Fenum.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/Fenum.java index 6ad5cc97218d..25fc822b7577 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/Fenum.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/Fenum.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * A generic fake enumeration qualifier that is parameterized by a name. It is written in source diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/FenumBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/FenumBottom.java similarity index 91% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/FenumBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/FenumBottom.java index 5607bd081560..b48b127a6221 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/FenumBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/FenumBottom.java @@ -1,14 +1,15 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.DefaultFor; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the Fenum type system. Programmers should rarely write this type. @@ -21,7 +22,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) // Subtype relationships are set up by passing this class as a bottom // to the multigraph hierarchy constructor. @SubtypeOf({}) diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/FenumTop.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/FenumTop.java similarity index 83% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/FenumTop.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/FenumTop.java index 764b7f3e6324..8f0c43b5bff8 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/FenumTop.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/FenumTop.java @@ -1,14 +1,15 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.DefaultFor; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The top of the fake enumeration type hierarchy. @@ -18,7 +19,12 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({ + TypeUseLocation.LOWER_BOUND, + TypeUseLocation.UPPER_BOUND, + TypeUseLocation.LOCAL_VARIABLE, + TypeUseLocation.RESOURCE_VARIABLE +}) @SubtypeOf({}) @DefaultFor({TypeUseLocation.LOCAL_VARIABLE, TypeUseLocation.RESOURCE_VARIABLE}) public @interface FenumTop {} diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/FenumUnqualified.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/FenumUnqualified.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/FenumUnqualified.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/FenumUnqualified.java index 4eeaf9692145..d516e69de09b 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/FenumUnqualified.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/FenumUnqualified.java @@ -1,14 +1,15 @@ package org.checkerframework.checker.fenum.qual; -import java.lang.annotation.Documented; -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.DefaultQualifierInHierarchy; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.TypeUseLocation; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + /** * An unqualified type. Such a type is incomparable to (that is, neither a subtype nor a supertype * of) any fake enum type. diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/PolyFenum.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/PolyFenum.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/PolyFenum.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/PolyFenum.java index d203d9a861a8..47757d9b4869 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/PolyFenum.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/PolyFenum.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * A polymorphic qualifier for the fake enum type system. diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingBoxOrientation.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingBoxOrientation.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingBoxOrientation.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingBoxOrientation.java index 1c079d5d7fa8..03bbf87590a1 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingBoxOrientation.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingBoxOrientation.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * SwingBoxOrientation. diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingCompassDirection.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingCompassDirection.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingCompassDirection.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingCompassDirection.java index f89cd2dbd29b..1e4dc12e68a9 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingCompassDirection.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingCompassDirection.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * SwingCompassDirection. diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingElementOrientation.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingElementOrientation.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingElementOrientation.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingElementOrientation.java index 8935db1e0d2f..e76c992d3868 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingElementOrientation.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingElementOrientation.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * SwingElementOrientation. diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingHorizontalOrientation.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingHorizontalOrientation.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingHorizontalOrientation.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingHorizontalOrientation.java index 569f8604fc2f..1b50e42af0fe 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingHorizontalOrientation.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingHorizontalOrientation.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * SwingHorizontalOrientation. diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingSplitPaneOrientation.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingSplitPaneOrientation.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingSplitPaneOrientation.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingSplitPaneOrientation.java index d555bf175e3c..a35095755c30 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingSplitPaneOrientation.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingSplitPaneOrientation.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * SwingSplitPaneOrientation. diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingTextOrientation.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingTextOrientation.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingTextOrientation.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingTextOrientation.java index 84833e6103dd..eac953203d62 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingTextOrientation.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingTextOrientation.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * SwingTextOrientation. diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingTitleJustification.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingTitleJustification.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingTitleJustification.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingTitleJustification.java index ac51e16cc980..cf1cad138921 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingTitleJustification.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingTitleJustification.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Vertical orientations for the title text of a {@link javax.swing.border.TitledBorder}. diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingTitlePosition.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingTitlePosition.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingTitlePosition.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingTitlePosition.java index e3ca36f90b19..bfed072887e7 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingTitlePosition.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingTitlePosition.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Justifications for the title text of a {@link javax.swing.border.TitledBorder}. diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingVerticalOrientation.java b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingVerticalOrientation.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingVerticalOrientation.java rename to checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingVerticalOrientation.java index 7a70857ddc83..f1318741fcb0 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/qual/SwingVerticalOrientation.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/fenum/qual/SwingVerticalOrientation.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.fenum.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * SwingVerticalOrientation. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java new file mode 100644 index 000000000000..294cea2abdd3 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java @@ -0,0 +1,376 @@ +package org.checkerframework.checker.formatter.qual; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.framework.qual.AnnotatedFor; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; + +/** + * Elements of this enumeration are used in a {@link Format Format} annotation to indicate the valid + * types that may be passed as a format parameter. For example: + * + *

+ * + *
{@literal @}Format({GENERAL, INT}) String f = "String '%s' has length %d";
+ *
+ * String.format(f, "Example", 7);
+ * + *
+ * + * The annotation indicates that the format string requires any Object as the first parameter + * ({@link ConversionCategory#GENERAL}) and an integer as the second parameter ({@link + * ConversionCategory#INT}). + * + * @see Format + * @checker_framework.manual #formatter-checker Format String Checker + */ +@SuppressWarnings("unchecked") // ".class" expressions in varargs position +@AnnotatedFor("nullness") +public enum ConversionCategory { + /** Use if the parameter can be of any type. Applicable for conversions b, B, h, H, s, S. */ + GENERAL("bBhHsS", (Class[]) null /* everything */), + + /** + * Use if the parameter is of a basic types which represent Unicode characters: char, Character, + * byte, Byte, short, and Short. This conversion may also be applied to the types int and + * Integer when Character.isValidCodePoint(int) returns true. Applicable for conversions c, C. + */ + CHAR("cC", Character.class, Byte.class, Short.class, Integer.class), + + /** + * Use if the parameter is an integral type: byte, Byte, short, Short, int and Integer, long, + * Long, and BigInteger. Applicable for conversions d, o, x, X. + */ + INT("doxX", Byte.class, Short.class, Integer.class, Long.class, BigInteger.class), + + /** + * Use if the parameter is a floating-point type: float, Float, double, Double, and BigDecimal. + * Applicable for conversions e, E, f, g, G, a, A. + */ + FLOAT("eEfgGaA", Float.class, Double.class, BigDecimal.class), + + /** + * Use if the parameter is a type which is capable of encoding a date or time: long, Long, + * Calendar, and Date. Applicable for conversions t, T. + */ + @SuppressWarnings("JdkObsolete") + TIME("tT", Long.class, Calendar.class, Date.class), + + /** + * Use if the parameter is both a char and an int. + * + *

In a format string, multiple conversions may be applied to the same parameter. This is + * seldom needed, but the following is an example of such use: + * + *

+     *   format("Test %1$c %1$d", (int)42);
+     * 
+ * + * In this example, the first parameter is interpreted as both a character and an int, therefore + * the parameter must be compatible with both conversion, and can therefore neither be char nor + * long. This intersection of conversions is called CHAR_AND_INT. + * + *

One other conversion intersection is interesting, namely the intersection of INT and TIME, + * resulting in INT_AND_TIME. + * + *

All other intersection either lead to an already existing type, or NULL, in which case it + * is illegal to pass object's of any type as parameter. + */ + CHAR_AND_INT(null, Byte.class, Short.class, Integer.class), + + /** + * Use if the parameter is both an int and a time. + * + * @see #CHAR_AND_INT + */ + INT_AND_TIME(null, Long.class), + + /** + * Use if no object of any type can be passed as parameter. In this case, the only legal value + * is null. This is seldomly needed, and indicates an error in most cases. For example: + * + *

+     *   format("Test %1$f %1$d", null);
+     * 
+ * + * Only null can be legally passed, passing a value such as 4 or 4.2 would lead to an exception. + */ + NULL(null), + + /** + * Use if a parameter is not used by the formatter. This is seldomly needed, and indicates an + * error in most cases. For example: + * + *
+     *   format("Test %1$s %3$s", "a","unused","b");
+     * 
+ * + * Only the first "a" and third "b" parameters are used, the second "unused" parameter is + * ignored. + */ + UNUSED(null, (Class[]) null /* everything */); + + /** The argument types. Null means every type. */ + @SuppressWarnings("ImmutableEnumChecker") // TODO: clean this up! + public final Class @Nullable [] types; + + /** The format specifier characters. Null means users cannot specify it directly. */ + public final @Nullable String chars; + + /** + * Create a new conversion category. + * + * @param chars the format specifier characters. Null means users cannot specify it directly. + * @param types the argument types. Null means every type. + */ + ConversionCategory(@Nullable String chars, Class @Nullable ... types) { + this.chars = chars; + if (types == null) { + this.types = types; + } else { + List> typesWithPrimitives = new ArrayList<>(types.length); + for (Class type : types) { + typesWithPrimitives.add(type); + Class unwrapped = unwrapPrimitive(type); + if (unwrapped != null) { + typesWithPrimitives.add(unwrapped); + } + } + this.types = typesWithPrimitives.toArray(new Class[typesWithPrimitives.size()]); + } + } + + /** + * If the given class is a primitive wrapper, return the corresponding primitive class. + * Otherwise return null. + * + * @param c a class + * @return the unwrapped primitive, or null + */ + private static @Nullable Class unwrapPrimitive(Class c) { + if (c == Byte.class) { + return byte.class; + } + if (c == Character.class) { + return char.class; + } + if (c == Short.class) { + return short.class; + } + if (c == Integer.class) { + return int.class; + } + if (c == Long.class) { + return long.class; + } + if (c == Float.class) { + return float.class; + } + if (c == Double.class) { + return double.class; + } + if (c == Boolean.class) { + return boolean.class; + } + return null; + } + + /** + * The conversion categories that have a corresponding conversion character. This lacks UNUSED, + * TIME_AND_INT, etc. + */ + private static final ConversionCategory[] conversionCategoriesWithChar = + new ConversionCategory[] {GENERAL, CHAR, INT, FLOAT, TIME}; + + /** + * Converts a conversion character to a category. For example: + * + *
{@code
+     * 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 : conversionCategoriesWithChar) { + if (v.chars.contains(String.valueOf(c))) { + return v; + } + } + throw new IllegalArgumentException("Bad conversion character " + c); + } + + private static Set arrayToSet(E[] a) { + return new HashSet<>(Arrays.asList(a)); + } + + public static boolean isSubsetOf(ConversionCategory a, ConversionCategory b) { + return intersect(a, b) == a; + } + + /** Conversion categories that need to be considered by {@link #intersect}. */ + private static final ConversionCategory[] conversionCategoriesForIntersect = + new ConversionCategory[] {CHAR, INT, FLOAT, TIME, CHAR_AND_INT, INT_AND_TIME, NULL}; + + /** + * Returns the intersection of two categories. This is seldomly needed. + * + *
+ * + *
+     * ConversionCategory.intersect(INT, TIME) == INT_AND_TIME;
+     * 
+ * + *
+ * + * @param a a category + * @param b a category + * @return the intersection of the two categories (their greatest lower bound) + */ + public static ConversionCategory intersect(ConversionCategory a, ConversionCategory b) { + if (a == UNUSED) { + return b; + } + if (b == UNUSED) { + return a; + } + if (a == GENERAL) { + return b; + } + if (b == GENERAL) { + return a; + } + + @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 : conversionCategoriesForIntersect) { + @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; + } + } + throw new RuntimeException(); + } + + /** Conversion categories that need to be considered by {@link #union}. */ + private static final ConversionCategory[] conversionCategoriesForUnion = + new ConversionCategory[] {NULL, CHAR_AND_INT, INT_AND_TIME, CHAR, INT, FLOAT, TIME}; + + /** + * Returns the union of two categories. This is seldomly needed. + * + *
+ * + *
+     * ConversionCategory.union(INT, TIME) == GENERAL;
+     * 
+ * + *
+ * + * @param a a category + * @param b a category + * @return the union of the two categories (their least upper bound) + */ + public static ConversionCategory union(ConversionCategory a, ConversionCategory b) { + if (a == UNUSED || b == UNUSED) { + return UNUSED; + } + if (a == GENERAL || b == GENERAL) { + return GENERAL; + } + if ((a == CHAR_AND_INT && b == INT_AND_TIME) || (a == INT_AND_TIME && b == CHAR_AND_INT)) { + // This is special-cased because the union of a.types and b.types + // does not include BigInteger.class, whereas the types for INT does. + // Returning INT here to prevent returning GENERAL below. + return INT; + } + + @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 : conversionCategoriesForUnion) { + @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; + } + } + + return GENERAL; + } + + /** + * Returns true if {@code argType} can be an argument used by this format specifier. + * + * @param argType an argument type + * @return true if {@code argType} can be an argument used by this format specifier + */ + public boolean isAssignableFrom(Class argType) { + if (types == null) { + return true; + } + if (argType == void.class) { + return true; + } + for (Class c : types) { + if (c.isAssignableFrom(argType)) { + return true; + } + } + return false; + } + + /** Returns a pretty printed {@link ConversionCategory}. */ + @Pure + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(name()); + sb.append(" conversion category"); + + if (types == null || types.length == 0) { + return sb.toString(); + } + + StringJoiner sj = new StringJoiner(", ", "(one of: ", ")"); + for (Class cls : types) { + sj.add(cls.getSimpleName()); + } + sb.append(" "); + sb.append(sj); + + return sb.toString(); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/qual/Format.java b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/Format.java similarity index 88% rename from checker/src/main/java/org/checkerframework/checker/formatter/qual/Format.java rename to checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/Format.java index b513d84cfc38..d8f1d9a2b8a9 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/qual/Format.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/Format.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.formatter.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * This annotation, attached to a String type, indicates that the String may be passed to {@link @@ -17,8 +18,8 @@ *
* *
- * {@literal @}Format({ConversionCategory.GENERAL, ConversionCategory.INT})
- *  String f = "String '%s' has length %d";
+ * {@literal @}Format({GENERAL, INT}) String f = "String '%s' has length %d";
+ *
  *  String.format(f, "Example", 7);
  * 
* @@ -26,7 +27,8 @@ * * The annotation indicates that the format string requires any Object as the first parameter * ({@link ConversionCategory#GENERAL}) and an integer as the second parameter ({@link - * ConversionCategory#INT}). + * ConversionCategory#INT}). The format string accepts any values as additional parameters (because + * it ignores them). * * @see ConversionCategory * @checker_framework.manual #formatter-checker Format String Checker diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/qual/FormatBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/FormatBottom.java similarity index 91% rename from checker/src/main/java/org/checkerframework/checker/formatter/qual/FormatBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/FormatBottom.java index 9ce0ec040f52..9438cf76820d 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/qual/FormatBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/FormatBottom.java @@ -1,14 +1,15 @@ package org.checkerframework.checker.formatter.qual; +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.DefaultFor; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the Format String type system. Programmers should rarely write this type. @@ -19,7 +20,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @SubtypeOf({Format.class, InvalidFormat.class}) @DefaultFor(value = {TypeUseLocation.LOWER_BOUND}) public @interface FormatBottom {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/FormatMethod.java b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/FormatMethod.java new file mode 100644 index 000000000000..1a851fbf7842 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/FormatMethod.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.formatter.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; + +/** + * If this annotation is attached to a {@link java.util.Formatter#format(String, Object...) + * Formatter.format}-like method, then the first parameter of type String is treated as a format + * string for the following arguments. The Format String Checker ensures that the arguments passed + * as varargs are compatible with the format string argument, and also permits them to be passed to + * {@link java.util.Formatter#format(String, Object...) Formatter.format}-like methods within the + * body. + * + * @checker_framework.manual #formatter-checker Format String Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +public @interface FormatMethod {} diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/qual/InvalidFormat.java b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/InvalidFormat.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/formatter/qual/InvalidFormat.java rename to checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/InvalidFormat.java index cce2d1556272..22c2ace39925 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/qual/InvalidFormat.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/InvalidFormat.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.formatter.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * This annotation, attached to a {@link java.lang.String String} type, indicates that the string is diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/qual/ReturnsFormat.java b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ReturnsFormat.java similarity index 91% rename from checker/src/main/java/org/checkerframework/checker/formatter/qual/ReturnsFormat.java rename to checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ReturnsFormat.java index 2a8d37cba7fb..0959fd4b7be3 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/qual/ReturnsFormat.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/ReturnsFormat.java @@ -17,7 +17,7 @@ *
  • On success, the method returns the passed format string unmodified. * * - * An example is {@link org.checkerframework.checker.formatter.FormatUtil#asFormat}. + * An example is {@code FormatUtil#asFormat()}. * * @checker_framework.manual #formatter-checker Format String Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/qual/UnknownFormat.java b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/UnknownFormat.java similarity index 83% rename from checker/src/main/java/org/checkerframework/checker/formatter/qual/UnknownFormat.java rename to checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/UnknownFormat.java index e4da196ae420..e0512950429c 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/qual/UnknownFormat.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/formatter/qual/UnknownFormat.java @@ -1,24 +1,25 @@ package org.checkerframework.checker.formatter.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.DefaultQualifierInHierarchy; import org.checkerframework.framework.qual.InvisibleQualifier; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.TargetLocations; import org.checkerframework.framework.qual.TypeUseLocation; +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; + /** * The top qualifier. * *

    A type annotation indicating that the run-time value might or might not be a valid format * string. * - *

    This annotation may not be written in source code; it is an implementation detail of the - * checker. + *

    It is usually not necessary to write this annotation in source code. It is an implementation + * detail of the checker. * * @checker_framework.manual #formatter-checker Format String Checker */ @@ -28,5 +29,5 @@ @InvisibleQualifier @SubtypeOf({}) @DefaultQualifierInHierarchy -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.ALL}) public @interface UnknownFormat {} diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/qual/AlwaysSafe.java b/checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/AlwaysSafe.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/guieffect/qual/AlwaysSafe.java rename to checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/AlwaysSafe.java index 07308112f718..d768a5bf921a 100644 --- a/checker/src/main/java/org/checkerframework/checker/guieffect/qual/AlwaysSafe.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/AlwaysSafe.java @@ -1,12 +1,13 @@ package org.checkerframework.checker.guieffect.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; /** * Annotation to override the UI effect on a class, and make a field or method safe for non-UI code diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/qual/PolyUI.java b/checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/PolyUI.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/guieffect/qual/PolyUI.java rename to checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/PolyUI.java index 7586919ef903..9ad0b5878c7f 100644 --- a/checker/src/main/java/org/checkerframework/checker/guieffect/qual/PolyUI.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/PolyUI.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.guieffect.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * Annotation for the polymorphic-UI effect. diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/qual/PolyUIEffect.java b/checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/PolyUIEffect.java similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/guieffect/qual/PolyUIEffect.java rename to checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/PolyUIEffect.java diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/qual/PolyUIType.java b/checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/PolyUIType.java similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/guieffect/qual/PolyUIType.java rename to checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/PolyUIType.java diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/qual/SafeEffect.java b/checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/SafeEffect.java similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/guieffect/qual/SafeEffect.java rename to checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/SafeEffect.java diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/qual/SafeType.java b/checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/SafeType.java similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/guieffect/qual/SafeType.java rename to checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/SafeType.java diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/qual/UI.java b/checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/UI.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/guieffect/qual/UI.java rename to checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/UI.java index 615b4daa42ee..fcf5682c66a2 100644 --- a/checker/src/main/java/org/checkerframework/checker/guieffect/qual/UI.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/UI.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.guieffect.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Annotation for the UI effect. diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/qual/UIEffect.java b/checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/UIEffect.java similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/guieffect/qual/UIEffect.java rename to checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/UIEffect.java diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/qual/UIPackage.java b/checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/UIPackage.java similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/guieffect/qual/UIPackage.java rename to checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/UIPackage.java diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/qual/UIType.java b/checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/UIType.java similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/guieffect/qual/UIType.java rename to checker-qual/src/main/java/org/checkerframework/checker/guieffect/qual/UIType.java diff --git a/checker/src/main/java/org/checkerframework/checker/i18n/qual/LocalizableKey.java b/checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/LocalizableKey.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/i18n/qual/LocalizableKey.java rename to checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/LocalizableKey.java index ce74615098ac..da2d8c06b48b 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18n/qual/LocalizableKey.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/LocalizableKey.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.i18n.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Indicates that the {@code String} is a key into a property file or resource bundle containing diff --git a/checker/src/main/java/org/checkerframework/checker/i18n/qual/LocalizableKeyBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/LocalizableKeyBottom.java similarity index 90% rename from checker/src/main/java/org/checkerframework/checker/i18n/qual/LocalizableKeyBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/LocalizableKeyBottom.java index 712244a65c81..f0727f5b9eb0 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18n/qual/LocalizableKeyBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/LocalizableKeyBottom.java @@ -1,14 +1,15 @@ package org.checkerframework.checker.i18n.qual; +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.DefaultFor; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the Internationalization type system. Programmers should rarely write this @@ -20,7 +21,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @SubtypeOf(LocalizableKey.class) @DefaultFor(TypeUseLocation.LOWER_BOUND) public @interface LocalizableKeyBottom {} diff --git a/checker/src/main/java/org/checkerframework/checker/i18n/qual/Localized.java b/checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/Localized.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/i18n/qual/Localized.java rename to checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/Localized.java index 32a937aa35a9..6d24a80559fd 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18n/qual/Localized.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/Localized.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.i18n.qual; +import org.checkerframework.framework.qual.LiteralKind; +import org.checkerframework.framework.qual.QualifierForLiterals; +import org.checkerframework.framework.qual.SubtypeOf; + 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.LiteralKind; -import org.checkerframework.framework.qual.QualifierForLiterals; -import org.checkerframework.framework.qual.SubtypeOf; /** * Indicates that the {@code String} type has been localized and formatted for the target output diff --git a/checker/src/main/java/org/checkerframework/checker/i18n/qual/UnknownLocalizableKey.java b/checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/UnknownLocalizableKey.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/i18n/qual/UnknownLocalizableKey.java rename to checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/UnknownLocalizableKey.java index 71a8448dccf1..1188f3aeac29 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18n/qual/UnknownLocalizableKey.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/UnknownLocalizableKey.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.i18n.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.InvisibleQualifier; -import org.checkerframework.framework.qual.SubtypeOf; /** * Indicates that the {@code String} type has an unknown localizable key property. diff --git a/checker/src/main/java/org/checkerframework/checker/i18n/qual/UnknownLocalized.java b/checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/UnknownLocalized.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/i18n/qual/UnknownLocalized.java rename to checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/UnknownLocalized.java index 2bb098038853..daccba594d73 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18n/qual/UnknownLocalized.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18n/qual/UnknownLocalized.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.i18n.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.InvisibleQualifier; -import org.checkerframework.framework.qual.SubtypeOf; /** * Indicates that the {@code String} type has unknown localization properties. diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nChecksFormat.java b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nChecksFormat.java similarity index 79% rename from checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nChecksFormat.java rename to checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nChecksFormat.java index 8e2a9423af52..9b82e20a37ee 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nChecksFormat.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nChecksFormat.java @@ -7,9 +7,8 @@ import java.lang.annotation.Target; /** - * This annotation is used internally to annotate {@link - * org.checkerframework.checker.i18nformatter.I18nFormatUtil#hasFormat} (and will potentially be - * used to annotate more such functions in the future). + * This annotation is used internally to annotate {@code I18nFormatUtil.hasFormat()} and similar + * methods. * *

    Attach this annotation to a method with the following properties: * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java new file mode 100644 index 000000000000..afeae5e1280f --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java @@ -0,0 +1,214 @@ +package org.checkerframework.checker.i18nformatter.qual; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.AnnotatedFor; + +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; +import java.util.StringJoiner; + +/** + * 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({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 { + + /** + * Use if a parameter is not used by the formatter. For example, in + * + *
    +     * MessageFormat.format("{1}", a, b);
    +     * 
    + * + * only the second argument ("b") is used. The first argument ("a") is ignored. + */ + UNUSED(null /* everything */, null), + + /** Use if the parameter can be of any type. */ + GENERAL(null /* everything */, null), + + /** Use if the parameter can be of date, time, or number types. */ + DATE(new Class[] {Date.class, Number.class}, new String[] {"date", "time"}), + + /** + * Use if the parameter can be of number or choice types. An example of choice: + * + *
    {@code
    +     * format("{0, choice, 0#zero|1#one|1<{0, number} is more than 1}", 2)
    +     * }
    + * + * This will print "2 is more than 1". + */ + NUMBER(new Class[] {Number.class}, new String[] {"number", "choice"}); + + @SuppressWarnings("ImmutableEnumChecker") // TODO: clean this up! + public final Class @Nullable [] types; + + @SuppressWarnings("ImmutableEnumChecker") // TODO: clean this up! + public final String @Nullable [] strings; + + I18nConversionCategory(Class @Nullable [] types, String @Nullable [] strings) { + this.types = types; + this.strings = strings; + } + + /** Used by {@link #stringToI18nConversionCategory}. */ + private static final I18nConversionCategory[] namedCategories = + new I18nConversionCategory[] {DATE, NUMBER}; + + /** + * Creates a conversion cagetogry from a string name. + * + *
    +     * I18nConversionCategory.stringToI18nConversionCategory("number") == I18nConversionCategory.NUMBER;
    +     * 
    + * + * @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) { + for (String s : v.strings) { + if (s.equals(string)) { + return v; + } + } + } + throw new IllegalArgumentException("Invalid format type " + string); + } + + private static Set arrayToSet(E[] a) { + return new HashSet<>(Arrays.asList(a)); + } + + /** + * Return true if a is a subset of b. + * + * @return true if a is a subset of b + */ + public static boolean isSubsetOf(I18nConversionCategory a, I18nConversionCategory b) { + return intersect(a, b) == a; + } + + /** Conversion categories that need to be considered by {@link #intersect}. */ + private static final I18nConversionCategory[] conversionCategoriesForIntersect = + new I18nConversionCategory[] {DATE, NUMBER}; + + /** + * Returns the intersection of the two given I18nConversionCategories. + * + *
    + * + *
    +     * I18nConversionCategory.intersect(DATE, NUMBER) == NUMBER;
    +     * 
    + * + *
    + */ + public static I18nConversionCategory intersect( + I18nConversionCategory a, I18nConversionCategory b) { + if (a == UNUSED) { + return b; + } + if (b == UNUSED) { + return a; + } + if (a == GENERAL) { + return b; + } + if (b == GENERAL) { + return a; + } + + @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 : conversionCategoriesForIntersect) { + @SuppressWarnings( + "nullness:argument.type.incompatible") // in those values, `types` field is + // non-null + Set> vs = arrayToSet(v.types); + if (vs.equals(as)) { + return v; + } + } + throw new RuntimeException(); + } + + /** + * Returns the union of the two given I18nConversionCategories. + * + *
    +     * I18nConversionCategory.intersect(DATE, NUMBER) == DATE;
    +     * 
    + */ + public static I18nConversionCategory union(I18nConversionCategory a, I18nConversionCategory b) { + if (a == UNUSED || b == UNUSED) { + return UNUSED; + } + if (a == GENERAL || b == GENERAL) { + return GENERAL; + } + if (a == DATE || b == DATE) { + return DATE; + } + return NUMBER; + } + + /** + * Returns true if {@code argType} can be an argument used by this format specifier. + * + * @param argType an argument type + * @return true if {@code argType} can be an argument used by this format specifier + */ + public boolean isAssignableFrom(Class argType) { + if (types == null) { + return true; + } + if (argType == void.class) { + return true; + } + for (Class c : types) { + if (c.isAssignableFrom(argType)) { + return true; + } + } + return false; + } + + /** Returns a pretty printed {@link I18nConversionCategory}. */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(this.name()); + if (this.types == null) { + sb.append(" conversion category (all types)"); + } else { + StringJoiner sj = new StringJoiner(", ", " conversion category (one of: ", ")"); + for (Class cls : this.types) { + sj.add(cls.getCanonicalName()); + } + sb.append(sj); + } + return sb.toString(); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormat.java b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormat.java similarity index 80% rename from checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormat.java rename to checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormat.java index 63968bc7fa5d..4ac4df7b6533 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormat.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormat.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.i18nformatter.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * This annotation, attached to a String type, indicates that the String may be passed to {@link @@ -14,10 +15,11 @@ *

    The annotation's value represents the valid arguments that may be passed to the format method. * For example: * - *

    {@literal @}I18nFormat({I18nConversionCategory.GENERAL, I18nConversionCategory.NUMBER})
    - * String f = "{0}{1, number}"; // valid
    - * String f = "{0} {1} {2}"; // error, the format string is stronger (more restrictive) than the specifiers.
    - * String f = "{0, number} {1, number}"; // error, the format string is stronger (NUMBER is a subtype of GENERAL).
    + * 
    {@literal @}I18nFormat({GENERAL, NUMBER}) String f;
    + *
    + * f = "{0}{1, number}"; // valid
    + * f = "{0} {1} {2}"; // error, the format string is stronger (more restrictive) than the specifiers.
    + * f = "{0, number} {1, number}"; // error, the format string is stronger (NUMBER is a subtype of GENERAL).
      * 
    * * The annotation indicates that the format string requires any object as the first parameter diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormatBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormatBottom.java similarity index 91% rename from checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormatBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormatBottom.java index 44637a30b819..ff45b2779701 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormatBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormatBottom.java @@ -1,14 +1,15 @@ package org.checkerframework.checker.i18nformatter.qual; +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.DefaultFor; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the Internationalization Format String type system. Programmers should rarely @@ -20,7 +21,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @SubtypeOf({I18nFormat.class, I18nInvalidFormat.class, I18nFormatFor.class}) @DefaultFor(value = {TypeUseLocation.LOWER_BOUND}) public @interface I18nFormatBottom {} diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormatFor.java b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormatFor.java similarity index 79% rename from checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormatFor.java rename to checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormatFor.java index 1dc4899c5e72..38d4aa8d05c4 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormatFor.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nFormatFor.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.i18nformatter.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * This annotation indicates that when a string of the annotated type is passed as the first @@ -32,11 +33,12 @@ @SubtypeOf(I18nUnknownFormat.class) public @interface I18nFormatFor { /** - * Java expression that is an array that can be passed as the second argument to {@link - * java.text.MessageFormat#format(String, Object...)}, when the annotated String is the first - * argument. + * Indicates which formal parameter is the arguments to the format method. The value should be + * {@code #} followed by the 1-based index of the formal parameter that is the arguments to the + * format method, e.g., {@code "#2"}. * - * @checker_framework.manual #java-expressions-as-arguments Syntax of Java expressions + * @return {@code #} followed by the 1-based index of the formal parameter that is the arguments + * to the format method */ String value(); } diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nInvalidFormat.java b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nInvalidFormat.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nInvalidFormat.java rename to checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nInvalidFormat.java index c750d3267c7f..4515d25e127f 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nInvalidFormat.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nInvalidFormat.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.i18nformatter.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * This annotation, attached to a {@link java.lang.String String} type, indicates that if the String diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nMakeFormat.java b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nMakeFormat.java similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nMakeFormat.java rename to checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nMakeFormat.java diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nUnknownFormat.java b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nUnknownFormat.java similarity index 85% rename from checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nUnknownFormat.java rename to checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nUnknownFormat.java index f8dbc7d05527..66b587b2653d 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nUnknownFormat.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nUnknownFormat.java @@ -1,28 +1,32 @@ package org.checkerframework.checker.i18nformatter.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.DefaultQualifierInHierarchy; import org.checkerframework.framework.qual.InvisibleQualifier; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.TargetLocations; import org.checkerframework.framework.qual.TypeUseLocation; +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; + /** * The top qualifier. * *

    A type annotation indicating that the run-time value might or might not be a valid i18n format * string. * + *

    It is usually not necessary to write this annotation in source code. It is an implementation + * detail of the checker. + * * @checker_framework.manual #i18n-formatter-checker Internationalization Format String Checker */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.ALL}) @InvisibleQualifier @SubtypeOf({}) @DefaultQualifierInHierarchy diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nValidFormat.java b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nValidFormat.java similarity index 78% rename from checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nValidFormat.java rename to checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nValidFormat.java index d43146b2775d..61c7674821ed 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nValidFormat.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nValidFormat.java @@ -7,8 +7,7 @@ import java.lang.annotation.Target; /** - * This annotation is used internally to annotate {@link - * org.checkerframework.checker.i18nformatter.I18nFormatUtil#isFormat}. + * This annotation is used internally to annotate {@code I18nFormatUtil.isFormat()}. * * @checker_framework.manual #i18n-formatter-checker Internationalization Format String Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/EnsuresLTLengthOf.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/EnsuresLTLengthOf.java similarity index 98% rename from checker/src/main/java/org/checkerframework/checker/index/qual/EnsuresLTLengthOf.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/EnsuresLTLengthOf.java index 6db1d09de607..abc186b83212 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/EnsuresLTLengthOf.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/EnsuresLTLengthOf.java @@ -1,15 +1,16 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.JavaExpression; +import org.checkerframework.framework.qual.PostconditionAnnotation; +import org.checkerframework.framework.qual.QualifierArgument; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.InheritedAnnotation; -import org.checkerframework.framework.qual.JavaExpression; -import org.checkerframework.framework.qual.PostconditionAnnotation; -import org.checkerframework.framework.qual.QualifierArgument; /** * Indicates that the value expressions evaluate to an integer whose value is less than the lengths @@ -45,6 +46,7 @@ * calling {@code shiftIndex(x)}, {@code end} has an annotation that allows the {@code end + x} to * be accepted as {@code @LTLengthOf("array")}. * + * @see EnsuresLTLengthOfIf * @see LTLengthOf * @checker_framework.manual #index-checker Index Checker */ @@ -94,7 +96,7 @@ @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) @PostconditionAnnotation(qualifier = LTLengthOf.class) @InheritedAnnotation - @interface List { + public static @interface List { /** * Return the repeatable annotations. * diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/EnsuresLTLengthOfIf.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/EnsuresLTLengthOfIf.java similarity index 90% rename from checker/src/main/java/org/checkerframework/checker/index/qual/EnsuresLTLengthOfIf.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/EnsuresLTLengthOfIf.java index 56b27f70198d..b0630a0e2f39 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/EnsuresLTLengthOfIf.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/EnsuresLTLengthOfIf.java @@ -1,15 +1,16 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; +import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.JavaExpression; +import org.checkerframework.framework.qual.QualifierArgument; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; -import org.checkerframework.framework.qual.InheritedAnnotation; -import org.checkerframework.framework.qual.JavaExpression; -import org.checkerframework.framework.qual.QualifierArgument; /** * Indicates that the given expressions evaluate to an integer whose value is less than the lengths @@ -58,17 +59,23 @@ @InheritedAnnotation @Repeatable(EnsuresLTLengthOfIf.List.class) public @interface EnsuresLTLengthOfIf { + /** + * The return value of the method that needs to hold for the postcondition to hold. + * + * @return the return value of the method that needs to hold for the postcondition to hold + */ + boolean result(); + /** * Java expression(s) that are less than the length of the given sequences after the method * returns the given result. * + * @return Java expression(s) that are less than the length of the given sequences after the + * method returns the given result * @checker_framework.manual #java-expressions-as-arguments Syntax of Java expressions */ String[] expression(); - /** The return value of the method that needs to hold for the postcondition to hold. */ - boolean result(); - /** * Sequences, each of which is longer than each of the expressions' value after the method * returns the given result. @@ -99,7 +106,7 @@ @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) @ConditionalPostconditionAnnotation(qualifier = LTLengthOf.class) @InheritedAnnotation - @interface List { + public static @interface List { /** * Return the repeatable annotations. * diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/GTENegativeOne.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/GTENegativeOne.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/index/qual/GTENegativeOne.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/GTENegativeOne.java index 034dc325d925..1d7ead646505 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/GTENegativeOne.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/GTENegativeOne.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * The annotated expression evaluates to an integer greater than or equal to -1. ("GTE" stands for diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/HasSubsequence.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/HasSubsequence.java similarity index 92% rename from checker/src/main/java/org/checkerframework/checker/index/qual/HasSubsequence.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/HasSubsequence.java index 4c8c22c4848b..85e265a19401 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/HasSubsequence.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/HasSubsequence.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.JavaExpression; + 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.JavaExpression; /** * The annotated sequence contains a subsequence that is equal to the value of some other @@ -30,9 +31,9 @@ * annotations: * *

      - *
    • If {@code i} is {@code @IndexFor("this")}, then {@code start + i} is + *
    • If {@code i} is {@code @IndexFor("this")}, then {@code this.start + i} is * {@code @IndexFor("array")}. - *
    • If {@code j} is {@code @IndexFor("array")}, then {@code j - start } is + *
    • If {@code j} is {@code @IndexFor("array")}, then {@code j - this.start } is * {@code @IndexFor("this")}. *
    * @@ -46,8 +47,8 @@ * * * The Index Checker verifies the first 3 facts, but always issues a warning because it cannot prove - * the 4th fact. The programmer should should manually verify that the {@code subsequence} field is - * equal to the given subsequence and then suppress the warning. + * the 4th fact. The programmer should manually verify that the {@code subsequence} field is equal + * to the given subsequence and then suppress the warning. * *

    For an example of how this annotation is used in practice, see the test GuavaPrimitives.java * in /checker/tests/index/. diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/IndexFor.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/IndexFor.java similarity index 95% rename from checker/src/main/java/org/checkerframework/checker/index/qual/IndexFor.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/IndexFor.java index f87955539964..e08773c837ef 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/IndexFor.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/IndexFor.java @@ -14,7 +14,7 @@ * different lengths. * *

    The + * href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/String.html#charAt(int)"> * {@code String.charAt(int)} method is declared as * *

    {@code
    diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/IndexOrHigh.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/IndexOrHigh.java
    similarity index 95%
    rename from checker/src/main/java/org/checkerframework/checker/index/qual/IndexOrHigh.java
    rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/IndexOrHigh.java
    index e00fdaae2b1f..c91497e94aaf 100644
    --- a/checker/src/main/java/org/checkerframework/checker/index/qual/IndexOrHigh.java
    +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/IndexOrHigh.java
    @@ -11,7 +11,7 @@
      * sequence's length.
      *
      * 

    The + * href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Arrays.html#binarySearch(java.lang.Object%5B%5D,int,int,java.lang.Object)"> * {@code Arrays.binarySearch} method is declared as * *

    {@code
    diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/IndexOrLow.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/IndexOrLow.java
    similarity index 95%
    rename from checker/src/main/java/org/checkerframework/checker/index/qual/IndexOrLow.java
    rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/IndexOrLow.java
    index b73b548d5837..e4cd74d2b6d0 100644
    --- a/checker/src/main/java/org/checkerframework/checker/index/qual/IndexOrLow.java
    +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/IndexOrLow.java
    @@ -10,7 +10,7 @@
      * An integer that is either -1 or is a valid index for each of the given sequences.
      *
      * 

    The + * href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/String.html#indexOf(java.lang.String)"> * {@code String.indexOf(String)} method is declared as * *

    
    diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/LTEqLengthOf.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LTEqLengthOf.java
    similarity index 85%
    rename from checker/src/main/java/org/checkerframework/checker/index/qual/LTEqLengthOf.java
    rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/LTEqLengthOf.java
    index f64e0b4b18ef..80c0ce3f45e1 100644
    --- a/checker/src/main/java/org/checkerframework/checker/index/qual/LTEqLengthOf.java
    +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LTEqLengthOf.java
    @@ -1,12 +1,13 @@
     package org.checkerframework.checker.index.qual;
     
    +import org.checkerframework.framework.qual.JavaExpression;
    +import org.checkerframework.framework.qual.SubtypeOf;
    +
     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.JavaExpression;
    -import org.checkerframework.framework.qual.SubtypeOf;
     
     /**
      * The annotated expression evaluates to an integer whose value is less than or equal to the lengths
    @@ -16,6 +17,10 @@
      * both {@code a.length} and {@code b.length}. The sequences {@code a} and {@code b} might have
      * different lengths.
      *
    + * 

    {@code @LTEqLengthOf({"a"})} = {@code @LTLengthOf(value={"a"}, offset=-1)}, and
    + * {@code @LTEqLengthOf(value={"a"}, offset=x)} = {@code @LTLengthOf(value={"a"}, offset=x-1)} for + * any x. + * * @checker_framework.manual #index-checker Index Checker */ @Documented diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/LTLengthOf.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LTLengthOf.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/index/qual/LTLengthOf.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/LTLengthOf.java index 2e9acb6e8189..d022ccd49389 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/LTLengthOf.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LTLengthOf.java @@ -1,12 +1,13 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.JavaExpression; +import org.checkerframework.framework.qual.SubtypeOf; + 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.JavaExpression; -import org.checkerframework.framework.qual.SubtypeOf; /** * The annotated expression evaluates to an integer whose value is less than the lengths of all the diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/LTOMLengthOf.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LTOMLengthOf.java similarity index 86% rename from checker/src/main/java/org/checkerframework/checker/index/qual/LTOMLengthOf.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/LTOMLengthOf.java index 0754c4f91a01..2d361ad16944 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/LTOMLengthOf.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LTOMLengthOf.java @@ -1,12 +1,13 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.JavaExpression; +import org.checkerframework.framework.qual.SubtypeOf; + 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.JavaExpression; -import org.checkerframework.framework.qual.SubtypeOf; /** * The annotated expression evaluates to an integer whose value is at least 2 less than the lengths @@ -19,6 +20,10 @@ * *

    In the annotation's name, "LTOM" stands for "less than one minus". * + *

    {@code @LTOMLengthOf({"a"})} = {@code @LTLengthOf(value={"a"}, offset=1)}, and
    + * {@code @LTOMLengthOf(value={"a"}, offset=x)} = {@code @LTLengthOf(value={"a"}, offset=x+1)} for + * any x. + * * @checker_framework.manual #index-checker Index Checker */ @Documented diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/LengthOf.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LengthOf.java similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/index/qual/LengthOf.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/LengthOf.java diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/LessThan.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LessThan.java similarity index 91% rename from checker/src/main/java/org/checkerframework/checker/index/qual/LessThan.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/LessThan.java index 54ca4a7bafcd..46b878530156 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/LessThan.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LessThan.java @@ -1,12 +1,13 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.JavaExpression; +import org.checkerframework.framework.qual.SubtypeOf; + 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.JavaExpression; -import org.checkerframework.framework.qual.SubtypeOf; /** * An annotation indicating the relationship between values with a byte, short, char, int, or long @@ -15,8 +16,6 @@ *

    If an expression's type has this annotation, then at run time, the expression evaluates to a * value that is less than the value of the expression in the annotation. * - *

    {@code @LessThan("end + 1")} is equivalent to {@code @LessThanOrEqual("end")}. - * *

    Subtyping: * *

      @@ -24,7 +23,7 @@ *
    • {@code @LessThan({"a", "b"})} is not related to {@code @LessThan({"a", "c"})}. *
    * - * @checker_framework.manual #index-inequalities Index Chceker Inequalities + * @checker_framework.manual #index-inequalities Index Checker Inequalities */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/LessThanBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LessThanBottom.java similarity index 90% rename from checker/src/main/java/org/checkerframework/checker/index/qual/LessThanBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/LessThanBottom.java index 422f87e68060..3eda6aea6929 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/LessThanBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LessThanBottom.java @@ -1,16 +1,17 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * The bottom type in the LessThan type system. Programmers should rarely write this type. * - * @checker_framework.manual #index-inequalities Index Chceker Inequalities + * @checker_framework.manual #index-inequalities Index Checker Inequalities * @checker_framework.manual #bottom-type the bottom type */ @Documented diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/LessThanUnknown.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LessThanUnknown.java similarity index 83% rename from checker/src/main/java/org/checkerframework/checker/index/qual/LessThanUnknown.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/LessThanUnknown.java index 3b456c3469d9..ca5b2f1a98fe 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/LessThanUnknown.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LessThanUnknown.java @@ -1,22 +1,25 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; /** * The top qualifier for the LessThan type hierarchy. It indicates that no other expression is known * to be larger than the annotated one. * - * @checker_framework.manual #index-inequalities Index Chceker Inequalities + * @checker_framework.manual #index-inequalities Index Checker Inequalities */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @SubtypeOf({}) @DefaultQualifierInHierarchy +@InvisibleQualifier public @interface LessThanUnknown {} diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/LowerBoundBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LowerBoundBottom.java similarity index 89% rename from checker/src/main/java/org/checkerframework/checker/index/qual/LowerBoundBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/LowerBoundBottom.java index 3cfe92bdd597..4bff20599c31 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/LowerBoundBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LowerBoundBottom.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type of the lower bound type system. A variable annotated with this value cannot take @@ -18,6 +19,6 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @SubtypeOf({Positive.class}) public @interface LowerBoundBottom {} diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/LowerBoundUnknown.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LowerBoundUnknown.java similarity index 90% rename from checker/src/main/java/org/checkerframework/checker/index/qual/LowerBoundUnknown.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/LowerBoundUnknown.java index f9c9031d5e86..4d61ceab2ddf 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/LowerBoundUnknown.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/LowerBoundUnknown.java @@ -1,12 +1,14 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; /** * The annotated expression evaluates to value that might be -2 or lower. This is the top type for @@ -19,4 +21,5 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy +@InvisibleQualifier public @interface LowerBoundUnknown {} diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/NegativeIndexFor.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/NegativeIndexFor.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/index/qual/NegativeIndexFor.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/NegativeIndexFor.java index 310ff971e19b..254c3281f335 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/NegativeIndexFor.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/NegativeIndexFor.java @@ -1,12 +1,13 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.JavaExpression; +import org.checkerframework.framework.qual.SubtypeOf; + 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.JavaExpression; -import org.checkerframework.framework.qual.SubtypeOf; /** * The annotated expression is between {@code -1} and {@code -a.length - 1}, inclusive, for each diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/NonNegative.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/NonNegative.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/index/qual/NonNegative.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/NonNegative.java index 40806f19a32b..95d4af7435ac 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/NonNegative.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/NonNegative.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * The annotated expression evaluates to an integer greater than or equal to 0. diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/PolyIndex.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolyIndex.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/index/qual/PolyIndex.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolyIndex.java index fa5c811ee6c8..fc121728825a 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/PolyIndex.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolyIndex.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * A polymorphic qualifier for the Lower Bound and Upper Bound type systems. diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/PolyLength.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolyLength.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/index/qual/PolyLength.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolyLength.java index 4d5123723a70..6f2660722033 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/PolyLength.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolyLength.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * Syntactic sugar for both @PolyValue and @PolySameLen. diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/PolyLowerBound.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolyLowerBound.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/index/qual/PolyLowerBound.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolyLowerBound.java index 824609a8f5eb..4548013e2100 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/PolyLowerBound.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolyLowerBound.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * A polymorphic qualifier for the Lower Bound type system. diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/PolySameLen.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolySameLen.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/index/qual/PolySameLen.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolySameLen.java index 89e7fc431af6..43c2518779c4 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/PolySameLen.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolySameLen.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * A polymorphic qualifier for the SameLen type system. diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/PolyUpperBound.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolyUpperBound.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/index/qual/PolyUpperBound.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolyUpperBound.java index c8eaa92a5d28..54d550855998 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/PolyUpperBound.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/PolyUpperBound.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * A polymorphic qualifier for the Upper Bound type system. diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/Positive.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/Positive.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/index/qual/Positive.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/Positive.java index 7321a024340f..14b40101de4a 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/Positive.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/Positive.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * The annotated expression evaluates to an integer greater than or equal to 1. diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/SameLen.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SameLen.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/index/qual/SameLen.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/SameLen.java index ab5d54c88001..a4ab7d9bb735 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/SameLen.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SameLen.java @@ -1,12 +1,13 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.JavaExpression; +import org.checkerframework.framework.qual.SubtypeOf; + 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.JavaExpression; -import org.checkerframework.framework.qual.SubtypeOf; /** * An expression whose type has this annotation evaluates to a value that is a sequence, and that diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/SameLenBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SameLenBottom.java similarity index 89% rename from checker/src/main/java/org/checkerframework/checker/index/qual/SameLenBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/SameLenBottom.java index df9b82b2a198..5f4360c00d55 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/SameLenBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SameLenBottom.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the SameLen type system. Programmers should rarely write this type. @@ -18,6 +19,6 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @SubtypeOf(SameLen.class) public @interface SameLenBottom {} diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/SameLenUnknown.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SameLenUnknown.java similarity index 90% rename from checker/src/main/java/org/checkerframework/checker/index/qual/SameLenUnknown.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/SameLenUnknown.java index f090c956e1b0..181811977e67 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/SameLenUnknown.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SameLenUnknown.java @@ -1,12 +1,14 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; /** * This type represents any variable that isn't known to have the same length as another sequence. @@ -20,4 +22,5 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy +@InvisibleQualifier public @interface SameLenUnknown {} diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/SearchIndexBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SearchIndexBottom.java similarity index 89% rename from checker/src/main/java/org/checkerframework/checker/index/qual/SearchIndexBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/SearchIndexBottom.java index 54ac9ebcb4d0..5b36dc568baf 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/SearchIndexBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SearchIndexBottom.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the Search Index type system. Programmers should rarely write this type. @@ -18,6 +19,6 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @SubtypeOf(NegativeIndexFor.class) public @interface SearchIndexBottom {} diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/SearchIndexFor.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SearchIndexFor.java similarity index 87% rename from checker/src/main/java/org/checkerframework/checker/index/qual/SearchIndexFor.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/SearchIndexFor.java index 227922e0211b..978318f07f8a 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/SearchIndexFor.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SearchIndexFor.java @@ -1,19 +1,20 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.JavaExpression; +import org.checkerframework.framework.qual.SubtypeOf; + 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.JavaExpression; -import org.checkerframework.framework.qual.SubtypeOf; /** * The annotated expression evaluates to an integer whose length is between {@code -a.length - 1} * and {@code a.length - 1}, inclusive, for all sequences {@code a} listed in the annotation. * - *

    This is the return type of {@link java.util.Arrays#binarySearch(Object[],Object) {@code - * Arrays.binarySearch}} in the JDK. + *

    This is the return type of {@link java.util.Arrays#binarySearch(Object[],Object) + * Arrays.binarySearch} in the JDK. * * @checker_framework.manual #index-checker Index Checker */ @@ -24,7 +25,7 @@ public @interface SearchIndexFor { /** * Sequences for which the annotated expression has the type of the result of a call to {@link - * java.util.Arrays#binarySearch(Object[],Object) {@code Arrays.binarySearch}}. + * java.util.Arrays#binarySearch(Object[],Object) Arrays.binarySearch}. */ @JavaExpression public String[] value(); diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/SearchIndexUnknown.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SearchIndexUnknown.java similarity index 90% rename from checker/src/main/java/org/checkerframework/checker/index/qual/SearchIndexUnknown.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/SearchIndexUnknown.java index 75bc3432bf30..87b4913b2d0e 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/SearchIndexUnknown.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SearchIndexUnknown.java @@ -1,12 +1,14 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; /** * The top type for the SearchIndex type system. This indicates that the Index checker does not know @@ -19,4 +21,5 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy +@InvisibleQualifier public @interface SearchIndexUnknown {} diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexBottom.java similarity index 89% rename from checker/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexBottom.java index 797aea403e64..2fe7a440d4c2 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexBottom.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the Substring Index type system. Programmers should rarely write this type. @@ -18,6 +19,6 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @SubtypeOf(SubstringIndexFor.class) public @interface SubstringIndexBottom {} diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexFor.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexFor.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexFor.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexFor.java index 21dcfd9b17da..2ce25597311a 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexFor.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexFor.java @@ -1,12 +1,13 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.JavaExpression; +import org.checkerframework.framework.qual.SubtypeOf; + 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.JavaExpression; -import org.checkerframework.framework.qual.SubtypeOf; /** * The annotated expression evaluates to either -1 or a non-negative integer less than the lengths diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexUnknown.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexUnknown.java similarity index 90% rename from checker/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexUnknown.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexUnknown.java index 94c4480d38a0..814109809788 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexUnknown.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/SubstringIndexUnknown.java @@ -1,12 +1,14 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; /** * The top type for the Substring Index type system. This indicates that the Index Checker does not @@ -19,4 +21,5 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy +@InvisibleQualifier public @interface SubstringIndexUnknown {} diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/UpperBoundBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/UpperBoundBottom.java similarity index 85% rename from checker/src/main/java/org/checkerframework/checker/index/qual/UpperBoundBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/UpperBoundBottom.java index 3b6bb74880fc..29b373fc5bb2 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/UpperBoundBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/UpperBoundBottom.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the Upper Bound type system. Programmers should rarely write this type. @@ -18,6 +19,6 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) -@SubtypeOf(LTOMLengthOf.class) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) +@SubtypeOf({LTOMLengthOf.class, UpperBoundLiteral.class}) public @interface UpperBoundBottom {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/index/qual/UpperBoundLiteral.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/UpperBoundLiteral.java new file mode 100644 index 000000000000..2f29d0d337c6 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/UpperBoundLiteral.java @@ -0,0 +1,31 @@ +package org.checkerframework.checker.index.qual; + +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + +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; + +/** + * A literal value. Programmers should rarely write this type. + * + * @checker_framework.manual #index-checker Index Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) +@SubtypeOf(LTEqLengthOf.class) +public @interface UpperBoundLiteral { + + /** + * Returns the value of the literal. + * + * @return the value of the literal + */ + int value(); +} diff --git a/checker/src/main/java/org/checkerframework/checker/index/qual/UpperBoundUnknown.java b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/UpperBoundUnknown.java similarity index 90% rename from checker/src/main/java/org/checkerframework/checker/index/qual/UpperBoundUnknown.java rename to checker-qual/src/main/java/org/checkerframework/checker/index/qual/UpperBoundUnknown.java index 8548028b56ef..e47fd0249702 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/qual/UpperBoundUnknown.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/index/qual/UpperBoundUnknown.java @@ -1,12 +1,14 @@ package org.checkerframework.checker.index.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; /** * A variable not known to have a relation to any sequence length. This is the top type of the Upper @@ -19,4 +21,5 @@ @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) @DefaultQualifierInHierarchy +@InvisibleQualifier public @interface UpperBoundUnknown {} diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/qual/FBCBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/FBCBottom.java similarity index 80% rename from checker/src/main/java/org/checkerframework/checker/initialization/qual/FBCBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/FBCBottom.java index 23e2ff4a4cf7..0f54368bbc62 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/qual/FBCBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/FBCBottom.java @@ -1,13 +1,16 @@ package org.checkerframework.checker.initialization.qual; +import org.checkerframework.framework.qual.LiteralKind; +import org.checkerframework.framework.qual.QualifierForLiterals; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the initialization type system. Programmers should rarely write this type. @@ -21,6 +24,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @SubtypeOf({UnderInitialization.class, Initialized.class}) +@QualifierForLiterals(LiteralKind.NULL) public @interface FBCBottom {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/HoldsForDefaultValue.java b/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/HoldsForDefaultValue.java new file mode 100644 index 000000000000..b3b3112b6b82 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/HoldsForDefaultValue.java @@ -0,0 +1,39 @@ +package org.checkerframework.checker.initialization.qual; + +import org.checkerframework.framework.qual.RelevantJavaTypes; + +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; + +/** + * A meta-annotation that indicates that the qualifier can be applied to the default value of every + * relevant Java type (as per {@link RelevantJavaTypes}). It is used by the Initialization Checker + * to know which fields that are not initialized can still be considered initialized. + * + *

    This meta-annotation should not be applied to the top qualifier in a hierarchy, as the top + * qualifier must always respect this property by default. It should also not be applied to + * monotonic or polymorphic qualifiers. + * + *

    For example, the default value of every class-typed variable is {@code null}. Thus, in a + * nullness types system, {@code Nullable} holds for default values (but should not be annotated + * with this meta-annotation, since it is the top qualifier), but {@code NonNull} does not. For + * another example, the default value for numerical primitive types is {@code 0}. Thus, in a type + * system with qualifiers {@code Even}, {@code Odd}, and {@code Unknown}, the top qualifier {@code + * Unknown} holds for default values (but should not be annotated with this meta-annotation), {@code + * Even} holds for default values and should be annotated, and {@code Odd} does not hold for default + * values and should not be annotated. + * + *

    Unannotated qualifiers are treated conservatively. Therefore, {@code HoldsForDefaultValues} + * annotations can be added to qualifiers once the Initialization Checker is used by a type system + * to suppress false positive warnings. + * + *

    This is a trusted meta-annotation, meaning that it is not checked whether a qualifier + * actually holds for the default value. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.ANNOTATION_TYPE}) +public @interface HoldsForDefaultValue {} diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/qual/Initialized.java b/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/Initialized.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/initialization/qual/Initialized.java rename to checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/Initialized.java index 48c2bb426abe..f13abf0f8c59 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/qual/Initialized.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/Initialized.java @@ -1,16 +1,17 @@ package org.checkerframework.checker.initialization.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.checker.nullness.qual.NonNull; import org.checkerframework.framework.qual.DefaultFor; import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.TypeUseLocation; +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 type qualifier belongs to the freedom-before-commitment initialization tracking type-system. * This type-system is not used on its own, but in conjunction with some other type-system that diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/qual/NotOnlyInitialized.java b/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/NotOnlyInitialized.java similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/initialization/qual/NotOnlyInitialized.java rename to checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/NotOnlyInitialized.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/PolyInitialized.java b/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/PolyInitialized.java new file mode 100644 index 000000000000..529f322d8ad3 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/PolyInitialized.java @@ -0,0 +1,27 @@ +package org.checkerframework.checker.initialization.qual; + +import org.checkerframework.framework.qual.PolymorphicQualifier; + +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; + +/** + * A polymorphic qualifier for the freedom-before-commitment initialization tracking type-system. + * + *

    Any method written using {@link PolyInitialized} conceptually has multiple versions: one in + * which all instances of {@link PolyInitialized} in the method signature have been replaced by one + * of the following qualifiers: {@link Initialized}; {@link UnknownInitialization} and {@link + * UnderInitialization}, which take a class argument to represent different type frames; and {@link + * FBCBottom}. + * + * @checker_framework.manual #initialization-checker Initialization Checker + * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@PolymorphicQualifier(UnknownInitialization.class) +public @interface PolyInitialized {} diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/qual/UnderInitialization.java b/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/UnderInitialization.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/initialization/qual/UnderInitialization.java rename to checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/UnderInitialization.java index dc120906431d..cc9b92c56258 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/qual/UnderInitialization.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/UnderInitialization.java @@ -1,12 +1,13 @@ package org.checkerframework.checker.initialization.qual; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.SubtypeOf; + 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.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.SubtypeOf; /** * This type qualifier indicates that an object is (definitely) in the process of being diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/qual/UnknownInitialization.java b/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/UnknownInitialization.java similarity index 86% rename from checker/src/main/java/org/checkerframework/checker/initialization/qual/UnknownInitialization.java rename to checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/UnknownInitialization.java index cf24af714f50..73d66e2c4ca2 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/qual/UnknownInitialization.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/initialization/qual/UnknownInitialization.java @@ -1,14 +1,15 @@ package org.checkerframework.checker.initialization.qual; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.checker.nullness.qual.NonNull; -import org.checkerframework.framework.qual.DefaultFor; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TypeUseLocation; /** * This type qualifier indicates how much of an object has been fully initialized. An object is @@ -17,8 +18,16 @@ * *

    An expression of type {@code @UnknownInitialization(T.class)} refers to an object that has all * fields of {@code T} (and any super-classes) initialized. Just {@code @UnknownInitialization} is - * equivalent to {@code @UnknownInitialization(Object.class)}. Please see the manual for examples of - * how to use the annotation (the link appears below). + * equivalent to {@code @UnknownInitialization(Object.class)}. + * + *

    A common use is + * + *

    {@code
    + * void myMethod(@UnknownInitialization(MyClass.class) MyClass this, ...) { ... }
    + * }
    + * + * which allows {@code myMethod} to be called from the {@code MyClass} constructor. See the manual + * for more examples of how to use the annotation (the link appears below). * *

    Reading a field of an object of type {@code @UnknownInitialization} might yield a value that * does not correspond to the declared type qualifier for that field. For instance, consider a diff --git a/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/CompareToMethod.java b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/CompareToMethod.java new file mode 100644 index 000000000000..968c3e406bc4 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/CompareToMethod.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.interning.qual; + +import org.checkerframework.framework.qual.InheritedAnnotation; + +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; + +/** + * 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-qual/src/main/java/org/checkerframework/checker/interning/qual/EqualsMethod.java b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/EqualsMethod.java new file mode 100644 index 000000000000..921bf22d2f82 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/EqualsMethod.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.interning.qual; + +import org.checkerframework.framework.qual.InheritedAnnotation; + +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; + +/** + * 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-qual/src/main/java/org/checkerframework/checker/interning/qual/FindDistinct.java b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/FindDistinct.java new file mode 100644 index 000000000000..adda1fa7e68c --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/FindDistinct.java @@ -0,0 +1,23 @@ +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 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-qual/src/main/java/org/checkerframework/checker/interning/qual/InternMethod.java similarity index 87% rename from checker/src/main/java/org/checkerframework/checker/interning/qual/InternMethod.java rename to checker-qual/src/main/java/org/checkerframework/checker/interning/qual/InternMethod.java index 7385ba20cf0b..a5d618a7b0a4 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/qual/InternMethod.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/InternMethod.java @@ -1,8 +1,9 @@ package org.checkerframework.checker.interning.qual; +import org.checkerframework.framework.qual.InheritedAnnotation; + 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; @@ -16,5 +17,5 @@ @Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) -@Inherited +@InheritedAnnotation public @interface InternMethod {} diff --git a/checker/src/main/java/org/checkerframework/checker/interning/qual/Interned.java b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/Interned.java similarity index 93% rename from checker/src/main/java/org/checkerframework/checker/interning/qual/Interned.java rename to checker-qual/src/main/java/org/checkerframework/checker/interning/qual/Interned.java index 946fb68c0b90..c75877f3f128 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/qual/Interned.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/Interned.java @@ -1,16 +1,17 @@ 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.DefaultFor; import org.checkerframework.framework.qual.LiteralKind; import org.checkerframework.framework.qual.QualifierForLiterals; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.TypeKind; +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; + /** * Indicates that a variable has been interned, i.e., that the variable refers to the canonical * representation of an object. @@ -24,9 +25,6 @@ * This is equivalent to annotating every use of MyInternedClass, in a declaration or elsewhere. For * example, enum classes are implicitly so annotated. * - *

    This annotation is associated with the {@link - * org.checkerframework.checker.interning.InterningChecker}. - * * @see org.checkerframework.checker.interning.InterningChecker * @checker_framework.manual #interning-checker Interning Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/interning/qual/InternedDistinct.java b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/InternedDistinct.java similarity index 95% rename from checker/src/main/java/org/checkerframework/checker/interning/qual/InternedDistinct.java rename to checker-qual/src/main/java/org/checkerframework/checker/interning/qual/InternedDistinct.java index 7c6aa725c293..ad2dd9516db0 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/qual/InternedDistinct.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/InternedDistinct.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.interning.qual; +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.DefaultFor; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TypeUseLocation; /** * Indicates that no other value is {@code equals()} to the given value. Therefore, it is correct to @@ -16,6 +17,8 @@ *

    This is a stronger property than {@link Interned}, but a weaker property than every value of a * Java type being interned. * + *

    This annotation is trusted, not verified. + * * @see org.checkerframework.checker.interning.InterningChecker * @checker_framework.manual #interning-checker Interning Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/interning/qual/PolyInterned.java b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/PolyInterned.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/interning/qual/PolyInterned.java rename to checker-qual/src/main/java/org/checkerframework/checker/interning/qual/PolyInterned.java index fb657e407061..aaa1be3c2dd2 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/qual/PolyInterned.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/PolyInterned.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.interning.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * A polymorphic qualifier for the Interning type system. diff --git a/checker/src/main/java/org/checkerframework/checker/interning/qual/UnknownInterned.java b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/UnknownInterned.java similarity index 89% rename from checker/src/main/java/org/checkerframework/checker/interning/qual/UnknownInterned.java rename to checker-qual/src/main/java/org/checkerframework/checker/interning/qual/UnknownInterned.java index e170645f2af6..f0801cd68e1d 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/qual/UnknownInterned.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/UnknownInterned.java @@ -1,21 +1,19 @@ package org.checkerframework.checker.interning.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.InvisibleQualifier; -import org.checkerframework.framework.qual.SubtypeOf; /** * The top qualifier for the Interning Checker. It indicates lack of knowledge about whether values * are interned or not. It is not written by programmers, but is used internally by the type system. * - *

    This annotation is associated with the {@link - * org.checkerframework.checker.interning.InterningChecker}. - * * @see org.checkerframework.checker.interning.InterningChecker * @checker_framework.manual #interning-checker Interning Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/interning/qual/UsesObjectEquals.java b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/UsesObjectEquals.java similarity index 89% rename from checker/src/main/java/org/checkerframework/checker/interning/qual/UsesObjectEquals.java rename to checker-qual/src/main/java/org/checkerframework/checker/interning/qual/UsesObjectEquals.java index d4b734e95576..003e3cdd53e2 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/qual/UsesObjectEquals.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/interning/qual/UsesObjectEquals.java @@ -15,9 +15,6 @@ * subtypes, overrides {@code equals}. Therefore, it cannot be written on {@code Object} itself. It * is most commonly written on a direct subclass of {@code Object}. * - *

    This annotation is associated with the {@link - * org.checkerframework.checker.interning.InterningChecker}. - * * @see org.checkerframework.checker.interning.InterningChecker * @checker_framework.manual #interning-checker Interning Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeld.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeld.java similarity index 93% rename from checker/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeld.java rename to checker-qual/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeld.java index f2186a1656e8..8b76dfd8562d 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeld.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeld.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.lock.qual; +import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.PostconditionAnnotation; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.InheritedAnnotation; -import org.checkerframework.framework.qual.PostconditionAnnotation; /** * Indicates that the given expressions are held if the method terminates successfully. @@ -29,7 +30,7 @@ * * @return Java expressions whose values are locks that are held after successful method * termination - * @see Syntax of + * @see Syntax of * Java expressions */ String[] value(); @@ -45,7 +46,7 @@ @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) @PostconditionAnnotation(qualifier = LockHeld.class) @InheritedAnnotation - @interface List { + public static @interface List { /** * Return the repeatable annotations. * diff --git a/checker/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeldIf.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeldIf.java similarity index 88% rename from checker/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeldIf.java rename to checker-qual/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeldIf.java index 5b5c264edaa6..2e4ca1c556a7 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeldIf.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/EnsuresLockHeldIf.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.lock.qual; +import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; +import org.checkerframework.framework.qual.InheritedAnnotation; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; -import org.checkerframework.framework.qual.InheritedAnnotation; /** * Indicates that the given expressions are held if the method terminates successfully and returns @@ -24,27 +25,27 @@ @InheritedAnnotation @Repeatable(EnsuresLockHeldIf.List.class) public @interface EnsuresLockHeldIf { + /** + * Returns the return value of the method under which the postconditions hold. + * + * @return the return value of the method under which the postconditions hold + */ + boolean result(); + /** * Returns Java expressions whose values are locks that are held after the method returns the * given result. * * @return Java expressions whose values are locks that are held after the method returns the * given result - * @see Syntax of + * @see Syntax of * Java expressions */ // It would be clearer for users if this field were named "lock". - // However, method ContractUtils.getConditionalPostconditions in the CF implementation assumes - // that conditional postconditions have a field named "expression". + // However, method ContractsFromMethod.getConditionalPostconditions in the CF implementation + // assumes that conditional postconditions have a field named "expression". String[] expression(); - /** - * Returns the return value of the method under which the postconditions hold. - * - * @return the return value of the method under which the postconditions hold - */ - boolean result(); - /** * A wrapper annotation that makes the {@link EnsuresLockHeldIf} annotation repeatable. * @@ -56,7 +57,7 @@ @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) @ConditionalPostconditionAnnotation(qualifier = LockHeld.class) @InheritedAnnotation - @interface List { + public static @interface List { /** * Return the repeatable annotations. * diff --git a/checker/src/main/java/org/checkerframework/checker/lock/qual/GuardSatisfied.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/GuardSatisfied.java similarity index 77% rename from checker/src/main/java/org/checkerframework/checker/lock/qual/GuardSatisfied.java rename to checker-qual/src/main/java/org/checkerframework/checker/lock/qual/GuardSatisfied.java index 0bed9f5750b3..248b373eb12c 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/qual/GuardSatisfied.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/GuardSatisfied.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.lock.qual; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * If a variable {@code x} has type {@code @GuardSatisfied}, then all lock expressions for {@code @@ -32,12 +33,21 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE_USE) -@TargetLocations({TypeUseLocation.RECEIVER, TypeUseLocation.PARAMETER, TypeUseLocation.RETURN}) +@TargetLocations({ + TypeUseLocation.RECEIVER, + TypeUseLocation.PARAMETER, + TypeUseLocation.RETURN, + TypeUseLocation.FIELD, + TypeUseLocation.LOCAL_VARIABLE, + TypeUseLocation.CONSTRUCTOR_RESULT +}) @SubtypeOf(GuardedByUnknown.class) // TODO: Should @GuardSatisfied be in its own hierarchy? public @interface GuardSatisfied { /** - * The index on the GuardSatisfied polymorphic qualifier. Defaults to -1 so that the user can - * write any index starting from 0. + * The index on the GuardSatisfied polymorphic qualifier, if any. Defaults to -1 so that, if the + * user writes 0, that is different than writing no index. Writing no index is the usual case. + * + * @return the index on the GuardSatisfied polymorphic qualifier, or -1 if none */ int value() default -1; } diff --git a/checker/src/main/java/org/checkerframework/checker/lock/qual/GuardedBy.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/GuardedBy.java similarity index 80% rename from checker/src/main/java/org/checkerframework/checker/lock/qual/GuardedBy.java rename to checker-qual/src/main/java/org/checkerframework/checker/lock/qual/GuardedBy.java index 31a8fc3330c8..9735d961fba9 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/qual/GuardedBy.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/GuardedBy.java @@ -1,16 +1,18 @@ package org.checkerframework.checker.lock.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.DefaultFor; import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; import org.checkerframework.framework.qual.JavaExpression; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.TypeKind; import org.checkerframework.framework.qual.TypeUseLocation; +import org.checkerframework.framework.qual.UpperBoundFor; + +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; /** * Indicates that a thread may dereference the value referred to by the annotated variable only if @@ -19,7 +21,7 @@ *

    {@code @GuardedBy({})} is the default type qualifier. * *

    The argument is a string or set of strings that indicates the expression(s) that must be held, - * using the syntax of + * using the syntax of * Java expressions described in the manual. The expressions evaluate to an intrinsic (built-in, * synchronization) monitor or an explicit {@link java.util.concurrent.locks.Lock}. The expression * {@code ""} is also permitted; the type {@code @GuardedBy("") Object o} indicates that @@ -52,12 +54,24 @@ 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. * - * @see Syntax of + * @see Syntax of * Java expressions */ @JavaExpression diff --git a/checker/src/main/java/org/checkerframework/checker/lock/qual/GuardedByBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/GuardedByBottom.java similarity index 86% rename from checker/src/main/java/org/checkerframework/checker/lock/qual/GuardedByBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/lock/qual/GuardedByBottom.java index 91a66e383162..793eb8069c97 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/qual/GuardedByBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/GuardedByBottom.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.lock.qual; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the GuardedBy type system. Programmers should rarely write this type. @@ -21,6 +22,6 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) -@SubtypeOf({GuardedBy.class, GuardSatisfied.class}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) +@SubtypeOf({NewObject.class}) public @interface GuardedByBottom {} diff --git a/checker/src/main/java/org/checkerframework/checker/lock/qual/GuardedByUnknown.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/GuardedByUnknown.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/lock/qual/GuardedByUnknown.java rename to checker-qual/src/main/java/org/checkerframework/checker/lock/qual/GuardedByUnknown.java index 08def367e70e..e516921d9f6e 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/qual/GuardedByUnknown.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/GuardedByUnknown.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.lock.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * It is unknown what locks guard the value referred to by the annotated variable. Therefore, the diff --git a/checker/src/main/java/org/checkerframework/checker/lock/qual/Holding.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/Holding.java similarity index 85% rename from checker/src/main/java/org/checkerframework/checker/lock/qual/Holding.java rename to checker-qual/src/main/java/org/checkerframework/checker/lock/qual/Holding.java index 27bfc5786d2e..090fe8be23bf 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/qual/Holding.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/Holding.java @@ -1,18 +1,19 @@ package org.checkerframework.checker.lock.qual; +import org.checkerframework.framework.qual.PreconditionAnnotation; + 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.PreconditionAnnotation; /** * Indicates a method precondition: the specified expressions must be held when the annotated method * is invoked. * *

    The argument is a string or set of strings that indicates the expression(s) that must be held, - * using the syntax of + * using the syntax of * Java expressions described in the manual. The expressions evaluate to an intrinsic (built-in, * synchronization) monitor, or an explicit {@link java.util.concurrent.locks.Lock}. * @@ -28,7 +29,7 @@ /** * The Java expressions that need to be held. * - * @see Syntax of + * @see Syntax of * Java expressions */ String[] value(); diff --git a/checker/src/main/java/org/checkerframework/checker/lock/qual/LockHeld.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/LockHeld.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/lock/qual/LockHeld.java rename to checker-qual/src/main/java/org/checkerframework/checker/lock/qual/LockHeld.java index d77d8c0842f6..4d8af9898f86 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/qual/LockHeld.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/LockHeld.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.lock.qual; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.InvisibleQualifier; -import org.checkerframework.framework.qual.SubtypeOf; /** * Indicates that an expression is used as a lock and the lock is known to be held on the current diff --git a/checker/src/main/java/org/checkerframework/checker/lock/qual/LockPossiblyHeld.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/LockPossiblyHeld.java similarity index 81% rename from checker/src/main/java/org/checkerframework/checker/lock/qual/LockPossiblyHeld.java rename to checker-qual/src/main/java/org/checkerframework/checker/lock/qual/LockPossiblyHeld.java index f9216d78b8d5..0cc89dae2033 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/qual/LockPossiblyHeld.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/LockPossiblyHeld.java @@ -1,22 +1,24 @@ package org.checkerframework.checker.lock.qual; -import java.lang.annotation.Documented; -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.DefaultQualifierInHierarchy; import org.checkerframework.framework.qual.InvisibleQualifier; import org.checkerframework.framework.qual.LiteralKind; import org.checkerframework.framework.qual.QualifierForLiterals; import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; import org.checkerframework.framework.qual.TypeUseLocation; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + /** * Indicates that an expression is not known to be {@link LockHeld}. * - *

    This annotation may not be written in source code; it is an implementation detail of the - * checker. + *

    It is usually not necessary to write this annotation in source code. It is an implementation + * detail of the checker. * * @see LockHeld * @checker_framework.manual #lock-checker Lock Checker @@ -25,8 +27,9 @@ @Retention(RetentionPolicy.RUNTIME) @Target({}) @InvisibleQualifier -@SubtypeOf({}) // The top type in the hierarchy +@SubtypeOf({}) @DefaultQualifierInHierarchy @DefaultFor(value = TypeUseLocation.LOWER_BOUND, types = Void.class) +@TargetLocations({TypeUseLocation.ALL}) @QualifierForLiterals(LiteralKind.NULL) public @interface LockPossiblyHeld {} diff --git a/checker/src/main/java/org/checkerframework/checker/lock/qual/LockingFree.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/LockingFree.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/lock/qual/LockingFree.java rename to checker-qual/src/main/java/org/checkerframework/checker/lock/qual/LockingFree.java index e9737ea034ee..5c8043426de6 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/qual/LockingFree.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/LockingFree.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.lock.qual; +import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.framework.qual.InheritedAnnotation; + 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.dataflow.qual.Pure; -import org.checkerframework.dataflow.qual.SideEffectFree; -import org.checkerframework.framework.qual.InheritedAnnotation; /** * The method neither acquires nor releases locks, nor do any of the methods that it calls. More diff --git a/checker/src/main/java/org/checkerframework/checker/lock/qual/MayReleaseLocks.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/MayReleaseLocks.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/lock/qual/MayReleaseLocks.java rename to checker-qual/src/main/java/org/checkerframework/checker/lock/qual/MayReleaseLocks.java index 80742faa2527..be2247b9aff8 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/qual/MayReleaseLocks.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/MayReleaseLocks.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.lock.qual; +import org.checkerframework.framework.qual.InheritedAnnotation; + 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; /** * The method, or one of the methods it calls, might release locks that were held prior to the diff --git a/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/NewObject.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/NewObject.java new file mode 100644 index 000000000000..2fe65e1fac0e --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/NewObject.java @@ -0,0 +1,34 @@ +package org.checkerframework.checker.lock.qual; + +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.LiteralKind; +import org.checkerframework.framework.qual.QualifierForLiterals; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + +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; + +/** + * A type that represents a newly-constructed object. It can be treated as having any + * {@code @}{@link GuardedBy} type. Typically, it is only written on factory method return types. + * + * @checker_framework.manual #lock-checker Lock Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@TargetLocations({ + TypeUseLocation.LOWER_BOUND, + TypeUseLocation.UPPER_BOUND, + TypeUseLocation.CONSTRUCTOR_RESULT, + TypeUseLocation.RETURN +}) +@SubtypeOf({GuardedBy.class, GuardSatisfied.class}) +@DefaultFor(TypeUseLocation.CONSTRUCTOR_RESULT) +@QualifierForLiterals({LiteralKind.STRING, LiteralKind.PRIMITIVE}) +public @interface NewObject {} diff --git a/checker/src/main/java/org/checkerframework/checker/lock/qual/PolyGuardedBy.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/PolyGuardedBy.java similarity index 83% rename from checker/src/main/java/org/checkerframework/checker/lock/qual/PolyGuardedBy.java rename to checker-qual/src/main/java/org/checkerframework/checker/lock/qual/PolyGuardedBy.java index a0ff940d74c9..36ed882977cf 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/qual/PolyGuardedBy.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/PolyGuardedBy.java @@ -3,13 +3,6 @@ package org.checkerframework.checker.lock.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.PolymorphicQualifier; /** * A polymorphic qualifier for the GuardedBy type system. diff --git a/checker/src/main/java/org/checkerframework/checker/lock/qual/ReleasesNoLocks.java b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/ReleasesNoLocks.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/lock/qual/ReleasesNoLocks.java rename to checker-qual/src/main/java/org/checkerframework/checker/lock/qual/ReleasesNoLocks.java index dc43239f906f..f595ff969b6c 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/qual/ReleasesNoLocks.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/lock/qual/ReleasesNoLocks.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.lock.qual; +import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.framework.qual.InheritedAnnotation; + 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.dataflow.qual.Pure; -import org.checkerframework.dataflow.qual.SideEffectFree; -import org.checkerframework.framework.qual.InheritedAnnotation; /** * The method maintains a strictly nondecreasing lock held count on the current thread for any locks diff --git a/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/CreatesMustCallFor.java b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/CreatesMustCallFor.java new file mode 100644 index 000000000000..b1c71880a613 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/CreatesMustCallFor.java @@ -0,0 +1,113 @@ +package org.checkerframework.checker.mustcall.qual; + +import org.checkerframework.checker.calledmethods.qual.CalledMethods; +import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.JavaExpression; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the method resets the expression's must-call type to its declared type. This + * effectively undoes flow-sensitive type refinement. The expression is the {@code value} + * argument/element. More precisely, a call to a method annotated by this annotation changes the + * expression's must-call type to the least upper bound of its current must-call type and its + * declared must-call type. + * + *

    When calling a method annotated as {@code @CreatesMustCallFor("}expression{@code ")}, + * the {@code expression}'s static type in the Called Methods type system must be {@code @}{@link + * CalledMethods}{@code ({})}. That is, {@code expression}'s CalledMethods type must be empty. + * + *

    {@code @CreatesMustCallFor("obj")} must be written on any method that assigns a non-final, + * owning field of {@code obj} whose declared type has a must-call obligation. + * + *

    Because this annotation can only add obligations, it can be written safely on any method, even + * one that does not actually create a new obligation. Writing this annotation on a method that does + * not actually create any new obligations may lead to false alarms (warnings at correct code), but + * never to missed alarms (lack of warnings at incorrect code). + * + *

    As an example, consider the following code, which uses a {@code @CreatesMustCallFor} + * annotation to indicate that the {@code reset()} method re-assigns the {@code socket} field: + * + *

    + * @MustCall("stop")
    + * class SocketContainer {
    + *     // Note that @MustCall("close") is the default type for java.net.Socket, but it
    + *     // is included on the next line for illustrative purposes. This example would function
    + *     // identically if that qualifier were omitted.
    + *     private @Owning @MustCall("close") Socket socket = ...;
    + *
    + *     @EnsuresCalledMethods(value="this.socket", methods="close")
    + *     public void stop() throws IOException {
    + *       socket.close();
    + *     }
    + *
    + *    @CreatesMustCallFor("this")
    + *    public void reset() {
    + *      if (socket.isClosed()) {
    + *        socket = new Socket(...);
    + *      }
    + *    }
    + * }
    + * 
    + * + * A client of {@code SocketContainer} is permitted to call {@code reset()} arbitrarily many times. + * Each time it does so, a new {@code Socket} might be created. A {@code SocketContainer}'s + * must-call obligation of "stop" is fulfilled only if {@code stop()} is called after the last call + * to {@code reset()}. The {@code @CreatesMustCallFor} annotation on {@code reset()}'s declaration + * enforces this requirement: at any call to {@code reset()}, all called-methods information about + * the receiver is removed from the store of the Must Call Checker and the store of the Called + * Methods Checker, so the client has to "start over" as if a fresh {@code SocketContainer} object + * had been created. + * + *

    When the {@code -AnoCreatesMustCallFor} command-line argument is passed to the checker, this + * annotation is ignored and all fields are treated as non-owning. + * + * @checker_framework.manual #resource-leak-checker Resource Leak Checker + */ +@Target({ElementType.METHOD}) +@InheritedAnnotation +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(CreatesMustCallFor.List.class) +public @interface CreatesMustCallFor { + + /** + * Returns the expression whose must-call type is reset after a call to a method with this + * annotation. The expression must be visible in the scope immediately before each call site, so + * it can only refer to fields, the method's parameters (which should be referenced via the "#X" + * syntax, where "#1" is the first argument, #2 is the second, etc.), or {@code "this"}. The + * default is {@code "this"}. At call-sites, the viewpoint-adapted referent of expression must + * be owning (an owning field, a local variable tracked in a resource alias set, etc.) or a + * {@code reset.not.owning} error is issued. + * + * @return the expression to which must-call obligations are added when the annotated method is + * invoked + */ + @JavaExpression + String value() default "this"; + + /** + * A wrapper annotation that makes the {@link CreatesMustCallFor} annotation repeatable. + * + *

    Programmers generally do not need to write this. It is created by Java when a programmer + * writes more than one {@link CreatesMustCallFor} annotation at the same location. + * + * @checker_framework.manual #must-call-checker Must Call Checker + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD}) + @InheritedAnnotation + public static @interface List { + /** + * Return the repeatable annotations. + * + * @return the repeatable annotations + */ + CreatesMustCallFor[] value(); + } +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/InheritableMustCall.java b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/InheritableMustCall.java new file mode 100644 index 000000000000..396205c7a4a3 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/InheritableMustCall.java @@ -0,0 +1,26 @@ +package org.checkerframework.checker.mustcall.qual; + +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; + +/** + * This annotation is an alias for {@link MustCall} that applies to the type on which it is written + * and all of its subtypes. It prevents the need to annotate each subtype with an {@link + * MustCall} annotation. This annotation may only be written on a class declaration. + * + * @checker_framework.manual #must-call-checker Must Call Checker + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface InheritableMustCall { + /** + * Methods that might need to be called on the expression whose type is annotated. + * + * @return methods that might need to be called + */ + public String[] value() default {}; +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/MustCall.java b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/MustCall.java new file mode 100644 index 000000000000..c9357960e9ff --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/MustCall.java @@ -0,0 +1,39 @@ +package org.checkerframework.checker.mustcall.qual; + +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TypeUseLocation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An expression of type {@code @MustCall({"m1", "m2"})} may be obligated to call {@code m1()} + * and/or {@code m2()} before it is deallocated, but it is not obligated to call any other methods. + * + *

    This annotation is enforced by the Object Construction Checker's {@code -AcheckMustCall} mode. + * It enforces that the methods {@code m1()} and {@code m2()} are called on the annotated expression + * before it is deallocated. + * + *

    The subtyping relationship is: + * + *

    {@code @MustCall({"m1"}) <: @MustCall({"m1", "m2"})}
    + * + * @checker_framework.manual #must-call-checker Must Call Checker + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({MustCallUnknown.class}) +@DefaultQualifierInHierarchy +@DefaultFor({TypeUseLocation.EXCEPTION_PARAMETER, TypeUseLocation.UPPER_BOUND}) +public @interface MustCall { + /** + * Methods that might need to be called on the expression whose type is annotated. + * + * @return methods that might need to be called + */ + public String[] value() default {}; +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/MustCallAlias.java b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/MustCallAlias.java new file mode 100644 index 000000000000..731ad6597017 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/MustCallAlias.java @@ -0,0 +1,84 @@ +package org.checkerframework.checker.mustcall.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 polymorphic annotation represents an either-or must-call obligation. This annotation should + * always be used in pairs. On a method, it is written on some formal parameter type and on the + * method return type. On a constructor, it is written on some formal parameter type and on the + * result type. Fulfilling the must-call obligation of one is equivalent to fulfilling the must-call + * obligation of the other. Beyond its impact as a polymorphic annotation on {@code MustCall} types, + * the Resource Leak Checker uses {@link MustCallAlias} annotations to more precisely determine when + * a must-call obligation has been satisfied. + * + *

    This annotation is useful for wrapper objects. For example, consider the declaration of {@code + * java.net.Socket#getOutputStream}: + * + *

    + * @MustCall("close")
    + * class Socket {
    + *   @MustCallAlias OutputStream getOutputStream(@MustCallAlias Socket this) { ... }
    + * }
    + * + * Calling {@code close()} on the returned {@code OutputStream} will close the underlying socket, + * but the Socket may also be closed directly, which has the same effect. + * + *

    Type system semantics

    + * + * Within the Must Call Checker's type system, {@code @MustCallAlias} annotations have a semantics + * different from a standard polymorphic annotation, in that the relevant actual parameter type and + * return type at a call site are not equated in all cases. Given an actual parameter {@code p} + * passed in a {@code @MustCallAlias} position at a call site, the return type of the call is + * defined as follows: + * + *
      + *
    • If the base return type has a non-empty {@code @InheritableMustCall("m")} annotation on its + * declaration, and {@code p} has a non-empty {@code @MustCall} type, then the return type is + * {@code @MustCall("m")}. + *
    • In all other cases, the return type has the same {@code @MustCall} type as {@code p}. + *
    + * + * {@link PolyMustCall} has an identical type system semantics. This special treatment is required + * to allow for a wrapper object to have a must-call method with a different name than the must-call + * method name for the wrapped object. + * + *

    Verifying {@code @MustCallAlias} annotations

    + * + * Suppose that {@code @MustCallAlias} is written on the type of formal parameter {@code p}. + * + *

    For a constructor: + * + *

      + *
    • The constructor must always write p into exactly one field {@code f} of the new object. + *
    • Field {@code f} must be annotated {@code @}{@link Owning}. + *
    + * + * For a method: + * + *
      + *
    • All return sites must be calls to other methods or constructors with {@code @MustCallAlias} + * return types, and this method's {@code @MustCallAlias} parameter must be passed in the + * {@code MustCallAlias} position to that method or constructor (i.e., the calls must pass + * {@code @MustCallAlias} parameter through a chain of {@code @MustCallAlias}-annotated + * parameters and returns). + *
    + * + * When the -AnoResourceAliases command-line argument is passed to the checker, this annotation is + * treated identically to {@link PolyMustCall}. That is, the annotation still impacts {@link + * MustCall} types as a polymorphic annotation (see "Type system semantics" above), but it is not + * used by the Resource Leak Checker to more precisely reason about when obligations have been + * satisfied. + * + * @checker_framework.manual #resource-leak-checker Resource Leak Checker + * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +// In Java 11, this can be: +// @Target({ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.METHOD}) +@Target({ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE_USE}) +public @interface MustCallAlias {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/MustCallUnknown.java b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/MustCallUnknown.java new file mode 100644 index 000000000000..7f234fc1faca --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/MustCallUnknown.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.mustcall.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The top qualifier in the Must Call type hierarchy. It represents a type that might have an + * obligation to call any set (even an infinite set!) of methods. This type contains every object. + * This type should rarely be written by a programmer. + * + *

    The Object Construction Checker cannot verify that the property represented by this annotation + * is enforced; that is, the Object Construction Checker will always issue a warning when the value + * of an expression with this type might be de-allocated. + * + * @checker_framework.manual #must-call-checker Must Call Checker + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) +public @interface MustCallUnknown {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/NotOwning.java b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/NotOwning.java new file mode 100644 index 000000000000..9fdc5ee744bc --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/NotOwning.java @@ -0,0 +1,23 @@ +package org.checkerframework.checker.mustcall.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation indicating that ownership should not be transferred to the annotated parameter, field, + * or method's call sites, for the purposes of Must Call checking. For a full description of the + * semantics, see the documentation of {@link Owning}. + * + *

    Formal parameters and fields are {@link NotOwning} by default. Method return types are + * {@code @Owning} by default. Constructor results are always {@code @Owning}. + * + *

    When the {@code -AnoLightweightOwnership} command-line argument is passed to the checker, this + * annotation and {@link Owning} are ignored. + * + * @checker_framework.manual #resource-leak-checker Resource Leak Checker + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) +public @interface NotOwning {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/Owning.java b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/Owning.java new file mode 100644 index 000000000000..dd2473fcfc20 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/Owning.java @@ -0,0 +1,36 @@ +package org.checkerframework.checker.mustcall.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation indicating that ownership should be transferred to the annotated element for the + * purposes of Must Call checking. When written on a parameter, the annotation indicates that Must + * Call checking should be performed in the body of the method, not at call sites. When written on a + * method, the annotation indicates that return expressions do not need to be checked in the method + * body, but at call sites. When written on a field, the annotation indicates that fulfilling the + * must-call obligations of an instance of the class in which the field is declared also results in + * the annotated field's must-call obligations being satisfied. Static fields cannot be owning. + * + *

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

    Constructor results are always {@code @Owning}. Method return types are {@code @Owning} by + * default. Formal parameters and fields are {@link NotOwning} by default. + * + *

    When the {@code -AnoLightweightOwnership} command-line argument is passed to the checker, this + * annotation and {@link NotOwning} are ignored. + * + * @checker_framework.manual #resource-leak-checker Resource Leak Checker + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD}) +public @interface Owning {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/PolyMustCall.java b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/PolyMustCall.java new file mode 100644 index 000000000000..3e76f1dfb7ad --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/PolyMustCall.java @@ -0,0 +1,23 @@ +package org.checkerframework.checker.mustcall.qual; + +import org.checkerframework.framework.qual.PolymorphicQualifier; + +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; + +/** + * The polymorphic qualifier for the Must Call type system. The semantics of this qualifier differ + * from that of a standard polymorphic qualifier; see {@link MustCallAlias} for documentation of its + * semantics. + * + * @checker_framework.manual #must-call-checker Must Call Checker + * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@PolymorphicQualifier(MustCallUnknown.class) +public @interface PolyMustCall {} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/qual/AssertNonNullIfNonNull.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/AssertNonNullIfNonNull.java similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/nullness/qual/AssertNonNullIfNonNull.java rename to checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/AssertNonNullIfNonNull.java diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresKeyFor.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresKeyFor.java similarity index 98% rename from checker/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresKeyFor.java rename to checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresKeyFor.java index b9498fcd6758..84b0200fba11 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresKeyFor.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresKeyFor.java @@ -1,15 +1,16 @@ package org.checkerframework.checker.nullness.qual; +import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.JavaExpression; +import org.checkerframework.framework.qual.PostconditionAnnotation; +import org.checkerframework.framework.qual.QualifierArgument; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.InheritedAnnotation; -import org.checkerframework.framework.qual.JavaExpression; -import org.checkerframework.framework.qual.PostconditionAnnotation; -import org.checkerframework.framework.qual.QualifierArgument; /** * Indicates that the value expressions evaluate to a value that is a key in all the given maps, if @@ -66,7 +67,7 @@ @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) @PostconditionAnnotation(qualifier = KeyFor.class) @InheritedAnnotation - @interface List { + public static @interface List { /** * Returns the repeatable annotations. * diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresKeyForIf.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresKeyForIf.java similarity index 98% rename from checker/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresKeyForIf.java rename to checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresKeyForIf.java index 82c184c7abdf..017b10c554f9 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresKeyForIf.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresKeyForIf.java @@ -1,15 +1,16 @@ package org.checkerframework.checker.nullness.qual; +import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; +import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.JavaExpression; +import org.checkerframework.framework.qual.QualifierArgument; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; -import org.checkerframework.framework.qual.InheritedAnnotation; -import org.checkerframework.framework.qual.JavaExpression; -import org.checkerframework.framework.qual.QualifierArgument; /** * Indicates that the given expressions evaluate to a value that is a key in all the given maps, if @@ -69,7 +70,7 @@ @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) @ConditionalPostconditionAnnotation(qualifier = KeyFor.class) @InheritedAnnotation - @interface List { + public static @interface List { /** * Returns the repeatable annotations. * diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNull.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNull.java similarity index 80% rename from checker/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNull.java rename to checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNull.java index 109f2ace91ce..849c8d340cf2 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNull.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNull.java @@ -1,38 +1,41 @@ package org.checkerframework.checker.nullness.qual; +import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.PostconditionAnnotation; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.InheritedAnnotation; -import org.checkerframework.framework.qual.PostconditionAnnotation; -// TODO: In a fix for https://tinyurl.com/cfissue/1917, add the text: -// Every prefix expression is also non-null; for example, {@code -// @EnsuresNonNull(expression="a.b.c")} implies that both {@code a.b} and {@code a.b.c} are -// non-null. +// TODO: In a fix for https://tinyurl.com/cfissue/1917, add the text: Every prefix expression is +// also non-null; for example, {@code @EnsuresNonNull(expression="a.b.c")} implies that both {@code +// a.b} and {@code a.b.c} are non-null. /** - * Indicates that the value expressions are non-null, if the method terminates successfully. + * Indicates that the value expressions are non-null just after a method call, if the method + * terminates successfully. * *

    This postcondition annotation is useful for methods that initialize a field: * *

    
    - * {@literal @}EnsuresNonNull("theMap")
    - *  public static void initialize() {
    + *  {@literal @}EnsuresNonNull("theMap")
    + *  void initialize() {
      *    theMap = new HashMap<>();
      *  }
      * 
    * - * It can also be used for a method that fails if a given expression is null: + * It can also be used for a method that fails if a given expression is null, indicating that the + * argument is null if the method returns normally: * *
    
      *  /** Throws an exception if the argument is null. */
    - * {@literal @}EnsuresNonNull("#1")
    + *  {@literal @}EnsuresNonNull("#1")
      *  void assertNonNull(Object arg) { ... }
      * 
    * + * @see EnsuresNonNullIf * @see NonNull * @see org.checkerframework.checker.nullness.NullnessChecker * @checker_framework.manual #nullness-checker Nullness Checker @@ -63,7 +66,7 @@ @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) @PostconditionAnnotation(qualifier = NonNull.class) @InheritedAnnotation - @interface List { + public static @interface List { /** * Returns the repeatable annotations. * diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNullIf.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNullIf.java similarity index 91% rename from checker/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNullIf.java rename to checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNullIf.java index 415e52a4f0bf..464c6f570edd 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNullIf.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/EnsuresNonNullIf.java @@ -1,18 +1,18 @@ package org.checkerframework.checker.nullness.qual; +import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; +import org.checkerframework.framework.qual.InheritedAnnotation; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; -import org.checkerframework.framework.qual.InheritedAnnotation; -// TODO: In a fix for https://tinyurl.com/cfissue/1917, add the text: -// Every prefix expression is also non-null; for example, -// {@code @EnsuresNonNullIf(expression="a.b.c", results=true)} implies that both {@code a.b} and -// {@code a.b.c} are non-null if the method returns {@code true}. +// TODO: In a fix for https://tinyurl.com/cfissue/1917, add the text: Every prefix expression is +// also non-null; for example, {@code @EnsuresNonNullIf(expression="a.b.c", results=true)} implies +// that both {@code a.b} and {@code a.b.c} are non-null if the method returns {@code true}. /** * Indicates that the given expressions are non-null, if the method returns the given result (either * true or false). @@ -47,8 +47,7 @@ * }}
    * * An {@code EnsuresNonNullIf} annotation that refers to a private field is useful for verifying - * that client code performs needed checks in the right order, even if the client code cannot - * directly affect the field. + * that a method establishes a property, even though client code cannot directly affect the field. * *

    Method calls: If {@link Class#isArray()} returns true, then {@link * Class#getComponentType()} returns non-null. You can express this relationship as: @@ -77,19 +76,19 @@ @Repeatable(EnsuresNonNullIf.List.class) public @interface EnsuresNonNullIf { /** - * Returns Java expression(s) that are non-null after the method returns the given result. + * Returns the return value of the method under which the postcondition holds. * - * @return Java expression(s) that are non-null after the method returns the given result - * @checker_framework.manual #java-expressions-as-arguments Syntax of Java expressions + * @return the return value of the method under which the postcondition holds */ - String[] expression(); + boolean result(); /** - * Returns the return value of the method under which the postcondition holds. + * Returns Java expression(s) that are non-null after the method returns the given result. * - * @return the return value of the method under which the postcondition holds + * @return Java expression(s) that are non-null after the method returns the given result + * @checker_framework.manual #java-expressions-as-arguments Syntax of Java expressions */ - boolean result(); + String[] expression(); /** * * A wrapper annotation that makes the {@link EnsuresNonNullIf} annotation repeatable. @@ -102,7 +101,7 @@ @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) @ConditionalPostconditionAnnotation(qualifier = NonNull.class) @InheritedAnnotation - @interface List { + public static @interface List { /** * Returns the repeatable annotations. * diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/qual/KeyFor.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/KeyFor.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/nullness/qual/KeyFor.java rename to checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/KeyFor.java index c79d650e2e33..cf0013289f95 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/qual/KeyFor.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/KeyFor.java @@ -1,12 +1,13 @@ package org.checkerframework.checker.nullness.qual; +import org.checkerframework.framework.qual.JavaExpression; +import org.checkerframework.framework.qual.SubtypeOf; + 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.JavaExpression; -import org.checkerframework.framework.qual.SubtypeOf; /** * Indicates that the value assigned to the annotated variable is a key for at least the given diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/qual/KeyForBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/KeyForBottom.java similarity index 91% rename from checker/src/main/java/org/checkerframework/checker/nullness/qual/KeyForBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/KeyForBottom.java index c87995610edc..5ed2010f703b 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/qual/KeyForBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/KeyForBottom.java @@ -1,14 +1,15 @@ package org.checkerframework.checker.nullness.qual; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.InvisibleQualifier; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the Map Key type system. Programmers should rarely write this type. @@ -21,7 +22,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @InvisibleQualifier @SubtypeOf(KeyFor.class) public @interface KeyForBottom {} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/qual/MonotonicNonNull.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/MonotonicNonNull.java similarity index 75% rename from checker/src/main/java/org/checkerframework/checker/nullness/qual/MonotonicNonNull.java rename to checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/MonotonicNonNull.java index 7eacf3f7677e..7fae2732ed35 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/qual/MonotonicNonNull.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/MonotonicNonNull.java @@ -1,12 +1,13 @@ package org.checkerframework.checker.nullness.qual; +import org.checkerframework.framework.qual.MonotonicQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + 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.MonotonicQualifier; -import org.checkerframework.framework.qual.SubtypeOf; /** * Indicates that once the field (or variable) becomes non-null, it never becomes null again. There @@ -23,23 +24,20 @@ * * *

    When the field is first read within a method, the field cannot be assumed to be non-null. - * After a check that a {@code MonotonicNonNull} field holds a non-null value, all subsequent + * After a check that a {@code @MonotonicNonNull} field holds a non-null value, all subsequent * accesses within that method can be assumed to be non-null, even after arbitrary external * method calls that might access the field. * - *

    {@code MonotonicNonNull} gives stronger guarantees than {@link Nullable}. After a check that a - * {@link Nullable} field holds a non-null value, only accesses until the next non-{@link + *

    {@code @MonotonicNonNull} gives stronger guarantees than {@link Nullable}. After a check that + * a {@link Nullable} field holds a non-null value, only accesses until the next non-{@link * org.checkerframework.dataflow.qual.SideEffectFree} method is called can be assumed to be * non-null. * - *

    To indicate that a {@code MonotonicNonNull} or {@code Nullable} field is non-null whenever a - * particular method is called, use {@link RequiresNonNull}. + *

    To indicate that a {@code @MonotonicNonNull} or {@code @Nullable} field is non-null whenever a + * particular method is called, use {@code @}{@link RequiresNonNull}. * *

    Final fields are treated as MonotonicNonNull by default. * - *

    This annotation is associated with the {@link - * org.checkerframework.checker.nullness.NullnessChecker}. - * * @see EnsuresNonNull * @see RequiresNonNull * @see MonotonicQualifier diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/NonNull.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/NonNull.java new file mode 100644 index 000000000000..1eb704f02578 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/NonNull.java @@ -0,0 +1,57 @@ +package org.checkerframework.checker.nullness.qual; + +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.LiteralKind; +import org.checkerframework.framework.qual.QualifierForLiterals; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TypeKind; +import org.checkerframework.framework.qual.TypeUseLocation; +import org.checkerframework.framework.qual.UpperBoundFor; + +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; + +/** + * If an expression's type is qualified by {@code @NonNull}, then the expression never evaluates to + * {@code null}. (Unless the program has a bug; annotations specify intended behavior.) + * + *

    For fields of a class, the {@link NonNull} annotation indicates that this field is never + * {@code null} after the class has been fully initialized. For static fields, the {@link + * NonNull} annotation indicates that this field is never {@code null} after the containing + * class is initialized. "Fully initialized" essentially means that the Java constructor has + * completed. See the Initialization Checker + * documentation for more details. + * + *

    This annotation is rarely written in source code, because it is the default. + * + * @see Nullable + * @see MonotonicNonNull + * @checker_framework.manual #nullness-checker Nullness Checker + * @checker_framework.manual #initialization-checker Initialization Checker + * @checker_framework.manual #bottom-type the bottom type + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(MonotonicNonNull.class) +@DefaultQualifierInHierarchy +@QualifierForLiterals(LiteralKind.STRING) +@DefaultFor(TypeUseLocation.EXCEPTION_PARAMETER) +@UpperBoundFor( + typeKinds = { + TypeKind.PACKAGE, + TypeKind.INT, + TypeKind.BOOLEAN, + TypeKind.CHAR, + TypeKind.DOUBLE, + TypeKind.FLOAT, + TypeKind.LONG, + TypeKind.SHORT, + TypeKind.BYTE + }) +public @interface NonNull {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/Nullable.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/Nullable.java new file mode 100644 index 000000000000..74a3bf012366 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/Nullable.java @@ -0,0 +1,36 @@ +package org.checkerframework.checker.nullness.qual; + +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.LiteralKind; +import org.checkerframework.framework.qual.QualifierForLiterals; +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * {@link Nullable} is a type annotation that makes no commitments about whether the value is {@code + * null}. Equivalently, the type includes the {@code null} value. + * + *

    The Nullness Checker issues an error if {@code null} is assigned an expression of {@link + * NonNull} type. + * + *

    Programmers typically write {@code @Nullable} to indicate that the value is not known to be + * {@link NonNull}. However, since {@code @Nullable} is a supertype of {@code @NonNull}, an + * expression that never evaluates to {@code null} can have a declared type of {@code @Nullable}. + * + * @see NonNull + * @see MonotonicNonNull + * @see org.checkerframework.checker.nullness.NullnessChecker + * @checker_framework.manual #nullness-checker Nullness Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) +@QualifierForLiterals(LiteralKind.NULL) +@DefaultFor(types = Void.class) +public @interface Nullable {} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/qual/PolyKeyFor.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/PolyKeyFor.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/nullness/qual/PolyKeyFor.java rename to checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/PolyKeyFor.java index 6a89836561b6..c5dfc57550d4 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/qual/PolyKeyFor.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/PolyKeyFor.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.nullness.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * A polymorphic qualifier for the Map Key (@KeyFor) type system. diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/qual/PolyNull.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/PolyNull.java similarity index 76% rename from checker/src/main/java/org/checkerframework/checker/nullness/qual/PolyNull.java rename to checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/PolyNull.java index 1ae6b204c800..bf42619c94b3 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/qual/PolyNull.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/PolyNull.java @@ -1,19 +1,19 @@ package org.checkerframework.checker.nullness.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * A polymorphic qualifier for the non-null type system. * *

    Any method written using {@link PolyNull} conceptually has two versions: one in which every - * instance of {@link PolyNull} has been replaced by {@link - * org.checkerframework.checker.nullness.qual.NonNull}, and one in which every instance of {@link - * PolyNull} has been replaced by {@link org.checkerframework.checker.nullness.qual.Nullable}. + * instance of {@link PolyNull} has been replaced by {@link NonNull}, and one in which every + * instance of {@link PolyNull} has been replaced by {@link Nullable}. * * @checker_framework.manual #nullness-checker Nullness Checker * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism diff --git a/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/RequiresNonNull.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/RequiresNonNull.java new file mode 100644 index 000000000000..29c5dc2b7f57 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/RequiresNonNull.java @@ -0,0 +1,97 @@ +package org.checkerframework.checker.nullness.qual; + +import org.checkerframework.framework.qual.PreconditionAnnotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +// TODO: In a fix for https://tinyurl.com/cfissue/1917, add the text: Every prefix expression must +// also be non-null; for example, {@code @RequiresNonNull(expression="a.b.c")} implies that both +// {@code a.b} and {@code a.b.c} must be non-null. +/** + * Indicates a method precondition: the method expects the specified expressions to be non-null when + * the annotated method is invoked. + * + *

    For example: + * + * + *

    + * import org.checkerframework.checker.nullness.qual.RequiresNonNull;
    + * import org.checkerframework.checker.nullness.qual.NonNull;
    + * import org.checkerframework.checker.nullness.qual.Nullable;
    + *
    + * class MyClass {
    + *   @Nullable Object field1;
    + *   @Nullable Object field2;
    + *
    + *   @RequiresNonNull({"field1", "#1.field1"})
    + *   void method1(@NonNull MyClass other) {
    + *     field1.toString();           // OK, this.field1 is known to be non-null
    + *     field2.toString();           // error, might throw NullPointerException
    + *     other.field1.toString();     // OK, other.field1 is known to be non-null
    + *     other.field2.toString();     // error, might throw NullPointerException
    + *   }
    + *
    + *   void method2() {
    + *     MyClass other = new MyClass();
    + *
    + *     field1 = new Object();
    + *     other.field1 = new Object();
    + *     method1(other);                   // OK, satisfies method precondition
    + *
    + *     field1 = null;
    + *     other.field1 = new Object();
    + *     method1(other);                   // error, does not satisfy this.field1 method precondition
    + *
    + *     field1 = new Object();
    + *     other.field1 = null;
    + *     method1(other);                   // error, does not satisfy other.field1 method precondition
    + *   }
    + * }
    + * 
    + * + * Do not use this annotation for formal parameters (instead, give them a {@code @NonNull} type, + * which is the default and need not be written). The {@code @RequiresNonNull} annotation is + * intended for non-parameter expressions, such as field accesses or method calls. + * + * @checker_framework.manual #nullness-checker Nullness Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@Repeatable(RequiresNonNull.List.class) +@PreconditionAnnotation(qualifier = NonNull.class) +public @interface RequiresNonNull { + /** + * The Java expressions that need to be {@link + * org.checkerframework.checker.nullness.qual.NonNull}. + * + * @return the Java expressions that need to be {@link + * org.checkerframework.checker.nullness.qual.NonNull} + * @checker_framework.manual #java-expressions-as-arguments Syntax of Java expressions + */ + String[] value(); + + /** + * A wrapper annotation that makes the {@link RequiresNonNull} annotation repeatable. + * + *

    Programmers generally do not need to write this. It is created by Java when a programmer + * writes more than one {@link RequiresNonNull} annotation at the same location. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) + @PreconditionAnnotation(qualifier = NonNull.class) + public static @interface List { + /** + * Returns the repeatable annotations. + * + * @return the repeatable annotations + */ + RequiresNonNull[] value(); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/qual/UnknownKeyFor.java b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/UnknownKeyFor.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/nullness/qual/UnknownKeyFor.java rename to checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/UnknownKeyFor.java index 19e423ca910f..72fbdf4f4848 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/qual/UnknownKeyFor.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/nullness/qual/UnknownKeyFor.java @@ -1,10 +1,5 @@ package org.checkerframework.checker.nullness.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.DefaultFor; import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; import org.checkerframework.framework.qual.InvisibleQualifier; @@ -13,6 +8,12 @@ import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.TypeUseLocation; +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; + /** * Used internally by the type system; should never be written by a programmer. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/EnsuresPresent.java b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/EnsuresPresent.java new file mode 100644 index 000000000000..a0037970bf8f --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/EnsuresPresent.java @@ -0,0 +1,50 @@ +package org.checkerframework.checker.optional.qual; + +import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.PostconditionAnnotation; + +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; + +/** + * Indicates that the expression evaluates to a non-empty Optional, if the method terminates + * successfully. + * + *

    This postcondition annotation is useful for methods that construct a non-empty Optional: + * + *

    
    + *   {@literal @}EnsuresPresent("optStr")
    + *   void initialize() {
    + *     optStr = Optional.of("abc");
    + *   }
    + * 
    + * + * It can also be used for a method that fails if a given Optional value is empty, indicating that + * the argument is present if the method returns normally: + * + *
    
    + *   /** Throws an exception if the argument is empty. */
    + *   {@literal @}EnsuresPresent("#1")
    + *   void useTheOptional(Optional<T> arg) { ... }
    + * 
    + * + * @see Present + * @see org.checkerframework.checker.optional.OptionalChecker + * @checker_framework.manual #optional-checker Optional Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@PostconditionAnnotation(qualifier = Present.class) +@InheritedAnnotation +public @interface EnsuresPresent { + /** + * The expression (of Optional type) that is present, if the method returns normally. + * + * @return the expression (of Optional type) that is present, if the method returns normally + */ + String[] value(); +} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/EnsuresPresentIf.java b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/EnsuresPresentIf.java new file mode 100644 index 000000000000..0e4fa63f8463 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/EnsuresPresentIf.java @@ -0,0 +1,91 @@ +package org.checkerframework.checker.optional.qual; + +import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; +import org.checkerframework.framework.qual.InheritedAnnotation; + +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; + +/** + * Indicates that the given expressions of type Optional<T> are present, if the method returns + * the given result (either true or false). + * + *

    Here are ways this conditional postcondition annotation can be used: + * + *

    Method parameters: Suppose that a method has two arguments of type Optional<T> + * and returns true if they are both equal and present. You could annotate the method as + * follows: + * + *

      @EnsuresPresentIf(expression="#1", result=true)
    + *  @EnsuresPresentIf(expression="#2", result=true)
    + *  public <T> boolean isPresentAndEqual(Optional<T> optA, Optional<T> optB) { ... }
    + * 
    + * + * because, if {@code isPresentAndEqual} returns true, then the first (#1) argument to {@code + * isPresentAndEqual} was present, and so was the second (#2) argument. Note that you can write two + * {@code @EnsuresPresentIf} annotations on a single method. + * + *

    Fields: The value expressions can refer to fields, even private ones. For example: + * + *

      @EnsuresPresentIf(expression="this.optShape", result=true)
    + *  public boolean isShape() {
    + *    return (this.optShape != null && this.optShape.isPresent());
    + *  }
    + * + * An {@code @EnsuresPresentIf} annotation that refers to a private field is useful for verifying + * that a method establishes a property, even though client code cannot directly affect the field. + * + *

    Method postconditions: Suppose that if a method {@code isRectangle()} returns true, + * then {@code getRectangle()} will return a present Optional. You an express this relationship as: + * + *

    {@code  @EnsuresPresentIf(result=true, expression="getRectangle()")
    + * public @Pure isRectangle() { ... }}
    + * + * @see Present + * @see EnsuresPresent + * @checker_framework.manual #optional-checker Optional Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@ConditionalPostconditionAnnotation(qualifier = Present.class) +@InheritedAnnotation +public @interface EnsuresPresentIf { + /** + * Returns the return value of the method under which the postcondition holds. + * + * @return the return value of the method under which the postcondition holds + */ + boolean result(); + + /** + * Returns the Java expressions of type {@code Optional} that are present after the method + * returns the given result. + * + * @return the Java expressions of type {@code Optional} that are present after the method + * returns the given result + * @checker_framework.manual #java-expressions-as-arguments Syntax of Java expressions + */ + String[] expression(); + + /** + * A wrapper annotation that makes the {@link EnsuresPresentIf} annotation repeatable. + * + *

    Programmers generally do not need to write this. It is created by Java when a programmer + * writes more than one {@link EnsuresPresentIf} annotation at the same location. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) + @ConditionalPostconditionAnnotation(qualifier = Present.class) + public static @interface List { + /** + * Returns the repeatable annotations. + * + * @return the repeatable annotations + */ + EnsuresPresentIf[] value(); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/optional/qual/MaybePresent.java b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/MaybePresent.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/optional/qual/MaybePresent.java rename to checker-qual/src/main/java/org/checkerframework/checker/optional/qual/MaybePresent.java index b37ac127ffd0..49455dbadf07 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/qual/MaybePresent.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/MaybePresent.java @@ -1,12 +1,13 @@ package org.checkerframework.checker.optional.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; /** * The {@link java.util.Optional Optional} container may or may not contain a value. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/OptionalBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/OptionalBottom.java new file mode 100644 index 000000000000..3205701f76bb --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/OptionalBottom.java @@ -0,0 +1,21 @@ +package org.checkerframework.checker.optional.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * The bottom type qualifier for the Optional Checker. The only value of this type is {@code null}. + * Programmers rarely write this annotation. + * + * @checker_framework.manual #optional-checker Optional Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({Present.class}) +public @interface OptionalBottom {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/OptionalCreator.java b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/OptionalCreator.java new file mode 100644 index 000000000000..327773dc1bd9 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/OptionalCreator.java @@ -0,0 +1,16 @@ +package org.checkerframework.checker.optional.qual; + +import org.checkerframework.framework.qual.InheritedAnnotation; + +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; + +/** Method annotation for methods that create an {@link java.util.Optional}. */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@InheritedAnnotation +public @interface OptionalCreator {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/OptionalEliminator.java b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/OptionalEliminator.java new file mode 100644 index 000000000000..1e2fc09ad7f5 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/OptionalEliminator.java @@ -0,0 +1,19 @@ +package org.checkerframework.checker.optional.qual; + +import org.checkerframework.framework.qual.InheritedAnnotation; + +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; + +/** + * Method annotation for methods whose receiver is an {@link java.util.Optional} and return a + * non-optional. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@InheritedAnnotation +public @interface OptionalEliminator {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/OptionalPropagator.java b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/OptionalPropagator.java new file mode 100644 index 000000000000..9a0238686296 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/OptionalPropagator.java @@ -0,0 +1,19 @@ +package org.checkerframework.checker.optional.qual; + +import org.checkerframework.framework.qual.InheritedAnnotation; + +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; + +/** + * Method annotation for methods whose receiver is an {@link java.util.Optional} and return an + * {@code Optional}. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@InheritedAnnotation +public @interface OptionalPropagator {} diff --git a/checker/src/main/java/org/checkerframework/checker/optional/qual/PolyPresent.java b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/PolyPresent.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/optional/qual/PolyPresent.java rename to checker-qual/src/main/java/org/checkerframework/checker/optional/qual/PolyPresent.java index e462ed7732a7..d159dd3a6bbd 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/qual/PolyPresent.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/PolyPresent.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.optional.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * A polymorphic qualifier for the Optional type system. diff --git a/checker/src/main/java/org/checkerframework/checker/optional/qual/Present.java b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/Present.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/optional/qual/Present.java rename to checker-qual/src/main/java/org/checkerframework/checker/optional/qual/Present.java index 90c59e2c849b..90261fe72e69 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/qual/Present.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/Present.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.optional.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * The {@link java.util.Optional Optional} container definitely contains a (non-null) value. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/RequiresPresent.java b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/RequiresPresent.java new file mode 100644 index 000000000000..d6173ca97a73 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/optional/qual/RequiresPresent.java @@ -0,0 +1,92 @@ +package org.checkerframework.checker.optional.qual; + +import org.checkerframework.framework.qual.PreconditionAnnotation; + +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; + +/** + * Indicates a method precondition: the specified expressions of type Optional must be present + * (i.e., non-empty) when the annotated method is invoked. + * + *

    For example: + * + *

    + * import java.util.Optional;
    + *
    + * import org.checkerframework.checker.optional.qual.RequiresPresent;
    + * import org.checkerframework.checker.optional.qual.Present;
    + *
    + * class MyClass {
    + *   Optional<String> optId1;
    + *   Optional<String> optId2;
    + *
    + *   @RequiresPresent({"optId1", "#1.optId1"})
    + *   void method1(MyClass other) {
    + *     optId1.get().length()       // OK, this.optID1 is known to be present.
    + *     optId2.get().length()       // error, might throw NoSuchElementException.
    + *
    + *     other.optId1.get().length() // OK, this.optID1 is known to be present.
    + *     other.optId2.get().length() // error, might throw NoSuchElementException.
    + *   }
    + *
    + *   void method2() {
    + *     MyClass other = new MyClass();
    + *
    + *     optId1 = Optional.of("abc");
    + *     other.optId1 = Optional.of("def")
    + *     method1(other);                       // OK, satisfies method precondition.
    + *
    + *     optId1 = Optional.empty();
    + *     other.optId1 = Optional.empty("abc");
    + *     method1(other);                       // error, does not satisfy this.optId1 method precondition.
    + *
    + *     optId1 = Optional.empty("abc");
    + *     other.optId1 = Optional.empty();
    + *     method1(other);                       // error. does not satisfy other.optId1 method precondition.
    + *   }
    + * }
    + * 
    + * + * Do not use this annotation for formal parameters (instead, give them a {@code @Present} type). + * The {@code @RequiresNonNull} annotation is intended for non-parameter expressions, such as field + * accesses or method calls. + * + * @checker_framework.manual #optional-checker Optional Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@PreconditionAnnotation(qualifier = Present.class) +public @interface RequiresPresent { + + /** + * The Java expressions that that need to be {@link Present}. + * + * @return the Java expressions that need to be {@link Present} + * @checker_framework.manual #java-expressions-as-arguments Syntax of Java expressions + */ + String[] value(); + + /** + * A wrapper annotation that makes the {@link RequiresPresent} annotation repeatable. + * + *

    Programmers generally do not need to write this. It is created by Java when a programmer + * writes more than one {@link RequiresPresent} annotation at the same location. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) + @PreconditionAnnotation(qualifier = Present.class) + public static @interface List { + /** + * Returns the repeatable annotations. + * + * @return the repeatable annotations + */ + RequiresPresent[] value(); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/propkey/qual/PropertyKey.java b/checker-qual/src/main/java/org/checkerframework/checker/propkey/qual/PropertyKey.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/propkey/qual/PropertyKey.java rename to checker-qual/src/main/java/org/checkerframework/checker/propkey/qual/PropertyKey.java index 712fc8f4b275..ecb47f99bcd9 100644 --- a/checker/src/main/java/org/checkerframework/checker/propkey/qual/PropertyKey.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/propkey/qual/PropertyKey.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.propkey.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Indicates that the {@code String} type can be used as key in a property file or resource bundle. diff --git a/checker/src/main/java/org/checkerframework/checker/propkey/qual/PropertyKeyBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/propkey/qual/PropertyKeyBottom.java similarity index 91% rename from checker/src/main/java/org/checkerframework/checker/propkey/qual/PropertyKeyBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/propkey/qual/PropertyKeyBottom.java index e3b40a9c625f..e45d61a8ab84 100644 --- a/checker/src/main/java/org/checkerframework/checker/propkey/qual/PropertyKeyBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/propkey/qual/PropertyKeyBottom.java @@ -1,14 +1,15 @@ package org.checkerframework.checker.propkey.qual; +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.DefaultFor; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the PropertyKeyChecker (and associated checkers) qualifier hierarchy. @@ -20,7 +21,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @SubtypeOf(PropertyKey.class) @DefaultFor(TypeUseLocation.LOWER_BOUND) public @interface PropertyKeyBottom {} diff --git a/checker/src/main/java/org/checkerframework/checker/propkey/qual/UnknownPropertyKey.java b/checker-qual/src/main/java/org/checkerframework/checker/propkey/qual/UnknownPropertyKey.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/propkey/qual/UnknownPropertyKey.java rename to checker-qual/src/main/java/org/checkerframework/checker/propkey/qual/UnknownPropertyKey.java index 89a55a0fb36f..65f9516faafd 100644 --- a/checker/src/main/java/org/checkerframework/checker/propkey/qual/UnknownPropertyKey.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/propkey/qual/UnknownPropertyKey.java @@ -1,12 +1,13 @@ package org.checkerframework.checker.propkey.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; /** * Indicates that the {@code String} type has an unknown property key property. diff --git a/checker/src/main/java/org/checkerframework/checker/regex/qual/PartialRegex.java b/checker-qual/src/main/java/org/checkerframework/checker/regex/qual/PartialRegex.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/regex/qual/PartialRegex.java rename to checker-qual/src/main/java/org/checkerframework/checker/regex/qual/PartialRegex.java index 168dc48e3536..edfd6b591e27 100644 --- a/checker/src/main/java/org/checkerframework/checker/regex/qual/PartialRegex.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/regex/qual/PartialRegex.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.regex.qual; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.InvisibleQualifier; -import org.checkerframework.framework.qual.SubtypeOf; /** * Indicates a String that is not a syntactically valid regular expression. The String itself can be diff --git a/checker-qual/src/main/java/org/checkerframework/checker/regex/qual/PolyRegex.java b/checker-qual/src/main/java/org/checkerframework/checker/regex/qual/PolyRegex.java new file mode 100644 index 000000000000..2ed68f54b64a --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/regex/qual/PolyRegex.java @@ -0,0 +1,27 @@ +package org.checkerframework.checker.regex.qual; + +import org.checkerframework.framework.qual.PolymorphicQualifier; + +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; + +/** + * A polymorphic qualifier for the Regex type system. + * + *

    Any method written using {@link PolyRegex} conceptually has multiple versions: one in which + * all instances of {@link PolyRegex} in the method signature have been replaced by one of the + * following qualifiers: {@link Regex}, which takes an integer argument to represent different + * capturing groups; {@link PartialRegex}, which takes a string argument to represent different + * partial regexes; {@link UnknownRegex}; and {@link RegexBottom}. + * + * @checker_framework.manual #regex-checker Regex Checker + * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@PolymorphicQualifier(UnknownRegex.class) +public @interface PolyRegex {} diff --git a/checker/src/main/java/org/checkerframework/checker/regex/qual/Regex.java b/checker-qual/src/main/java/org/checkerframework/checker/regex/qual/Regex.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/regex/qual/Regex.java rename to checker-qual/src/main/java/org/checkerframework/checker/regex/qual/Regex.java index 919a9c804f96..d7d253e0c1a0 100644 --- a/checker/src/main/java/org/checkerframework/checker/regex/qual/Regex.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/regex/qual/Regex.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.regex.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * If a type is annotated as {@code @Regex(n)}, then the run-time value is a regular expression with diff --git a/checker/src/main/java/org/checkerframework/checker/regex/qual/RegexBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/regex/qual/RegexBottom.java similarity index 91% rename from checker/src/main/java/org/checkerframework/checker/regex/qual/RegexBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/regex/qual/RegexBottom.java index e0f226b752ad..301b6b74fc4d 100644 --- a/checker/src/main/java/org/checkerframework/checker/regex/qual/RegexBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/regex/qual/RegexBottom.java @@ -1,16 +1,17 @@ package org.checkerframework.checker.regex.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.DefaultFor; import org.checkerframework.framework.qual.InvisibleQualifier; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.TargetLocations; import org.checkerframework.framework.qual.TypeUseLocation; +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; + /** * The bottom type in the Regex type system. Programmers should rarely write this type. * @@ -20,7 +21,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @InvisibleQualifier @SubtypeOf({Regex.class, org.checkerframework.checker.regex.qual.PartialRegex.class}) @DefaultFor(value = {TypeUseLocation.LOWER_BOUND}) diff --git a/checker/src/main/java/org/checkerframework/checker/regex/qual/UnknownRegex.java b/checker-qual/src/main/java/org/checkerframework/checker/regex/qual/UnknownRegex.java similarity index 90% rename from checker/src/main/java/org/checkerframework/checker/regex/qual/UnknownRegex.java rename to checker-qual/src/main/java/org/checkerframework/checker/regex/qual/UnknownRegex.java index ea1948b19ea7..398b04b7398b 100644 --- a/checker/src/main/java/org/checkerframework/checker/regex/qual/UnknownRegex.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/regex/qual/UnknownRegex.java @@ -1,16 +1,17 @@ package org.checkerframework.checker.regex.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.DefaultQualifierInHierarchy; import org.checkerframework.framework.qual.InvisibleQualifier; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.TargetLocations; import org.checkerframework.framework.qual.TypeUseLocation; +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; + /** * Represents the top of the Regex qualifier hierarchy. * @@ -19,8 +20,8 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.ALL}) @InvisibleQualifier -@DefaultQualifierInHierarchy @SubtypeOf({}) +@DefaultQualifierInHierarchy public @interface UnknownRegex {} diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/ArrayWithoutPackage.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/ArrayWithoutPackage.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/ArrayWithoutPackage.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/ArrayWithoutPackage.java index 880dcc449ee1..3ec7a61428ce 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/ArrayWithoutPackage.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/ArrayWithoutPackage.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * An identifier or primitive type, followed by any number of array square brackets. diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/BinaryName.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/BinaryName.java similarity index 95% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/BinaryName.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/BinaryName.java index c1a8acb3f1cc..d3d00e20f738 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/BinaryName.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/BinaryName.java @@ -1,15 +1,16 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Represents a binary name as defined in the Java Language + * href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-13.html#jls-13.1">Java Language * Specification, section 13.1. * *

    For example, in diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/BinaryNameOrPrimitiveType.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/BinaryNameOrPrimitiveType.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/BinaryNameOrPrimitiveType.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/BinaryNameOrPrimitiveType.java index c2d36f78e26e..c2474f3a486f 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/BinaryNameOrPrimitiveType.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/BinaryNameOrPrimitiveType.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Represents a primitive type name or a {@link BinaryName binary name}. diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/BinaryNameWithoutPackage.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/BinaryNameWithoutPackage.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/BinaryNameWithoutPackage.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/BinaryNameWithoutPackage.java index 7a28a75be7ed..becf579afed1 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/BinaryNameWithoutPackage.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/BinaryNameWithoutPackage.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Represents a string that is a {@link BinaryName}, an {@link InternalForm}, and a {@link diff --git a/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/CanonicalName.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/CanonicalName.java new file mode 100644 index 000000000000..862398dfc867 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/CanonicalName.java @@ -0,0 +1,50 @@ +package org.checkerframework.checker.signature.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * Canonical names have the same syntactic form as {@link FullyQualifiedName fully-qualified name}s. + * Every canonical name is a fully-qualified name, but not every fully-qualified name is a canonical + * name. + * + *

    JLS section + * 6.7 gives the following example: + * + *

    + * + * The difference between a fully qualified name and a canonical name can be seen in code such as: + * + *
    {@code
    + * package p;
    + * class O1 { class I {} }
    + * class O2 extends O1 {}
    + * }
    + * + * Both {@code p.O1.I} and {@code p.O2.I} are fully qualified names that denote the member class + * {@code I}, but only {@code p.O1.I} is its canonical name. + * + *
    + * + * Given a character sequence that is a fully-qualified name, there is no way to know whether or not + * it is a canonical name, without examining the program it refers to. Type-checking determines that + * a string is a {@code CanonicalName} based on provenance (what method produced the string), rather + * than the contents of the string. + * + * @see FullyQualifiedName + * @checker_framework.manual #signature-checker Signature Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({ + FullyQualifiedName.class, + CanonicalNameOrEmpty.class, + CanonicalNameOrPrimitiveType.class +}) +public @interface CanonicalName {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/CanonicalNameAndBinaryName.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/CanonicalNameAndBinaryName.java new file mode 100644 index 000000000000..67df3bee483e --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/CanonicalNameAndBinaryName.java @@ -0,0 +1,25 @@ +package org.checkerframework.checker.signature.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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 is a string that is a valid {@linkplain + * org.checkerframework.checker.signature.qual.CanonicalName canonical name} and a valid {@linkplain + * org.checkerframework.checker.signature.qual.BinaryName binary name}. It represents a non-array, + * non-inner, non-primitive class. + * + *

    Examples: int, MyClass, java.lang, java.lang.Integer + * + * @checker_framework.manual #signature-checker Signature Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({CanonicalName.class, DotSeparatedIdentifiers.class}) +public @interface CanonicalNameAndBinaryName {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/CanonicalNameOrEmpty.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/CanonicalNameOrEmpty.java new file mode 100644 index 000000000000..b0d093b2f6fb --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/CanonicalNameOrEmpty.java @@ -0,0 +1,21 @@ +package org.checkerframework.checker.signature.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * Either a {@link CanonicalName} or the empty string. + * + * @see CanonicalName + * @checker_framework.manual #signature-checker Signature Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(SignatureUnknown.class) +public @interface CanonicalNameOrEmpty {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/CanonicalNameOrPrimitiveType.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/CanonicalNameOrPrimitiveType.java new file mode 100644 index 000000000000..1dcec1621e16 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/CanonicalNameOrPrimitiveType.java @@ -0,0 +1,25 @@ +package org.checkerframework.checker.signature.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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 is a string that is a valid {@linkplain CanonicalName canonical name} and a valid + * {@linkplain BinaryNameOrPrimitiveType binary name or primitive type}. It represents a primitive + * type or a non-array, non-inner class, where the latter is represented by dot-separated + * identifiers. + * + *

    Examples: int, MyClass, java.lang.Integer + * + * @checker_framework.manual #signature-checker Signature Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({DotSeparatedIdentifiersOrPrimitiveType.class}) +public @interface CanonicalNameOrPrimitiveType {} diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/ClassGetName.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/ClassGetName.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/ClassGetName.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/ClassGetName.java index c5bed3923f78..07166937a1b9 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/ClassGetName.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/ClassGetName.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * The type representation used by the {@link Class#getName()}, {@link Class#forName(String)}, and diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/ClassGetSimpleName.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/ClassGetSimpleName.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/ClassGetSimpleName.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/ClassGetSimpleName.java index ca75e879b7fc..8cf7afd71467 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/ClassGetSimpleName.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/ClassGetSimpleName.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * The format produced by the {@link Class#getSimpleName()} method. It is an identifier or the empty diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/DotSeparatedIdentifiers.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/DotSeparatedIdentifiers.java similarity index 86% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/DotSeparatedIdentifiers.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/DotSeparatedIdentifiers.java index 79e63100fc8f..091f39f00e25 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/DotSeparatedIdentifiers.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/DotSeparatedIdentifiers.java @@ -1,19 +1,20 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * This is a string that is a valid {@linkplain * org.checkerframework.checker.signature.qual.FullyQualifiedName fully qualified name} and a valid * {@linkplain org.checkerframework.checker.signature.qual.BinaryName binary name}. It represents a - * non-array, non-inner class: dot-separated identifiers. + * non-array, non-inner, non-primitive class: dot-separated identifiers. * - *

    Examples: int, MyClass, java.lang.Integer + *

    Examples: int, MyClass, java.lang, java.lang.Integer * * @checker_framework.manual #signature-checker Signature Checker */ diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/DotSeparatedIdentifiersOrPrimitiveType.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/DotSeparatedIdentifiersOrPrimitiveType.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/DotSeparatedIdentifiersOrPrimitiveType.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/DotSeparatedIdentifiersOrPrimitiveType.java index f50ce6cdd9a5..d779449e3865 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/DotSeparatedIdentifiersOrPrimitiveType.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/DotSeparatedIdentifiersOrPrimitiveType.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * This is a string that is a valid {@linkplain diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptor.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptor.java similarity index 94% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptor.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptor.java index a06989841868..1171091f8bd3 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptor.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptor.java @@ -1,15 +1,16 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Represents a field descriptor (JVM type format) as defined in the Java Virtual + * href="https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-4.html#jvms-4.3.2">Java Virtual * Machine Specification, section 4.3.2. * *

    For example, in diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptorForPrimitive.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptorForPrimitive.java similarity index 92% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptorForPrimitive.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptorForPrimitive.java index 3b4bb26b766c..3854074f2afd 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptorForPrimitive.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptorForPrimitive.java @@ -1,15 +1,16 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Represents a field descriptor (JVM type format) for a primitive as defined in the Java Virtual + * href="https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-4.html#jvms-4.3.2">Java Virtual * Machine Specification, section 4.3.2. * *

    Must be one of B, C, D, F, I, J, S, Z. diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptorWithoutPackage.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptorWithoutPackage.java similarity index 93% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptorWithoutPackage.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptorWithoutPackage.java index ac2817eae6c2..8c1daa1f44f6 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptorWithoutPackage.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FieldDescriptorWithoutPackage.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Represents a {@link FieldDescriptor field descriptor} for a primitive or for an array whose base @@ -14,7 +15,7 @@ *

    Examples: I [[J MyClass MyClass$22 [LMyClass; [LMyClass$22 * *

    Field descriptor (JVM type format) is defined in the Java Virtual + * href="https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-4.html#jvms-4.3.2">Java Virtual * Machine Specification, section 4.3.2. * * @checker_framework.manual #signature-checker Signature Checker diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/FqBinaryName.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FqBinaryName.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/FqBinaryName.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FqBinaryName.java index 32af0d6646ee..e34ca136d6d5 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/FqBinaryName.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FqBinaryName.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * An extension of binary name format to represent primitives and arrays. It is just like diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/FullyQualifiedName.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FullyQualifiedName.java similarity index 95% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/FullyQualifiedName.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FullyQualifiedName.java index 4ddb441e6ccf..0dc218ba972b 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/FullyQualifiedName.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/FullyQualifiedName.java @@ -1,16 +1,17 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * A sequence of dot-separated identifiers, followed by any number of array square brackets. * Represents a fully-qualified name as defined in the Java Language + * href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-6.html#jls-6.7">Java Language * Specification, section 6.7. * *

    Examples: diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/Identifier.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/Identifier.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/Identifier.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/Identifier.java index e434b14d4ba7..31126dd06443 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/Identifier.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/Identifier.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * An identifier. diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/IdentifierOrPrimitiveType.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/IdentifierOrPrimitiveType.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/IdentifierOrPrimitiveType.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/IdentifierOrPrimitiveType.java index 64313c883ed8..9cb8038b149f 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/IdentifierOrPrimitiveType.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/IdentifierOrPrimitiveType.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * An identifier or a primitive type. diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/InternalForm.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/InternalForm.java similarity index 94% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/InternalForm.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/InternalForm.java index cec44eed807d..9757c081f478 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/InternalForm.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/InternalForm.java @@ -1,15 +1,16 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * The syntax for binary names that appears in a class file, as defined in the JVM + * href="https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-4.html#jvms-4.2">JVM * Specification, section 4.2. A {@linkplain BinaryName binary name} is conceptually the name * for the class or interface in a compiled binary, but the actual representation of that name in * its class file is slightly different. diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/MethodDescriptor.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/MethodDescriptor.java similarity index 94% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/MethodDescriptor.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/MethodDescriptor.java index 61f4e703b86b..a6eb69e83360 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/MethodDescriptor.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/MethodDescriptor.java @@ -1,15 +1,16 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Represents a method descriptor (JVM representation of method signature) as defined in the Java Virtual + * href="https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-4.html#jvms-4.3.3">Java Virtual * Machine Specification, section 4.3.3. * *

    Example: diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/PolySignature.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/PolySignature.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/PolySignature.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/PolySignature.java index 89c05ef24bde..b54cae68ff9e 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/PolySignature.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/PolySignature.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * A polymorphic qualifier for the Signature type system. diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/PrimitiveType.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/PrimitiveType.java similarity index 90% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/PrimitiveType.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/PrimitiveType.java index 8782861c7f00..551f4824151e 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/PrimitiveType.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/PrimitiveType.java @@ -1,18 +1,19 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * A primitive type. One of: boolean, byte, char, double, float, int, long, short. * * @checker_framework.manual #signature-checker Signature Checker */ -@SubtypeOf({IdentifierOrPrimitiveType.class}) +@SubtypeOf({IdentifierOrPrimitiveType.class, CanonicalName.class}) @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/SignatureBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/SignatureBottom.java similarity index 80% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/SignatureBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/SignatureBottom.java index 842f98ec0786..91cdba36d259 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/SignatureBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/SignatureBottom.java @@ -1,14 +1,15 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.DefaultFor; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the Signature String type system. Programmers should rarely write this type. @@ -19,7 +20,12 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) -@SubtypeOf({FieldDescriptorForPrimitive.class, PrimitiveType.class, MethodDescriptor.class}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) +@SubtypeOf({ + FieldDescriptorForPrimitive.class, + PrimitiveType.class, + CanonicalNameAndBinaryName.class, + MethodDescriptor.class +}) @DefaultFor({TypeUseLocation.LOWER_BOUND}) public @interface SignatureBottom {} diff --git a/checker/src/main/java/org/checkerframework/checker/signature/qual/SignatureUnknown.java b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/SignatureUnknown.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/signature/qual/SignatureUnknown.java rename to checker-qual/src/main/java/org/checkerframework/checker/signature/qual/SignatureUnknown.java index ddac82ff3f8f..a74c04d84fe1 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/qual/SignatureUnknown.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signature/qual/SignatureUnknown.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.signature.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; + import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; /** * Top qualifier in the type hierarchy. @@ -18,6 +19,6 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({}) // empty target prevents programmers from writing this in a program -@DefaultQualifierInHierarchy @SubtypeOf({}) +@DefaultQualifierInHierarchy public @interface SignatureUnknown {} diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/qual/PolySigned.java b/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/PolySigned.java similarity index 76% rename from checker/src/main/java/org/checkerframework/checker/signedness/qual/PolySigned.java rename to checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/PolySigned.java index d07dd514656e..67243c0a62a1 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/qual/PolySigned.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/PolySigned.java @@ -1,15 +1,20 @@ package org.checkerframework.checker.signedness.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * A polymorphic qualifier for the signedness type system. * + *

    When two formal parameter types are annotated with {@code @PolySigned}, the two arguments must + * have the same signedness type annotation. (This differs from the standard rule for polymorphic + * qualifiers.) + * * @checker_framework.manual #signedness-checker Signedness Checker * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism */ diff --git a/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/Signed.java b/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/Signed.java new file mode 100644 index 000000000000..5683f170df5a --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/Signed.java @@ -0,0 +1,48 @@ +package org.checkerframework.checker.signedness.qual; + +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TypeKind; +import org.checkerframework.framework.qual.TypeUseLocation; +import org.checkerframework.framework.qual.UpperBoundFor; + +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; + +/** + * The value is to be interpreted as signed. That is, if the most significant bit in the bitwise + * representation is set, then the bits should be interpreted as a negative number. + * + * @checker_framework.manual #signedness-checker Signedness Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({UnknownSignedness.class}) +@DefaultQualifierInHierarchy +@DefaultFor( + typeKinds = { + TypeKind.BYTE, + TypeKind.INT, + TypeKind.LONG, + TypeKind.SHORT, + TypeKind.FLOAT, + TypeKind.DOUBLE + }, + types = { + java.lang.Byte.class, + java.lang.Integer.class, + java.lang.Long.class, + java.lang.Short.class, + java.lang.Float.class, + java.lang.Double.class + }, + value = TypeUseLocation.EXCEPTION_PARAMETER) +@UpperBoundFor( + typeKinds = {TypeKind.FLOAT, TypeKind.DOUBLE}, + types = {java.lang.Float.class, java.lang.Double.class}) +public @interface Signed {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/SignedPositive.java b/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/SignedPositive.java new file mode 100644 index 000000000000..52416f01445b --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/SignedPositive.java @@ -0,0 +1,31 @@ +package org.checkerframework.checker.signedness.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * The expression's value is in the signed positive range; that is, its most significant bit is + * zero. The value has the same interpretation as {@code @}{@link Signed} and {@code @}{@link + * Unsigned} — both interpretations are equivalent. + * + *

    Programmers should rarely write {@code @SignedPositive}. Instead, the programmer should write + * {@code @}{@link Signed} or {@code @}{@link Unsigned} to indicate how the programmer intends the + * value to be interpreted. + * + *

    {@code @SignedPositive} corresponds to {@code @}{@link + * org.checkerframework.checker.index.qual.NonNegative NonNegative} in the Index Checker's type + * system. + * + * @see SignednessGlb + * @checker_framework.manual #signedness-checker Signedness Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(SignednessGlb.class) +public @interface SignedPositive {} diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/qual/SignednessBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/SignednessBottom.java similarity index 86% rename from checker/src/main/java/org/checkerframework/checker/signedness/qual/SignednessBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/SignednessBottom.java index 243bc6fd6f03..2cb269fbf625 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/qual/SignednessBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/SignednessBottom.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.signedness.qual; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the Signedness type system. Programmers should rarely write this type. @@ -20,6 +21,9 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) -@SubtypeOf({SignednessGlb.class}) +@TargetLocations({ + TypeUseLocation.LOWER_BOUND, + TypeUseLocation.UPPER_BOUND, +}) +@SubtypeOf({SignedPositive.class}) public @interface SignednessBottom {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/SignednessGlb.java b/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/SignednessGlb.java new file mode 100644 index 000000000000..36f8e10c7ee5 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/SignednessGlb.java @@ -0,0 +1,40 @@ +package org.checkerframework.checker.signedness.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * Client code may interpret the value either as {@link Signed} or as {@link Unsigned}. This + * primarily applies to values whose most significant bit is not set (i.e., {@link SignedPositive}), + * and thus the value has the same interpretation as signed or unsigned. + * + *

    The programmer should not write this annotation. Instead, the programmer should write + * {@link Signed} or {@link Unsigned} to indicate how the programmer intends the value to be + * interpreted. For a value whose most significant bit is not set and different clients may treat it + * differently (say, the return value of certain library routines, or certain constant fields), the + * programmer should write {@code @}{@link SignedPositive} instead of {@code @SignednessGlb}. + * + *

    The Signedness Checker applies this annotation to manifest literals. This permits a value like + * {@code -1} or {@code 255} or {@code 0xFF} to be used in both signed and unsigned contexts. The + * Signedness Checker has no way of knowing how a programmer intended a literal to be used, so it + * does not issue warnings for any uses of a literal. (An alternate design would require the + * programmer to explicitly annotate every manifest literal whose most significant bit is set. That + * might detect more errors, at the cost of much greater programmer annotation effort.) + * + *

    The "Glb" in the name stands for "greatest lower bound", because this type is the greatest + * lower bound of the types {@link Signed} and {@link Unsigned}; that is, this type is a subtype of + * both of those types. + * + * @see SignedPositive + * @checker_framework.manual #signedness-checker Signedness Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({Unsigned.class, Signed.class}) +public @interface SignednessGlb {} diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/qual/UnknownSignedness.java b/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/UnknownSignedness.java similarity index 87% rename from checker/src/main/java/org/checkerframework/checker/signedness/qual/UnknownSignedness.java rename to checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/UnknownSignedness.java index 0049b7ceaada..80cf59539b16 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/qual/UnknownSignedness.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/UnknownSignedness.java @@ -1,12 +1,12 @@ package org.checkerframework.checker.signedness.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; /** * The value's signedness is not known to the Signedness Checker. This is also used for non-numeric @@ -18,5 +18,4 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @SubtypeOf({}) -@DefaultQualifierInHierarchy public @interface UnknownSignedness {} diff --git a/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/Unsigned.java b/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/Unsigned.java new file mode 100644 index 000000000000..a3d1ce928945 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/signedness/qual/Unsigned.java @@ -0,0 +1,31 @@ +package org.checkerframework.checker.signedness.qual; + +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TypeKind; +import org.checkerframework.framework.qual.UpperBoundFor; + +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; + +/** + * The value is to be interpreted as unsigned. That is, if the most significant bit in the bitwise + * representation is set, then the bits should be interpreted as a large positive number rather than + * as a negative number. + * + * @checker_framework.manual #signedness-checker Signedness Checker + */ +@Documented +@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/tainting/qual/PolyTainted.java b/checker-qual/src/main/java/org/checkerframework/checker/tainting/qual/PolyTainted.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/tainting/qual/PolyTainted.java rename to checker-qual/src/main/java/org/checkerframework/checker/tainting/qual/PolyTainted.java index 0f481e6ab566..643903b8e73a 100644 --- a/checker/src/main/java/org/checkerframework/checker/tainting/qual/PolyTainted.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/tainting/qual/PolyTainted.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.tainting.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * A polymorphic qualifier for the Tainting type system. diff --git a/checker/src/main/java/org/checkerframework/checker/tainting/qual/Tainted.java b/checker-qual/src/main/java/org/checkerframework/checker/tainting/qual/Tainted.java similarity index 82% rename from checker/src/main/java/org/checkerframework/checker/tainting/qual/Tainted.java rename to checker-qual/src/main/java/org/checkerframework/checker/tainting/qual/Tainted.java index 86187facd494..bf029cf75012 100644 --- a/checker/src/main/java/org/checkerframework/checker/tainting/qual/Tainted.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/tainting/qual/Tainted.java @@ -1,19 +1,17 @@ package org.checkerframework.checker.tainting.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; /** * Denotes a possibly-tainted value: at run time, the value might be tainted or might be untainted. * - *

    This is the top qualifier of the tainting type system. This annotation is associated with the - * {@link org.checkerframework.checker.tainting.TaintingChecker}. - * * @see Untainted * @see org.checkerframework.checker.tainting.TaintingChecker * @checker_framework.manual #tainting-checker Tainting Checker @@ -21,6 +19,6 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@DefaultQualifierInHierarchy @SubtypeOf({}) +@DefaultQualifierInHierarchy public @interface Tainted {} diff --git a/checker/src/main/java/org/checkerframework/checker/tainting/qual/Untainted.java b/checker-qual/src/main/java/org/checkerframework/checker/tainting/qual/Untainted.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/tainting/qual/Untainted.java rename to checker-qual/src/main/java/org/checkerframework/checker/tainting/qual/Untainted.java index 83d48f5186b1..f4850dcd5b3a 100644 --- a/checker/src/main/java/org/checkerframework/checker/tainting/qual/Untainted.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/tainting/qual/Untainted.java @@ -1,16 +1,17 @@ package org.checkerframework.checker.tainting.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.DefaultFor; import org.checkerframework.framework.qual.LiteralKind; import org.checkerframework.framework.qual.QualifierForLiterals; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.TypeUseLocation; +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; + /** * Denotes a reference that is untainted, i.e. can be trusted. * diff --git a/checker-qual/src/main/java/org/checkerframework/checker/units/qual/A.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/A.java new file mode 100644 index 000000000000..de91234f103c --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/A.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.units.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * Ampere. + * + * @checker_framework.manual #units-checker Units Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(Current.class) +public @interface A { + Prefix value() default Prefix.one; +} diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/Acceleration.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Acceleration.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/Acceleration.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/Acceleration.java index fcd64b667de9..52c7c72de28a 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/Acceleration.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Acceleration.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Units of acceleration. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/Angle.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Angle.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/Angle.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/Angle.java index bc764db07ad2..71c9faf2bfc9 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/Angle.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Angle.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Units of measure for angles. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/Area.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Area.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/Area.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/Area.java index 14dcf9388d26..701248e0a8d2 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/Area.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Area.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Units of areas. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/units/qual/C.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/C.java new file mode 100644 index 000000000000..86bd63c9bf24 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/C.java @@ -0,0 +1,20 @@ +package org.checkerframework.checker.units.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * Degree Centigrade (Celsius). + * + * @checker_framework.manual #units-checker Units Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(Temperature.class) +public @interface C {} diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/Current.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Current.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/Current.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/Current.java index fc49e8c06e8b..a061c0c47bfd 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/Current.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Current.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Electric current. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Force.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Force.java new file mode 100644 index 000000000000..2244fb89eeb2 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Force.java @@ -0,0 +1,20 @@ +package org.checkerframework.checker.units.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * Units of force. + * + * @checker_framework.manual #units-checker Units Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(UnknownUnits.class) +public @interface Force {} diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/K.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/K.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/K.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/K.java index 0a20c37766e0..1c0f8be34731 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/K.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/K.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Kelvin (unit of temperature). diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/Length.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Length.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/Length.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/Length.java index 994acf2651b1..201a5335b871 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/Length.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Length.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Units of length. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/Luminance.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Luminance.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/Luminance.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/Luminance.java index 0d7dd66d3661..5ec3a4a6dbad 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/Luminance.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Luminance.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Units of luminance. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/Mass.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Mass.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/Mass.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/Mass.java index b0337b2a90cc..3badbaab126f 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/Mass.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Mass.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Units of mass. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/MixedUnits.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/MixedUnits.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/MixedUnits.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/MixedUnits.java index 08dcc3790155..e8f5993067a2 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/MixedUnits.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/MixedUnits.java @@ -1,10 +1,11 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.SubtypeOf; /** * MixedUnits is the result of multiplying or dividing units, where no more specific unit is known diff --git a/checker-qual/src/main/java/org/checkerframework/checker/units/qual/N.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/N.java new file mode 100644 index 000000000000..3c5c20e7aad6 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/N.java @@ -0,0 +1,23 @@ +package org.checkerframework.checker.units.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * Newton. + * + * @checker_framework.manual #units-checker Units Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(Force.class) +@SuppressWarnings("checkstyle:typename") +public @interface N { + Prefix value() default Prefix.one; +} diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/PolyUnit.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/PolyUnit.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/PolyUnit.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/PolyUnit.java index 717616ec89ea..9ec7269c08c2 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/PolyUnit.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/PolyUnit.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * A polymorphic qualifier for the units-of-measure type system implemented by the Units Checker. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/Prefix.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Prefix.java similarity index 97% rename from checker/src/main/java/org/checkerframework/checker/units/qual/Prefix.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/Prefix.java index 437c4e797abd..3aa2428f32be 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/Prefix.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Prefix.java @@ -17,7 +17,7 @@ * kilo k 1000^1 10^3 1000 Thousand 1795 * hecto h 1000^2/3 10^2 100 Hundred 1795 * deca da 1000^1/3 10^1 10 Ten 1795 - * 1000^0 10^0 1 One + * 1000^0 10^0 1 One * deci d 1000^-1/3 10^-1 0.1 Tenth 1795 * centi c 1000^-2/3 10^-2 0.01 Hundredth 1795 * milli m 1000^-1 10^-3 0.001 Thousandth 1795 diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/Speed.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Speed.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/Speed.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/Speed.java index 94fe348e037e..088cd46b8ad1 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/Speed.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Speed.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Units of speed. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/Substance.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Substance.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/Substance.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/Substance.java index 214f981fefc7..799b25516dfd 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/Substance.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Substance.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Units of substance, such as mole (@{@link mol}). diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/Temperature.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Temperature.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/Temperature.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/Temperature.java index e251f8c2179e..87d8f99fa1eb 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/Temperature.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Temperature.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Units of temperature. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/Time.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Time.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/Time.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/Time.java index ecf7b9128d49..a3e09a7dff6a 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/Time.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Time.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Units of time. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/UnitsBottom.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/UnitsBottom.java similarity index 90% rename from checker/src/main/java/org/checkerframework/checker/units/qual/UnitsBottom.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/UnitsBottom.java index 8e1f45413ec1..1c0eb735f019 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/UnitsBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/UnitsBottom.java @@ -1,14 +1,15 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.DefaultFor; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the Units type system. Programmers should rarely write this type. @@ -19,7 +20,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @SubtypeOf({}) // needs to be done programmatically @DefaultFor(TypeUseLocation.LOWER_BOUND) public @interface UnitsBottom {} diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/UnitsMultiple.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/UnitsMultiple.java similarity index 100% rename from checker/src/main/java/org/checkerframework/checker/units/qual/UnitsMultiple.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/UnitsMultiple.java diff --git a/checker-qual/src/main/java/org/checkerframework/checker/units/qual/UnitsRelations.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/UnitsRelations.java new file mode 100644 index 000000000000..8b4e787f8b96 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/UnitsRelations.java @@ -0,0 +1,29 @@ +package org.checkerframework.checker.units.qual; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Specify the class that knows how to handle the meta-annotated unit when put in relation (plus, + * multiply, ...) with another unit. That class is a subtype of interface {@link + * org.checkerframework.checker.units.UnitsRelations}. + * + * @see org.checkerframework.checker.units.UnitsRelations + * @checker_framework.manual #units-checker Units Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface UnitsRelations { + /** + * Returns the subclass of {@link org.checkerframework.checker.units.UnitsRelations} to use. + * + * @return the subclass of {@link org.checkerframework.checker.units.UnitsRelations} to use + */ + // The more precise type is Class, + // but org.checkerframework.checker.units.UnitsRelations is not in checker-qual.jar, nor can + // it be since it uses AnnotatedTypeMirrors. So this declaration uses a less precise type, and + // UnitsAnnotatedTypeFactory checks that the argument implements + // org.checkerframework.checker.units.UnitsRelations. + Class value(); +} diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/UnknownUnits.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/UnknownUnits.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/UnknownUnits.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/UnknownUnits.java index 70d98c07bc9e..968115b2d303 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/UnknownUnits.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/UnknownUnits.java @@ -1,13 +1,14 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.InvisibleQualifier; -import org.checkerframework.framework.qual.SubtypeOf; /** * UnknownUnits is the top type of the type hierarchy. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Volume.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Volume.java new file mode 100644 index 000000000000..5d9769a3a25e --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/Volume.java @@ -0,0 +1,20 @@ +package org.checkerframework.checker.units.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * Units of volume. + * + * @checker_framework.manual #units-checker Units Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(UnknownUnits.class) +public @interface Volume {} diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/cd.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/cd.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/cd.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/cd.java index bf17fdb0e00c..91bc988a4cc1 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/cd.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/cd.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Candela (unit of luminance). diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/degrees.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/degrees.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/degrees.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/degrees.java index a4d7ae62b67d..55bccafee67c 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/degrees.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/degrees.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Degrees. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/g.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/g.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/g.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/g.java index 2b7f08c4057d..711f7439497e 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/g.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/g.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Gram. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/h.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/h.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/h.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/h.java index 6a568a9fd5aa..9260815e0cf6 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/h.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/h.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Hour. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/units/qual/kN.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/kN.java new file mode 100644 index 000000000000..2294dd067b54 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/kN.java @@ -0,0 +1,22 @@ +package org.checkerframework.checker.units.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * Kilonewton. + * + * @checker_framework.manual #units-checker Units Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(Force.class) +@UnitsMultiple(quantity = N.class, prefix = Prefix.kilo) +@SuppressWarnings("checkstyle:typename") +public @interface kN {} diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/kg.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/kg.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/kg.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/kg.java index 714cdfb29726..ab2160812ee4 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/kg.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/kg.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Kilogram. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/km.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/km.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/km.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/km.java index c2cb32d87dde..5f200d1f5535 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/km.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/km.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Kilometer. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/km2.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/km2.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/km2.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/km2.java index 590d0fe2584a..2e026b26ce0a 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/km2.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/km2.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Square kilometer. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/units/qual/km3.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/km3.java new file mode 100644 index 000000000000..a4f36d27aa97 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/km3.java @@ -0,0 +1,21 @@ +package org.checkerframework.checker.units.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * Cubic kilometer. + * + * @checker_framework.manual #units-checker Units Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(Volume.class) +@SuppressWarnings("checkstyle:typename") +public @interface km3 {} diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/kmPERh.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/kmPERh.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/kmPERh.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/kmPERh.java index 28abf4bd1c0e..4fd6e24ec2e5 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/kmPERh.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/kmPERh.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Kilometer per hour. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/m.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/m.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/m.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/m.java index c3b94628ed7b..688f365dfd0d 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/m.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/m.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Meter. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/m2.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/m2.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/m2.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/m2.java index 4f7c29fe76c2..eb33c261ce70 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/m2.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/m2.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Square meter. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/units/qual/m3.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/m3.java new file mode 100644 index 000000000000..7c88127862ba --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/m3.java @@ -0,0 +1,21 @@ +package org.checkerframework.checker.units.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * Cubic meter. + * + * @checker_framework.manual #units-checker Units Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(Volume.class) +@SuppressWarnings("checkstyle:typename") +public @interface m3 {} diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/mPERs.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/mPERs.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/mPERs.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/mPERs.java index ab97dfb94653..9c2c18dded57 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/mPERs.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/mPERs.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Meter per second. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/mPERs2.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/mPERs2.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/mPERs2.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/mPERs2.java index 0af7169340f2..42cf3bba1358 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/mPERs2.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/mPERs2.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Meter per second squared. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/min.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/min.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/min.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/min.java index b729aa02c239..f1fa0c3e2298 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/min.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/min.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Minute. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/mm.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/mm.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/mm.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/mm.java index dff99618b667..86f55d889765 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/mm.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/mm.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Millimeter. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/mm2.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/mm2.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/mm2.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/mm2.java index 2e5569887cb8..f51200b74595 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/mm2.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/mm2.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Square millimeter. diff --git a/checker-qual/src/main/java/org/checkerframework/checker/units/qual/mm3.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/mm3.java new file mode 100644 index 000000000000..f670cf1da812 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/mm3.java @@ -0,0 +1,21 @@ +package org.checkerframework.checker.units.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * Cubic millimeter. + * + * @checker_framework.manual #units-checker Units Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(Volume.class) +@SuppressWarnings("checkstyle:typename") +public @interface mm3 {} diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/mol.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/mol.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/mol.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/mol.java index b02a1441e309..06870b3b7d51 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/mol.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/mol.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Mole (unit of {@link Substance}). diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/radians.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/radians.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/radians.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/radians.java index 9cc186db2013..4af13288755a 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/radians.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/radians.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * Radians. diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/s.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/s.java similarity index 99% rename from checker/src/main/java/org/checkerframework/checker/units/qual/s.java rename to checker-qual/src/main/java/org/checkerframework/checker/units/qual/s.java index ed5d5234f9c7..bda43dda3ba3 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/s.java +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/s.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.units.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * A second (1/60 of a minute). diff --git a/checker-qual/src/main/java/org/checkerframework/checker/units/qual/t.java b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/t.java new file mode 100644 index 000000000000..845dae7bba1c --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/checker/units/qual/t.java @@ -0,0 +1,23 @@ +package org.checkerframework.checker.units.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * Metric ton. + * + * @checker_framework.manual #units-checker Units Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf(Mass.class) +// TODO: support arbitrary factors? +// @UnitsMultiple(quantity=kg.class, factor=1000) +@SuppressWarnings("checkstyle:typename") +public @interface t {} diff --git a/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/LeakedToResult.java b/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/LeakedToResult.java new file mode 100644 index 000000000000..2fa75945acb8 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/LeakedToResult.java @@ -0,0 +1,33 @@ +package org.checkerframework.common.aliasing.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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 annotation is used on a formal parameter to indicate that the parameter may be returned, but + * it is not otherwise leaked. (A parameter is leaked if it is stored in a field where it could be + * accessed later, and in that case this annotation would not apply.) + * + *

    For example, the receiver parameter of {@link StringBuffer#append(String s)} is annotated as + * {@code @LeakedToResult}, because the method returns the updated receiver. + * + *

    This annotation is currently trusted, not checked. + * + * @see NonLeaked + * @checker_framework.manual #aliasing-checker Aliasing Checker + */ + +// This is a type qualifier because of a Checker Framework limitation (Issue 383), but its hierarchy +// is ignored. Once the stub parser gets updated to read non-type-qualifiers annotations on stub +// files, this annotation won't be a type qualifier anymore. + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE}) +@SubtypeOf({NonLeaked.class}) +public @interface LeakedToResult {} diff --git a/framework/src/main/java/org/checkerframework/common/aliasing/qual/MaybeAliased.java b/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/MaybeAliased.java similarity index 99% rename from framework/src/main/java/org/checkerframework/common/aliasing/qual/MaybeAliased.java rename to checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/MaybeAliased.java index d17d6b6e9e1f..409382e15e37 100644 --- a/framework/src/main/java/org/checkerframework/common/aliasing/qual/MaybeAliased.java +++ b/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/MaybeAliased.java @@ -1,14 +1,15 @@ package org.checkerframework.common.aliasing.qual; +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.DefaultFor; -import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TypeUseLocation; /** * An expression with this type might have an alias. In other words, some other expression, diff --git a/framework/src/main/java/org/checkerframework/common/aliasing/qual/MaybeLeaked.java b/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/MaybeLeaked.java similarity index 99% rename from framework/src/main/java/org/checkerframework/common/aliasing/qual/MaybeLeaked.java rename to checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/MaybeLeaked.java index 9038b6b75b01..0d421fc3242b 100644 --- a/framework/src/main/java/org/checkerframework/common/aliasing/qual/MaybeLeaked.java +++ b/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/MaybeLeaked.java @@ -1,12 +1,13 @@ package org.checkerframework.common.aliasing.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.InvisibleQualifier; -import org.checkerframework.framework.qual.SubtypeOf; /** * Temporary type qualifier: diff --git a/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/NonLeaked.java b/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/NonLeaked.java new file mode 100644 index 000000000000..2417f2e4275e --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/NonLeaked.java @@ -0,0 +1,32 @@ +package org.checkerframework.common.aliasing.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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 annotation is used on a formal parameter to indicate that the parameter is not leaked + * (stored in a location that could be accessed later) nor returned by the method body. + * + *

    For example, the parameter of {@link String#String(String s)} is {@code @NonLeaked}, because + * the method only uses the parameter to make a copy of it. + * + *

    This annotation is currently trusted, not checked. + * + * @see LeakedToResult + * @checker_framework.manual #aliasing-checker Aliasing Checker + */ + +// This is a type qualifier because of a Checker Framework limitation (Issue 383), but its hierarchy +// is ignored. Once the stub parser gets updated to read non-type-qualifiers annotations on stub +// files, this annotation won't be a type qualifier anymore. + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE}) +@SubtypeOf({}) +public @interface NonLeaked {} diff --git a/framework/src/main/java/org/checkerframework/common/aliasing/qual/Unique.java b/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/Unique.java similarity index 99% rename from framework/src/main/java/org/checkerframework/common/aliasing/qual/Unique.java rename to checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/Unique.java index 61945114567d..af66458ffa7b 100644 --- a/framework/src/main/java/org/checkerframework/common/aliasing/qual/Unique.java +++ b/checker-qual/src/main/java/org/checkerframework/common/aliasing/qual/Unique.java @@ -1,11 +1,12 @@ package org.checkerframework.common.aliasing.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * An expression with this type has no aliases. In other words, no other expression, evaluated at diff --git a/checker-qual/src/main/java/org/checkerframework/common/initializedfields/qual/EnsuresInitializedFields.java b/checker-qual/src/main/java/org/checkerframework/common/initializedfields/qual/EnsuresInitializedFields.java new file mode 100644 index 000000000000..264e931be793 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/common/initializedfields/qual/EnsuresInitializedFields.java @@ -0,0 +1,60 @@ +package org.checkerframework.common.initializedfields.qual; + +import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.PostconditionAnnotation; +import org.checkerframework.framework.qual.QualifierArgument; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A method postcondition annotation indicates which fields the method definitely initializes. + * + * @checker_framework.manual #initialized-fields-checker Initialized Fields Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +@PostconditionAnnotation(qualifier = InitializedFields.class) +@InheritedAnnotation +@Repeatable(EnsuresInitializedFields.List.class) +public @interface EnsuresInitializedFields { + /** + * The object whose fields this method initializes. + * + * @return object whose fields are initialized + */ + public String[] value() default {"this"}; + + /** + * Fields that this method initializes. + * + * @return fields that this method initializes + */ + @QualifierArgument("value") + public String[] fields(); + + /** + * A wrapper annotation that makes the {@link EnsuresInitializedFields} annotation repeatable. + * + *

    Programmers generally do not need to write this. It is created by Java when a programmer + * writes more than one {@link EnsuresInitializedFields} annotation at the same location. + */ + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) + @PostconditionAnnotation(qualifier = InitializedFields.class) + @InheritedAnnotation + public static @interface List { + /** + * Return the repeatable annotations. + * + * @return the repeatable annotations + */ + EnsuresInitializedFields[] value(); + } +} diff --git a/checker-qual/src/main/java/org/checkerframework/common/initializedfields/qual/InitializedFields.java b/checker-qual/src/main/java/org/checkerframework/common/initializedfields/qual/InitializedFields.java new file mode 100644 index 000000000000..18ea2e2693ac --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/common/initializedfields/qual/InitializedFields.java @@ -0,0 +1,27 @@ +package org.checkerframework.common.initializedfields.qual; + +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates which fields have definitely been initialized. + * + * @checker_framework.manual #initialized-fields-checker Initialized Fields Checker + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) +@DefaultQualifierInHierarchy +public @interface InitializedFields { + /** + * Fields that have been initialized. + * + * @return the initialized fields + */ + public String[] value() default {}; +} diff --git a/checker-qual/src/main/java/org/checkerframework/common/initializedfields/qual/InitializedFieldsBottom.java b/checker-qual/src/main/java/org/checkerframework/common/initializedfields/qual/InitializedFieldsBottom.java new file mode 100644 index 000000000000..628c199eca50 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/common/initializedfields/qual/InitializedFieldsBottom.java @@ -0,0 +1,22 @@ +package org.checkerframework.common.initializedfields.qual; + +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The bottom type qualifier for the Initialized Fields type system. It is the type of {@code null}. + * Programmers should rarely write this qualifier. + * + * @checker_framework.manual #initialized-fields-checker Initialized Fields Checker + */ +@SubtypeOf({InitializedFields.class}) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) +public @interface InitializedFieldsBottom {} diff --git a/checker-qual/src/main/java/org/checkerframework/common/initializedfields/qual/PolyInitializedFields.java b/checker-qual/src/main/java/org/checkerframework/common/initializedfields/qual/PolyInitializedFields.java new file mode 100644 index 000000000000..6fc0b75789eb --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/common/initializedfields/qual/PolyInitializedFields.java @@ -0,0 +1,15 @@ +package org.checkerframework.common.initializedfields.qual; + +import org.checkerframework.framework.qual.PolymorphicQualifier; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Polymorphic qualifier for the Initialized Fields type system. + * + * @checker_framework.manual #initialized-fields-checker Initialized Fields Checker + */ +@PolymorphicQualifier(InitializedFields.class) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface PolyInitializedFields {} diff --git a/framework/src/main/java/org/checkerframework/common/reflection/qual/ClassBound.java b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/ClassBound.java similarity index 95% rename from framework/src/main/java/org/checkerframework/common/reflection/qual/ClassBound.java rename to checker-qual/src/main/java/org/checkerframework/common/reflection/qual/ClassBound.java index d8505f0aa513..8aa0aee983f6 100644 --- a/framework/src/main/java/org/checkerframework/common/reflection/qual/ClassBound.java +++ b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/ClassBound.java @@ -1,11 +1,12 @@ package org.checkerframework.common.reflection.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * This represents a {@code Class} object whose run-time value is equal to or a subtype of one of @@ -19,7 +20,7 @@ @SubtypeOf({UnknownClass.class}) public @interface ClassBound { /** - * The binary + * The binary * name of the class or classes that upper-bound the values of this Class object. */ String[] value(); diff --git a/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/ClassVal.java b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/ClassVal.java new file mode 100644 index 000000000000..3fb622077f00 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/ClassVal.java @@ -0,0 +1,32 @@ +package org.checkerframework.common.reflection.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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 represents a {@link java.lang.Class Class<T>} object where the set of possible values + * of T is known at compile time. If only one argument is given, then the exact value of T is known. + * If more than one argument is given, then the value of T is one of those classes. + * + * @checker_framework.manual #methodval-and-classval-checkers ClassVal Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({UnknownClass.class}) +public @interface ClassVal { + /** + * The name of the type that this Class object represents. The name is a "fully-qualified binary + * name" ({@link org.checkerframework.checker.signature.qual.FqBinaryName}): a primitive or binary + * name, possibly followed by some number of array brackets. + * + * @return the name of the type that this Class object represents + */ + String[] value(); +} diff --git a/framework/src/main/java/org/checkerframework/common/reflection/qual/ClassValBottom.java b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/ClassValBottom.java similarity index 90% rename from framework/src/main/java/org/checkerframework/common/reflection/qual/ClassValBottom.java rename to checker-qual/src/main/java/org/checkerframework/common/reflection/qual/ClassValBottom.java index 757845f4e039..e68c82816867 100644 --- a/framework/src/main/java/org/checkerframework/common/reflection/qual/ClassValBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/ClassValBottom.java @@ -1,14 +1,15 @@ package org.checkerframework.common.reflection.qual; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.InvisibleQualifier; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the ClassVal type system. Programmers should rarely write this type. @@ -19,7 +20,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @InvisibleQualifier @SubtypeOf({ClassVal.class, ClassBound.class}) public @interface ClassValBottom {} diff --git a/framework/src/main/java/org/checkerframework/common/reflection/qual/ForName.java b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/ForName.java similarity index 88% rename from framework/src/main/java/org/checkerframework/common/reflection/qual/ForName.java rename to checker-qual/src/main/java/org/checkerframework/common/reflection/qual/ForName.java index 93773fbae40f..0a568e9cd71e 100644 --- a/framework/src/main/java/org/checkerframework/common/reflection/qual/ForName.java +++ b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/ForName.java @@ -10,7 +10,7 @@ * Annotation for methods like {@code Class.forName}. Their signature is * *

    
    - *  @ClassVal("name") Class method(@ForName String name) {...}
    + *  @ClassVal("name") Class method(@BinaryName String name) {...}
      * 
    * * @checker_framework.manual #reflection-resolution Reflection resolution diff --git a/framework/src/main/java/org/checkerframework/common/reflection/qual/GetClass.java b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/GetClass.java similarity index 100% rename from framework/src/main/java/org/checkerframework/common/reflection/qual/GetClass.java rename to checker-qual/src/main/java/org/checkerframework/common/reflection/qual/GetClass.java diff --git a/framework/src/main/java/org/checkerframework/common/reflection/qual/GetConstructor.java b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/GetConstructor.java similarity index 85% rename from framework/src/main/java/org/checkerframework/common/reflection/qual/GetConstructor.java rename to checker-qual/src/main/java/org/checkerframework/common/reflection/qual/GetConstructor.java index 0ca3d27f4a87..db4b9bc409ce 100644 --- a/framework/src/main/java/org/checkerframework/common/reflection/qual/GetConstructor.java +++ b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/GetConstructor.java @@ -8,7 +8,7 @@ /** * Annotation for methods like {@code Class.getConstructor}, whose signature is:
    - * {@code {@link MethodVal}(classname=c, methodname="", params=p) Constructor + * {@code @}{@link MethodVal}{@code (classname=c, methodname="", params=p) Constructor * method(Class this, Object... params)} * * @checker_framework.manual #reflection-resolution Reflection resolution diff --git a/framework/src/main/java/org/checkerframework/common/reflection/qual/GetMethod.java b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/GetMethod.java similarity index 100% rename from framework/src/main/java/org/checkerframework/common/reflection/qual/GetMethod.java rename to checker-qual/src/main/java/org/checkerframework/common/reflection/qual/GetMethod.java diff --git a/framework/src/main/java/org/checkerframework/common/reflection/qual/Invoke.java b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/Invoke.java similarity index 100% rename from framework/src/main/java/org/checkerframework/common/reflection/qual/Invoke.java rename to checker-qual/src/main/java/org/checkerframework/common/reflection/qual/Invoke.java diff --git a/framework/src/main/java/org/checkerframework/common/reflection/qual/MethodVal.java b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/MethodVal.java similarity index 97% rename from framework/src/main/java/org/checkerframework/common/reflection/qual/MethodVal.java rename to checker-qual/src/main/java/org/checkerframework/common/reflection/qual/MethodVal.java index 7087e3ee2a62..1a053e6a1a41 100644 --- a/framework/src/main/java/org/checkerframework/common/reflection/qual/MethodVal.java +++ b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/MethodVal.java @@ -1,11 +1,12 @@ package org.checkerframework.common.reflection.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * This represents a set of {@link java.lang.reflect.Method Method} or {@link @@ -24,7 +25,7 @@ @SubtypeOf({UnknownMethod.class}) public @interface MethodVal { /** - * The binary + * The binary * name of the class that declares this method. */ String[] className(); diff --git a/framework/src/main/java/org/checkerframework/common/reflection/qual/MethodValBottom.java b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/MethodValBottom.java similarity index 90% rename from framework/src/main/java/org/checkerframework/common/reflection/qual/MethodValBottom.java rename to checker-qual/src/main/java/org/checkerframework/common/reflection/qual/MethodValBottom.java index c99f8740ccf0..3e39e6b04d2d 100644 --- a/framework/src/main/java/org/checkerframework/common/reflection/qual/MethodValBottom.java +++ b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/MethodValBottom.java @@ -1,14 +1,15 @@ package org.checkerframework.common.reflection.qual; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.InvisibleQualifier; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the MethodVal type system. Programmers should rarely write this type. @@ -19,7 +20,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) @InvisibleQualifier @SubtypeOf({MethodVal.class}) public @interface MethodValBottom {} diff --git a/framework/src/main/java/org/checkerframework/common/reflection/qual/NewInstance.java b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/NewInstance.java similarity index 100% rename from framework/src/main/java/org/checkerframework/common/reflection/qual/NewInstance.java rename to checker-qual/src/main/java/org/checkerframework/common/reflection/qual/NewInstance.java diff --git a/framework/src/main/java/org/checkerframework/common/reflection/qual/UnknownClass.java b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/UnknownClass.java similarity index 91% rename from framework/src/main/java/org/checkerframework/common/reflection/qual/UnknownClass.java rename to checker-qual/src/main/java/org/checkerframework/common/reflection/qual/UnknownClass.java index f89c5e9f2c50..ccea4c45929b 100644 --- a/framework/src/main/java/org/checkerframework/common/reflection/qual/UnknownClass.java +++ b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/UnknownClass.java @@ -1,16 +1,17 @@ package org.checkerframework.common.reflection.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.DefaultQualifierInHierarchy; import org.checkerframework.framework.qual.InvisibleQualifier; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.TargetLocations; import org.checkerframework.framework.qual.TypeUseLocation; +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; + /** * Represents a Class object whose run-time value is not known at compile time. Also represents * non-Class values. @@ -22,7 +23,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.ALL}) @InvisibleQualifier @SubtypeOf({}) @DefaultQualifierInHierarchy diff --git a/framework/src/main/java/org/checkerframework/common/reflection/qual/UnknownMethod.java b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/UnknownMethod.java similarity index 92% rename from framework/src/main/java/org/checkerframework/common/reflection/qual/UnknownMethod.java rename to checker-qual/src/main/java/org/checkerframework/common/reflection/qual/UnknownMethod.java index 34547759c139..bf5569096323 100644 --- a/framework/src/main/java/org/checkerframework/common/reflection/qual/UnknownMethod.java +++ b/checker-qual/src/main/java/org/checkerframework/common/reflection/qual/UnknownMethod.java @@ -1,16 +1,17 @@ package org.checkerframework.common.reflection.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.DefaultQualifierInHierarchy; import org.checkerframework.framework.qual.InvisibleQualifier; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.TargetLocations; import org.checkerframework.framework.qual.TypeUseLocation; +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; + /** * Represents a {@link java.lang.reflect.Method Method} or {@link java.lang.reflect.Constructor * Constructor} expression whose run-time value is not known at compile time. Also represents @@ -23,7 +24,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.ALL}) @InvisibleQualifier @SubtypeOf({}) @DefaultQualifierInHierarchy diff --git a/framework/src/main/java/org/checkerframework/common/returnsreceiver/qual/BottomThis.java b/checker-qual/src/main/java/org/checkerframework/common/returnsreceiver/qual/BottomThis.java similarity index 89% rename from framework/src/main/java/org/checkerframework/common/returnsreceiver/qual/BottomThis.java rename to checker-qual/src/main/java/org/checkerframework/common/returnsreceiver/qual/BottomThis.java index 99dd4fcb56bb..a3e0f92d8bf7 100644 --- a/framework/src/main/java/org/checkerframework/common/returnsreceiver/qual/BottomThis.java +++ b/checker-qual/src/main/java/org/checkerframework/common/returnsreceiver/qual/BottomThis.java @@ -1,12 +1,13 @@ package org.checkerframework.common.returnsreceiver.qual; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type for the Returns Receiver Checker's type system. Programmers should rarely write @@ -18,5 +19,5 @@ @SubtypeOf({UnknownThis.class}) @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) public @interface BottomThis {} diff --git a/framework/src/main/java/org/checkerframework/common/returnsreceiver/qual/This.java b/checker-qual/src/main/java/org/checkerframework/common/returnsreceiver/qual/This.java similarity index 99% rename from framework/src/main/java/org/checkerframework/common/returnsreceiver/qual/This.java rename to checker-qual/src/main/java/org/checkerframework/common/returnsreceiver/qual/This.java index 4d30a59c72a7..646aee27a900 100644 --- a/framework/src/main/java/org/checkerframework/common/returnsreceiver/qual/This.java +++ b/checker-qual/src/main/java/org/checkerframework/common/returnsreceiver/qual/This.java @@ -1,12 +1,13 @@ package org.checkerframework.common.returnsreceiver.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.PolymorphicQualifier; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * Write {@code @This} on the return type of a method that always returns its receiver ({@code diff --git a/checker-qual/src/main/java/org/checkerframework/common/returnsreceiver/qual/UnknownThis.java b/checker-qual/src/main/java/org/checkerframework/common/returnsreceiver/qual/UnknownThis.java new file mode 100644 index 000000000000..eafa025cdfdc --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/common/returnsreceiver/qual/UnknownThis.java @@ -0,0 +1,29 @@ +package org.checkerframework.common.returnsreceiver.qual; + +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.LiteralKind; +import org.checkerframework.framework.qual.QualifierForLiterals; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TypeUseLocation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The top type for the Returns Receiver Checker's type system. Values of the annotated type might + * be the receiver ({@code this}) or might not. Programmers should rarely write this type. + * + * @checker_framework.manual #returns-receiver-checker Returns Receiver Checker + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) +@DefaultQualifierInHierarchy +@QualifierForLiterals(LiteralKind.NULL) +@DefaultFor(value = TypeUseLocation.LOWER_BOUND) +@InvisibleQualifier +public @interface UnknownThis {} diff --git a/checker-qual/src/main/java/org/checkerframework/common/subtyping/qual/Bottom.java b/checker-qual/src/main/java/org/checkerframework/common/subtyping/qual/Bottom.java new file mode 100644 index 000000000000..93c521d89641 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/common/subtyping/qual/Bottom.java @@ -0,0 +1,33 @@ +package org.checkerframework.common.subtyping.qual; + +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * A special annotation intended solely for representing the bottom type in the qualifier hierarchy. + * It should not be used! Instead, each type system should define its own dedicated bottom type. + * + *

    This qualifier is used automatically if the existing qualifiers do not have a bottom type. + * This only works the user never runs two type systems together. Furthermore, because it has no + * {@code @RetentionPolicy} meta-annotation, this qualifier will not be stored in bytecode. So, only + * use this qualifier during prototyping of very simple type systems. For realistic systems, + * introduce a top and bottom qualifier that gets stored in bytecode. + * + *

    To use this qualifier, the type system designer needs to use methods like {@code + * ImplicitsTreeAnnotator.addTreeKind()} to add default annotations and needs to manually add the + * bottom qualifier to the qualifier hierarchy. + * + *

    See {@code QualifierHierarchy.getBottomAnnotations()} + * + * @checker_framework.manual #subtyping-checker Subtyping Checker + */ +@Documented +@SubtypeOf({}) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) +public @interface Bottom {} diff --git a/framework/src/main/java/org/checkerframework/common/subtyping/qual/Unqualified.java b/checker-qual/src/main/java/org/checkerframework/common/subtyping/qual/Unqualified.java similarity index 83% rename from framework/src/main/java/org/checkerframework/common/subtyping/qual/Unqualified.java rename to checker-qual/src/main/java/org/checkerframework/common/subtyping/qual/Unqualified.java index 9dcd93a1df82..ab339ca71eae 100644 --- a/framework/src/main/java/org/checkerframework/common/subtyping/qual/Unqualified.java +++ b/checker-qual/src/main/java/org/checkerframework/common/subtyping/qual/Unqualified.java @@ -1,11 +1,12 @@ package org.checkerframework.common.subtyping.qual; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.InvisibleQualifier; -import org.checkerframework.framework.qual.SubtypeOf; /** * A special annotation intended solely for representing an unqualified type in the qualifier @@ -14,7 +15,7 @@ *

    This annotation may not be written in source code; it is an implementation detail of the * checker. * - *

    Only use this qualifier when experimenting with very simple type systems. For any more + *

    Use this qualifier only when experimenting with very simple type systems. For any more * realistic type systems, introduce a top and bottom qualifier that gets stored in bytecode. * * @checker_framework.manual #subtyping-checker Subtyping Checker @@ -24,6 +25,4 @@ @Target({}) // empty target prevents programmers from writing this in a program. @InvisibleQualifier @SubtypeOf({}) -// At the moment defaulting is done in SubtypingATF to prevent errors in checker-framework-inference -// @DefaultFor(TypeUseLocation.OTHERWISE) public @interface Unqualified {} diff --git a/framework/src/main/java/org/checkerframework/common/util/report/qual/ReportCall.java b/checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportCall.java similarity index 100% rename from framework/src/main/java/org/checkerframework/common/util/report/qual/ReportCall.java rename to checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportCall.java diff --git a/framework/src/main/java/org/checkerframework/common/util/report/qual/ReportCreation.java b/checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportCreation.java similarity index 100% rename from framework/src/main/java/org/checkerframework/common/util/report/qual/ReportCreation.java rename to checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportCreation.java diff --git a/framework/src/main/java/org/checkerframework/common/util/report/qual/ReportInherit.java b/checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportInherit.java similarity index 100% rename from framework/src/main/java/org/checkerframework/common/util/report/qual/ReportInherit.java rename to checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportInherit.java diff --git a/framework/src/main/java/org/checkerframework/common/util/report/qual/ReportOverride.java b/checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportOverride.java similarity index 100% rename from framework/src/main/java/org/checkerframework/common/util/report/qual/ReportOverride.java rename to checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportOverride.java diff --git a/framework/src/main/java/org/checkerframework/common/util/report/qual/ReportReadWrite.java b/checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportReadWrite.java similarity index 100% rename from framework/src/main/java/org/checkerframework/common/util/report/qual/ReportReadWrite.java rename to checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportReadWrite.java diff --git a/framework/src/main/java/org/checkerframework/common/util/report/qual/ReportUnqualified.java b/checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportUnqualified.java similarity index 90% rename from framework/src/main/java/org/checkerframework/common/util/report/qual/ReportUnqualified.java rename to checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportUnqualified.java index 2354ceffe075..8345f9bfa2ce 100644 --- a/framework/src/main/java/org/checkerframework/common/util/report/qual/ReportUnqualified.java +++ b/checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportUnqualified.java @@ -1,17 +1,18 @@ package org.checkerframework.common.util.report.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.InvisibleQualifier; -import org.checkerframework.framework.qual.SubtypeOf; /** * An annotation intended solely for representing an unqualified type in the qualifier hierarchy for - * the Report Checker. + * the Report Checker. This is the only type qualifier in the type hierarchy. */ @Documented @Retention(RetentionPolicy.SOURCE) // do not store in class file diff --git a/framework/src/main/java/org/checkerframework/common/util/report/qual/ReportUse.java b/checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportUse.java similarity index 100% rename from framework/src/main/java/org/checkerframework/common/util/report/qual/ReportUse.java rename to checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportUse.java diff --git a/framework/src/main/java/org/checkerframework/common/util/report/qual/ReportWrite.java b/checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportWrite.java similarity index 100% rename from framework/src/main/java/org/checkerframework/common/util/report/qual/ReportWrite.java rename to checker-qual/src/main/java/org/checkerframework/common/util/report/qual/ReportWrite.java diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/ArrayLen.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/ArrayLen.java similarity index 99% rename from framework/src/main/java/org/checkerframework/common/value/qual/ArrayLen.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/ArrayLen.java index ac7dbb7410b9..85c2f5b4d6b1 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/ArrayLen.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/ArrayLen.java @@ -1,11 +1,12 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * An annotation indicating the length of an array or a string. If an expression's type has this diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/ArrayLenRange.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/ArrayLenRange.java similarity index 77% rename from framework/src/main/java/org/checkerframework/common/value/qual/ArrayLenRange.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/ArrayLenRange.java index a1dc709dbf01..1dfd0d97e749 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/ArrayLenRange.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/ArrayLenRange.java @@ -1,11 +1,12 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * An expression with this type evaluates to an array or a string whose length is in the given @@ -19,8 +20,17 @@ @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @SubtypeOf(UnknownVal.class) public @interface ArrayLenRange { - /** Smallest value in the range, inclusive. */ + /** + * Smallest value in the range, inclusive. + * + * @return the smallest value in the range, inclusive + */ int from() default 0; - /** Largest value in the range, inclusive. */ + + /** + * Largest value in the range, inclusive. + * + * @return the largest value in the range, inclusive + */ int to() default Integer.MAX_VALUE; } diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/BoolVal.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/BoolVal.java similarity index 99% rename from framework/src/main/java/org/checkerframework/common/value/qual/BoolVal.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/BoolVal.java index cc88fde7bc37..adbb4d2d06fa 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/BoolVal.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/BoolVal.java @@ -1,11 +1,12 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * An annotation indicating the possible values for a bool type. If an expression's type has this diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/BottomVal.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/BottomVal.java similarity index 89% rename from framework/src/main/java/org/checkerframework/common/value/qual/BottomVal.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/BottomVal.java index 6c71fbe1ee0a..26b00d638276 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/BottomVal.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/BottomVal.java @@ -1,14 +1,15 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + 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.InvisibleQualifier; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TargetLocations; -import org.checkerframework.framework.qual.TypeUseLocation; /** * The bottom type in the Constant Value type system. Programmers should rarely write this type. @@ -19,13 +20,15 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@TargetLocations({TypeUseLocation.EXPLICIT_LOWER_BOUND, TypeUseLocation.EXPLICIT_UPPER_BOUND}) +@TargetLocations({TypeUseLocation.UPPER_BOUND, TypeUseLocation.LOWER_BOUND}) @SubtypeOf({ ArrayLen.class, BoolVal.class, DoubleVal.class, IntVal.class, StringVal.class, + MatchesRegex.class, + DoesNotMatchRegex.class, ArrayLenRange.class, IntRange.class, IntRangeFromPositive.class, diff --git a/checker-qual/src/main/java/org/checkerframework/common/value/qual/DoesNotMatchRegex.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/DoesNotMatchRegex.java new file mode 100644 index 000000000000..8fb1c2667e16 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/DoesNotMatchRegex.java @@ -0,0 +1,33 @@ +package org.checkerframework.common.value.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * An annotation indicating the possible values for a String type. The annotation's arguments are + * Java regular expressions. If an expression's type has this annotation, then at run time, the + * expression evaluates to a string that matches none of the regular expressions. Matching is via + * the java.lang.String#matches + * method, which matches against the entire string (it does not look for a match against a + * substring). + * + * @checker_framework.manual #constant-value-checker Constant Value Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) +@SubtypeOf({UnknownVal.class}) +public @interface DoesNotMatchRegex { + /** + * A set of Java regular expressions. + * + * @return the regular expressions + */ + String[] value(); +} diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/DoubleVal.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/DoubleVal.java similarity index 99% rename from framework/src/main/java/org/checkerframework/common/value/qual/DoubleVal.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/DoubleVal.java index cd180efdcd3f..612ec4da291d 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/DoubleVal.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/DoubleVal.java @@ -1,11 +1,12 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * An annotation indicating the possible values for a double or float type. If an expression's type diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/EnsuresMinLenIf.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/EnsuresMinLenIf.java similarity index 97% rename from framework/src/main/java/org/checkerframework/common/value/qual/EnsuresMinLenIf.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/EnsuresMinLenIf.java index 239f2c741be0..298b1ba3edd5 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/EnsuresMinLenIf.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/EnsuresMinLenIf.java @@ -1,14 +1,15 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; +import org.checkerframework.framework.qual.InheritedAnnotation; +import org.checkerframework.framework.qual.QualifierArgument; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.ConditionalPostconditionAnnotation; -import org.checkerframework.framework.qual.InheritedAnnotation; -import org.checkerframework.framework.qual.QualifierArgument; /** * Indicates that the value of the given expression is a sequence containing at least the given @@ -27,6 +28,13 @@ @InheritedAnnotation @Repeatable(EnsuresMinLenIf.List.class) public @interface EnsuresMinLenIf { + /** + * Returns the return value of the method under which the postcondition holds. + * + * @return the return value of the method under which the postcondition holds + */ + boolean result(); + /** * Returns Java expression(s) that are a sequence with the given minimum length after the method * returns {@link #result}. @@ -37,13 +45,6 @@ */ String[] expression(); - /** - * Returns the return value of the method under which the postcondition to hold. - * - * @return the return value of the method under which the postcondition to hold - */ - boolean result(); - /** * Returns the minimum number of elements in the sequence. * @@ -63,7 +64,7 @@ @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) @ConditionalPostconditionAnnotation(qualifier = MinLen.class) @InheritedAnnotation - @interface List { + public static @interface List { /** * Return the repeatable annotations. * diff --git a/checker-qual/src/main/java/org/checkerframework/common/value/qual/EnumVal.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/EnumVal.java new file mode 100644 index 000000000000..cfd60863271b --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/EnumVal.java @@ -0,0 +1,30 @@ +package org.checkerframework.common.value.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; + +/** + * An annotation indicating the possible values for an enum type. If an expression's type has this + * annotation, then at run time, the expression evaluates to one of the enum values named by the + * arguments. EnumVal uses the simple name of the enum value: the EnumVal type corresponding to + * {@code MyEnum.MY_VALUE} is {@code @EnumVal("MY_VALUE")}. + * + *

    This annotation is treated as {@link StringVal} internally by the Constant Value Checker. + * + * @checker_framework.manual #constant-value-checker Constant Value Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) +public @interface EnumVal { + /** + * The simple names of the possible enum values for an expression with the annotated type. + * + * @return the simple names of the possible enum values for an expression with the annotated + * type + */ + String[] value(); +} diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/IntRange.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/IntRange.java similarity index 80% rename from framework/src/main/java/org/checkerframework/common/value/qual/IntRange.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/IntRange.java index 5ee28577902c..db16428bb024 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/IntRange.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/IntRange.java @@ -1,11 +1,12 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * An expression with this type evaluates to an integral value (byte, short, char, int, or long) in @@ -16,7 +17,7 @@ * *

    If only one of the {@code to} and {@code from} fields is set, then the other will default to * the max/min value of the type of the variable that is annotated. (In other words, the defaults - * {@code Long.MIN_VALUE} and {@code Long.MAX_VALUE} are used only for \code{long}; appropriate + * {@code Long.MIN_VALUE} and {@code Long.MAX_VALUE} are used only for {@code long}; appropriate * values are used for other types.) * * @checker_framework.manual #constant-value-checker Constant Value Checker @@ -26,8 +27,17 @@ @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @SubtypeOf(UnknownVal.class) public @interface IntRange { - /** Smallest value in the range, inclusive. */ + /** + * Smallest value in the range, inclusive. + * + * @return the smallest value in the range, inclusive + */ long from() default Long.MIN_VALUE; - /** Largest value in the range, inclusive. */ + + /** + * Largest value in the range, inclusive. + * + * @return the largest value in the range, inclusive + */ long to() default Long.MAX_VALUE; } diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/IntRangeFromGTENegativeOne.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/IntRangeFromGTENegativeOne.java similarity index 99% rename from framework/src/main/java/org/checkerframework/common/value/qual/IntRangeFromGTENegativeOne.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/IntRangeFromGTENegativeOne.java index fc65c4a9f203..231f91160870 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/IntRangeFromGTENegativeOne.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/IntRangeFromGTENegativeOne.java @@ -1,10 +1,11 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.SubtypeOf; + import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.SubtypeOf; /** * An expression with this type is exactly the same as an {@link IntRange} annotation whose {@code diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/IntRangeFromNonNegative.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/IntRangeFromNonNegative.java similarity index 99% rename from framework/src/main/java/org/checkerframework/common/value/qual/IntRangeFromNonNegative.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/IntRangeFromNonNegative.java index 434ff259de39..0d39d1bf6015 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/IntRangeFromNonNegative.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/IntRangeFromNonNegative.java @@ -1,10 +1,11 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.SubtypeOf; + import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.SubtypeOf; /** * An expression with this type is exactly the same as an {@link IntRange} annotation whose {@code diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/IntRangeFromPositive.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/IntRangeFromPositive.java similarity index 99% rename from framework/src/main/java/org/checkerframework/common/value/qual/IntRangeFromPositive.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/IntRangeFromPositive.java index 4941505975a3..38fc9f88a184 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/IntRangeFromPositive.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/IntRangeFromPositive.java @@ -1,10 +1,11 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.SubtypeOf; + import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.checkerframework.framework.qual.SubtypeOf; /** * An expression with this type is exactly the same as an {@link IntRange} annotation whose {@code diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/IntVal.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/IntVal.java similarity index 99% rename from framework/src/main/java/org/checkerframework/common/value/qual/IntVal.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/IntVal.java index ad9658ab1f45..6ee20087e74b 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/IntVal.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/IntVal.java @@ -1,11 +1,12 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * An annotation indicating the possible values for a byte, short, char, int, or long type. If an diff --git a/checker-qual/src/main/java/org/checkerframework/common/value/qual/MatchesRegex.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/MatchesRegex.java new file mode 100644 index 000000000000..49fa1377a88d --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/MatchesRegex.java @@ -0,0 +1,33 @@ +package org.checkerframework.common.value.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +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; + +/** + * An annotation indicating the possible values for a String type. The annotation's arguments are + * Java regular expressions. If an expression's type has this annotation, then at run time, the + * expression evaluates to a string that matches at least one of the regular expressions. Matching + * is via the java.lang.String#matches + * method, which matches against the entire string (it does not look for a match against a + * substring). + * + * @checker_framework.manual #constant-value-checker Constant Value Checker + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) +@SubtypeOf({UnknownVal.class}) +public @interface MatchesRegex { + /** + * A set of Java regular expressions. + * + * @return the regular expressions + */ + String[] value(); +} diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/MinLen.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/MinLen.java similarity index 93% rename from framework/src/main/java/org/checkerframework/common/value/qual/MinLen.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/MinLen.java index 0290b93b321c..63a5a04367b6 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/MinLen.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/MinLen.java @@ -11,7 +11,7 @@ * elements. An alias for an {@link ArrayLenRange} annotation with a {@code from} field and the * maximum possible value for an array length ({@code Integer.MAX_VALUE}) as its {@code to} field. * - *

    This annotation is used extensively by the Index Chcker. + *

    This annotation is used extensively by the Index Checker. * * @checker_framework.manual #constant-value-checker Constant Value Checker */ diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/MinLenFieldInvariant.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/MinLenFieldInvariant.java similarity index 99% rename from framework/src/main/java/org/checkerframework/common/value/qual/MinLenFieldInvariant.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/MinLenFieldInvariant.java index d11a0aa5c0f1..ffe7d404fd02 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/MinLenFieldInvariant.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/MinLenFieldInvariant.java @@ -1,12 +1,13 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.FieldInvariant; + 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.FieldInvariant; /** * A specialization of {@link FieldInvariant} for specifying the minimum length of an array. A class diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/PolyValue.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/PolyValue.java similarity index 99% rename from framework/src/main/java/org/checkerframework/common/value/qual/PolyValue.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/PolyValue.java index 8ae998b9b7fb..99e6ddec3539 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/PolyValue.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/PolyValue.java @@ -1,11 +1,12 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.PolymorphicQualifier; + 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.PolymorphicQualifier; /** * A polymorphic qualifier for the Constant Value Checker. diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/StaticallyExecutable.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/StaticallyExecutable.java similarity index 100% rename from framework/src/main/java/org/checkerframework/common/value/qual/StaticallyExecutable.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/StaticallyExecutable.java diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/StringVal.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/StringVal.java similarity index 99% rename from framework/src/main/java/org/checkerframework/common/value/qual/StringVal.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/StringVal.java index 458a456fd06a..95472e9746fa 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/StringVal.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/StringVal.java @@ -1,11 +1,12 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.SubtypeOf; + 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.SubtypeOf; /** * An annotation indicating the possible values for a String type. If an expression's type has this diff --git a/framework/src/main/java/org/checkerframework/common/value/qual/UnknownVal.java b/checker-qual/src/main/java/org/checkerframework/common/value/qual/UnknownVal.java similarity index 89% rename from framework/src/main/java/org/checkerframework/common/value/qual/UnknownVal.java rename to checker-qual/src/main/java/org/checkerframework/common/value/qual/UnknownVal.java index bcc6b9d11865..fdbd1ff373cb 100644 --- a/framework/src/main/java/org/checkerframework/common/value/qual/UnknownVal.java +++ b/checker-qual/src/main/java/org/checkerframework/common/value/qual/UnknownVal.java @@ -1,12 +1,14 @@ package org.checkerframework.common.value.qual; +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.InvisibleQualifier; +import org.checkerframework.framework.qual.SubtypeOf; + 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.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.SubtypeOf; /** * UnknownVal is a type annotation indicating that the expression's value is not known at compile @@ -19,4 +21,5 @@ @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) @SubtypeOf({}) @DefaultQualifierInHierarchy +@InvisibleQualifier public @interface UnknownVal {} diff --git a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/AssertMethod.java b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/AssertMethod.java new file mode 100644 index 000000000000..5622d6de0f2a --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/AssertMethod.java @@ -0,0 +1,69 @@ +package org.checkerframework.dataflow.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; + +/** + * {@code AssertMethod} is a method annotation that indicates that a method throws an exception if + * the value of a boolean argument is false. This can be used to annotate methods such as JUnit's + * {@code Assertions.assertTrue(...)}. + * + *

    The annotation enables flow-sensitive type refinement to be more precise. For example, if + * {@code Assertions.assertTrue} is annotated as follows: + * + *

    @AssertMethod(value = AssertionFailedError.class)
    + * public static void assertFalse(boolean condition);
    + * 
    + * + * Then, in the code below, the Optional Checker can determine that {@code optional} has a value and + * the call to {@code Optional#get} will not throw an exception. + * + *
    + * Assertions.assertTrue(optional.isPresent());
    + * Object o = optional.get();
    + * 
    + * + *

    This annotation is a trusted annotation, meaning that the Checker Framework does not + * check whether the annotated method really does throw an exception depending on the boolean + * expression. + * + * @checker_framework.manual #type-refinement Automatic type refinement (flow-sensitive type + * qualifier inference) + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface AssertMethod { + + /** + * The class of the exception thrown by this method. The default is {@link AssertionError}. + * + * @return class of the exception thrown by this method + */ + Class value() default AssertionError.class; + + /** + * The one-based index of the boolean parameter that is tested. + * + * @return the one-based index of the boolean parameter that is tested + */ + int parameter() default 1; + + /** + * Returns whether this method asserts that the boolean expression is false. + * + *

    For example, JUnit's Assertions.assertFalse(...) + * throws an exception if the first argument is false. So it is annotated as follows: + * + *

    @AssertMethod(value = AssertionFailedError.class, isAssertFalse = true)
    +     * public static void assertFalse(boolean condition);
    +     * 
    + * + * @return the value for {@link #parameter} on which the method throws an exception + */ + boolean isAssertFalse() default false; +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/qual/Deterministic.java b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/Deterministic.java similarity index 86% rename from dataflow/src/main/java/org/checkerframework/dataflow/qual/Deterministic.java rename to checker-qual/src/main/java/org/checkerframework/dataflow/qual/Deterministic.java index 5d2825cadc7e..c56484f0dd67 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/qual/Deterministic.java +++ b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/Deterministic.java @@ -8,7 +8,7 @@ /** * A method is called deterministic if it returns the same value (according to {@code ==}) - * every time it is called with the same parameters and in the same environment. The parameters + * every time it is called with the same arguments and in the same environment. The arguments * include the receiver, and the environment includes all of the Java heap (that is, all fields of * all objects and all static variables). * @@ -40,7 +40,9 @@ * of the following Java constructs: * *
      - *
    1. Assignment to any expression, except for local variables (and method parameters). + *
    2. Assignment to any expression, except for local variables and method parameters.
      + * (Note that storing into an array element, such a {@code a[i] = x}, is not an assignment to + * a variable and is therefore forbidden.) *
    3. A method invocation of a method that is not {@link Deterministic}. *
    4. Construction of a new object. *
    5. Catching any exceptions. This restriction prevents a method from obtaining a reference to a @@ -60,9 +62,10 @@ *
    * * - * A constructor can be {@code @Pure}, but a constructor invocation is not deterministic - * since it returns a different new object each time. TODO: Side-effect-free constructors could be - * allowed to set their own fields. + * When a constructor is annotated as {@code Deterministic} (or {@code @Pure}), that means that all + * the fields are deterministic (the same values, if the arguments are the same). The constructed + * object itself is different. That is, a constructor invocation is never deterministic + * since it returns a different new object each time. * *

    Note that the rules for checking currently imply that every {@code Deterministic} method is * also {@link SideEffectFree}. This might change in the future; in general, a deterministic method diff --git a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/Impure.java b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/Impure.java new file mode 100644 index 000000000000..e9c88a85c31f --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/Impure.java @@ -0,0 +1,29 @@ +package org.checkerframework.dataflow.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; + +/** + * {@code Impure} is a method annotation that means the method might have side effects and/or might + * be nondeterministic. Conceptually, it completes the "lattice" of purity annotations by serving as + * a top element. That is, any {@code @}{@link Pure} method can be treated as {@code @}{@link + * SideEffectFree} or {@code @}{@link Deterministic}, and any {@code @}{@link SideEffectFree} or + * {@code @}{@link Deterministic} method can be treated as {@code @Impure}. + * + *

    This annotation should not be written by a programmer, because leaving a method unannotated is + * equivalent to writing this annotation. + * + *

    The purpose of this annotation is for use by tools. A tool may distinguish between unannotated + * methods (that the tool has not yet examined) and {@code @Impure} methods (that the tool has + * determined to be neither {@code @SideEffectFree} nor {@code @Deterministic}). + * + * @checker_framework.manual #type-refinement-purity Side effects, determinism, purity, and + * flow-sensitive analysis + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +public @interface Impure {} diff --git a/checker-qual/src/main/java/org/checkerframework/dataflow/qual/Pure.java b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/Pure.java new file mode 100644 index 000000000000..1b57e655fd99 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/Pure.java @@ -0,0 +1,37 @@ +package org.checkerframework.dataflow.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; + +/** + * {@code Pure} is a method annotation that means both {@link SideEffectFree} and {@link + * Deterministic}. The more important of these, when performing pluggable type-checking, is usually + * {@link SideEffectFree}. + * + *

    For a discussion of the meaning of {@code Pure} on a constructor, see the documentation of + * {@link Deterministic}. + * + *

    This annotation is inherited by subtypes, just as if it were meta-annotated with + * {@code @InheritedAnnotation}. + * + * @checker_framework.manual #type-refinement-purity Side effects, determinism, purity, and + * flow-sensitive analysis + */ +// @InheritedAnnotation cannot be written here, because "dataflow" project cannot depend on +// "framework" project. +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) +public @interface Pure { + /** The type of purity. */ + enum Kind { + /** The method has no visible side effects. */ + SIDE_EFFECT_FREE, + + /** The method returns exactly the same value when called in the same environment. */ + DETERMINISTIC + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/qual/SideEffectFree.java b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/SideEffectFree.java similarity index 94% rename from dataflow/src/main/java/org/checkerframework/dataflow/qual/SideEffectFree.java rename to checker-qual/src/main/java/org/checkerframework/dataflow/qual/SideEffectFree.java index b3942c2a25d6..da33d80443e7 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/qual/SideEffectFree.java +++ b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/SideEffectFree.java @@ -29,7 +29,9 @@ * of the following Java constructs: * *

      - *
    1. Assignment to any expression, except for local variables and method parameters. + *
    2. Assignment to any expression, except for local variables and method parameters.
      + * (Note that storing into an array element, such a {@code a[i] = x}, is not an assignment to + * a variable and is therefore forbidden.) *
    3. A method invocation of a method that is not {@code @SideEffectFree}. *
    4. Construction of a new object where the constructor is not {@code @SideEffectFree}. *
    diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/qual/TerminatesExecution.java b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/TerminatesExecution.java similarity index 78% rename from dataflow/src/main/java/org/checkerframework/dataflow/qual/TerminatesExecution.java rename to checker-qual/src/main/java/org/checkerframework/dataflow/qual/TerminatesExecution.java index 412c4be48ecf..71d4e8769506 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/qual/TerminatesExecution.java +++ b/checker-qual/src/main/java/org/checkerframework/dataflow/qual/TerminatesExecution.java @@ -28,6 +28,17 @@ *

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

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

    + * ...
    + * myTerminatingMethod();
    + * throw new Error("unreachable");
    + * 
    + * * @checker_framework.manual #type-refinement Automatic type refinement (flow-sensitive type * qualifier inference) */ diff --git a/framework/src/main/java/org/checkerframework/framework/qual/AnnotatedFor.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/AnnotatedFor.java similarity index 93% rename from framework/src/main/java/org/checkerframework/framework/qual/AnnotatedFor.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/AnnotatedFor.java index f6be7e33c507..48c4f7ab47f7 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/AnnotatedFor.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/AnnotatedFor.java @@ -9,7 +9,8 @@ /** * Indicates that this class has been annotated for the given type system. For example, * {@code @AnnotatedFor({"nullness", "regex"})} indicates that the class has been annotated with - * annotations such as {@code @Nullable} and {@code @Regex}. + * annotations such as {@code @Nullable} and {@code @Regex}. The argument to {@code AnnotatedFor} is + * not an annotation name, but a checker name. * *

    You should only use this annotation in a partially-annotated library. There is no point to * using it in a fully-annotated library nor in an application that does not export APIs for diff --git a/framework/src/main/java/org/checkerframework/framework/qual/CFComment.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/CFComment.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/CFComment.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/CFComment.java diff --git a/framework/src/main/java/org/checkerframework/framework/qual/ConditionalPostconditionAnnotation.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/ConditionalPostconditionAnnotation.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/ConditionalPostconditionAnnotation.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/ConditionalPostconditionAnnotation.java diff --git a/framework/src/main/java/org/checkerframework/framework/qual/Covariant.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/Covariant.java similarity index 78% rename from framework/src/main/java/org/checkerframework/framework/qual/Covariant.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/Covariant.java index 58b91c209884..ac79c2e99dbb 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/Covariant.java +++ b/checker-qual/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/checker-qual/src/main/java/org/checkerframework/framework/qual/DefaultFor.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/DefaultFor.java new file mode 100644 index 000000000000..e14916deb290 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/DefaultFor.java @@ -0,0 +1,87 @@ +package org.checkerframework.framework.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; + +/** + * A meta-annotation applied to the declaration of a type qualifier. It specifies that the given + * annotation should be the default for: + * + *
      + *
    • 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 + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface DefaultFor { + /** + * Returns the locations to which the annotation should be applied. + * + * @return the locations to which the annotation should be applied + */ + TypeUseLocation[] value() default {}; + + /** + * Returns {@link TypeKind}s of types for which an annotation should be implicitly added. + * + * @return {@link TypeKind}s of types for which an annotation should be implicitly added + */ + 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}. + * + *

    Only the given types, not their subtypes, receive the default. For instance, if the {@code + * types} element contains only {@code Iterable}, then the default does not apply to a variable + * or expression of type {@code Collection} which is a subtype of {@code Iterable}. + * + * @return {@link Class}es for which an annotation should be applied + */ + Class[] types() default {}; + + /** + * Returns regular expressions matching names of variables, to whose types the annotation should + * be applied as a default. If a regular expression matches the name of a method, the annotation + * is applied as a default to the return type. + * + *

    The regular expression must match the entire variable or method name. For example, to + * match any name that contains "foo", use ".*foo.*". + * + *

    The default does not apply if the name matches any of the regexes in {@link + * #namesExceptions}. + * + *

    This affects formal parameter types only if one of the following is true: + * + *

      + *
    • The method's source code is available; that is, the method is type-checked along with + * client calls. + *
    • When the method was compiled in a previous run of javac, either the processor was run + * or the {@code -g} command-line option was provided. + *
    + * + * @return regular expressions matching variables to whose type an annotation should be applied + */ + String[] names() default {}; + + /** + * Returns exceptions to regular expression rules. + * + * @return exceptions to regular expression rules + * @see #names + */ + String[] namesExceptions() default {}; +} diff --git a/framework/src/main/java/org/checkerframework/framework/qual/DefaultQualifier.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/DefaultQualifier.java similarity index 89% rename from framework/src/main/java/org/checkerframework/framework/qual/DefaultQualifier.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/DefaultQualifier.java index 7089c382dd3e..1d993322abc9 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/DefaultQualifier.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/DefaultQualifier.java @@ -26,6 +26,9 @@ *   class MyClass { ... } *
    * + *

    Defaults on a package also apply to subpackages, unless the {@code applyToSubpackages} field + * is set to false. + * *

    This annotation currently has no effect in stub files. * * @see org.checkerframework.framework.qual.TypeUseLocation @@ -63,6 +66,13 @@ */ TypeUseLocation[] locations() default {TypeUseLocation.ALL}; + /** + * When used on a package, whether the defaults should also apply to subpackages. + * + * @return whether the default should be inherited by subpackages + */ + boolean applyToSubpackages() default true; + /** * A wrapper annotation that makes the {@link DefaultQualifier} annotation repeatable. * @@ -80,7 +90,7 @@ ElementType.LOCAL_VARIABLE, ElementType.PARAMETER }) - @interface List { + public static @interface List { /** * Return the repeatable annotations. * diff --git a/framework/src/main/java/org/checkerframework/framework/qual/DefaultQualifierForUse.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/DefaultQualifierForUse.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/DefaultQualifierForUse.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/DefaultQualifierForUse.java diff --git a/framework/src/main/java/org/checkerframework/framework/qual/DefaultQualifierInHierarchy.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/DefaultQualifierInHierarchy.java similarity index 84% rename from framework/src/main/java/org/checkerframework/framework/qual/DefaultQualifierInHierarchy.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/DefaultQualifierInHierarchy.java index 69515f78251e..5d2309ac66fe 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/DefaultQualifierInHierarchy.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/DefaultQualifierInHierarchy.java @@ -12,8 +12,9 @@ * for the location. * *

    Other defaults can be specified for a checker via the {@link DefaultFor} meta-annotation, - * which takes precedence over {@code DefaultQualifierInHierarchy}, or via {@link - * org.checkerframework.framework.type.GenericAnnotatedTypeFactory#addCheckedCodeDefaults(org.checkerframework.framework.util.defaults.QualifierDefaults)}. + * which takes precedence over {@code DefaultQualifierInHierarchy}, or via {@code + * GenericAnnotatedTypeFactory.addCheckedCodeDefaults()}. Also, the CLIMB-to-top rule applies unless + * explicitly overruled. * *

    The {@link DefaultQualifier} annotation, which targets Java code elements, takes precedence * over {@code DefaultQualifierInHierarchy}. @@ -23,6 +24,7 @@ * * @checker_framework.manual #effective-qualifier The effective qualifier on a type (defaults and * inference) + * @checker_framework.manual #defaults Default qualifiers for unannotated types * @see org.checkerframework.framework.qual.DefaultQualifier */ @Documented diff --git a/framework/src/main/java/org/checkerframework/framework/qual/EnsuresQualifier.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/EnsuresQualifier.java similarity index 98% rename from framework/src/main/java/org/checkerframework/framework/qual/EnsuresQualifier.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/EnsuresQualifier.java index ec1dbdbd3968..f6d4232f9779 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/EnsuresQualifier.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/EnsuresQualifier.java @@ -63,7 +63,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) @InheritedAnnotation - @interface List { + public static @interface List { /** * Return the repeatable annotations. * diff --git a/framework/src/main/java/org/checkerframework/framework/qual/EnsuresQualifierIf.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/EnsuresQualifierIf.java similarity index 97% rename from framework/src/main/java/org/checkerframework/framework/qual/EnsuresQualifierIf.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/EnsuresQualifierIf.java index 6aad619d1db4..4d502b6d38d9 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/EnsuresQualifierIf.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/EnsuresQualifierIf.java @@ -19,7 +19,7 @@ * *

    
      *   {@literal @}EnsuresQualifierIf(result = true, expression = "#1", qualifier = Odd.class)
    - *    boolean isOdd(final int p1, int p2) {
    + *    boolean isOdd(int p1, int p2) {
      *        return p1 % 2 == 1;
      *    }
      * 
    @@ -39,6 +39,13 @@ @InheritedAnnotation @Repeatable(EnsuresQualifierIf.List.class) public @interface EnsuresQualifierIf { + /** + * Returns the return value of the method that needs to hold for the postcondition to hold. + * + * @return the return value of the method that needs to hold for the postcondition to hold + */ + boolean result(); + /** * Returns the Java expressions for which the qualifier holds if the method terminates with * return value {@link #result()}. @@ -58,13 +65,6 @@ */ Class qualifier(); - /** - * Returns the return value of the method that needs to hold for the postcondition to hold. - * - * @return the return value of the method that needs to hold for the postcondition to hold - */ - boolean result(); - /** * A wrapper annotation that makes the {@link EnsuresQualifierIf} annotation repeatable. * @@ -75,7 +75,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @InheritedAnnotation - @interface List { + public static @interface List { /** * Return the repeatable annotations. * diff --git a/framework/src/main/java/org/checkerframework/framework/qual/FieldInvariant.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/FieldInvariant.java similarity index 77% rename from framework/src/main/java/org/checkerframework/framework/qual/FieldInvariant.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/FieldInvariant.java index e59798066bdf..2f5d65e8cf5a 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/FieldInvariant.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/FieldInvariant.java @@ -14,11 +14,10 @@ * *

    The {@code @FieldInvariant} annotation does not currently accommodate type qualifiers with * attributes, such as {@code @MinLen(1)}. In this case, the type system should implement its own - * field invariant annotation and override {@link - * org.checkerframework.framework.type.AnnotatedTypeFactory#getFieldInvariantDeclarationAnnotations()} - * and {@link - * org.checkerframework.framework.type.AnnotatedTypeFactory#getFieldInvariants(javax.lang.model.element.TypeElement)}. - * See {@link org.checkerframework.common.value.qual.MinLenFieldInvariant} for example. + * field invariant annotation and override {@code + * AnnotatedTypeFactory.getFieldInvariantDeclarationAnnotations()} and {@code + * AnnotatedTypeFactory.getFieldInvariants()}. See {@link + * org.checkerframework.common.value.qual.MinLenFieldInvariant} for an example. * * @checker_framework.manual #field-invariants Field invariants */ diff --git a/framework/src/main/java/org/checkerframework/framework/qual/FromByteCode.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/FromByteCode.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/FromByteCode.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/FromByteCode.java diff --git a/framework/src/main/java/org/checkerframework/framework/qual/FromStubFile.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/FromStubFile.java similarity index 85% rename from framework/src/main/java/org/checkerframework/framework/qual/FromStubFile.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/FromStubFile.java index d829774593fc..de528eebb37c 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/FromStubFile.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/FromStubFile.java @@ -8,7 +8,7 @@ /** * If a method is annotated with this declaration annotation, then its signature was read from a - * stub file. It is added by the StubParser and is not visible to the end user. + * stub file. It is added by the AnnotationFileParser and is not visible to the end user. */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/framework/src/main/java/org/checkerframework/framework/qual/HasQualifierParameter.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/HasQualifierParameter.java similarity index 85% rename from framework/src/main/java/org/checkerframework/framework/qual/HasQualifierParameter.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/HasQualifierParameter.java index 5865b90f0b74..b04026166b86 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/HasQualifierParameter.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/HasQualifierParameter.java @@ -3,6 +3,7 @@ import java.lang.annotation.Annotation; 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; @@ -54,15 +55,16 @@ * {@code @Untainted} MyStringBuffer *

    * - *

    This annotation may not be written on the same class as {@code NoQualifierParameter} for the - * same hierarchy. + *

    When a class is {@code @HasQualifierParameter}, all its subclasses are as well. * - *

    When {@code @HasQualifierParameter} is written on a package, it is equivalent to writing it on - * each class in that package with the same arguments, including classes in sub-packages. It can be - * disabled on a specific class by writing {@code @NoQualifierParameter} on that class. + *

    When {@code @HasQualifierParameter} is written on a package, it is equivalent to writing that + * annotation on each class in the package or in a sub-package. It can be disabled on a specific + * class and its subclasses by writing {@code @NoQualifierParameter} on that class. This annotation + * may not be written on the same class as {@code NoQualifierParameter} for the same hierarchy. * * @see NoQualifierParameter */ +@Inherited @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.PACKAGE}) diff --git a/framework/src/main/java/org/checkerframework/framework/qual/IgnoreInWholeProgramInference.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/IgnoreInWholeProgramInference.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/IgnoreInWholeProgramInference.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/IgnoreInWholeProgramInference.java diff --git a/checker-qual/src/main/java/org/checkerframework/framework/qual/InheritedAnnotation.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/InheritedAnnotation.java new file mode 100644 index 000000000000..cd1db57dc269 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/InheritedAnnotation.java @@ -0,0 +1,16 @@ +package org.checkerframework.framework.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; + +/** + * A meta-annotation that specifies if a declaration annotation should be inherited. This should not + * be written on type annotations. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface InheritedAnnotation {} diff --git a/framework/src/main/java/org/checkerframework/framework/qual/InvisibleQualifier.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/InvisibleQualifier.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/InvisibleQualifier.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/InvisibleQualifier.java diff --git a/framework/src/main/java/org/checkerframework/framework/qual/JavaExpression.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/JavaExpression.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/JavaExpression.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/JavaExpression.java diff --git a/framework/src/main/java/org/checkerframework/framework/qual/LiteralKind.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/LiteralKind.java similarity index 96% rename from framework/src/main/java/org/checkerframework/framework/qual/LiteralKind.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/LiteralKind.java index 403415f44024..e027ae6153f7 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/LiteralKind.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/LiteralKind.java @@ -57,6 +57,6 @@ public static List allLiteralKinds() { * @return list of LiteralKinds except for NULL and STRING */ public static List primitiveLiteralKinds() { - return new ArrayList<>(Arrays.asList(INT, LONG, FLOAT, DOUBLE, BOOLEAN, CHAR)); + return Arrays.asList(INT, LONG, FLOAT, DOUBLE, BOOLEAN, CHAR); } } diff --git a/framework/src/main/java/org/checkerframework/framework/qual/MonotonicQualifier.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/MonotonicQualifier.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/MonotonicQualifier.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/MonotonicQualifier.java diff --git a/framework/src/main/java/org/checkerframework/framework/qual/NoDefaultQualifierForUse.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/NoDefaultQualifierForUse.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/NoDefaultQualifierForUse.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/NoDefaultQualifierForUse.java diff --git a/checker-qual/src/main/java/org/checkerframework/framework/qual/NoQualifierParameter.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/NoQualifierParameter.java new file mode 100644 index 000000000000..b61b15aa44e8 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/NoQualifierParameter.java @@ -0,0 +1,41 @@ +package org.checkerframework.framework.qual; + +import java.lang.annotation.Annotation; +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; + +/** + * This is a declaration annotation that applies to type declarations. Some classes conceptually + * take a type qualifier parameter. This annotation indicates that this class and its subclasses + * explicitly do not do so. The only reason to write this annotation is when {@code + * HasQualifierParameter} is enabled by default, by writing {@code HasQualifierParameter} on a + * package. + * + *

    When a class is {@code @NoQualifierParameter}, all its subclasses are as well. + * + *

    One or more top qualifiers must be given for the hierarchies for which there are no qualifier + * parameters. This annotation may not be written on the same class as {@code HasQualifierParameter} + * for the same hierarchy. + * + *

    It is an error for a superclass to be {@code @HasQualifierParameter} but a subclass to be + * {@code @NoQualifierParameter} for the same hierarchy. + * + * @see HasQualifierParameter + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface NoQualifierParameter { + + /** + * Class of the top qualifier for the hierarchy for which this class has no qualifier parameter. + * + * @return the value + */ + Class[] value(); +} diff --git a/checker-qual/src/main/java/org/checkerframework/framework/qual/ParametricTypeVariableUseQualifier.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/ParametricTypeVariableUseQualifier.java new file mode 100644 index 000000000000..c5829652b903 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/ParametricTypeVariableUseQualifier.java @@ -0,0 +1,40 @@ +package org.checkerframework.framework.qual; + +import java.lang.annotation.Annotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A meta-annotation that indicates that an annotation can be used on a type variable use to + * explicitly indicate that the type variable is parametric in that type hierarchy. + * + *

    This is useful in combination with defaults for {@link TypeUseLocation#TYPE_VARIABLE_USE}, as + * it provides a way to have a default qualifier that expresses parametricity, which is usually + * expressed by the absence of an annotation on the type variable use. + * + *

    An annotation meta-annotated with {@code ParametricTypeVariableUseQualifier} will usually not + * be written explicitly in source code. + * + *

    This annotation is currently only for documentation, but will in the future also be used for + * automatic support for parametric type variable use qualifiers. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.ANNOTATION_TYPE}) +@AnnotatedFor("nullness") +public @interface ParametricTypeVariableUseQualifier { + /** + * 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 parametric + * 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 Annotation.class as default value and + // then ensure there is a single top qualifier to use. + Class value() default Annotation.class; +} diff --git a/framework/src/main/java/org/checkerframework/framework/qual/PolymorphicQualifier.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/PolymorphicQualifier.java similarity index 79% rename from framework/src/main/java/org/checkerframework/framework/qual/PolymorphicQualifier.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/PolymorphicQualifier.java index efb664a822ce..c47a25f15d4a 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/PolymorphicQualifier.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/PolymorphicQualifier.java @@ -19,14 +19,17 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.ANNOTATION_TYPE}) +@AnnotatedFor("nullness") public @interface PolymorphicQualifier { /** * 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/PostconditionAnnotation.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/PostconditionAnnotation.java similarity index 79% rename from framework/src/main/java/org/checkerframework/framework/qual/PostconditionAnnotation.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/PostconditionAnnotation.java index 3d60bad7991a..1678614c67c3 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/PostconditionAnnotation.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/PostconditionAnnotation.java @@ -20,10 +20,12 @@ * * *

    The established postcondition P has type specified by the {@code qualifier} field of this - * annotation. If the annotation E has elements annotated by {@link QualifierArgument}, their values - * are copied to the arguments (elements) of annotation P with the same names. Different element - * names may be used in E and P, if a {@link QualifierArgument} in E gives the name of the - * corresponding element in P. + * annotation. + * + *

    If the annotation E has elements annotated by {@link QualifierArgument}, their values are + * copied to the arguments (elements) of annotation P with the same names. Different element names + * may be used in E and P, if a {@link QualifierArgument} in E gives the name of the corresponding + * element in P. * *

    For example, the following code declares a postcondition annotation for the {@link * org.checkerframework.common.value.qual.MinLen} qualifier: @@ -31,10 +33,10 @@ *

    
      * {@literal @}PostconditionAnnotation(qualifier = MinLen.class)
      * {@literal @}Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
    - * public {@literal @}interface EnsuresMinLen {
    - *   String[] value();
    - *   {@literal @}QualifierArgument("value")
    - *   int targetValue() default 0;
    + *  public {@literal @}interface EnsuresMinLen {
    + *    String[] value();
    + *    {@literal @}QualifierArgument("value")
    + *    int targetValue() default 0;
      * 
    * * The {@code value} element holds the expressions to which the qualifier applies and {@code @@ -46,9 +48,9 @@ * *
    
      * {@literal @}EnsuresMinLen(value = "field", targetValue = 2")
    - * public void setField(String argument) {
    - *   field = "(" + argument + ")";
    - * }
    + *  public void setField(String argument) {
    + *    field = "(" + argument + ")";
    + *  }
      * 
    * * @see EnsuresQualifier diff --git a/framework/src/main/java/org/checkerframework/framework/qual/PreconditionAnnotation.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/PreconditionAnnotation.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/PreconditionAnnotation.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/PreconditionAnnotation.java diff --git a/checker-qual/src/main/java/org/checkerframework/framework/qual/PurityUnqualified.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/PurityUnqualified.java new file mode 100644 index 000000000000..231f999ab987 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/PurityUnqualified.java @@ -0,0 +1,21 @@ +package org.checkerframework.framework.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; + +/** + * An annotation intended solely for representing an unqualified type in the qualifier hierarchy for + * the Purity Checker. + * + * @checker_framework.manual #purity-checker Purity Checker + */ +@Documented +@Retention(RetentionPolicy.SOURCE) // do not store in .class file +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({}) +@DefaultQualifierInHierarchy +@InvisibleQualifier +public @interface PurityUnqualified {} diff --git a/framework/src/main/java/org/checkerframework/framework/qual/QualifierArgument.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/QualifierArgument.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/QualifierArgument.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/QualifierArgument.java diff --git a/framework/src/main/java/org/checkerframework/framework/qual/QualifierForLiterals.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/QualifierForLiterals.java similarity index 83% rename from framework/src/main/java/org/checkerframework/framework/qual/QualifierForLiterals.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/QualifierForLiterals.java index 134c8a7c6d09..6c75761f6024 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/QualifierForLiterals.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/QualifierForLiterals.java @@ -8,7 +8,8 @@ /** * A meta-annotation that indicates what qualifier should be given to literals. - * {@code @QualifierForLiterals} is equivalent to {@code @QualfierForLiterals(LiteralKind.ALL)} + * {@code @QualifierForLiterals} (without any annotation elements) is equivalent to + * {@code @QualifierForLiterals(LiteralKind.ALL)} */ @Documented @Retention(RetentionPolicy.RUNTIME) @@ -27,7 +28,7 @@ /** * A string literal that matches any of these patterns has this qualifier. * - *

    If patterns for multiple qualifers match, then the string literal is given the greatest + *

    If patterns for multiple qualifiers match, then the string literal is given the greatest * lower bound of all the matches. */ String[] stringPatterns() default {}; diff --git a/checker-qual/src/main/java/org/checkerframework/framework/qual/RelevantJavaTypes.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/RelevantJavaTypes.java new file mode 100644 index 000000000000..ce53d197aa58 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/RelevantJavaTypes.java @@ -0,0 +1,43 @@ +package org.checkerframework.framework.qual; + +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; + +/** + * An annotation on a SourceChecker subclass to specify which Java types are processed by the + * checker. In source code, the checker's type qualifiers may only appear on the given types and + * their subtypes. If a checker is not annotated with this annotation, then the checker's qualifiers + * may appear on any type. + * + *

    This restriction is coarse-grained in that it applies to all type annotations for a given + * checker. To have different restrictions for different Java types, override {@code + * org.checkerframework.common.basetype.BaseTypeVisitor#visitAnnotatedType(List, Tree)}. + * + *

    This is orthogonal to Java's {@code @Target} annotation; each enforces a different type of + * restriction on what can be written in source code. + * + * @checker_framework.manual #creating-relevant-java-types Relevant Java types + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface RelevantJavaTypes { + /** + * Classes where a type annotation supported by this checker may be written. + * + *

    {@code Object[].class} means that the checker processes all array types. No distinction + * among array types is currently made, and no other array class should be supplied to + * {@code @RelevantJavaTypes}. + * + *

    If a checker processes both primitive and boxed types, both must be specified separately, + * for example as {@code int.class} and {@code Integer.class}. + * + * @return classes where a type annotation supported by this checker may be written + */ + Class[] value(); +} diff --git a/framework/src/main/java/org/checkerframework/framework/qual/RequiresQualifier.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/RequiresQualifier.java similarity index 98% rename from framework/src/main/java/org/checkerframework/framework/qual/RequiresQualifier.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/RequiresQualifier.java index 78839a0afb9f..2092c396e1aa 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/RequiresQualifier.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/RequiresQualifier.java @@ -45,7 +45,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) - @interface List { + public static @interface List { /** * Returns the repeatable annotations. * diff --git a/framework/src/main/java/org/checkerframework/framework/qual/StubFiles.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/StubFiles.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/StubFiles.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/StubFiles.java diff --git a/framework/src/main/java/org/checkerframework/framework/qual/SubtypeOf.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/SubtypeOf.java similarity index 81% rename from framework/src/main/java/org/checkerframework/framework/qual/SubtypeOf.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/SubtypeOf.java index 2bdd5023978a..240c0a4eb730 100644 --- a/framework/src/main/java/org/checkerframework/framework/qual/SubtypeOf.java +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/SubtypeOf.java @@ -10,9 +10,8 @@ /** * A meta-annotation to specify all the qualifiers that the given qualifier is an immediate subtype * of. This provides a declarative way to specify the type qualifier hierarchy. (Alternatively, the - * hierarchy can be defined procedurally by subclassing {@link - * org.checkerframework.framework.type.QualifierHierarchy} or {@link - * org.checkerframework.framework.type.TypeHierarchy}.) + * hierarchy can be defined procedurally by subclassing {@code QualifierHierarchy} or {@code + * TypeHierarchy}.) * *

    Example: * @@ -30,7 +29,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 @@ -38,6 +38,7 @@ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) +@AnnotatedFor("nullness") public @interface SubtypeOf { /** An array of the supertype qualifiers of the annotated qualifier. */ Class[] value(); diff --git a/checker-qual/src/main/java/org/checkerframework/framework/qual/TargetLocations.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/TargetLocations.java new file mode 100644 index 000000000000..d1f544e70176 --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/TargetLocations.java @@ -0,0 +1,43 @@ +package org.checkerframework.framework.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; + +/** + * A meta-annotation that restricts the type-use locations where a type qualifier may be applied. + * When written together with {@code @Target({ElementType.TYPE_USE})}, the given type qualifier may + * be applied only at locations listed in the {@code @TargetLocations(...)} meta-annotation. + * {@code @Target({ElementType.TYPE_USE})} together with no {@code @TargetLocations(...)} means that + * the qualifier can be applied to any type use. {@code @TargetLocations({})} means that the + * qualifier should not be used in source code. The same goal can be achieved by writing + * {@code @Target({})}, which is enforced by javac itself. {@code @TargetLocations({...})} is + * enforced by the checker. The resulting errors from the checker can either be suppressed using + * {@code @SuppressWarnings("type.invalid.annotations.on.location")} or can be ignored by providing + * the {@code -AignoreTargetLocations} option. + * + *

    This enables a type system designer to permit a qualifier to be applied only in certain + * locations. For example, some type systems' top and bottom qualifier (such as {@link + * org.checkerframework.checker.regex.qual.RegexBottom}) should only be written on an explicit + * wildcard upper or lower bound. This meta-annotation is a declarative, coarse-grained approach to + * enable that. For finer-grained control, override {@code visit*} methods that visit trees in + * BaseTypeVisitor. + * + *

    {@code @TargetLocations} are used for all appearances of qualifiers regardless of whether they + * are provided explicitly or implicitly (inferred or computed). Therefore, only use type-use + * locations {@code LOWER_BOUND/UPPER_BOUND} instead of the {@code IMPLICIT_XX/EXPLICIT_XX} + * alternatives. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface TargetLocations { + /** + * Type uses at which the qualifier is permitted to be applied in source code. + * + * @return type-use locations declared in this meta-annotation + */ + TypeUseLocation[] value(); +} diff --git a/framework/src/main/java/org/checkerframework/framework/qual/TypeKind.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/TypeKind.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/TypeKind.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/TypeKind.java diff --git a/checker-qual/src/main/java/org/checkerframework/framework/qual/TypeUseLocation.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/TypeUseLocation.java new file mode 100644 index 000000000000..32014f858b1d --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/TypeUseLocation.java @@ -0,0 +1,147 @@ +package org.checkerframework.framework.qual; + +/** + * Specifies the locations to which a {@link DefaultQualifier} annotation applies. + * + *

    The order of enums is important. Defaults are applied in this order. In particular, this means + * that OTHERWISE and ALL should be last. + * + *

    For an annotation on a variable which has element kind ENUM_CONSTANT, the annotation's + * type-use location is either {@code TypeUseLocation.FIELD} or {@code + * TypeUseLocation.CONSTRUCTOR_RESULT}. + * + *

    Note: The use locations listed here are not complete, for more details see EISOP Issue #340 + * + * @see DefaultQualifier + * @see javax.lang.model.element.ElementKind + */ +public enum TypeUseLocation { + + /** Apply default annotations to all unannotated raw types of fields. */ + FIELD, + + /** + * Apply default annotations to all unannotated raw types of local variables, casts, and + * instanceof. + */ + LOCAL_VARIABLE, + + /** Apply default annotations to all unannotated raw types of resource variables. */ + RESOURCE_VARIABLE, + + /** Apply default annotations to all unannotated raw types of exception parameters. */ + EXCEPTION_PARAMETER, + + /** Apply default annotations to all unannotated raw types of receiver types. */ + RECEIVER, + + /** + * Apply default annotations to all unannotated raw types of formal parameter types, excluding + * the receiver. + */ + PARAMETER, + + /** Apply default annotations to all unannotated raw types of return types. */ + RETURN, + + /** Apply default annotations to all unannotated raw types of constructor result types. */ + CONSTRUCTOR_RESULT, + + /** + * Apply default annotations to unannotated lower bounds for type parameters and wildcards, both + * explicit ones in {@code super} clauses, and implicit lower bounds when no explicit {@code + * extends} or {@code super} clause is present. + */ + LOWER_BOUND, + + /** + * Apply default annotations to unannotated, but explicit lower bounds of wildcards: {@code }. Type parameters have no syntax for explicit lower bound types. + */ + EXPLICIT_LOWER_BOUND, + + /** + * Apply default annotations to unannotated, but implicit lower bounds for type parameters and + * wildcards: {@code } and {@code }, possibly with explicit upper bounds. + */ + // Note: no distinction between implicit lower bound when upper bound is explicit or not, in + // contrast to what we do for upper bounds. We can add that if a type system needs it. + IMPLICIT_LOWER_BOUND, + + /** + * Apply default annotations to unannotated upper bounds for type parameters and wildcards: both + * explicit ones in {@code extends} clauses, and implicit upper bounds when no explicit {@code + * extends} or {@code super} clause is present. + * + *

    Especially useful for parametrized classes that provide a lot of static methods with the + * same generic parameters as the class. + */ + UPPER_BOUND, + + /** + * Apply default annotations to unannotated, but explicit type parameter and wildcard upper + * bounds: {@code } and {@code }. + */ + EXPLICIT_UPPER_BOUND, + + /** + * Apply default annotations to unannotated, but explicit type parameter upper bounds: {@code }. + */ + EXPLICIT_TYPE_PARAMETER_UPPER_BOUND, + + /** + * Apply default annotations to unannotated, but explicit wildcard upper bounds: {@code }. + */ + EXPLICIT_WILDCARD_UPPER_BOUND, + + /** + * Apply default annotations to unannotated upper bounds for type parameters and wildcards + * without explicit upper bounds: {@code }, {@code }, and {@code }. + */ + IMPLICIT_UPPER_BOUND, + + /** + * Apply default annotations to unannotated upper bounds for type parameters without explicit + * upper bounds: {@code }. + */ + IMPLICIT_TYPE_PARAMETER_UPPER_BOUND, + + /** + * Apply default annotations to unannotated upper bounds for wildcards without a super bound: + * {@code }. + */ + IMPLICIT_WILDCARD_UPPER_BOUND_NO_SUPER, + + /** + * Apply default annotations to unannotated upper bounds for wildcards with a super bound: + * {@code }. + */ + IMPLICIT_WILDCARD_UPPER_BOUND_SUPER, + + /** + * Apply default annotations to unannotated upper bounds for wildcards with or without a super + * bound: {@code } or {@code }. + */ + IMPLICIT_WILDCARD_UPPER_BOUND, + + /** + * Apply default annotations to unannotated type variable uses: {@code T}. + * + *

    To get parametric polymorphism: add a qualifier that is meta-annotated with {@link + * ParametricTypeVariableUseQualifier} to your type system and use it as default for {@code + * TYPE_VARIABLE_USE}, which is treated like no annotation on the type variable use. + */ + TYPE_VARIABLE_USE, + + /** Apply if nothing more concrete is provided. TODO: clarify relation to ALL. */ + OTHERWISE, + + /** + * Apply default annotations to all type uses other than uses of type parameters. Does not allow + * any of the other constants. Usually you want OTHERWISE. + */ + ALL; +} diff --git a/framework/src/main/java/org/checkerframework/framework/qual/Unused.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/Unused.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/Unused.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/Unused.java diff --git a/checker-qual/src/main/java/org/checkerframework/framework/qual/UpperBoundFor.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/UpperBoundFor.java new file mode 100644 index 000000000000..98aebcd7658e --- /dev/null +++ b/checker-qual/src/main/java/org/checkerframework/framework/qual/UpperBoundFor.java @@ -0,0 +1,49 @@ +package org.checkerframework.framework.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; + +/** + * A meta-annotation applied to the declaration of a type qualifier. It specifies that the + * annotation should be the upper bound for + * + *

      + *
    • 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 +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface UpperBoundFor { + /** + * 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 that get an upper bound + */ + TypeKind[] typeKinds() default {}; + + /** + * Returns {@link Class}es that should get an upper bound. The meta-annotated annotation is the + * upper bound. + * + * @return {@link Class}es that get an upper bound + */ + Class[] types() default {}; +} diff --git a/framework/src/main/java/org/checkerframework/framework/qual/package-info.java b/checker-qual/src/main/java/org/checkerframework/framework/qual/package-info.java similarity index 100% rename from framework/src/main/java/org/checkerframework/framework/qual/package-info.java rename to checker-qual/src/main/java/org/checkerframework/framework/qual/package-info.java diff --git a/checker-util/LICENSE.txt b/checker-util/LICENSE.txt new file mode 100644 index 000000000000..47fa7199abf0 --- /dev/null +++ b/checker-util/LICENSE.txt @@ -0,0 +1,22 @@ +Checker Framework utilities +Copyright 2004-present by the Checker Framework developers + +MIT License: + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/checker-util/build.gradle b/checker-util/build.gradle new file mode 100644 index 000000000000..693edfd979f0 --- /dev/null +++ b/checker-util/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'java-library' +} + +dependencies { + api project(':checker-qual') + // Don't add implementation dependencies; checker-util.jar should have no dependencies. + + testImplementation "junit:junit:${versions.junit}" +} + +apply from: rootProject.file('gradle-mvn-push.gradle') + +/** Adds information to the publication for uploading to Maven repositories. */ +final checkerUtilPom(publication) { + sharedPublicationConfiguration(publication) + publication.from components.java + publication.pom { + name = 'Checker Util' + description = 'checker-util contains utility classes for programmers to use at run time.' + licenses { + license { + name = 'The MIT License' + url = 'http://opensource.org/licenses/MIT' + distribution = 'repo' + } + } + } +} + +publishing { + publications { + checkerUtil(MavenPublication) { + checkerUtilPom it + } + } +} + +signing { + sign publishing.publications.checkerUtil +} diff --git a/checker-util/src/main/java/org/checkerframework/checker/formatter/util/FormatUtil.java b/checker-util/src/main/java/org/checkerframework/checker/formatter/util/FormatUtil.java new file mode 100644 index 000000000000..24d9473d16f5 --- /dev/null +++ b/checker-util/src/main/java/org/checkerframework/checker/formatter/util/FormatUtil.java @@ -0,0 +1,331 @@ +package org.checkerframework.checker.formatter.util; + +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; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IllegalFormatConversionException; +import java.util.IllegalFormatException; +import java.util.Map; +import java.util.MissingFormatArgumentException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** 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; + } + } + + /** + * Returns the first argument if the format string is satisfiable, and if the format's + * parameters match the passed {@link ConversionCategory}s. Otherwise throws an exception. + * + * @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 + @ReturnsFormat + public static String asFormat(String format, ConversionCategory... cc) + throws IllegalFormatException { + ConversionCategory[] fcc = formatParameterCategories(format); + if (fcc.length != cc.length) { + throw new ExcessiveOrMissingFormatArgumentException(cc.length, fcc.length); + } + + for (int i = 0; i < cc.length; i++) { + if (cc[i] != fcc[i]) { + throw new IllegalFormatConversionCategoryException(cc[i], fcc[i]); + } + } + + return format; + } + + /** + * Throws an exception if the format is not syntactically valid. + * + * @param format a format string + * @throws IllegalFormatException if the format string is invalid + */ + public static void tryFormatSatisfiability(String format) throws IllegalFormatException { + @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 (null is supplied to each + // format specifier). + "formatter:format.string.invalid", // this is a test of format string validity + }) + String unused = String.format(format, (Object[]) null); + } + + /** + * Returns a {@link ConversionCategory} for every conversion found in the format string. + * + *

    Throws an exception if the format is not syntactically valid. + */ + public static ConversionCategory[] formatParameterCategories(String format) + throws IllegalFormatException { + tryFormatSatisfiability(format); + + int last = -1; // index of last argument referenced + int lasto = -1; // last ordinary index + int maxindex = -1; + + Conversion[] cs = parse(format); + Map conv = new HashMap<>(cs.length); + + for (Conversion c : cs) { + int index = c.index(); + switch (index) { + case -1: // relative index + break; + case 0: // ordinary index + lasto++; + last = lasto; + break; + default: // explicit index + last = index - 1; + break; + } + maxindex = Math.max(maxindex, last); + Integer lastKey = last; + conv.put( + last, + ConversionCategory.intersect( + conv.containsKey(lastKey) + ? conv.get(lastKey) + : ConversionCategory.UNUSED, + c.category())); + } + + ConversionCategory[] res = new ConversionCategory[maxindex + 1]; + for (int i = 0; i <= maxindex; ++i) { + Integer key = i; // autoboxing prevents recognizing that containsKey => get() != null + res[i] = conv.containsKey(key) ? conv.get(key) : ConversionCategory.UNUSED; + } + return res; + } + + /** + * 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; + + /** + * 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 { + 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 + } + } + return index; + } + + /** + * 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 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 private method {@code + * #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<>(); + @Regex(7) Matcher m = fsPattern.matcher(format); + while (m.find()) { + char c = conversionCharFromFormat(m); + switch (c) { + case '%': + case 'n': + break; + default: + cs.add(new Conversion(c, indexFromFormat(m))); + } + } + return cs.toArray(new Conversion[cs.size()]); + } + + public static class ExcessiveOrMissingFormatArgumentException + extends MissingFormatArgumentException { + private static final long serialVersionUID = 17000126L; + + private final int expected; + private final int found; + + /** + * Constructs an instance of this class with the actual argument length and the expected + * one. + */ + public ExcessiveOrMissingFormatArgumentException(int expected, int found) { + super("-"); + this.expected = expected; + this.found = found; + } + + public int getExpected() { + return expected; + } + + public int getFound() { + return found; + } + + @Override + public String getMessage() { + return String.format("Expected %d arguments but found %d.", expected, found); + } + } + + public static class IllegalFormatConversionCategoryException + extends IllegalFormatConversionException { + private static final long serialVersionUID = 17000126L; + + private final ConversionCategory expected; + private final ConversionCategory found; + + /** + * Constructs an instance of this class with the mismatched conversion and the expected one. + */ + public IllegalFormatConversionCategoryException( + ConversionCategory expected, ConversionCategory found) { + super( + 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; + } + + public ConversionCategory getExpected() { + return expected; + } + + public ConversionCategory getFound() { + return found; + } + + @Override + public String getMessage() { + return String.format("Expected category %s but found %s.", expected, found); + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatUtil.java b/checker-util/src/main/java/org/checkerframework/checker/i18nformatter/util/I18nFormatUtil.java similarity index 85% rename from checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatUtil.java rename to checker-util/src/main/java/org/checkerframework/checker/i18nformatter/util/I18nFormatUtil.java index 05d80036c81a..251579e69687 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatUtil.java +++ b/checker-util/src/main/java/org/checkerframework/checker/i18nformatter/util/I18nFormatUtil.java @@ -1,4 +1,14 @@ -package org.checkerframework.checker.i18nformatter; +package org.checkerframework.checker.i18nformatter.util; + +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; import java.text.ChoiceFormat; import java.text.DecimalFormat; @@ -11,15 +21,13 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import org.checkerframework.checker.i18nformatter.qual.I18nChecksFormat; -import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory; -import org.checkerframework.checker.i18nformatter.qual.I18nValidFormat; /** * 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 +35,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); } @@ -43,23 +54,26 @@ public static I18nConversionCategory[] formatParameterCategories(String format) I18nConversion[] cs = MessageFormatParser.parse(format); int maxIndex = -1; - Map conv = new HashMap<>(); + Map conv = new HashMap<>(cs.length); 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; } @@ -98,10 +112,20 @@ public static boolean isFormat(String format) { return true; } + /** An I18n conversion directive. */ private static class I18nConversion { - public int index; - public I18nConversionCategory category; + /** The index into the string. */ + public final int index; + + /** The conversion category. */ + public final I18nConversionCategory category; + /** + * Creates a new I18nConversion. + * + * @param index the index into the string + * @param category the conversion category + */ public I18nConversion(int index, I18nConversionCategory category) { this.index = index; this.category = category; @@ -117,18 +141,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 +189,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 +203,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 +293,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]; @@ -287,7 +319,7 @@ private static void makeFormat(int offsetNumber, StringBuilder[] textSegments) { argumentIndices.add(argumentNumber); // now get the format - I18nConversionCategory category = null; + final I18nConversionCategory category; if (segments[SEG_TYPE].length() != 0) { int type = findKeyword(segments[SEG_TYPE], TYPE_KEYWORDS); switch (type) { @@ -364,7 +396,7 @@ private static void makeFormat(int offsetNumber, StringBuilder[] textSegments) { * Return the index of s in list. If not found, return the index of * s.trim().toLowerCase(Locale.ROOT) in list. If still not found, return -1. */ - private static final int findKeyword(String s, String[] list) { + private static int findKeyword(String s, String[] list) { for (int i = 0; i < list.length; ++i) { if (s.equals(list[i])) { return i; @@ -372,7 +404,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-util/src/main/java/org/checkerframework/checker/nullness/util/NullnessUtil.java b/checker-util/src/main/java/org/checkerframework/checker/nullness/util/NullnessUtil.java new file mode 100644 index 000000000000..0312586d458c --- /dev/null +++ b/checker-util/src/main/java/org/checkerframework/checker/nullness/util/NullnessUtil.java @@ -0,0 +1,314 @@ +package org.checkerframework.checker.nullness.util; + +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. + * + *

    To avoid the need to write the NullnessUtil class name, do: + * + *

    import static org.checkerframework.checker.nullness.util.NullnessUtil.castNonNull;
    + * + * or + * + *
    import static org.checkerframework.checker.nullness.util.NullnessUtil.*;
    + * + *

    Runtime Dependency: If you use this class, you must distribute (or link to) {@code + * checker-qual.jar}, along with your binaries. Or, you 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() { + throw new AssertionError("shouldn't be instantiated"); + } + + /** + * A method that suppresses warnings from the Nullness Checker. + * + *

    The method takes a possibly-null reference, unsafely casts it to have the @NonNull type + * qualifier, and returns it. The Nullness Checker considers both the return value, and also the + * argument, to be non-null after the method call. Therefore, the {@code castNonNull} method can + * be used either as a cast expression or as a statement. The Nullness Checker issues no + * warnings in any of the following code: + * + *

    
    +     *   // one way to use as a cast:
    +     *  {@literal @}NonNull String s = castNonNull(possiblyNull1);
    +     *
    +     *   // another way to use as a cast:
    +     *   castNonNull(possiblyNull2).toString();
    +     *
    +     *   // one way to use as a statement:
    +     *   castNonNull(possiblyNull3);
    +     *   possiblyNull3.toString();
    +     * 
    + * + * The {@code castNonNull} method is intended to be used in situations where the programmer + * definitively knows that a given reference is not null, but the type system is unable to make + * this deduction. It is not intended for defensive programming, in which a programmer cannot + * prove that the value is not null but wishes to have an earlier indication if it is. See the + * Checker Framework Manual for further discussion. + * + *

    The method throws {@link AssertionError} if Java assertions are enabled and the argument + * 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 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 + */ + @EnsuresNonNull("#1") + public static @NonNull T castNonNull(@Nullable T ref) { + assert ref != null : "Misuse of castNonNull: called with a null argument"; + 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 + */ + @EnsuresNonNull("#1") + public static @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) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [] castNonNullDeep( + T @Nullable [] 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 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) + */ + @EnsuresNonNull("#1") + public static @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) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][] castNonNullDeep( + T @Nullable [] @Nullable [] 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 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) + */ + @EnsuresNonNull("#1") + public static @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) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] 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 (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) + */ + @EnsuresNonNull("#1") + public static @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) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] @Nullable [] 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) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][][] castNonNullDeep( + T @Nullable [] @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 (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) + */ + @EnsuresNonNull("#1") + public static @NonNull T @NonNull [][][][][] castNonNullDeep( + T @Nullable [] @Nullable [] @Nullable [] @Nullable [] @Nullable [] 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) + */ + @EnsuresNonNull("#1") + public static @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, @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" + + ((message == null) ? "" : (": " + message)); + checkIfArray(arr[i], message); + } + return (@NonNull T[]) arr; + } + + /** + * 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. + if (comp.isPrimitive()) { + // Nothing to do for arrays of primitive type: primitives are + // never null. + } else { + castNonNullArray((Object[]) ref, message); + } + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/Opt.java b/checker-util/src/main/java/org/checkerframework/checker/nullness/util/Opt.java similarity index 80% rename from checker/src/main/java/org/checkerframework/checker/nullness/Opt.java rename to checker-util/src/main/java/org/checkerframework/checker/nullness/util/Opt.java index 149b9f4f9ebb..06631c75689f 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/Opt.java +++ b/checker-util/src/main/java/org/checkerframework/checker/nullness/util/Opt.java @@ -1,35 +1,35 @@ -package org.checkerframework.checker.nullness; +package org.checkerframework.checker.nullness.util; + +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; import java.util.NoSuchElementException; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; -import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; /** - * Utility class for the Nullness Checker, providing every method in {@link java.util.Optional}, but - * written for possibly-null references rather than for the {@code Optional} type. + * Utility class providing every method in {@link java.util.Optional}, but written for possibly-null + * references rather than for the {@code Optional} type. * *

    To avoid the need to write the {@code Opt} class name at invocation sites, do: * - *

    import static org.checkerframework.checker.nullness.Opt.orElse;
    + *
    import static org.checkerframework.checker.nullness.util.Opt.orElse;
    * * or * - *
    import static org.checkerframework.checker.nullness.Opt.*;
    - * - *

    Runtime Dependency + *

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

    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 copy this class into your own project. * * @see java.util.Optional */ +@AnnotatedFor("nullness") +@SuppressWarnings("NullableWildcard") // Set upper and lower bound of wildcards public final class Opt { /** The Opt class cannot be instantiated. */ @@ -40,6 +40,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 @@ -74,6 +75,8 @@ public static void ifPresent(T primary, Consumer<@NonNull ? super @NonNull T } } + // TODO: Add ifPresentOrElse. + /** * If primary is non-null, and its value matches the given predicate, return the value. If * primary is null or its non-null value does not match the predicate, return null. @@ -116,8 +119,8 @@ public static void ifPresent(T primary, Consumer<@NonNull ? super @NonNull T } /** - * Return primary if it is non-null. If primary is null, invoke {@code other} and return the - * result of that invocation. + * Return {@code primary} if it is non-null. If {@code primary} is null, invoke {@code other} + * and return the result of that invocation. * * @see java.util.Optional#orElseGet(Supplier) */ diff --git a/checker-util/src/main/java/org/checkerframework/checker/optional/util/OptionalUtil.java b/checker-util/src/main/java/org/checkerframework/checker/optional/util/OptionalUtil.java new file mode 100644 index 000000000000..b8dcf7803b8a --- /dev/null +++ b/checker-util/src/main/java/org/checkerframework/checker/optional/util/OptionalUtil.java @@ -0,0 +1,76 @@ +package org.checkerframework.checker.optional.util; + +import org.checkerframework.checker.optional.qual.EnsuresPresent; +import org.checkerframework.checker.optional.qual.MaybePresent; +import org.checkerframework.checker.optional.qual.Present; +import org.checkerframework.framework.qual.AnnotatedFor; + +import java.util.Optional; + +/** + * This is a utility class for the Optional Checker. + * + *

    To avoid the need to write the OptionalUtil class name, do: + * + *

    import static org.checkerframework.checker.optional.util.OptionalUtil.castPresent;
    + * + * or + * + *
    import static org.checkerframework.checker.optional.util.OptionalUtil.*;
    + * + *

    Runtime Dependency: If you use this class, you must distribute (or link to) {@code + * checker-qual.jar}, along with your binaries. Or, you can copy this class into your own project. + */ +@SuppressWarnings({ + "optional", // Optional utilities are trusted regarding the Optional type. + "cast" // Casts look redundant if Optional Checker is not run. +}) +@AnnotatedFor("optional") +public final class OptionalUtil { + + /** The OptionalUtil class should not be instantiated. */ + private OptionalUtil() { + throw new AssertionError("do not instantiate"); + } + + /** + * A method that suppresses warnings from the Optional Checker. + * + *

    The method takes a possibly-empty Optional reference, unsafely casts it to have + * the @Present type qualifier, and returns it. The Optional Checker considers both the return + * value, and also the argument, to be present after the method call. Therefore, the {@code + * castPresent} method can be used either as a cast expression or as a statement. + * + *

    
    +     *   // one way to use as a cast:
    +     *  {@literal @}Present String s = castPresent(possiblyEmpty1);
    +     *
    +     *   // another way to use as a cast:
    +     *   castPresent(possiblyEmpty2).toString();
    +     *
    +     *   // one way to use as a statement:
    +     *   castPresent(possiblyEmpty3);
    +     *   possiblyEmpty3.toString();
    +     * 
    + * + * The {@code castPresent} method is intended to be used in situations where the programmer + * definitively knows that a given Optional reference is present, but the type system is unable + * to make this deduction. It is not intended for defensive programming, in which a programmer + * cannot prove that the value is not empty but wishes to have an earlier indication if it is. + * See the Checker Framework Manual for further discussion. + * + *

    The method throws {@link AssertionError} if Java assertions are enabled and the argument + * is empty. 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 empty. + * + * @param the type of content of the Optional + * @param ref an Optional reference of @MaybePresent type, that is present at run time + * @return the argument, casted to have the type qualifier @Present + */ + @EnsuresPresent("#1") + public static @Present Optional castPresent( + @MaybePresent Optional ref) { + assert ref.isPresent() : "Misuse of castPresent: called with an empty Optional"; + return (@Present Optional) ref; + } +} diff --git a/checker-util/src/main/java/org/checkerframework/checker/regex/util/RegexUtil.java b/checker-util/src/main/java/org/checkerframework/checker/regex/util/RegexUtil.java new file mode 100644 index 000000000000..1abadef0a2f9 --- /dev/null +++ b/checker-util/src/main/java/org/checkerframework/checker/regex/util/RegexUtil.java @@ -0,0 +1,484 @@ +// This class should be kept in sync with org.plumelib.util.RegexUtil in the plume-util project. +// (Warning suppressions may differ.) + +package org.checkerframework.checker.regex.util; + +import org.checkerframework.checker.index.qual.GTENegativeOne; +import org.checkerframework.checker.lock.qual.GuardSatisfied; +import org.checkerframework.checker.mustcall.qual.MustCallUnknown; +import org.checkerframework.checker.nullness.qual.KeyForBottom; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.UnknownKeyFor; +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; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.RandomAccess; +import java.util.function.Function; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * Utility methods for regular expressions, most notably for testing whether a string is a regular + * expression. + * + *

    For an example of intended use, see section Testing whether a string is a regular + * expression in the Checker Framework manual. + * + *

    Runtime Dependency: If you use this class, you must distribute (or link to) {@code + * checker-qual.jar}, along with your binaries. Or, you can copy this class into your own project. + */ +@AnnotatedFor("nullness") +public final class RegexUtil { + + /** This class is a collection of methods; it does not represent anything. */ + private RegexUtil() { + throw new Error("do not instantiate"); + } + + /** + * A checked version of {@link PatternSyntaxException}. + * + *

    This exception is useful when an illegal regex is detected but the contextual information + * to report a helpful error message is not available at the current depth in the call stack. By + * using a checked PatternSyntaxException the error must be handled up the call stack where a + * better error message can be reported. + * + *

    Typical usage is: + * + *

    +     * void myMethod(...) throws CheckedPatternSyntaxException {
    +     *   ...
    +     *   if (! isRegex(myString)) {
    +     *     throw new CheckedPatternSyntaxException(...);
    +     *   }
    +     *   ... Pattern.compile(myString) ...
    +     * 
    + * + * Simply calling {@code Pattern.compile} would have a similar effect, in that {@code + * PatternSyntaxException} would be thrown at run time if {@code myString} is not a regular + * expression. There are two problems with such an approach. First, a client of {@code myMethod} + * might forget to handle the exception, since {@code PatternSyntaxException} is not checked. + * Also, the Regex Checker would issue a warning about the call to {@code Pattern.compile} that + * might throw an exception. The above usage pattern avoids both problems. + * + * @see PatternSyntaxException + */ + public static class CheckedPatternSyntaxException extends Exception { + + /** Unique identifier for serialization. If you add or remove fields, change this number. */ + private static final long serialVersionUID = 6266881831979001480L; + + /** The PatternSyntaxException that this is a wrapper around. */ + private final PatternSyntaxException pse; + + /** + * Constructs a new CheckedPatternSyntaxException equivalent to the given {@link + * PatternSyntaxException}. + * + *

    Consider calling this constructor with the result of {@link RegexUtil#regexError}. + * + * @param pse the PatternSyntaxException to be wrapped + */ + public CheckedPatternSyntaxException(PatternSyntaxException pse) { + this.pse = pse; + } + + /** + * Constructs a new CheckedPatternSyntaxException. + * + * @param desc a description of the error + * @param regex the erroneous pattern + * @param index the approximate index in the pattern of the error, or {@code -1} if the + * index is not known + */ + public CheckedPatternSyntaxException(String desc, String regex, @GTENegativeOne int index) { + this(new PatternSyntaxException(desc, regex, index)); + } + + /** + * Retrieves the description of the error. + * + * @return the description of the error + */ + public String getDescription() { + return pse.getDescription(); + } + + /** + * Retrieves the error index. + * + * @return the approximate index in the pattern of the error, or {@code -1} if the index is + * not known + */ + public int getIndex() { + return pse.getIndex(); + } + + /** + * Returns a multi-line string containing the description of the syntax error and its index, + * the erroneous regular-expression pattern, and a visual indication of the error index + * within the pattern. + * + * @return the full detail message + */ + @Override + @Pure + public String getMessage(@GuardSatisfied CheckedPatternSyntaxException this) { + return pse.getMessage(); + } + + /** + * Retrieves the erroneous regular-expression pattern. + * + * @return the erroneous pattern + */ + public String getPattern() { + return pse.getPattern(); + } + } + + /** + * Returns true if the argument is a syntactically valid regular expression. + * + * @param s string to check for being a regular expression + * @return true iff s is a regular expression + */ + @Pure + @EnsuresQualifierIf(result = true, expression = "#1", qualifier = Regex.class) + public static boolean isRegex(String s) { + return isRegex(s, 0); + } + + /** + * Returns true if the argument is a syntactically valid regular expression with at least the + * given number of groups. + * + * @param s string to check for being a regular expression + * @param groups number of groups expected + * @return true iff s is a regular expression with {@code groups} groups + */ + @SuppressWarnings("regex") // RegexUtil + @Pure + // @EnsuresQualifierIf annotation is extraneous because this method is special-cased + // in RegexTransfer. + @EnsuresQualifierIf(result = true, expression = "#1", qualifier = Regex.class) + public static boolean isRegex(String s, int groups) { + Pattern p; + try { + p = Pattern.compile(s); + } catch (PatternSyntaxException e) { + return false; + } + return getGroupCount(p) >= groups; + } + + /** + * Returns true if the argument is a syntactically valid regular expression. + * + * @param c char to check for being a regular expression + * @return true iff c is a regular expression + */ + @SuppressWarnings({ + "regex", "lock" + }) // RegexUtil; temp value used in pure method is equal up to equals but not up to == + @Pure + @EnsuresQualifierIf(result = true, expression = "#1", qualifier = Regex.class) + public static boolean isRegex(char c) { + return isRegex(Character.toString(c)); + } + + /** + * Returns the argument as a {@code @Regex String} if it is a regex, otherwise throws an error. + * The purpose of this method is to suppress Regex Checker warnings. It should be very rarely + * needed. + * + * @param s string to check for being a regular expression + * @return its argument + * @throws Error if argument is not a regex + */ + @SideEffectFree + // The return type annotation is irrelevant; this method is special-cased by + // RegexAnnotatedTypeFactory. + public static @Regex String asRegex(String s) { + return asRegex(s, 0); + } + + /** + * Returns the argument as a {@code @Regex(groups) String} if it is a regex with at least the + * given number of groups, otherwise throws an error. The purpose of this method is to suppress + * Regex Checker warnings. It should be very rarely needed. + * + * @param s string to check for being a regular expression + * @param groups number of groups expected + * @return its argument + * @throws Error if argument is not a regex + */ + @SuppressWarnings("regex") // RegexUtil + @SideEffectFree + // The return type annotation is irrelevant; this method is special-cased by + // RegexAnnotatedTypeFactory. + public static @Regex String asRegex(String s, int groups) { + try { + Pattern p = Pattern.compile(s); + int actualGroups = getGroupCount(p); + if (actualGroups < groups) { + throw new Error(regexErrorMessage(s, groups, actualGroups)); + } + return s; + } catch (PatternSyntaxException e) { + throw new Error(e); + } + } + + /** + * Returns null if the argument is a syntactically valid regular expression. Otherwise returns a + * string describing why the argument is not a regex. + * + * @param s string to check for being a regular expression + * @return null, or a string describing why the argument is not a regex + */ + @SideEffectFree + public static @Nullable String regexError(String s) { + return regexError(s, 0); + } + + /** + * Returns null if the argument is a syntactically valid regular expression with at least the + * given number of groups. Otherwise returns a string describing why the argument is not a + * regex. + * + * @param s string to check for being a regular expression + * @param groups number of groups expected + * @return null, or a string describing why the argument is not a regex + */ + @SuppressWarnings({"regex", "not.sef"}) // RegexUtil; + @SideEffectFree + public static @Nullable String regexError(String s, int groups) { + try { + Pattern p = Pattern.compile(s); + int actualGroups = getGroupCount(p); + if (actualGroups < groups) { + return regexErrorMessage(s, groups, actualGroups); + } + } catch (PatternSyntaxException e) { + return e.getMessage(); + } + return null; + } + + /** + * Returns null if the argument is a syntactically valid regular expression. Otherwise returns a + * PatternSyntaxException describing why the argument is not a regex. + * + * @param s string to check for being a regular expression + * @return null, or a PatternSyntaxException describing why the argument is not a regex + */ + @SideEffectFree + public static @Nullable PatternSyntaxException regexException(String s) { + return regexException(s, 0); + } + + /** + * Returns null if the argument is a syntactically valid regular expression with at least the + * given number of groups. Otherwise returns a PatternSyntaxException describing why the + * argument is not a regex. + * + * @param s string to check for being a regular expression + * @param groups number of groups expected + * @return null, or a PatternSyntaxException describing why the argument is not a regex + */ + @SuppressWarnings("regex") // RegexUtil + @SideEffectFree + public static @Nullable PatternSyntaxException regexException(String s, int groups) { + try { + Pattern p = Pattern.compile(s); + int actualGroups = getGroupCount(p); + if (actualGroups < groups) { + return new PatternSyntaxException( + regexErrorMessage(s, groups, actualGroups), s, -1); + } + } catch (PatternSyntaxException pse) { + return pse; + } + return null; + } + + /** + * Generates an error message for s when expectedGroups are needed, but s only has actualGroups. + * + * @param s string to check for being a regular expression + * @param expectedGroups the number of needed capturing groups + * @param actualGroups the number of groups that {@code s} has + * @return an error message for s when expectedGroups groups are needed, but s only has + * actualGroups groups + */ + @SideEffectFree + private static String regexErrorMessage(String s, int expectedGroups, int actualGroups) { + return "regex \"" + + s + + "\" has " + + actualGroups + + " groups, but " + + expectedGroups + + " groups are needed."; + } + + /** + * Returns the count of groups in the argument. + * + * @param p pattern whose groups to count + * @return the count of groups in the argument + */ + @SuppressWarnings("lock") // does not depend on object identity + @Pure + private static int getGroupCount(Pattern p) { + return p.matcher("").groupCount(); + } + + /** + * Return the strings such that any one of the regexes matches it. + * + * @param strings a collection of strings + * @param regexes a collection of regular expressions + * @return the strings such that any one of the regexes matches it + */ + public static List matchesSomeRegex( + Collection strings, Collection<@Regex String> regexes) { + List patterns = mapList(Pattern::compile, regexes); + List result = new ArrayList(strings.size()); + for (String s : strings) { + for (Pattern p : patterns) { + if (p.matcher(s).matches()) { + result.add(s); + break; + } + } + } + return result; + } + + /** + * Return true if every string is matched by at least one regex. + * + * @param strings a collection of strings + * @param regexes a collection of regular expressions + * @return true if every string is matched by at least one regex + */ + public static boolean everyStringMatchesSomeRegex( + Collection strings, Collection<@Regex String> regexes) { + List patterns = mapList(Pattern::compile, regexes); + outer: + for (String s : strings) { + for (Pattern p : patterns) { + if (p.matcher(s).matches()) { + continue outer; + } + } + return false; + } + return true; + } + + /** + * Return the strings that are matched by no regex. + * + * @param strings a collection of strings + * @param regexes a collection of regular expressions + * @return the strings such that none of the regexes matches it + */ + public static List matchesNoRegex( + Collection strings, Collection<@Regex String> regexes) { + List patterns = mapList(Pattern::compile, regexes); + List result = new ArrayList(strings.size()); + outer: + for (String s : strings) { + for (Pattern p : patterns) { + if (p.matcher(s).matches()) { + continue outer; + } + } + result.add(s); + } + return result; + } + + /** + * Return true if no string is matched by any regex. + * + * @param strings a collection of strings + * @param regexes a collection of regular expressions + * @return true if no string is matched by any regex + */ + public static boolean noStringMatchesAnyRegex( + Collection strings, Collection<@Regex String> regexes) { + for (String regex : regexes) { + Pattern p = Pattern.compile(regex); + for (String s : strings) { + if (p.matcher(s).matches()) { + return false; + } + } + } + return true; + } + + /// + /// Utilities + /// + + // This is from CollectionsPlume, but is here to make the file self-contained. + + /** + * Applies the function to each element of the given iterable, producing a list of the results. + * + *

    The point of this method is to make mapping operations more concise. Import it with + * + *

    import static org.plumelib.util.CollectionsPlume.mapList;
    + * + * This method is just like {@code transform}, but with the arguments in the other order. + * + *

    To perform replacement in place, see {@code List.replaceAll}. + * + * @param the type of elements of the given iterable + * @param the type of elements of the result list + * @param f a function + * @param iterable an iterable + * @return a list of the results of applying {@code f} to the elements of {@code iterable} + */ + public static < + @KeyForBottom FROM extends @Nullable @UnknownKeyFor Object, + @KeyForBottom TO extends @Nullable @UnknownKeyFor Object> + List mapList( + @MustCallUnknown Function<@MustCallUnknown ? super FROM, ? extends TO> f, + Iterable iterable) { + List result; + + if (iterable instanceof RandomAccess) { + // Per the Javadoc of RandomAccess, an indexed for loop is faster than a foreach loop. + List list = (List) iterable; + int size = list.size(); + result = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + result.add(f.apply(list.get(i))); + } + return result; + } + + if (iterable instanceof Collection) { + result = new ArrayList<>(((Collection) iterable).size()); + } else { + result = new ArrayList<>(); // no information about size is available + } + for (FROM elt : iterable) { + result.add(f.apply(elt)); + } + return result; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessUtil.java b/checker-util/src/main/java/org/checkerframework/checker/signedness/util/SignednessUtil.java similarity index 93% rename from checker/src/main/java/org/checkerframework/checker/signedness/SignednessUtil.java rename to checker-util/src/main/java/org/checkerframework/checker/signedness/util/SignednessUtil.java index 6a846d2f4c1c..8f68a610b7d6 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessUtil.java +++ b/checker-util/src/main/java/org/checkerframework/checker/signedness/util/SignednessUtil.java @@ -1,20 +1,27 @@ -package org.checkerframework.checker.signedness; +package org.checkerframework.checker.signedness.util; + +import org.checkerframework.checker.signedness.qual.Unsigned; +import org.checkerframework.framework.qual.AnnotatedFor; import java.io.IOException; import java.io.RandomAccessFile; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.IntBuffer; -import org.checkerframework.checker.signedness.qual.Unsigned; /** - * Provides static utility methods for unsigned values. Most of these re-implement functionality - * that was introduced in JDK 8, making it available in earlier versions of Java. Others provide new - * functionality. {@link SignednessUtilExtra} has more methods that reference packages that Android - * does not provide. + * Provides static utility methods for unsigned values, beyond what is available in the JDK's + * Unsigned Integer API. The JDK's Unsigned Integer API is methods in primitive wrapper classes and + * in classes {@code Arrays}, {@code RandomAccessFile}, {@code ObjectInputStream}, and {@code + * DataInputStream}. + * + *

    {@link SignednessUtilExtra} has more methods that reference packages that Android does not + * provide. That is, {@code SignednessUtil} can be used anywhere, and {@link SignednessUtilExtra} + * can be used anywhere except on Android. * * @checker_framework.manual #signedness-utilities Utility routines for manipulating unsigned values */ +@AnnotatedFor("nullness") public final class SignednessUtil { private SignednessUtil() { @@ -338,9 +345,16 @@ public static void getUnsigned(ByteBuffer b, @Unsigned byte[] bs) { /** * Compares two unsigned shorts x and y. * + *

    In Java 11 or later, use Short.compareUnsigned. + * + * @param x the first value to compare + * @param y the second value to compare * @return a negative number iff x {@literal <} y, a positive number iff x {@literal >} y, and * zero iff x == y. */ + // TODO: deprecate when we require Java 9, which defines Short.compareUnsigned() + // * @deprecated use Short.compareUnsigned + // @Deprecated // use Short.compareUnsigned @SuppressWarnings("signedness") public static int compareUnsigned(@Unsigned short x, @Unsigned short y) { return Integer.compareUnsigned(Short.toUnsignedInt(x), Short.toUnsignedInt(y)); @@ -349,9 +363,16 @@ public static int compareUnsigned(@Unsigned short x, @Unsigned short y) { /** * Compares two unsigned bytes x and y. * + *

    In Java 11 or later, use Byte.compareUnsigned. + * + * @param x the first value to compare + * @param y the second value to compare * @return a negative number iff x {@literal <} y, a positive number iff x {@literal >} y, and * zero iff x == y. */ + // TODO: deprecate when we require Java 9, which defines Byte.compareUnsigned() + // * @deprecated use Byte.compareUnsigned + // @Deprecated // use Byte.compareUnsigned @SuppressWarnings("signedness") public static int compareUnsigned(@Unsigned byte x, @Unsigned byte y) { return Integer.compareUnsigned(Byte.toUnsignedInt(x), Byte.toUnsignedInt(y)); diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessUtilExtra.java b/checker-util/src/main/java/org/checkerframework/checker/signedness/util/SignednessUtilExtra.java similarity index 86% rename from checker/src/main/java/org/checkerframework/checker/signedness/SignednessUtilExtra.java rename to checker-util/src/main/java/org/checkerframework/checker/signedness/util/SignednessUtilExtra.java index 464e37c50ae6..215cd1351fec 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessUtilExtra.java +++ b/checker-util/src/main/java/org/checkerframework/checker/signedness/util/SignednessUtilExtra.java @@ -1,16 +1,23 @@ -package org.checkerframework.checker.signedness; +package org.checkerframework.checker.signedness.util; + +import org.checkerframework.checker.signedness.qual.Unsigned; +import org.checkerframework.framework.qual.AnnotatedFor; import java.awt.Dimension; import java.awt.image.BufferedImage; -import org.checkerframework.checker.signedness.qual.Unsigned; /** * Provides more static utility methods for unsigned values. These methods use Java packages not - * included in Android. {@link SignednessUtil} has more methods. + * included in Android. + * + *

    {@link SignednessUtil} has more methods that can be used anywhere, whereas {@code + * SignednessUtilExtra} can be used anywhere except on Android. * * @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/units/UnitsTools.java b/checker-util/src/main/java/org/checkerframework/checker/units/util/UnitsTools.java similarity index 78% rename from checker/src/main/java/org/checkerframework/checker/units/UnitsTools.java rename to checker-util/src/main/java/org/checkerframework/checker/units/util/UnitsTools.java index 2559f5cf9238..9069300a1697 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsTools.java +++ b/checker-util/src/main/java/org/checkerframework/checker/units/util/UnitsTools.java @@ -1,30 +1,39 @@ -package org.checkerframework.checker.units; +package org.checkerframework.checker.units.util; import org.checkerframework.checker.units.qual.A; import org.checkerframework.checker.units.qual.C; import org.checkerframework.checker.units.qual.K; +import org.checkerframework.checker.units.qual.N; import org.checkerframework.checker.units.qual.cd; import org.checkerframework.checker.units.qual.degrees; import org.checkerframework.checker.units.qual.g; import org.checkerframework.checker.units.qual.h; +import org.checkerframework.checker.units.qual.kN; import org.checkerframework.checker.units.qual.kg; import org.checkerframework.checker.units.qual.km; import org.checkerframework.checker.units.qual.km2; +import org.checkerframework.checker.units.qual.km3; import org.checkerframework.checker.units.qual.kmPERh; import org.checkerframework.checker.units.qual.m; import org.checkerframework.checker.units.qual.m2; +import org.checkerframework.checker.units.qual.m3; import org.checkerframework.checker.units.qual.mPERs; import org.checkerframework.checker.units.qual.mPERs2; import org.checkerframework.checker.units.qual.min; import org.checkerframework.checker.units.qual.mm; import org.checkerframework.checker.units.qual.mm2; +import org.checkerframework.checker.units.qual.mm3; import org.checkerframework.checker.units.qual.mol; import org.checkerframework.checker.units.qual.radians; import org.checkerframework.checker.units.qual.s; +import org.checkerframework.checker.units.qual.t; +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; @@ -46,6 +55,11 @@ public class UnitsTools { public static final @m2 int m2 = 1; public static final @km2 int km2 = 1; + // Volume + public static final @mm3 int mm3 = 1; + public static final @m3 int m3 = 1; + public static final @km3 int km3 = 1; + // Current public static final @A int A = 1; @@ -76,6 +90,7 @@ public class UnitsTools { // Mass public static final @g int g = 1; public static final @kg int kg = 1; + public static final @t int t = 1; public static @kg int fromGramToKiloGram(@g int g) { return g / 1000; @@ -85,6 +100,26 @@ public class UnitsTools { return kg * 1000; } + public static @t int fromKiloGramToMetricTon(@kg int kg) { + return kg / 1000; + } + + public static @kg int fromMetricTonToKiloGram(@t int t) { + return t * 1000; + } + + // Force + public static final @N int N = 1; + public static final @kN int kN = 1; + + public static @kN int fromNewtonToKiloNewton(@N int N) { + return N / 1000; + } + + public static @N int fromKiloNewtonToNewton(@kN int kN) { + return kN * 1000; + } + // Speed public static final @mPERs int mPERs = 1; public static final @kmPERh int kmPERh = 1; diff --git a/checker-util/src/test/java/org/checkerframework/checker/optional/util/OptionalUtilTest.java b/checker-util/src/test/java/org/checkerframework/checker/optional/util/OptionalUtilTest.java new file mode 100644 index 000000000000..285e44fc2b62 --- /dev/null +++ b/checker-util/src/test/java/org/checkerframework/checker/optional/util/OptionalUtilTest.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.optional.util; + +import org.checkerframework.checker.optional.qual.Present; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Optional; + +public final class OptionalUtilTest { + + @Test + public void test_castPresent() { + + Optional nonEmptyOpt = Optional.of("non-empty"); + Optional emptyOpt = Optional.empty(); + + Assert.assertTrue(nonEmptyOpt.isPresent()); + @Present Optional foo = OptionalUtil.castPresent(nonEmptyOpt); + Assert.assertEquals(foo.get(), "non-empty"); + + Assert.assertFalse(emptyOpt.isPresent()); + Assert.assertThrows(Error.class, () -> OptionalUtil.castPresent(emptyOpt)); + } +} diff --git a/checker-util/src/test/java/org/checkerframework/checker/regex/util/RegexUtilTest.java b/checker-util/src/test/java/org/checkerframework/checker/regex/util/RegexUtilTest.java new file mode 100644 index 000000000000..12a10f70fede --- /dev/null +++ b/checker-util/src/test/java/org/checkerframework/checker/regex/util/RegexUtilTest.java @@ -0,0 +1,267 @@ +// This class should be kept in sync with org.plumelib.util.RegexUtilTest in the plume-util project. + +package org.checkerframework.checker.regex.util; + +import org.checkerframework.checker.regex.qual.Regex; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public final class RegexUtilTest { + + @Test + public void test_isRegex_and_asRegex() { + + String s1 = "colo(u?)r"; + String s2 = "(brown|beige)"; + String s3 = "colou?r"; + String s4 = "1) first point"; + + Assert.assertTrue(RegexUtil.isRegex(s1)); + RegexUtil.asRegex(s1); + Assert.assertTrue(RegexUtil.isRegex(s1, 0)); + RegexUtil.asRegex(s1, 0); + Assert.assertTrue(RegexUtil.isRegex(s1, 1)); + RegexUtil.asRegex(s1, 1); + Assert.assertFalse(RegexUtil.isRegex(s1, 2)); + Assert.assertThrows(Error.class, () -> RegexUtil.asRegex(s1, 2)); + + Assert.assertTrue(RegexUtil.isRegex(s2)); + RegexUtil.asRegex(s2); + Assert.assertTrue(RegexUtil.isRegex(s2, 0)); + RegexUtil.asRegex(s2, 0); + Assert.assertTrue(RegexUtil.isRegex(s2, 1)); + RegexUtil.asRegex(s2, 1); + Assert.assertFalse(RegexUtil.isRegex(s2, 2)); + Assert.assertThrows(Error.class, () -> RegexUtil.asRegex(s2, 2)); + + Assert.assertTrue(RegexUtil.isRegex(s3)); + RegexUtil.asRegex(s3); + Assert.assertTrue(RegexUtil.isRegex(s3, 0)); + RegexUtil.asRegex(s3, 0); + Assert.assertFalse(RegexUtil.isRegex(s3, 1)); + Assert.assertThrows(Error.class, () -> RegexUtil.asRegex(s3, 1)); + Assert.assertFalse(RegexUtil.isRegex(s3, 2)); + Assert.assertThrows(Error.class, () -> RegexUtil.asRegex(s3, 2)); + + Assert.assertFalse(RegexUtil.isRegex(s4)); + Assert.assertThrows(Error.class, () -> RegexUtil.asRegex(s4)); + Assert.assertFalse(RegexUtil.isRegex(s4, 0)); + Assert.assertThrows(Error.class, () -> RegexUtil.asRegex(s4, 0)); + Assert.assertFalse(RegexUtil.isRegex(s4, 1)); + Assert.assertThrows(Error.class, () -> RegexUtil.asRegex(s4, 1)); + Assert.assertFalse(RegexUtil.isRegex(s4, 2)); + Assert.assertThrows(Error.class, () -> RegexUtil.asRegex(s4, 2)); + } + + List s1 = Arrays.asList(new String[] {"a", "b", "c"}); + List s2 = Arrays.asList(new String[] {"a", "b", "c", "d"}); + List s3 = Arrays.asList(new String[] {"aa", "bb", "cc"}); + List s4 = Arrays.asList(new String[] {"a", "aa", "b", "bb", "c"}); + List s5 = Arrays.asList(new String[] {"d", "ee", "fff"}); + List s6 = Arrays.asList(new String[] {"a", "d", "ee", "fff"}); + + List<@Regex String> r1 = Arrays.asList(new @Regex String[] {}); + List<@Regex String> r2 = Arrays.asList(new @Regex String[] {"a", "b", "c"}); + List<@Regex String> r3 = Arrays.asList(new @Regex String[] {"a+", "b+", "c"}); + List<@Regex String> r4 = Arrays.asList(new @Regex String[] {"a+", "b+", "c+"}); + List<@Regex String> r5 = Arrays.asList(new @Regex String[] {".*"}); + + List<@Regex String> r6 = Arrays.asList(new @Regex String[] {"a?b", "a*"}); + List<@Regex String> r7 = Arrays.asList(new @Regex String[] {"a?b+", "a*"}); + + List empty = Collections.emptyList(); + List onlyA = Arrays.asList(new String[] {"a"}); + List onlyAA = Arrays.asList(new String[] {"aa"}); + List onlyC = Arrays.asList(new String[] {"c"}); + List onlyCC = Arrays.asList(new String[] {"cc"}); + List onlyD = Arrays.asList(new String[] {"d"}); + List aaab = Arrays.asList(new String[] {"a", "aa", "b"}); + List ab = Arrays.asList(new String[] {"a", "b"}); + List aabb = Arrays.asList(new String[] {"aa", "bb"}); + List aacc = Arrays.asList(new String[] {"aa", "cc"}); + List bbc = Arrays.asList(new String[] {"bb", "c"}); + List bbcc = Arrays.asList(new String[] {"bb", "cc"}); + List cc = Arrays.asList(new String[] {"cc"}); + List cd = Arrays.asList(new String[] {"c", "d"}); + List eefff = Arrays.asList(new String[] {"ee", "fff"}); + + @Test + public void test_matchesSomeRegex() { + Assert.assertEquals(RegexUtil.matchesSomeRegex(s1, r1), empty); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s2, r1), empty); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s3, r1), empty); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s4, r1), empty); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s5, r1), empty); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s6, r1), empty); + + Assert.assertEquals(RegexUtil.matchesSomeRegex(s1, r2), s1); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s2, r2), s1); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s3, r2), empty); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s4, r2), s1); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s5, r2), empty); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s6, r2), onlyA); + + Assert.assertEquals(RegexUtil.matchesSomeRegex(s1, r3), s1); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s2, r3), s1); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s3, r3), aabb); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s4, r3), s4); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s5, r3), empty); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s6, r3), onlyA); + + Assert.assertEquals(RegexUtil.matchesSomeRegex(s1, r4), s1); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s2, r4), s1); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s3, r4), s3); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s4, r4), s4); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s5, r4), empty); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s6, r4), onlyA); + + Assert.assertEquals(RegexUtil.matchesSomeRegex(s1, r5), s1); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s2, r5), s2); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s3, r5), s3); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s4, r5), s4); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s5, r5), s5); + Assert.assertEquals(RegexUtil.matchesSomeRegex(s6, r5), s6); + + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s1, r1)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s2, r1)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s3, r1)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s4, r1)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s5, r1)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s6, r1)); + + Assert.assertTrue(RegexUtil.everyStringMatchesSomeRegex(s1, r2)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s2, r2)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s3, r2)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s4, r2)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s5, r2)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s6, r2)); + + Assert.assertTrue(RegexUtil.everyStringMatchesSomeRegex(s1, r3)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s2, r3)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s3, r3)); + Assert.assertTrue(RegexUtil.everyStringMatchesSomeRegex(s4, r3)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s5, r3)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s6, r3)); + + Assert.assertTrue(RegexUtil.everyStringMatchesSomeRegex(s1, r4)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s2, r4)); + Assert.assertTrue(RegexUtil.everyStringMatchesSomeRegex(s3, r4)); + Assert.assertTrue(RegexUtil.everyStringMatchesSomeRegex(s4, r4)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s5, r4)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s6, r4)); + + Assert.assertTrue(RegexUtil.everyStringMatchesSomeRegex(s1, r5)); + Assert.assertTrue(RegexUtil.everyStringMatchesSomeRegex(s2, r5)); + Assert.assertTrue(RegexUtil.everyStringMatchesSomeRegex(s3, r5)); + Assert.assertTrue(RegexUtil.everyStringMatchesSomeRegex(s4, r5)); + Assert.assertTrue(RegexUtil.everyStringMatchesSomeRegex(s5, r5)); + Assert.assertTrue(RegexUtil.everyStringMatchesSomeRegex(s6, r5)); + } + + @Test + public void test_matchesNoRegex() { + Assert.assertEquals(RegexUtil.matchesNoRegex(s1, r1), s1); + Assert.assertEquals(RegexUtil.matchesNoRegex(s2, r1), s2); + Assert.assertEquals(RegexUtil.matchesNoRegex(s3, r1), s3); + Assert.assertEquals(RegexUtil.matchesNoRegex(s4, r1), s4); + Assert.assertEquals(RegexUtil.matchesNoRegex(s5, r1), s5); + Assert.assertEquals(RegexUtil.matchesNoRegex(s6, r1), s6); + + Assert.assertEquals(RegexUtil.matchesNoRegex(s1, r2), empty); + Assert.assertEquals(RegexUtil.matchesNoRegex(s2, r2), onlyD); + Assert.assertEquals(RegexUtil.matchesNoRegex(s3, r2), s3); + Assert.assertEquals(RegexUtil.matchesNoRegex(s4, r2), aabb); + Assert.assertEquals(RegexUtil.matchesNoRegex(s5, r2), s5); + Assert.assertEquals(RegexUtil.matchesNoRegex(s6, r2), s5); + + Assert.assertEquals(RegexUtil.matchesNoRegex(s1, r3), empty); + Assert.assertEquals(RegexUtil.matchesNoRegex(s2, r3), onlyD); + Assert.assertEquals(RegexUtil.matchesNoRegex(s3, r3), cc); + Assert.assertEquals(RegexUtil.matchesNoRegex(s4, r3), empty); + Assert.assertEquals(RegexUtil.matchesNoRegex(s5, r3), s5); + Assert.assertEquals(RegexUtil.matchesNoRegex(s6, r3), s5); + + Assert.assertEquals(RegexUtil.matchesNoRegex(s1, r4), empty); + Assert.assertEquals(RegexUtil.matchesNoRegex(s2, r4), onlyD); + Assert.assertEquals(RegexUtil.matchesNoRegex(s3, r4), empty); + Assert.assertEquals(RegexUtil.matchesNoRegex(s4, r4), empty); + Assert.assertEquals(RegexUtil.matchesNoRegex(s5, r4), s5); + Assert.assertEquals(RegexUtil.matchesNoRegex(s6, r4), s5); + + Assert.assertEquals(RegexUtil.matchesNoRegex(s1, r5), empty); + Assert.assertEquals(RegexUtil.matchesNoRegex(s2, r5), empty); + Assert.assertEquals(RegexUtil.matchesNoRegex(s3, r5), empty); + Assert.assertEquals(RegexUtil.matchesNoRegex(s4, r5), empty); + Assert.assertEquals(RegexUtil.matchesNoRegex(s5, r5), empty); + Assert.assertEquals(RegexUtil.matchesNoRegex(s6, r5), empty); + + Assert.assertTrue(RegexUtil.noStringMatchesAnyRegex(s1, r1)); + Assert.assertTrue(RegexUtil.noStringMatchesAnyRegex(s2, r1)); + Assert.assertTrue(RegexUtil.noStringMatchesAnyRegex(s3, r1)); + Assert.assertTrue(RegexUtil.noStringMatchesAnyRegex(s4, r1)); + Assert.assertTrue(RegexUtil.noStringMatchesAnyRegex(s5, r1)); + Assert.assertTrue(RegexUtil.noStringMatchesAnyRegex(s6, r1)); + + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s1, r2)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s2, r2)); + Assert.assertTrue(RegexUtil.noStringMatchesAnyRegex(s3, r2)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s4, r2)); + Assert.assertTrue(RegexUtil.noStringMatchesAnyRegex(s5, r2)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s6, r2)); + + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s1, r3)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s2, r3)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s3, r3)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s4, r3)); + Assert.assertTrue(RegexUtil.noStringMatchesAnyRegex(s5, r3)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s6, r3)); + + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s1, r4)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s2, r4)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s3, r4)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s4, r4)); + Assert.assertTrue(RegexUtil.noStringMatchesAnyRegex(s5, r4)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s6, r4)); + + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s1, r5)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s2, r5)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s3, r5)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s4, r5)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s5, r5)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s6, r5)); + } + + @Test + public void test_r6() { + Assert.assertEquals(ab, RegexUtil.matchesSomeRegex(s1, r6)); + Assert.assertEquals(ab, RegexUtil.matchesSomeRegex(s2, r6)); + Assert.assertEquals(onlyAA, RegexUtil.matchesSomeRegex(s3, r6)); + Assert.assertEquals(aaab, RegexUtil.matchesSomeRegex(s4, r6)); + Assert.assertEquals(empty, RegexUtil.matchesSomeRegex(s5, r6)); + Assert.assertEquals(onlyA, RegexUtil.matchesSomeRegex(s6, r6)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s1, r6)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s2, r6)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s3, r6)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s4, r6)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s5, r6)); + Assert.assertFalse(RegexUtil.everyStringMatchesSomeRegex(s6, r6)); + Assert.assertTrue(RegexUtil.everyStringMatchesSomeRegex(onlyA, r7)); + Assert.assertEquals(RegexUtil.matchesNoRegex(s1, r6), onlyC); + Assert.assertEquals(RegexUtil.matchesNoRegex(s2, r6), cd); + Assert.assertEquals(RegexUtil.matchesNoRegex(s3, r6), bbcc); + Assert.assertEquals(RegexUtil.matchesNoRegex(s3, r7), onlyCC); + Assert.assertEquals(RegexUtil.matchesNoRegex(s4, r6), bbc); + Assert.assertEquals(RegexUtil.matchesNoRegex(s5, r6), s5); + Assert.assertEquals(RegexUtil.matchesNoRegex(s6, r6), s5); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s1, r6)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s2, r6)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s3, r6)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s4, r6)); + Assert.assertTrue(RegexUtil.noStringMatchesAnyRegex(s5, r6)); + Assert.assertFalse(RegexUtil.noStringMatchesAnyRegex(s6, r6)); + } +} diff --git a/checker/bin-devel/Dockerfile-README b/checker/bin-devel/Dockerfile-README index c601f3f51656..89f30518cb3c 100644 --- a/checker/bin-devel/Dockerfile-README +++ b/checker/bin-devel/Dockerfile-README @@ -43,14 +43,17 @@ Preliminaries: Create the Docker image: # Alias to create the Docker image, in an empty directory, and upload to Docker Hub. -# Takes about 12 minutes for jdk8 or jdk11, about 1 hour for jdk8-plus or jdk11-plus. +# Takes about 12 minutes for jdk*, about 1 hour for jdk*-plus. +DOCKERTESTING="" +# DOCKERTESTING="-testing" alias create_upload_docker_image=' \ rm -rf dockerdir && \ mkdir -p dockerdir && \ (cd dockerdir && \ \cp -pf ../Dockerfile-$OS-$JDKVER Dockerfile && \ - docker build -t mdernst/$PROJECT-$OS-$JDKVER . && \ - docker push mdernst/$PROJECT-$OS-$JDKVER) && \ + DOCKERIMAGE="wmdietl/$PROJECT-$OS-$JDKVER$DOCKERTESTING" + docker build -t $DOCKERIMAGE . && \ + docker push $DOCKERIMAGE) && \ rm -rf dockerdir' export OS=ubuntu @@ -73,12 +76,58 @@ export JDKVER=jdk11-plus export PROJECT=cf create_upload_docker_image +export OS=ubuntu +export JDKVER=jdk17 +export PROJECT=cf +create_upload_docker_image + +export OS=ubuntu +export JDKVER=jdk17-plus +export PROJECT=cf +create_upload_docker_image + +export OS=ubuntu +export JDKVER=jdk21 +export PROJECT=cf +create_upload_docker_image + +export OS=ubuntu +export JDKVER=jdk21-plus +export PROJECT=cf +create_upload_docker_image + +export OS=ubuntu +export JDKVER=jdk-latest +export PROJECT=cf +create_upload_docker_image + +export OS=ubuntu +export JDKVER=jdk-latest-plus +export PROJECT=cf +create_upload_docker_image + +export OS=ubuntu +export JDKVER=jdk-next +export PROJECT=cf +create_upload_docker_image + +export OS=ubuntu +export JDKVER=jdk-next-plus +export PROJECT=cf +create_upload_docker_image + +Use numbered JDK releases for versions that should be supported longer term. +jdk-latest is for the latest release and jdk-next is for pre-releases of +the upcoming release. + Cleanup: -After creating docker images, consider deleting the docker containers, -which can take up a lot of disk space. +After creating docker images, if you are low on disk space, +consider deleting the docker containers. To stop and remove/delete all docker containers: docker stop $(docker ps -a -q) docker rm $(docker ps -a -q) or you can just remove some of them. +To really clean up the whole docker system: + docker system prune --all --volumes diff --git a/checker/bin-devel/Dockerfile-ubuntu-for-cfi-jdk11 b/checker/bin-devel/Dockerfile-ubuntu-for-cfi-jdk11 index f9be578f8bfd..55eb1634c54b 100644 --- a/checker/bin-devel/Dockerfile-ubuntu-for-cfi-jdk11 +++ b/checker/bin-devel/Dockerfile-ubuntu-for-cfi-jdk11 @@ -13,12 +13,6 @@ RUN apt-get -qqy update \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -RUN apt-get -qqy update \ -&& apt-get -qqy install python-pip \ -&& apt-get clean \ -&& rm -rf /var/lib/apt/lists/* - -RUN pip3 install --no-cache-dir lithium-reducer PyGithub pyyaml \ -&& pip install --no-cache-dir subprocess32 lithium-reducer PyGithub pyyaml +RUN pip3 install --no-cache-dir lithium-reducer PyGithub pyyaml RUN export JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF8 diff --git a/checker/bin-devel/Dockerfile-ubuntu-for-cfi-jdk8 b/checker/bin-devel/Dockerfile-ubuntu-for-cfi-jdk8 index 6d902cd1a00b..677e30f30a4b 100644 --- a/checker/bin-devel/Dockerfile-ubuntu-for-cfi-jdk8 +++ b/checker/bin-devel/Dockerfile-ubuntu-for-cfi-jdk8 @@ -13,12 +13,6 @@ RUN apt-get -qqy update \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -RUN apt-get -qqy update \ -&& apt-get -qqy install python-pip \ -&& apt-get clean \ -&& rm -rf /var/lib/apt/lists/* - -RUN pip3 install --no-cache-dir lithium-reducer PyGithub pyyaml \ -&& pip install --no-cache-dir subprocess32 lithium-reducer PyGithub pyyaml +RUN pip3 install --no-cache-dir lithium-reducer PyGithub pyyaml RUN export JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF8 diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk-latest b/checker/bin-devel/Dockerfile-ubuntu-jdk-latest new file mode 100644 index 000000000000..2df43c21e3a2 --- /dev/null +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk-latest @@ -0,0 +1,63 @@ +# Create a Docker image that is ready to run the main Checker Framework tests, +# using the latest OpenJDK release, currently OpenJDK 21. + +# "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. +# See releases at https://hub.docker.com/_/ubuntu for available images. +# See https://packages.ubuntu.com/search?suite=default§ion=all&arch=any&keywords=openjdk-21-jdk&searchon=names +# to see what Ubuntu versions support a particular OpenJDK version. +FROM ubuntu:23.10 +MAINTAINER Werner Dietl + +# According to +# https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/: +# * Put "apt-get update" and "apt-get install" and "apt cleanup" in the same RUN command. +# * Do not run "apt-get upgrade"; instead get upstream to update. + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& apt-get -y install aptitude \ +&& aptitude -y install \ + apt-utils + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + openjdk-17-jdk \ + openjdk-21-jdk + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + ant \ + binutils \ + build-essential \ + cpp \ + git \ + jq \ + libcurl3-gnutls \ + make \ + maven \ + pipx \ + python3-distutils \ + python3-requests \ + unzip \ + wget + +# Maven 3.8.7 is the default on Ubuntu 23.04, so the below is not needed. +# (Don't try to use a variable here for the Maven version.) +# RUN export DEBIAN_FRONTEND=noninteractive \ +# && wget https://mirrors.sonic.net/apache/maven/maven-3/3.9.2/binaries/apache-maven-3.9.2-bin.tar.gz \ +# && tar xzvf apache-maven-3.9.2-bin.tar.gz +# ENV PATH="/apache-maven-3.9.2/bin:$PATH" + +ENV PATH="/root/.local/bin:$PATH" +RUN pipx install --pip-args="--no-cache-dir" lithium-reducer + +RUN mkdir /python-env \ +&& python3 -m venv /python-env \ +&& /python-env/bin/pip install --no-cache-dir lithium-reducer PyGithub pyyaml + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get autoremove \ +&& apt-get clean \ +&& rm -rf /var/lib/apt/lists/* diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk-latest-plus b/checker/bin-devel/Dockerfile-ubuntu-jdk-latest-plus new file mode 100644 index 000000000000..4a7303b8bacb --- /dev/null +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk-latest-plus @@ -0,0 +1,103 @@ +# Create a Docker image that is ready to run the full Checker Framework tests, +# including building the manual and Javadoc, using the latest JDK release, +# currently JDK 21. + +# "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. +# See releases at https://hub.docker.com/_/ubuntu for available images. +# See https://packages.ubuntu.com/search?suite=default§ion=all&arch=any&keywords=openjdk-20-jdk&searchon=names +# to see what Ubuntu versions support a particular OpenJDK version. +FROM ubuntu:23.10 +MAINTAINER Werner Dietl + +# According to +# https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/: +# * Put "apt-get update" and "apt-get install" and "apt cleanup" in the same RUN command. +# * Do not run "apt-get upgrade"; instead get upstream to update. + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& apt-get -y install aptitude \ +&& aptitude -y install \ + apt-utils + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + openjdk-17-jdk \ + openjdk-21-jdk + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + ant \ + binutils \ + build-essential \ + cpp \ + git \ + jq \ + libcurl3-gnutls \ + make \ + maven \ + pipx \ + python3-distutils \ + python3-requests \ + unzip \ + wget + +# Maven 3.8.7 is the default on Ubuntu 23.04, so the below is not needed. +# (Don't try to use a variable here for the Maven version.) +# RUN export DEBIAN_FRONTEND=noninteractive \ +# && wget https://mirrors.sonic.net/apache/maven/maven-3/3.9.2/binaries/apache-maven-3.9.2-bin.tar.gz \ +# && tar xzvf apache-maven-3.9.2-bin.tar.gz +# ENV PATH="/apache-maven-3.9.2/bin:$PATH" + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + autoconf \ + devscripts \ + dia \ + hevea \ + imagemagick \ + junit \ + latexmk \ + librsvg2-bin \ + libasound2-dev \ + libcups2-dev \ + libfontconfig1-dev \ + libx11-dev \ + libxext-dev \ + libxrender-dev \ + libxrandr-dev \ + libxtst-dev \ + libxt-dev \ + pdf2svg \ + rsync \ + shellcheck \ + texlive-font-utils \ + texlive-fonts-recommended \ + texlive-latex-base \ + texlive-latex-extra \ + texlive-latex-recommended + +# `pipx ensurepath` only adds to the path in newly-started shells. +# BUT, setting the path for the current user is not enough. +# Azure creates a new user and runs jobs as it. +# So, install into /usr/local/bin which is already on every user's path. +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + pipx \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install black \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install flake8 \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install html5validator \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install lithium-reducer + +RUN mkdir /python-env \ +&& python3 -m venv /python-env \ +&& /python-env/bin/pip install --no-cache-dir lithium-reducer PyGithub pyyaml + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get autoremove \ +&& apt-get clean \ +&& rm -rf /var/lib/apt/lists/* diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk-next b/checker/bin-devel/Dockerfile-ubuntu-jdk-next new file mode 100644 index 000000000000..8c88e3d29eb8 --- /dev/null +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk-next @@ -0,0 +1,63 @@ +# Create a Docker image that is ready to run the main Checker Framework tests, +# using the next upcoming OpenJDK release, currently OpenJDK 22. + +# "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. +# See releases at https://hub.docker.com/_/ubuntu for available images. +# See https://packages.ubuntu.com/search?suite=default§ion=all&arch=any&keywords=openjdk-22-jdk&searchon=names +# to see what Ubuntu versions support a particular OpenJDK version. +FROM ubuntu:mantic +MAINTAINER Werner Dietl + +# According to +# https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/: +# * Put "apt-get update" and "apt-get install" and "apt cleanup" in the same RUN command. +# * Do not run "apt-get upgrade"; instead get upstream to update. + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& apt-get -y install aptitude \ +&& aptitude -y install \ + apt-utils + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + openjdk-17-jdk \ + openjdk-22-jdk + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + ant \ + binutils \ + build-essential \ + cpp \ + git \ + jq \ + libcurl3-gnutls \ + make \ + maven \ + pipx \ + python3-distutils \ + python3-requests \ + unzip \ + wget + +# Maven 3.8.7 is the default on Ubuntu 23.04, so the below is not needed. +# (Don't try to use a variable here for the Maven version.) +# RUN export DEBIAN_FRONTEND=noninteractive \ +# && wget https://mirrors.sonic.net/apache/maven/maven-3/3.9.2/binaries/apache-maven-3.9.2-bin.tar.gz \ +# && tar xzvf apache-maven-3.9.2-bin.tar.gz +# ENV PATH="/apache-maven-3.9.2/bin:$PATH" + +ENV PATH="/root/.local/bin:$PATH" +RUN pipx install --pip-args="--no-cache-dir" lithium-reducer + +RUN mkdir /python-env \ +&& python3 -m venv /python-env \ +&& /python-env/bin/pip install --no-cache-dir lithium-reducer PyGithub pyyaml + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get autoremove \ +&& apt-get clean \ +&& rm -rf /var/lib/apt/lists/* diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk-next-plus b/checker/bin-devel/Dockerfile-ubuntu-jdk-next-plus new file mode 100644 index 000000000000..8077ff69948b --- /dev/null +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk-next-plus @@ -0,0 +1,103 @@ +# Create a Docker image that is ready to run the full Checker Framework tests, +# including building the manual and Javadoc, using the next upcoming OpenJDK release, +# currently OpenJDK 22. + +# "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. +# See releases at https://hub.docker.com/_/ubuntu for available images. +# See https://packages.ubuntu.com/search?suite=default§ion=all&arch=any&keywords=openjdk-20-jdk&searchon=names +# to see what Ubuntu versions support a particular OpenJDK version. +FROM ubuntu:mantic +MAINTAINER Werner Dietl + +# According to +# https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/: +# * Put "apt-get update" and "apt-get install" and "apt cleanup" in the same RUN command. +# * Do not run "apt-get upgrade"; instead get upstream to update. + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& apt-get -y install aptitude \ +&& aptitude -y install \ + apt-utils + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + openjdk-17-jdk \ + openjdk-22-jdk + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + ant \ + binutils \ + build-essential \ + cpp \ + git \ + jq \ + libcurl3-gnutls \ + make \ + maven \ + pipx \ + python3-distutils \ + python3-requests \ + unzip \ + wget + +# Maven 3.8.7 is the default on Ubuntu 23.04, so the below is not needed. +# (Don't try to use a variable here for the Maven version.) +# RUN export DEBIAN_FRONTEND=noninteractive \ +# && wget https://mirrors.sonic.net/apache/maven/maven-3/3.9.2/binaries/apache-maven-3.9.2-bin.tar.gz \ +# && tar xzvf apache-maven-3.9.2-bin.tar.gz +# ENV PATH="/apache-maven-3.9.2/bin:$PATH" + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + autoconf \ + devscripts \ + dia \ + hevea \ + imagemagick \ + junit \ + latexmk \ + librsvg2-bin \ + libasound2-dev \ + libcups2-dev \ + libfontconfig1-dev \ + libx11-dev \ + libxext-dev \ + libxrender-dev \ + libxrandr-dev \ + libxtst-dev \ + libxt-dev \ + pdf2svg \ + rsync \ + shellcheck \ + texlive-font-utils \ + texlive-fonts-recommended \ + texlive-latex-base \ + texlive-latex-extra \ + texlive-latex-recommended + +# `pipx ensurepath` only adds to the path in newly-started shells. +# BUT, setting the path for the current user is not enough. +# Azure creates a new user and runs jobs as it. +# So, install into /usr/local/bin which is already on every user's path. +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + pipx \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install black \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install flake8 \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install html5validator \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install lithium-reducer + +RUN mkdir /python-env \ +&& python3 -m venv /python-env \ +&& /python-env/bin/pip install --no-cache-dir lithium-reducer PyGithub pyyaml + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get autoremove \ +&& apt-get clean \ +&& rm -rf /var/lib/apt/lists/* diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk11 b/checker/bin-devel/Dockerfile-ubuntu-jdk11 index 33d7c3237e62..af90925a8b1d 100644 --- a/checker/bin-devel/Dockerfile-ubuntu-jdk11 +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk11 @@ -1,47 +1,52 @@ # Create a Docker image that is ready to run the main Checker Framework tests, -# using JDK 11. +# using OpenJDK 11. +# "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. +# See releases at https://hub.docker.com/_/ubuntu for available images. +# See https://packages.ubuntu.com/search?suite=default§ion=all&arch=any&keywords=openjdk-20-jdk&searchon=names +# to see what Ubuntu versions support a particular OpenJDK version. FROM ubuntu -MAINTAINER Michael Ernst +MAINTAINER Werner Dietl # According to # https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/: -# * Put "apt-get update" and "apt-get install" and apt cleanup in the same RUN command. +# * Put "apt-get update" and "apt-get install" and "apt cleanup" in the same RUN command. # * Do not run "apt-get upgrade"; instead get upstream to update. RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ -&& apt-get -qqy install \ - openjdk-11-jdk +&& apt-get -y install aptitude \ +&& aptitude -y install \ + apt-utils RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ -&& apt-get -qqy install \ +&& aptitude -y install \ + openjdk-11-jdk \ + openjdk-17-jdk \ +&& update-java-alternatives -s java-1.11.0-openjdk-amd64 + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ ant \ + binutils \ + build-essential \ cpp \ git \ - gradle \ jq \ libcurl3-gnutls \ make \ maven \ - mercurial \ python3-pip \ + python3-distutils \ python3-requests \ unzip \ wget -# Ubuntu 18.04 has Maven 3.6.0, but 3.6.1 apparently has better retry -# behavior to work around network problems. -RUN export DEBIAN_FRONTEND=noninteractive \ -&& VER=3.6.3 \ -&& mkdir -p $HOME/bin/install \ -&& cd $HOME/bin/install \ -&& wget http://apache.mirrors.pair.com/maven/maven-3/${VER}/binaries/apache-maven-${VER}-bin.tar.gz \ -&& tar xzf apache-maven-${VER}-bin.tar.gz \ -&& (cd /usr/local/bin && ln -sf $HOME/bin/install/apache-maven-${VER}/bin/* .) \ -&& hash -r +RUN pip3 install --no-cache-dir lithium-reducer PyGithub pyyaml RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get autoremove \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk11-plus b/checker/bin-devel/Dockerfile-ubuntu-jdk11-plus index e52038bbacfc..2d39223c21e2 100644 --- a/checker/bin-devel/Dockerfile-ubuntu-jdk11-plus +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk11-plus @@ -1,67 +1,81 @@ # Create a Docker image that is ready to run the full Checker Framework tests, -# including building the manual and Javadoc, using JDK 11. +# including building the manual and Javadoc, using OpenJDK 11. +# "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. +# See releases at https://hub.docker.com/_/ubuntu for available images. +# See https://packages.ubuntu.com/search?suite=default§ion=all&arch=any&keywords=openjdk-20-jdk&searchon=names +# to see what Ubuntu versions support a particular OpenJDK version. FROM ubuntu -MAINTAINER Michael Ernst - -## Keep this file in sync with ../../docs/manual/troubleshooting.tex +MAINTAINER Werner Dietl # According to # https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/: -# * Put "apt-get update" and "apt-get install" and apt cleanup in the same RUN command. +# * Put "apt-get update" and "apt-get install" and "apt cleanup" in the same RUN command. # * Do not run "apt-get upgrade"; instead get upstream to update. RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ -&& apt-get -qqy install \ - openjdk-11-jdk +&& apt-get -y install aptitude \ +&& aptitude -y install \ + apt-utils + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + openjdk-11-jdk \ + openjdk-17-jdk \ +&& update-java-alternatives -s java-1.11.0-openjdk-amd64 RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ -&& apt-get -qqy install \ +&& aptitude -y install \ ant \ + binutils \ + build-essential \ cpp \ git \ - gradle \ jq \ libcurl3-gnutls \ make \ maven \ - mercurial \ - pdf2svg \ python3-pip \ + python3-distutils \ python3-requests \ unzip \ wget -# Ubuntu 18.04 has Maven 3.6.0, but 3.6.1 apparently has better retry -# behavior to work around network problems. -RUN export DEBIAN_FRONTEND=noninteractive \ -&& VER=3.6.3 \ -&& mkdir -p $HOME/bin/install \ -&& cd $HOME/bin/install \ -&& wget http://apache.mirrors.pair.com/maven/maven-3/${VER}/binaries/apache-maven-${VER}-bin.tar.gz \ -&& tar xzf apache-maven-${VER}-bin.tar.gz \ -&& (cd /usr/local/bin && ln -sf $HOME/bin/install/apache-maven-${VER}/bin/* .) \ -&& hash -r - RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ -&& apt-get -qqy install \ +&& aptitude -y install \ + autoconf \ + devscripts \ dia \ hevea \ imagemagick \ + junit \ latexmk \ librsvg2-bin \ + libasound2-dev \ + libcups2-dev \ + libfontconfig1-dev \ + libx11-dev \ + libxext-dev \ + libxrender-dev \ + libxrandr-dev \ + libxtst-dev \ + libxt-dev \ + pdf2svg \ rsync \ + shellcheck \ texlive-font-utils \ texlive-fonts-recommended \ texlive-latex-base \ texlive-latex-extra \ texlive-latex-recommended -RUN pip3 install html5validator +RUN pip3 install --no-cache-dir black flake8 html5validator lithium-reducer PyGithub pyyaml RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get autoremove \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk17 b/checker/bin-devel/Dockerfile-ubuntu-jdk17 new file mode 100644 index 000000000000..ff81cfb2a955 --- /dev/null +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk17 @@ -0,0 +1,59 @@ +# Create a Docker image that is ready to run the main Checker Framework tests, +# using OpenJDK 17. + +# "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. +# See releases at https://hub.docker.com/_/ubuntu for available images. +# See https://packages.ubuntu.com/search?suite=default§ion=all&arch=any&keywords=openjdk-17-jdk&searchon=names +# to see what Ubuntu versions support a particular OpenJDK version. +FROM ubuntu +MAINTAINER Werner Dietl + +# According to +# https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/: +# * Put "apt-get update" and "apt-get install" and "apt cleanup" in the same RUN command. +# * Do not run "apt-get upgrade"; instead get upstream to update. + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& apt-get -y install aptitude \ +&& aptitude -y install \ + apt-utils + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + openjdk-17-jdk + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + ant \ + binutils \ + build-essential \ + cpp \ + git \ + jq \ + libcurl3-gnutls \ + make \ + maven \ + python3-pip \ + python3-distutils \ + python3-requests \ + unzip \ + wget + +# Maven 3.6.3 (the default on Ubuntu 22.10) does not run under JDK 17. +# (Don't try to use a variable here for the Maven version.) +RUN export DEBIAN_FRONTEND=noninteractive \ +&& wget https://mirrors.sonic.net/apache/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz \ +&& tar xzvf apache-maven-3.9.5-bin.tar.gz +ENV PATH="/apache-maven-3.9.5/bin:$PATH" + +RUN pip3 install --no-cache-dir lithium-reducer PyGithub pyyaml + +RUN pip3 install --no-cache-dir lithium-reducer PyGithub pyyaml + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get autoremove \ +&& apt-get clean \ +&& rm -rf /var/lib/apt/lists/* diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk17-plus b/checker/bin-devel/Dockerfile-ubuntu-jdk17-plus new file mode 100644 index 000000000000..33c09dc66419 --- /dev/null +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk17-plus @@ -0,0 +1,86 @@ +# Create a Docker image that is ready to run the full Checker Framework tests, +# including building the manual and Javadoc, using OpenJDK 17. + +# "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. +# See releases at https://hub.docker.com/_/ubuntu for available images. +# See https://packages.ubuntu.com/search?suite=default§ion=all&arch=any&keywords=openjdk-17-jdk&searchon=names +# to see what Ubuntu versions support a particular OpenJDK version. +FROM ubuntu +MAINTAINER Werner Dietl + +# According to +# https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/: +# * Put "apt-get update" and "apt-get install" and "apt cleanup" in the same RUN command. +# * Do not run "apt-get upgrade"; instead get upstream to update. + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& apt-get -y install aptitude \ +&& aptitude -y install \ + apt-utils + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + openjdk-17-jdk + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + ant \ + binutils \ + build-essential \ + cpp \ + git \ + jq \ + libcurl3-gnutls \ + make \ + maven \ + python3-pip \ + python3-distutils \ + python3-requests \ + unzip \ + wget + +# Maven 3.6.3 (the default on Ubuntu 22.10) does not run under JDK 17. +# (Don't try to use a variable here for the Maven version.) +RUN export DEBIAN_FRONTEND=noninteractive \ +&& wget https://mirrors.sonic.net/apache/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz \ +&& tar xzvf apache-maven-3.9.5-bin.tar.gz +ENV PATH="/apache-maven-3.9.5/bin:$PATH" + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + autoconf \ + devscripts \ + dia \ + hevea \ + imagemagick \ + junit \ + latexmk \ + librsvg2-bin \ + libasound2-dev \ + libcups2-dev \ + libfontconfig1-dev \ + libx11-dev \ + libxext-dev \ + libxrender-dev \ + libxrandr-dev \ + libxtst-dev \ + libxt-dev \ + pdf2svg \ + rsync \ + shellcheck \ + texlive-font-utils \ + texlive-fonts-recommended \ + texlive-latex-base \ + texlive-latex-extra \ + texlive-latex-recommended + +RUN pip3 install --no-cache-dir black flake8 html5validator lithium-reducer PyGithub pyyaml + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get autoremove \ +&& apt-get clean \ +&& rm -rf /var/lib/apt/lists/* diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk20 b/checker/bin-devel/Dockerfile-ubuntu-jdk20 new file mode 100644 index 000000000000..62787e9a5913 --- /dev/null +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk20 @@ -0,0 +1,71 @@ +# Create a Docker image that is ready to run the main Checker Framework tests, +# using JDK 20. +# (This is OpenJDK, not Oracle JDK. There are different instructions for +# installing a LTS release of Java.) +# To convert this file to use a newer JDK, search (from the top level of the +# Checker Framework and Annotation Tools repositories) for: (java|jdk).?20\b + +# "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. +# See releases at https://hub.docker.com/_/ubuntu +FROM ubuntu:rolling +MAINTAINER Werner Dietl + +# According to +# https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/: +# * Put "apt-get update" and "apt-get install" and "apt cleanup" in the same RUN command. +# * Do not run "apt-get upgrade"; instead get upstream to update. + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& apt-get -y install aptitude \ +&& aptitude -y install \ + apt-utils + +# ca-certificates-java is a dependency of openjdk-20-jdk, but the installation +# process seems to fail sometimes when only openjdk-20-jdk is specified. +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + ca-certificates-java \ +&& aptitude -y install \ + openjdk-17-jdk \ + openjdk-20-jdk + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + ant \ + binutils \ + build-essential \ + cpp \ + git \ + jq \ + libcurl3-gnutls \ + make \ + maven \ + python3-distutils \ + python3-requests \ + unzip \ + wget \ +&& aptitude -y install \ + jtreg6 + +# Maven 3.8.7 is the default on Ubuntu 23.04, so the below is not needed. +# (Don't try to use a variable here for the Maven version.) +# RUN export DEBIAN_FRONTEND=noninteractive \ +# && wget https://mirrors.sonic.net/apache/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz \ +# && tar xzvf apache-maven-3.9.5-bin.tar.gz +# ENV PATH="/apache-maven-3.9.5/bin:$PATH" + +RUN pip3 install --no-cache-dir lithium-reducer PyGithub pyyaml + +# Bug fix to make jtreg runnable: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=754942;msg=2 +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + default-jre-headless + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get autoremove \ +&& apt-get clean \ +&& rm -rf /var/lib/apt/lists/* diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk20-plus b/checker/bin-devel/Dockerfile-ubuntu-jdk20-plus new file mode 100644 index 000000000000..65b10c148884 --- /dev/null +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk20-plus @@ -0,0 +1,106 @@ +# Create a Docker image that is ready to run the full Checker Framework tests, +# including building the manual and Javadoc, using OpenJDK 20. +# To convert this file to use a newer JDK, search (from the top level of the +# Checker Framework and Annotation Tools repositories) for: (java|jdk).?20\b + +# "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. +# See releases at https://hub.docker.com/_/ubuntu for available images. +# See https://packages.ubuntu.com/search?suite=default§ion=all&arch=any&keywords=openjdk-20-jdk&searchon=names +# to see what Ubuntu versions support a particular OpenJDK version. +FROM ubuntu:rolling +MAINTAINER Werner Dietl + +# According to +# https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/: +# * Put "apt-get update" and "apt-get install" and "apt cleanup" in the same RUN command. +# * Do not run "apt-get upgrade"; instead get upstream to update. + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& apt-get -y install aptitude \ +&& aptitude -y install \ + apt-utils + +# ca-certificates-java is a dependency of openjdk-20-jdk, but the installation +# process seems to fail sometimes when only openjdk-20-jdk is specified. +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + ca-certificates-java \ +&& aptitude -y install \ + openjdk-17-jdk \ + openjdk-20-jdk + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + ant \ + binutils \ + build-essential \ + cpp \ + git \ + jq \ + libcurl3-gnutls \ + make \ + maven \ + python3-distutils \ + python3-requests \ + unzip \ + wget \ +&& aptitude -y install \ + jtreg6 + +# Maven 3.8.7 is the default on Ubuntu 23.04, so the below is not needed. +# (Don't try to use a variable here for the Maven version.) +# RUN export DEBIAN_FRONTEND=noninteractive \ +# && wget https://mirrors.sonic.net/apache/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz \ +# && tar xzvf apache-maven-3.9.5-bin.tar.gz +# ENV PATH="/apache-maven-3.9.5/bin:$PATH" + +# Bug fix to make jtreg runnable: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=754942;msg=2 +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + default-jre-headless + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + autoconf \ + devscripts \ + dia \ + hevea \ + imagemagick \ + junit \ + latexmk \ + librsvg2-bin \ + libasound2-dev libcups2-dev libfontconfig1-dev \ + libx11-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxt-dev \ + pdf2svg \ + rsync \ + shellcheck \ + texlive-font-utils \ + texlive-fonts-recommended \ + texlive-latex-base \ + texlive-latex-extra \ + texlive-latex-recommended + +# `pipx ensurepath` only adds to the path in newly-started shells. +# BUT, setting the path for the current user is not enough. +# Azure creates a new user and runs jobs as it. +# So, install into /usr/local/bin which is already on every user's path. +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + pipx \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install black \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install flake8 \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install html5validator \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install lithium-reducer \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install PyGithub \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install pyyaml + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get autoremove \ +&& apt-get clean \ +&& rm -rf /var/lib/apt/lists/* diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk21 b/checker/bin-devel/Dockerfile-ubuntu-jdk21 new file mode 100644 index 000000000000..7dfa3f23d5db --- /dev/null +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk21 @@ -0,0 +1,63 @@ +# Create a Docker image that is ready to run the main Checker Framework tests, +# using OpenJDK 21. +# To convert this file to use a newer JDK, search (from the top level of the +# Checker Framework and Annotation Tools repositories) for: (java|jdk).?21\b + +# "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. +# As of 2023-11-15, "ubuntu:rolling" is still 23.04. +FROM ubuntu:23.10 +MAINTAINER Werner Dietl + +# According to +# https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/: +# * Put "apt-get update" and "apt-get install" and "apt cleanup" in the same RUN command. +# * Do not run "apt-get upgrade"; instead get upstream to update. + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& apt-get -y install aptitude \ +&& aptitude -y install \ + apt-utils + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + openjdk-17-jdk \ + openjdk-21-jdk + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + ant \ + binutils \ + build-essential \ + cpp \ + git \ + jq \ + libcurl3-gnutls \ + make \ + maven \ + pipx \ + python3-distutils \ + python3-requests \ + unzip \ + wget + +# Maven 3.8.7 is the default on Ubuntu 23.04, so the below is not needed. +# (Don't try to use a variable here for the Maven version.) +# RUN export DEBIAN_FRONTEND=noninteractive \ +# && wget https://mirrors.sonic.net/apache/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz \ +# && tar xzvf apache-maven-3.9.5-bin.tar.gz +# ENV PATH="/apache-maven-3.9.5/bin:$PATH" + +ENV PATH="/root/.local/bin:$PATH" +RUN pipx install --pip-args="--no-cache-dir" lithium-reducer + +RUN mkdir /python-env \ +&& python3 -m venv /python-env \ +&& /python-env/bin/pip install --no-cache-dir lithium-reducer PyGithub pyyaml + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get autoremove \ +&& apt-get clean \ +&& rm -rf /var/lib/apt/lists/* diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk21-plus b/checker/bin-devel/Dockerfile-ubuntu-jdk21-plus new file mode 100644 index 000000000000..80ac2607f859 --- /dev/null +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk21-plus @@ -0,0 +1,104 @@ +# Create a Docker image that is ready to run the full Checker Framework tests, +# including building the manual and Javadoc, using OpenJDK 21. +# To convert this file to use a newer JDK, search (from the top level of the +# Checker Framework and Annotation Tools repositories) for: (java|jdk).?21\b + +# "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. +# As of 2023-11-15, "ubuntu:rolling" is still 23.04. +FROM ubuntu:23.10 +MAINTAINER Werner Dietl + +## Keep this file in sync with ../../docs/manual/troubleshooting.tex + +# According to +# https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/: +# * Put "apt-get update" and "apt-get install" and "apt cleanup" in the same RUN command. +# * Do not run "apt-get upgrade"; instead get upstream to update. + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& apt-get -y install aptitude \ +&& aptitude -y install \ + apt-utils + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + openjdk-17-jdk \ + openjdk-21-jdk + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + ant \ + binutils \ + build-essential \ + cpp \ + git \ + jq \ + libcurl3-gnutls \ + make \ + maven \ + pipx \ + python3-distutils \ + python3-requests \ + unzip \ + wget + +# Maven 3.8.7 is the default on Ubuntu 23.04, so the below is not needed. +# (Don't try to use a variable here for the Maven version.) +# RUN export DEBIAN_FRONTEND=noninteractive \ +# && wget https://mirrors.sonic.net/apache/maven/maven-3/3.9.5/binaries/apache-maven-3.9.5-bin.tar.gz \ +# && tar xzvf apache-maven-3.9.5-bin.tar.gz +# ENV PATH="/apache-maven-3.9.5/bin:$PATH" + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + autoconf \ + devscripts \ + dia \ + hevea \ + imagemagick \ + junit \ + latexmk \ + librsvg2-bin \ + libasound2-dev \ + libcups2-dev \ + libfontconfig1-dev \ + libx11-dev \ + libxext-dev \ + libxrender-dev \ + libxrandr-dev \ + libxtst-dev \ + libxt-dev \ + pdf2svg \ + rsync \ + shellcheck \ + texlive-font-utils \ + texlive-fonts-recommended \ + texlive-latex-base \ + texlive-latex-extra \ + texlive-latex-recommended + +# `pipx ensurepath` only adds to the path in newly-started shells. +# BUT, setting the path for the current user is not enough. +# Azure creates a new user and runs jobs as it. +# So, install into /usr/local/bin which is already on every user's path. +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ + pipx \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install black \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install flake8 \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install html5validator \ +&& PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install lithium-reducer + +RUN mkdir /python-env \ +&& python3 -m venv /python-env \ +&& /python-env/bin/pip install --no-cache-dir lithium-reducer PyGithub pyyaml + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get autoremove \ +&& apt-get clean \ +&& rm -rf /var/lib/apt/lists/* diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk8 b/checker/bin-devel/Dockerfile-ubuntu-jdk8 index 33d8ece9f58c..0d31181e970f 100644 --- a/checker/bin-devel/Dockerfile-ubuntu-jdk8 +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk8 @@ -1,48 +1,52 @@ # Create a Docker image that is ready to run the main Checker Framework tests, -# using JDK 8. +# using OpenJDK 8. +# "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. +# See releases at https://hub.docker.com/_/ubuntu for available images. +# See https://packages.ubuntu.com/search?suite=default§ion=all&arch=any&keywords=openjdk-20-jdk&searchon=names +# to see what Ubuntu versions support a particular OpenJDK version. FROM ubuntu -MAINTAINER Michael Ernst +MAINTAINER Werner Dietl # According to # https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/: -# * Put "apt-get update" and "apt-get install" and apt cleanup in the same RUN command. +# * Put "apt-get update" and "apt-get install" and "apt cleanup" in the same RUN command. # * Do not run "apt-get upgrade"; instead get upstream to update. RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ -&& apt-get -qqy install \ +&& apt-get -y install aptitude \ +&& aptitude -y install \ + apt-utils + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ openjdk-8-jdk \ -&& update-java-alternatives --set java-1.8.0-openjdk-amd64 + openjdk-17-jdk \ +&& update-java-alternatives -s java-1.8.0-openjdk-amd64 RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ -&& apt-get -qqy install \ +&& aptitude -y install \ ant \ + binutils \ + build-essential \ cpp \ git \ - gradle \ jq \ libcurl3-gnutls \ make \ maven \ - mercurial \ python3-pip \ + python3-distutils \ python3-requests \ unzip \ wget -# Ubuntu 18.04 has Maven 3.6.0, but 3.6.1 apparently has better retry -# behavior to work around network problems. -RUN export DEBIAN_FRONTEND=noninteractive \ -&& VER=3.6.3 \ -&& mkdir -p $HOME/bin/install \ -&& cd $HOME/bin/install \ -&& wget http://apache.mirrors.pair.com/maven/maven-3/${VER}/binaries/apache-maven-${VER}-bin.tar.gz \ -&& tar xzf apache-maven-${VER}-bin.tar.gz \ -&& (cd /usr/local/bin && ln -sf $HOME/bin/install/apache-maven-${VER}/bin/* .) \ -&& hash -r +RUN pip3 install --no-cache-dir lithium-reducer PyGithub pyyaml RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get autoremove \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/checker/bin-devel/Dockerfile-ubuntu-jdk8-plus b/checker/bin-devel/Dockerfile-ubuntu-jdk8-plus index 3fef18379ccc..f1b060177538 100644 --- a/checker/bin-devel/Dockerfile-ubuntu-jdk8-plus +++ b/checker/bin-devel/Dockerfile-ubuntu-jdk8-plus @@ -1,66 +1,81 @@ # Create a Docker image that is ready to run the full Checker Framework tests, -# including building the manual and Javadoc, using JDK 8. +# including building the manual and Javadoc, using OpenJDK 8. +# "ubuntu" is the latest LTS release. "ubuntu:rolling" is the latest release. +# See releases at https://hub.docker.com/_/ubuntu for available images. +# See https://packages.ubuntu.com/search?suite=default§ion=all&arch=any&keywords=openjdk-20-jdk&searchon=names +# to see what Ubuntu versions support a particular OpenJDK version. FROM ubuntu -MAINTAINER Michael Ernst +MAINTAINER Werner Dietl # According to # https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/: -# * Put "apt-get update" and "apt-get install" and apt cleanup in the same RUN command. +# * Put "apt-get update" and "apt-get install" and "apt cleanup" in the same RUN command. # * Do not run "apt-get upgrade"; instead get upstream to update. RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ -&& apt-get -qqy install \ +&& apt-get -y install aptitude \ +&& aptitude -y install \ + apt-utils + +RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get -qqy update \ +&& aptitude -y install \ openjdk-8-jdk \ -&& update-java-alternatives --set java-1.8.0-openjdk-amd64 + openjdk-17-jdk \ +&& update-java-alternatives -s java-1.8.0-openjdk-amd64 RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ -&& apt-get -qqy install \ +&& aptitude -y install \ ant \ + binutils \ + build-essential \ cpp \ git \ - gradle \ jq \ libcurl3-gnutls \ make \ maven \ - mercurial \ python3-pip \ + python3-distutils \ python3-requests \ unzip \ wget -# Ubuntu 18.04 has Maven 3.6.0, but 3.6.1 apparently has better retry -# behavior to work around network problems. -RUN export DEBIAN_FRONTEND=noninteractive \ -&& VER=3.6.3 \ -&& mkdir -p $HOME/bin/install \ -&& cd $HOME/bin/install \ -&& wget http://apache.mirrors.pair.com/maven/maven-3/${VER}/binaries/apache-maven-${VER}-bin.tar.gz \ -&& tar xzf apache-maven-${VER}-bin.tar.gz \ -&& (cd /usr/local/bin && ln -sf $HOME/bin/install/apache-maven-${VER}/bin/* .) \ -&& hash -r - RUN export DEBIAN_FRONTEND=noninteractive \ && apt-get -qqy update \ -&& apt-get -qqy install \ +&& aptitude -y install \ + autoconf \ + devscripts \ dia \ hevea \ imagemagick \ + junit \ latexmk \ librsvg2-bin \ + libasound2-dev \ + libcups2-dev \ + libfontconfig1-dev \ + libx11-dev \ + libxext-dev \ + libxrender-dev \ + libxrandr-dev \ + libxtst-dev \ + libxt-dev \ pdf2svg \ rsync \ + shellcheck \ texlive-font-utils \ texlive-fonts-recommended \ texlive-latex-base \ texlive-latex-extra \ texlive-latex-recommended -RUN pip3 install html5validator +RUN pip3 install --no-cache-dir black flake8 html5validator lithium-reducer PyGithub pyyaml RUN export DEBIAN_FRONTEND=noninteractive \ +&& apt-get autoremove \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/checker/bin-devel/Makefile b/checker/bin-devel/Makefile new file mode 100644 index 000000000000..40e3ecec6ac3 --- /dev/null +++ b/checker/bin-devel/Makefile @@ -0,0 +1,10 @@ +SH_SCRIPTS = $(shell grep -r -l '^\#! \?\(/bin/\|/usr/bin/env \)sh' * | grep -v .git | grep -v "~") +BASH_SCRIPTS = $(shell grep -r -l '^\#! \?\(/bin/\|/usr/bin/env \)bash' * | grep -v .git | grep -v "~") + +shell-script-style: + shellcheck --format=gcc ${SH_SCRIPTS} ${BASH_SCRIPTS} + checkbashisms ${SH_SCRIPTS} + +showvars: + @echo "SH_SCRIPTS=${SH_SCRIPTS}" + @echo "BASH_SCRIPTS=${BASH_SCRIPTS}" diff --git a/checker/bin-devel/build.sh b/checker/bin-devel/build.sh index 6de99750bbda..fed0fd2363df 100755 --- a/checker/bin-devel/build.sh +++ b/checker/bin-devel/build.sh @@ -1,63 +1,15 @@ #!/bin/bash +echo Deprecated: use ./gradlew assemble instead. echo Entering checker/bin-devel/build.sh in "$(pwd)" # Fail the whole script if any command fails set -e -echo "initial CHECKERFRAMEWORK=$CHECKERFRAMEWORK" -export CHECKERFRAMEWORK="${CHECKERFRAMEWORK:-$(pwd -P)}" -echo "CHECKERFRAMEWORK=$CHECKERFRAMEWORK" +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +source "$SCRIPTDIR"/clone-related.sh -export SHELLOPTS -echo "SHELLOPTS=${SHELLOPTS}" - -if [ "$(uname)" == "Darwin" ] ; then - export JAVA_HOME=${JAVA_HOME:-$(/usr/libexec/java_home)} -else - # shellcheck disable=SC2230 - export JAVA_HOME=${JAVA_HOME:-$(dirname "$(dirname "$(readlink -f "$(which javac)")")")} -fi -echo "JAVA_HOME=${JAVA_HOME}" - -if [ -d "/tmp/$USER/plume-scripts" ] ; then - (cd "/tmp/$USER/plume-scripts" && git pull -q) -else - mkdir -p "/tmp/$USER" && (cd "/tmp/$USER" && (git clone --depth 1 -q https://github.com/plume-lib/plume-scripts.git || git clone --depth 1 -q https://github.com/plume-lib/plume-scripts.git)) -fi - -# Clone the annotated JDK into ../jdk . -"/tmp/$USER/plume-scripts/git-clone-related" opprop jdk ../jdk --depth 1 - -AFU="${AFU:-../annotation-tools/annotation-file-utilities}" -# Don't use `AT=${AFU}/..` which causes a git failure. -AT=$(dirname "${AFU}") - -## Build annotation-tools (Annotation File Utilities) -"/tmp/$USER/plume-scripts/git-clone-related" opprop annotation-tools "${AT}" -if [ ! -d ../annotation-tools ] ; then - ln -s "${AT}" ../annotation-tools -fi - -echo "Running: (cd ${AT} && ./.travis-build-without-test.sh)" -(cd "${AT}" && ./.travis-build-without-test.sh) -echo "... done: (cd ${AT} && ./.travis-build-without-test.sh)" - - -## Build stubparser -"/tmp/$USER/plume-scripts/git-clone-related" opprop stubparser -echo "Running: (cd ../stubparser/ && ./.travis-build-without-test.sh)" -(cd ../stubparser/ && ./.travis-build-without-test.sh) -echo "... done: (cd ../stubparser/ && ./.travis-build-without-test.sh)" - - -## Compile - -# Downloading the gradle wrapper sometimes fails. -# If so, the next command gets another chance to try the download. -(./gradlew help || sleep 10) > /dev/null 2>&1 - -echo "running \"./gradlew assemble\" for checker-framework" -./gradlew assemble --console=plain --warning-mode=all -s --no-daemon -Dorg.gradle.internal.http.socketTimeout=60000 -Dorg.gradle.internal.http.connectionTimeout=60000 +echo "running \"./gradlew assembleForJavac\" for checker-framework" +./gradlew assembleForJavac --console=plain -Dorg.gradle.internal.http.socketTimeout=60000 -Dorg.gradle.internal.http.connectionTimeout=60000 echo Exiting checker/bin-devel/build.sh in "$(pwd)" diff --git a/checker/bin-devel/checkout-historical.sh b/checker/bin-devel/checkout-historical.sh new file mode 100755 index 000000000000..dc543c2aa8e3 --- /dev/null +++ b/checker/bin-devel/checkout-historical.sh @@ -0,0 +1,156 @@ +#!/bin/sh + +# When run in a Checker Framework checkout/clone, this clones the related +# repositories, then builds them and the Checker Framework. It sets the related +# repositories to their last commit before the Checker Framework's clone. This +# approximates the state of the repositories at that time. + +# This is only an approximation because if the Checker Framework was on a +# branch, then the related repository might have also been on a branch that was +# subsequently merged (or squash-and-merged, so the commit no longer exists in +# the related repository). + +# This script works at least through mid-April 2019. +# It does not work for mid-January 2019. + +# # To verify that you can build an old version of the Checker Framework, do +# # preparation, then set two environment variables, and then run these +# # commands. See below for preparation and examples of setting the environment +# # variables. +# git clone https://github.com/typetools/checker-framework.git ${CHECKERFRAMEWORK} +# cd ${CHECKERFRAMEWORK} +# git checkout ${CFCOMMIT} +# /tmp/test-historical/checker-framework/checker/bin-devel/checkout-historical.sh +# +# # Preparation (only needs to be done once every) +# mkdir -p /tmp/test-historical +# git clone https://github.com/typetools/checker-framework.git +# +# # January 2023 +# export CHECKERFRAMEWORK=/tmp/test-historical/checker-framework-202301 +# export CFCOMMIT=9d60936fcd81827f3761d0244014a6e419133b16 +# +# # July 2022 +# export CHECKERFRAMEWORK=/tmp/test-historical/checker-framework-202207 +# export CFCOMMIT=c37aff5ef28569e5bdadf681c81210d084de24df +# +# # January 2022 +# export CHECKERFRAMEWORK=/tmp/test-historical/checker-framework-202201 +# export CFCOMMIT=24364449c1bac6cee1896759e1ab5fc87ad5a70d +# +# # January 2021 +# export CHECKERFRAMEWORK=/tmp/test-historical/checker-framework-202101 +# export CFCOMMIT=f3cc3d328a70ef8e834bf2693be6cbb6a94ece63 +# +# # January 2020 +# export CHECKERFRAMEWORK=/tmp/test-historical/checker-framework-202001 +# export CFCOMMIT=b7d026e424df2a04f8b9275bc2792cb03991425d +# +# # October 2019 +# export CHECKERFRAMEWORK=/tmp/test-historical/checker-framework-201910 +# export CFCOMMIT=b6e7558f3f0b0cf996f00039ca98a8d1fa798896 +# +# # July 2019; use JDK 8 +# export CHECKERFRAMEWORK=/tmp/test-historical/checker-framework-201907 +# export CFCOMMIT=5000c1ecb72581aeebd3c10a2851cf003eeb554c +# +# # April 2019; use JDK 8 +# export CHECKERFRAMEWORK=/tmp/test-historical/checker-framework-201904 +# export CFCOMMIT=cc3b007addee9e241e4ef560d009fd212c478819 +# +# # January 2019; use JDK 8 +# # This fails because it references deleted repository https://bitbucket.org/typetools/jsr308-langtools +# export CHECKERFRAMEWORK=/tmp/test-historical/checker-framework-201901 +# export CFCOMMIT=b76bd9dcd5839285a4dd9fd6c2d769647357f288 +# +# # January 2018; use JDK 8 +# # This fails, because plume-scripts does not exist. +# export CHECKERFRAMEWORK=/tmp/test-historical/checker-framework-201801 +# export CFCOMMIT=1f48ddb600620454731170eb2628e5f7efa93c3e + + +# Fail the whole script if any command fails +set -e + +# DEBUG=0 +# # To enable debugging, uncomment the following line. +# # DEBUG=1 + +echo "Entering checker/bin-devel/checkout-historical.sh in $(pwd)" + +commit_sha=$(git rev-parse HEAD) +commit_date=$(git show -s --format=%ci) + +echo "Commit ${commit_sha}, date ${commit_date}" + +git checkout -B __merge_eval__ + +# Initial commit is June 2018 +echo "plume-scripts" +PLUME_SCRIPTS="checker/bin-devel/.plume-scripts" +if [ ! -d "$PLUME_SCRIPTS" ] ; then + git clone -q https://github.com/plume-lib/plume-scripts.git "${PLUME_SCRIPTS}" +fi +COMMIT="$(cd "${PLUME_SCRIPTS}" && git rev-list -n 1 --first-parent --before="${commit_date}" master)" +if [ -n "${COMMIT}" ] ; then + # COMMIT is non-empty + (cd "${PLUME_SCRIPTS}" && git checkout -B __merge_eval__ "${COMMIT}") +fi + +# Initial commit is February 2018 +echo "html-tools" +HTML_TOOLS="checker/bin-devel/.plume-scripts" +COMMIT="$(cd "${HTML_TOOLS}" && git rev-list -n 1 --first-parent --before="${commit_date}" master)" +if [ ! -d "$HTML_TOOLS" ] ; then + git clone -q https://github.com/plume-lib/html-tools.git "${HTML_TOOLS}" +fi +if [ -n "${COMMIT}" ] ; then + # COMMIT is non-empty + (cd "${HTML_TOOLS}" && git checkout -B __merge_eval__ "${COMMIT}") +fi + +echo "Stubparser" +STUBPARSER="../stubparser" +if [ ! -d "${STUBPARSER}" ] ; then + git clone https://github.com/typetools/stubparser.git "${STUBPARSER}" +fi +(cd "${STUBPARSER}" && git checkout -B __merge_eval__ "$(git rev-list -n 1 --first-parent --before="${commit_date}" master)") +if [ -f ${STUBPARSER}/.build-without-test.sh ] ; then + STUBPARSER_BUILD=.build-without-test.sh +elif [ -f ${STUBPARSER}/.travis-build-without-test.sh ] ; then + STUBPARSER_BUILD=.travis-build-without-test.sh +else + echo "Can't find stubparser build script" + exit 1 +fi +echo "Running: (cd ../stubparser/ && ./${STUBPARSER_BUILD})" +(cd ../stubparser/ && ./"${STUBPARSER_BUILD}") +echo "... done: (cd ../stubparser/ && ./${STUBPARSER_BUILD})" + +echo "Annotation File Utilities" +AT="../annotation-tools" +if [ ! -d "${AT}" ] ; then + git clone https://github.com/typetools/annotation-tools.git "${AT}" +fi +(cd "${AT}" && git checkout -B __merge_eval__ "$(git rev-list -n 1 --first-parent --before="${commit_date}" master)") +if [ -f ${AT}/.build-without-test.sh ] ; then + AT_BUILD=.build-without-test.sh +elif [ -f ${AT}/.travis-build-without-test.sh ] ; then + AT_BUILD=.travis-build-without-test.sh +else + echo "Can't find stubparser build script" + exit 1 +fi +echo "Running: (cd ${AT} && ./${AT_BUILD})" +(cd "${AT}" && ./${AT_BUILD}) +echo "... done: (cd ${AT} && ./${AT_BUILD})" + +JDK_DIR="../jdk" +if [ ! -d "${JDK_DIR}" ] ; then + git clone https://github.com/typetools/jdk.git $JDK_DIR +fi +(cd "${JDK_DIR}" && git checkout -B __merge_eval__ "$(git rev-list -n 1 --first-parent --before="${commit_date}" master)") + +./gradlew assemble + +echo Exiting checker/bin-devel/checkout-historical.sh in "$(pwd)" diff --git a/checker/bin-devel/clone-related.sh b/checker/bin-devel/clone-related.sh new file mode 100755 index 000000000000..a0b701483395 --- /dev/null +++ b/checker/bin-devel/clone-related.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +echo Entering checker/bin-devel/clone-related.sh in "$(pwd)" + +# Fail the whole script if any command fails +set -e + +DEBUG=0 +# To enable debugging, uncomment the following line. +# DEBUG=1 + +if [ $DEBUG -eq 0 ] ; then + DEBUG_FLAG= +else + DEBUG_FLAG=--debug +fi + +echo "initial CHECKERFRAMEWORK=$CHECKERFRAMEWORK" +export CHECKERFRAMEWORK="${CHECKERFRAMEWORK:-$(pwd -P)}" +echo "CHECKERFRAMEWORK=$CHECKERFRAMEWORK" + +export SHELLOPTS +echo "SHELLOPTS=${SHELLOPTS}" + +echo "initial JAVA_HOME=${JAVA_HOME}" +if [ "$(uname)" == "Darwin" ] ; then + export JAVA_HOME=${JAVA_HOME:-$(/usr/libexec/java_home)} +else + # shellcheck disable=SC2230 + export JAVA_HOME=${JAVA_HOME:-$(dirname "$(dirname "$(readlink -f "$(which javac)")")")} +fi +echo "JAVA_HOME=${JAVA_HOME}" + +# Using `(cd "$CHECKERFRAMEWORK" && ./gradlew getPlumeScripts -q)` leads to infinite regress. +PLUME_SCRIPTS="$CHECKERFRAMEWORK/checker/bin-devel/.plume-scripts" +if [ -d "$PLUME_SCRIPTS" ] ; then + (cd "$PLUME_SCRIPTS" && (git pull -q || true)) +else + (cd "$CHECKERFRAMEWORK/checker/bin-devel" && \ + (git clone --filter=blob:none -q https://github.com/eisop-plume-lib/plume-scripts.git .plume-scripts || \ + (sleep 1m && git clone --filter=blob:none -q https://github.com/eisop-plume-lib/plume-scripts.git .plume-scripts))) +fi + +# Clone the annotated JDK into ../jdk . +"$PLUME_SCRIPTS/git-clone-related" ${DEBUG_FLAG} opprop jdk + +# AFU="${AFU:-../annotation-tools/annotation-file-utilities}" +# # Don't use `AT=${AFU}/..` which causes a git failure. +# AT=$(dirname "${AFU}") + +# ## Build annotation-tools (Annotation File Utilities) +# "$PLUME_SCRIPTS/git-clone-related" ${DEBUG_FLAG} eisop annotation-tools "${AT}" +# if [ ! -d ../annotation-tools ] ; then +# ln -s "${AT}" ../annotation-tools +# fi + +# echo "Running: (cd ${AT} && ./.build-without-test.sh)" +# (cd "${AT}" && ./.build-without-test.sh) +# echo "... done: (cd ${AT} && ./.build-without-test.sh)" + + +# Download dependencies, trying a second time if there is a failure. +(TERM=dumb timeout 300 ./gradlew --write-verification-metadata sha256 help --dry-run || \ + (sleep 1m && ./gradlew --write-verification-metadata sha256 help --dry-run)) + +echo Exiting checker/bin-devel/clone-related.sh in "$(pwd)" diff --git a/checker/bin-devel/count-suppression-reasons b/checker/bin-devel/count-suppression-reasons new file mode 100755 index 000000000000..e3463e8bea45 --- /dev/null +++ b/checker/bin-devel/count-suppression-reasons @@ -0,0 +1,144 @@ +#!/bin/bash + +# This command counts the approximate frequency of each distinct reason for +# warning suppressions, in all files under the current directory. +# To invoke it, pass a type system name; for example: +# count-suppression-reasons nullness +# The argument to this script is actually a regular expression. +# Command-line arguments (which must appear before the type system name): +# -d debug +# -i REGEX include matches for the grep basic regex +# -x REGEX exclude matches for the grep basic regex +# -y REGEX exclude matches for the grep basic regex +# The -i argument (if any) is processed before the -x argument (if any). + +# The "reason" for a warning suppression is the Java line comment after it: +# @SuppressWarnings("nullness") // here is the reason +# If warning suppression text contains a colon, this script prints only +# the text before the colon, under the assumption that the initial text +# is a category name. For example: +# @SuppressWarnings("nullness") // short reason: here is a longer explanation +# The script also works when there are multiple warning suppression +# strings on different lines, or when there is an error key: +# @SuppressWarnings({ +# "nullness:dereference", // reason for nullness suppression +# "interning:type.incompatible" // reason for interning suppression +# }) + +# Inclusions (-i) are processed before exclusions (-x, -y). + +# Some reasons are ignored, notably "true positive" and +# "count-suppression-reasons-ignore"; see below for the full list. + +# This script is useful to determine the most frequent reasons for warning +# suppressions, to help checker developers decide what featuers to add to +# their type systems. However, use common.util.count.AnnotationStatistics +# to count the total number of warning suppressions (for example, to report +# in a paper), because this script gives only an approximate count. + +# The -y command-line argument is exactly like -x, but avoids the need +# for the user to write a complex regex. + +# To debug, pass the -d command-line argument. +debug=0 + +while getopts di:x:y: flag +do + case "${flag}" in + d) debug=1;; + i) include=${OPTARG};; + x) exclude=${OPTARG};; + y) excludey=${OPTARG};; + \?) echo "Invalid option -$OPTARG"; exit 1;; + esac +done +shift $((OPTIND-1)) + +if [[ "$OSTYPE" == "darwin"* ]]; then + SED="gsed" + GREP="ggrep" +else + SED="sed" + GREP="grep" +fi + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 TYPESYSTEM" >&2 + exit 1 +fi + +regex="$1" + +# If argument is a compound checker, make the regex match them all. +if [ "$regex" = "nullness" ]; then + regex="\(nullness\|initialization\|keyfor\)" +fi +if [ "$regex" = "index" ]; then + regex="\(index\|lessthan\|lowerbound\|samelen\|searchindex\|substringindex\|upperbound\)" +fi +if [ "$regex" = "resourceleak" ]; then + regex="\(mustcall\|objectconstruction\|resourceleak\)" +fi + +# Diagnostics +if [ "$debug" -ne "0" ]; then + echo "checker regex=${regex}" +fi + +greplines=$(mktemp /tmp/count-suppression-reasons."$(date +%Y%m%d-%H%M%S)"-XXX) +countedreasons=$(mktemp /tmp/count-suppression-reasons."$(date +%Y%m%d-%H%M%S)"-XXX) + +# These are the two types of matching lines: +# * "checkername" or "chekername:..." +# This matches occurrences within @SuppressWarnings. The regex does not +# include "@SuppressWarnings" because it might appear on the previous line. +# * @AssumeAssertion(checkername) +# This grep command captures a few stray lines; users should ignore them. +# This grep command assumes that tests are not annotated, and it hard-codes ignoring "annotated-jdk", "jdk", "true positive", "TP" (as an alias for "true positive"), and "count-suppression-reasons-ignore". +${GREP} -n --recursive --include='*.java' "\"${regex}[:\"]\(.*[^;]\)\?\(\$\|//\)\|@AssumeAssertion(${regex})" \ + | grep -v "@AnnotatedFor" | grep -v "/tests/" \ + | grep -v "/annotated-jdk/" | grep -v "/jdk/" | grep -v "^jdk/" | grep -v "true positive" | grep -v "// TP" | grep -v "count-suppression-reasons-ignore" > "${greplines}" + +if [ -n "$include" ] ; then + mv "${greplines}" "${greplines}-i" + grep -- "$include" "${greplines}-i" > "${greplines}" +fi +if [ -n "$exclude" ] ; then + mv "${greplines}" "${greplines}-x" + grep -v -- "$exclude" "${greplines}-x" > "${greplines}" +fi +if [ -n "$excludey" ] ; then + mv "${greplines}" "${greplines}-y" + grep -v -- "$excludey" "${greplines}-y" > "${greplines}" +fi + + +total=$(wc -l < "${greplines}") +## Don't output a total, to avoid people using this approximate count. +# echo "Total: $total" +# shellcheck disable=SC2002 +cat "${greplines}" \ + | ${SED} 's/.*\/\/ //g' \ + | ${SED} "s/.*@AssumeAssertion([^)])[ :]*\([^\"]\+\)\";/\1/g" \ + | ${SED} 's/\([^0-9]\): [^:].*/\1/' \ + | ${SED} 's/ \+$//' \ + | sort | uniq -c | sort -rg > "${countedreasons}" + +# Add leading percentages to `uniq -c` output. Note that it rounds *down* to the nearest integer. +# (Digits after the decimal don't make a practical difference.) +while read -r line; do + count=$(echo "$line" | cut -f1 -d " "); + content=$(echo "$line" | cut -f2- -d " "); + percent=$(echo "scale=0; (100*$count/$total);" | bc); + if [ "$debug" -eq "0" ]; then + printf "%d%%\t%s\n" "$percent" "$content"; + else + printf "%d%%\t%s\t%s\n" "$percent" "$count" "$content"; + fi; +done < "${countedreasons}"; + +if [ "$debug" -eq "0" ]; then + rm -f "${greplines}" "${countedreasons}" +else + echo "Total is $total, Intermediate output is in: ${greplines} ${countedreasons}" +fi diff --git a/checker/bin-devel/count-suppressions b/checker/bin-devel/count-suppressions deleted file mode 100755 index 55e65c87c7da..000000000000 --- a/checker/bin-devel/count-suppressions +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash - -# This command counts the approximate frequency of each distinct reason for -# warning suppressions. -# To invoke it, pass a type system name; for example: -# count-suppressions nullness - -# If warning suppression text contains a colon, this script prints only -# the text before the colon, under the assumption that the initial text -# is a category name. - -# This script is useful to determine the most frequent reasons for warning -# suppressions, to help checker developers decide what featuers to add to -# their type systems. However, use common.util.count.AnnotationStatistics -# to count the total number of warning suppressions (for example, to report -# in a paper), because this script gives only an approximate count. - -debug=0 - -if [[ "$OSTYPE" == "darwin"* ]]; then - SED="gsed" - GREP="ggrep" -else - SED="sed" - GREP="grep" -fi - -if [ "$#" -ne 1 ]; then - echo "Usage: $0 TYPESYSTEM" >&2 - exit 1 -fi - -regex="$1" - -# If argument is a compound checker, make the regex match them all. -if [ "$regex" = "nullness" ]; then - regex="\(nullness\|initialization\|keyfor\)" -fi -if [ "$regex" = "index" ]; then - regex="\(index\|lessthan\|lowerbound\|samelen\|searchindex\|substringindex\|upperbound\)" -fi - -echo "regex=${regex}" - -greplines=$(mktemp /tmp/count-suppressions.XXXXXX) - -# These are the two types of matching lines: -# * "checkername" or "chekername:..." -# This matches occurrences within @SuppressWarnings. The regex does not -# include "@SuppressWarnings" because it might appear on the previous line. -# * @AssumeAssertion(checkername) -# This grep command captures a few stray lines; users should ignore them. -${GREP} -n --recursive --include='*.java' "\"${regex}[:\"]\(.*[^;]\)\?\(\$\|//\)\|@AssumeAssertion(${regex})" | grep -v "@AnnotatedFor" > "${greplines}" - -## Don't output a total, to avoid people using this approximate count. -# echo -n "Total: " -# cat ${greplines} | wc -l -# shellcheck disable=SC2002 -cat "${greplines}" \ - | ${SED} 's/.*\/\/ //g' \ - | ${SED} "s/.*@AssumeAssertion([^)])[ :]*\([^\"]\+\)\";/\1/g" \ - | ${SED} 's/\([^0-9]\): [^:].*/\1/' \ - | ${SED} 's/ \+$//' \ - | sort | uniq -c | sort -rg -if [ "$debug" -eq "0" ]; then - rm -f "${greplines}" -else - echo Output is in: "${greplines}" -fi diff --git a/checker/bin-devel/git.pre-commit b/checker/bin-devel/git.pre-commit index 486ad95cc47b..584149414754 100755 --- a/checker/bin-devel/git.pre-commit +++ b/checker/bin-devel/git.pre-commit @@ -7,23 +7,18 @@ # Fail if any command fails set -e -# "ant -e check-style" would check every file; on commit we only need to -# check files that changed. -# 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 - ./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) +# Check formatting. This is slow (3+ seconds). +# Could instead do "spotlessApply", but then the changes don't appear in this commit. +JAVA_VER=$(java -version 2>&1 | head -1 | cut -d'"' -f2 | sed '/^1\./s///' | cut -d'.' -f1 | sed 's/-ea//') +[ "$JAVA_VER" = "8" ] || ./gradlew spotlessCheck -q +CHANGED_JAVA_FILES=$(git diff --staged --name-only --diff-filter=ACM | grep '\.java$' | grep -v '/jdk/' | grep -v 'stubparser/' | grep -v '/nullness-javac-errors/' | grep -v 'dataflow/manual/examples/' | grep -v '/java17/' | grep -v 'records/') || true +# echo "CHANGED_JAVA_FILES=${CHANGED_JAVA_FILES}" +if [ -n "$CHANGED_JAVA_FILES" ]; then BRANCH=$(git rev-parse --abbrev-ref HEAD) if [ "$BRANCH" = "master" ]; then git diff --staged > /tmp/diff.txt - ./gradlew getPlumeScripts -q + ./gradlew --stacktrace getPlumeScripts (./gradlew requireJavadoc > /tmp/warnings-rjp.txt 2>&1) || true checker/bin-devel/.plume-scripts/lint-diff.py --guess-strip /tmp/diff.txt /tmp/warnings-rjp.txt (./gradlew javadocDoclintAll > /tmp/warnings-jda.txt 2>&1) || true @@ -34,11 +29,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$' | grep -v '\.patch$') || 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/javac b/checker/bin-devel/javac deleted file mode 100755 index fabcb422bf6e..000000000000 --- a/checker/bin-devel/javac +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/sh - -# -# This file simply redirects all passed arguments -# to org.checkerframework.framework.util.CheckerDevelMain -# -# This script loads the .class files found in the -# build directory before it uses any jar files. -# Thus, a user can just do -# ./gradlew compileJava -# to debug changes rather than creating .jar files with "./gradlew assemble". - -# When editing, keep this file in sync with javac-debug in this directory. - -mydir="`dirname $0`" -case `uname -s` in - CYGWIN*) - mydir=`cygpath -m $mydir` - ;; -esac - -binaryDir="${mydir}"/../dist - -javacJar=${binaryDir}/javac.jar - -cfDir="${mydir}"/../.. -annoToolsDir="${cfDir}"/../annotation-tools -stubparserDir="${cfDir}"/../stubparser -classes="build/classes/java/main" -resources="build/resources/main" - -buildDirs="${cfDir}"/dataflow/"${classes}":"${cfDir}"/javacutil/"${classes}":"${cfDir}"/framework/"${classes}":"${cfDir}"/framework/"${resources}":"${cfDir}"/checker/"${classes}":"${cfDir}"/checker/"${resources}":"${stubparserDir}"/javaparser-core/stubparser.jar:"${annoToolsDir}"/scene-lib/bin:"${annoToolsDir}"/annotation-file-utilities/annotation-file-utilities-all.jar:`(cd ${cfDir} && ./gradlew -q :checker:printPlumeUtilJarPath)` - -## Preserve quoting and spaces in arguments, which would otherwise be lost -## due to being passed through the shell twice. -# Unset IFS and use newline as arg separator to preserve spaces in args -DUALCASE=1 # for MKS: make case statement case-sensitive (6709498) -saveIFS="$IFS" -nl=' -' -for i in "$@" ; do - IFS= - args=$args$nl"'"$i"'" - IFS="$saveIFS" -done - -# TODO: We really only want the qualifiers on the CP, but there is no -# easy way to determine them here. ../bin/javac can use -# checker-qual.jar, but it might not exist yet. -# Set .cp to all build directories for now. - -# Add the following option for verbose output: -# "-DCheckerDevelMain.verbose=TRUE" \ - -eval "java" \ - "-ea " \ - "-DCheckerDevelMain.cp=${buildDirs} " \ - "-DCheckerDevelMain.pp=${buildDirs} " \ - "-DCheckerDevelMain.runtime.cp=${javacJar} " \ - "-DCheckerDevelMain.binary=${binaryDir} " \ - "-classpath ${buildDirs} " \ - "org.checkerframework.framework.util.CheckerDevelMain" \ - ${args} diff --git a/checker/bin-devel/javac-debug b/checker/bin-devel/javac-debug deleted file mode 100755 index 30ef08f3baa9..000000000000 --- a/checker/bin-devel/javac-debug +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/sh - -# -# This file simply redirects all passed arguments -# to org.checkerframework.framework.util.CheckerDevelMain -# -# This script loads the .class files found in the -# build directory before it uses any jar files. -# Thus, a user can just do -# ./gradlew compileJava -# to debug changes rather than creating .jar files with "./gradlew assemble". - -# When editing, keep this file in sync with javac-debug in this directory. - -mydir="$(dirname "$0")" -case $(uname -s) in - CYGWIN*) - mydir=$(cygpath -m "$mydir") - ;; -esac - -binaryDir="${mydir}"/../dist - -cfDir="${mydir}"/../.. -annoToolsDir="${cfDir}"/../annotation-tools -stubparserDir="${cfDir}"/../stubparser -classes="build/classes/java/main" -resources="build/resources/main" - -# Put afu jar files last, as they might contain out-of-date CF files. -buildDirs="${cfDir}"/dataflow/"${classes}":"${cfDir}"/javacutil/"${classes}":"${cfDir}"/framework/"${classes}":"${cfDir}"/framework/"${resources}":"${cfDir}"/checker/"${classes}":"${cfDir}"/checker/"${resources}":"${stubparserDir}"/stubparser.jar:"${annoToolsDir}"/scene-lib/bin:"${annoToolsDir}"/annotation-file-utilities/annotation-file-utilities-all.jar - -## Preserve quoting and spaces in arguments, which would otherwise be lost -## due to being passed through the shell twice. -# Unset IFS and use newline as arg separator to preserve spaces in args -DUALCASE=1 # for MKS: make case statement case-sensitive (6709498) -saveIFS="$IFS" -nl=' -' -for i in "$@" ; do - IFS= - args=$args$nl"'"$i"'" - IFS="$saveIFS" -done - -# TODO: We really only want the qualifiers on the CP, but there is no -# easy way to determine them here. ../bin/javac can use -# checker-qual.jar, but it might not exist yet. -# Set .cp to all build directories for now. - -# Add the following option for verbose output: -# "-DCheckerDevelMain.verbose=TRUE" \ - -eval "java" \ - "-ea " \ - "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005" \ - "-DCheckerDevelMain.cp=${buildDirs} " \ - "-DCheckerDevelMain.pp=${buildDirs} " \ - "-DCheckerDevelMain.runtime.cp=${JAVA_HOME}/lib/tools.jar " \ - "-DCheckerDevelMain.binary=${binaryDir} " \ - "-classpath ${buildDirs} " \ - "org.checkerframework.framework.util.CheckerDevelMain" \ - "${args}" diff --git a/checker/bin-devel/list-type-qualifiers.sh b/checker/bin-devel/list-type-qualifiers.sh new file mode 100755 index 000000000000..013663c36c7f --- /dev/null +++ b/checker/bin-devel/list-type-qualifiers.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# Outputs a list of all type qualifiers currently supported by the CF, in fully-qualified form. +# For a list that is not fully-qualified, run: +# list-type-qualifiers.sh | sed 's/.*\.//' | sort | uniq + +# TODO: +# This script ought to also output: +# Nested annotations (defined as a nested class). +# Aliased annotations, such as those in variable NONNULL_ALIASES. +# (A few instances of these are hacked around with a print statement below.) +# It would be nice to have a command-line argument that controls whether the output is fully-qualified. + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +if [ "${CHECKERFRAMEWORK}x" = x ] ; then + echo "CHECKERFRAMEWORK environment variable must be set to run this script. Run the following command to use this copy of the Checker Framework: +export CHECKERFRAMEWORK=${SCRIPT_DIR}/../../" + exit 1 +fi + +(cd "${CHECKERFRAMEWORK}" && +find \ + checker-qual/src/main/java \ + checker/src/test \ + docs/examples/units-extension \ + framework/src/main/java \ + framework/src/test/java \ + -name '*.java' -print0 \ + | xargs -0 grep --recursive --files-with-matches -e '^@Target\b.*TYPE_USE' \ + | sed 's/\.java$//' | sed 's/.*\/java\///' | sed 's/.*\/units-extension\///' \ + | awk '{print $1} END {print "NotNull.java"; print "UbTop.java"; print "LbTop.java"; print "UB_TOP.java"; print "LB_TOP.java";}' \ + | sed 's/\.java$//' | sed 's/\//./g' | sort | uniq \ +) diff --git a/checker/bin-devel/mvn-settings.xml b/checker/bin-devel/mvn-settings.xml new file mode 100644 index 000000000000..abbf1173debc --- /dev/null +++ b/checker/bin-devel/mvn-settings.xml @@ -0,0 +1,13 @@ + + + + central + + + + 120000 + 120000 + + + + diff --git a/checker/bin-devel/test-cf-inference.sh b/checker/bin-devel/test-cf-inference.sh index 824a369750a0..32b674db3872 100755 --- a/checker/bin-devel/test-cf-inference.sh +++ b/checker/bin-devel/test-cf-inference.sh @@ -7,17 +7,16 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) source "$SCRIPTDIR"/build.sh ## checker-framework-inference is a downstream test, but run it in its own -## script rather than in ./test/downstream.sh because it is most likely to fail, -## and it's helpful to see that only it, not other downstream tests, failed. +## script rather than in ./test/downstream.sh, because it needs a different +## Docker image. -"/tmp/$USER/plume-scripts/git-clone-related" opprop checker-framework-inference +"$SCRIPTDIR/.plume-scripts/git-clone-related" opprop checker-framework-inference -export AFU="${AFU:-$(cd ../annotation-tools/annotation-file-utilities >/dev/null 2>&1 && pwd -P)}" export PATH=$AFU/scripts:$PATH -(cd ../checker-framework-inference && ./.travis-build.sh) +cd ../checker-framework-inference +./.ci-build.sh diff --git a/checker/bin-devel/test-cftests-all.sh b/checker/bin-devel/test-cftests-all.sh index 8a59c52d3604..ff590af27645 100755 --- a/checker/bin-devel/test-cftests-all.sh +++ b/checker/bin-devel/test-cftests-all.sh @@ -1,6 +1,7 @@ #!/bin/bash -# This script test-cftests-all.sh = tests-cftests-junit.sh + tests-cftests-nonjunit.sh . +# This script test-cftests-all.sh = tests-cftests-junit.sh + tests-cftests-nonjunit.sh + tests-cftests-inference.sh + tests-typecheck.sh . +# Per comments in ../../build.gradle, allTests = test + nonJunitTests + inferenceTests + typecheck . set -e set -o verbose @@ -8,14 +9,14 @@ set -o xtrace export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" +export ORG_GRADLE_PROJECT_useJdk17Compiler=true SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +source "$SCRIPTDIR"/clone-related.sh -./gradlew allTests --console=plain --warning-mode=all --no-daemon +./gradlew allTests --console=plain --warning-mode=all # Moved example-tests out of all tests because it fails in # the release script because the newest maven artifacts are not published yet. -./gradlew :checker:exampleTests --console=plain --warning-mode=all --no-daemon +./gradlew :checker:exampleTests --console=plain --warning-mode=all diff --git a/checker/bin-devel/test-cftests-inference-part1.sh b/checker/bin-devel/test-cftests-inference-part1.sh new file mode 100755 index 000000000000..5e945cdf6f0b --- /dev/null +++ b/checker/bin-devel/test-cftests-inference-part1.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e +set -o verbose +set -o xtrace +export SHELLOPTS +echo "SHELLOPTS=${SHELLOPTS}" + +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh + + + +./gradlew inferenceTests-part1 --console=plain --warning-mode=all diff --git a/checker/bin-devel/test-cftests-inference-part2.sh b/checker/bin-devel/test-cftests-inference-part2.sh new file mode 100755 index 000000000000..34fcc84bb763 --- /dev/null +++ b/checker/bin-devel/test-cftests-inference-part2.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e +set -o verbose +set -o xtrace +export SHELLOPTS +echo "SHELLOPTS=${SHELLOPTS}" + +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh + + + +./gradlew inferenceTests-part2 --console=plain --warning-mode=all diff --git a/checker/bin-devel/test-cftests-inference.sh b/checker/bin-devel/test-cftests-inference.sh new file mode 100755 index 000000000000..24584a521833 --- /dev/null +++ b/checker/bin-devel/test-cftests-inference.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e +set -o verbose +set -o xtrace +export SHELLOPTS +echo "SHELLOPTS=${SHELLOPTS}" + +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh + + + +./gradlew inferenceTests --console=plain --warning-mode=all diff --git a/checker/bin-devel/test-cftests-junit.sh b/checker/bin-devel/test-cftests-junit.sh index 3ac347f44873..058d6a7ae9ca 100755 --- a/checker/bin-devel/test-cftests-junit.sh +++ b/checker/bin-devel/test-cftests-junit.sh @@ -7,10 +7,9 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh - -./gradlew test --console=plain --warning-mode=all --no-daemon +./gradlew test -x javadoc -x allJavadoc --console=plain --warning-mode=all diff --git a/checker/bin-devel/test-cftests-nonjunit.sh b/checker/bin-devel/test-cftests-nonjunit.sh index e84dd5d36e23..c96bdf0bf505 100755 --- a/checker/bin-devel/test-cftests-nonjunit.sh +++ b/checker/bin-devel/test-cftests-nonjunit.sh @@ -1,7 +1,5 @@ #!/bin/bash -# This script test-cftests-all.sh = tests-cftests-junit.sh + tests-cftests-nonjunit.sh . - set -e set -o verbose set -o xtrace @@ -9,13 +7,13 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh - +# shellcheck disable=SC1090# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh -./gradlew nonJunitTests --console=plain --warning-mode=all --no-daemon +./gradlew nonJunitTests -x javadoc -x allJavadoc --console=plain --warning-mode=all +./gradlew publishToMavenLocal -x javadoc -x allJavadoc --console=plain --warning-mode=all # Moved example-tests out of all tests because it fails in # the release script because the newest maven artifacts are not published yet. -./gradlew :checker:exampleTests --console=plain --warning-mode=all --no-daemon +./gradlew :checker:exampleTests -x javadoc -x allJavadoc --console=plain --warning-mode=all diff --git a/checker/bin-devel/test-daikon-part1.sh b/checker/bin-devel/test-daikon-part1.sh index 929917621306..7a7b2a6351db 100755 --- a/checker/bin-devel/test-daikon-part1.sh +++ b/checker/bin-devel/test-daikon-part1.sh @@ -7,13 +7,17 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh + +# Run assembleForJavac because it does not build the javadoc, so it is faster than assemble. +echo "running \"./gradlew assembleForJavac\" for checker-framework" +./gradlew assembleForJavac --console=plain -Dorg.gradle.internal.http.socketTimeout=60000 -Dorg.gradle.internal.http.connectionTimeout=60000 # daikon-typecheck: 15 minutes -"/tmp/$USER/plume-scripts/git-clone-related" codespecs daikon +"$SCRIPTDIR/.plume-scripts/git-clone-related" eisop-codespecs daikon cd ../daikon git log | head -n 5 make compile diff --git a/checker/bin-devel/test-daikon-part2.sh b/checker/bin-devel/test-daikon-part2.sh index e43138233206..4151eb723de2 100755 --- a/checker/bin-devel/test-daikon-part2.sh +++ b/checker/bin-devel/test-daikon-part2.sh @@ -7,13 +7,16 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh +# Run assembleForJavac because it does not build the javadoc, so it is faster than assemble. +echo "running \"./gradlew assembleForJavac\" for checker-framework" +./gradlew assembleForJavac --console=plain -Dorg.gradle.internal.http.socketTimeout=60000 -Dorg.gradle.internal.http.connectionTimeout=60000 # daikon-typecheck: 15 minutes -"/tmp/$USER/plume-scripts/git-clone-related" codespecs daikon +"$SCRIPTDIR/.plume-scripts/git-clone-related" eisop-codespecs daikon cd ../daikon git log | head -n 5 make compile diff --git a/checker/bin-devel/test-daikon.sh b/checker/bin-devel/test-daikon.sh index c5813e0eb70b..85610dc359fd 100755 --- a/checker/bin-devel/test-daikon.sh +++ b/checker/bin-devel/test-daikon.sh @@ -7,13 +7,16 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh +# Run assembleForJavac because it does not build the javadoc, so it is faster than assemble. +echo "running \"./gradlew assembleForJavac\" for checker-framework" +./gradlew assembleForJavac --console=plain -Dorg.gradle.internal.http.socketTimeout=60000 -Dorg.gradle.internal.http.connectionTimeout=60000 # daikon-typecheck: 15 minutes -"/tmp/$USER/plume-scripts/git-clone-related" codespecs daikon +"$SCRIPTDIR/.plume-scripts/git-clone-related" eisop-codespecs daikon -q --single-branch --depth 50 cd ../daikon git log | head -n 5 make compile diff --git a/checker/bin-devel/test-downstream.sh b/checker/bin-devel/test-downstream.sh index 902b2399c39a..31cc12dd15df 100755 --- a/checker/bin-devel/test-downstream.sh +++ b/checker/bin-devel/test-downstream.sh @@ -7,20 +7,20 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh ## downstream tests: projects that depend on the Checker Framework. -## These are here so they can be run by pull requests. (Pull requests -## currently don't trigger downstream jobs.) +## (There are none currently in this file.) +## These are here so they can be run by pull requests. ## Exceptions: -## * checker-framework-inference is run by test-cf-inference.sh ## * plume-lib is run by test-plume-lib.sh ## * daikon-typecheck is run as a separate CI project ## * guava is run as a separate CI project -# Checker Framework demos -"/tmp/$USER/plume-scripts/git-clone-related" typetools checker-framework.demos -./gradlew :checker:demosTests --console=plain --warning-mode=all --no-daemon +## This is moved to misc, because otherwise it would be the only work done by this script. +# # Checker Framework demos +# "$SCRIPTDIR/.plume-scripts/git-clone-related" eisop checker-framework.demos +# ./gradlew :checker:demosTests --console=plain --warning-mode=all diff --git a/checker/bin-devel/test-guava-formatter.sh b/checker/bin-devel/test-guava-formatter.sh index 809f62985943..33d7b9560ca4 100755 --- a/checker/bin-devel/test-guava-formatter.sh +++ b/checker/bin-devel/test-guava-formatter.sh @@ -7,12 +7,12 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh -"/tmp/$USER/plume-scripts/git-clone-related" typetools guava +"$SCRIPTDIR/.plume-scripts/git-clone-related" eisop guava cd ../guava ./typecheck.sh formatter diff --git a/checker/bin-devel/test-guava-index.sh b/checker/bin-devel/test-guava-index.sh index 45b0cbd66542..23707e37ab57 100755 --- a/checker/bin-devel/test-guava-index.sh +++ b/checker/bin-devel/test-guava-index.sh @@ -1,4 +1,5 @@ #!/bin/bash +# Use `bash` instead of `sh` because of use of BASH_SOURCE. set -e set -o verbose @@ -7,21 +8,21 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh -"/tmp/$USER/plume-scripts/git-clone-related" typetools guava +"$SCRIPTDIR/.plume-scripts/git-clone-related" eisop guava cd ../guava if [ "$TRAVIS" = "true" ] ; then # Travis which kills jobs that have not produced output for 10 minutes. echo "Setting up sleep-and-output jobs for Travis" - (sleep 1s && echo "1 second has elapsed") & - (sleep 5m && echo "5 minutes have elapsed") & - (sleep 14m && echo "14 minutes have elapsed") & - (sleep 23m && echo "23 minutes have elapsed") & + (sleep 1 && echo "1 second has elapsed") & + (sleep 300 && echo "5 minutes have elapsed") & + (sleep 840 && echo "14 minutes have elapsed") & + (sleep 1380 && echo "23 minutes have elapsed") & fi ./typecheck.sh index diff --git a/checker/bin-devel/test-guava-interning.sh b/checker/bin-devel/test-guava-interning.sh index 6b22f2558feb..e18a35078be3 100755 --- a/checker/bin-devel/test-guava-interning.sh +++ b/checker/bin-devel/test-guava-interning.sh @@ -7,12 +7,12 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh -"/tmp/$USER/plume-scripts/git-clone-related" typetools guava +"$SCRIPTDIR/.plume-scripts/git-clone-related" eisop guava cd ../guava ./typecheck.sh interning diff --git a/checker/bin-devel/test-guava-lock.sh b/checker/bin-devel/test-guava-lock.sh index 60d94cb21d42..2bea848dc256 100755 --- a/checker/bin-devel/test-guava-lock.sh +++ b/checker/bin-devel/test-guava-lock.sh @@ -7,12 +7,12 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh -"/tmp/$USER/plume-scripts/git-clone-related" typetools guava +"$SCRIPTDIR/.plume-scripts/git-clone-related" eisop guava cd ../guava ./typecheck.sh lock diff --git a/checker/bin-devel/test-guava-nullness.sh b/checker/bin-devel/test-guava-nullness.sh index c0ce6db006c6..72282d28e212 100755 --- a/checker/bin-devel/test-guava-nullness.sh +++ b/checker/bin-devel/test-guava-nullness.sh @@ -7,12 +7,12 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh -"/tmp/$USER/plume-scripts/git-clone-related" typetools guava +"$SCRIPTDIR/.plume-scripts/git-clone-related" eisop guava cd ../guava ./typecheck.sh nullness diff --git a/checker/bin-devel/test-guava-regex.sh b/checker/bin-devel/test-guava-regex.sh index 2dc45f93fa38..cb42020eab3a 100755 --- a/checker/bin-devel/test-guava-regex.sh +++ b/checker/bin-devel/test-guava-regex.sh @@ -7,12 +7,12 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh -"/tmp/$USER/plume-scripts/git-clone-related" typetools guava +"$SCRIPTDIR/.plume-scripts/git-clone-related" eisop guava cd ../guava ./typecheck.sh regex diff --git a/checker/bin-devel/test-guava-signature.sh b/checker/bin-devel/test-guava-signature.sh index b70bae265495..66751585b090 100755 --- a/checker/bin-devel/test-guava-signature.sh +++ b/checker/bin-devel/test-guava-signature.sh @@ -7,12 +7,12 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh -"/tmp/$USER/plume-scripts/git-clone-related" typetools guava +"$SCRIPTDIR/.plume-scripts/git-clone-related" eisop guava cd ../guava ./typecheck.sh signature diff --git a/checker/bin-devel/test-guava.sh b/checker/bin-devel/test-guava.sh index af376815c8e0..7bc125a4a9bf 100755 --- a/checker/bin-devel/test-guava.sh +++ b/checker/bin-devel/test-guava.sh @@ -7,12 +7,15 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh +./gradlew assembleForJavac --console=plain -Dorg.gradle.internal.http.socketTimeout=60000 -Dorg.gradle.internal.http.connectionTimeout=60000 +# TODO: Maybe I should move this into the CI job, and do it for all CI jobs. +cp "$SCRIPTDIR"/mvn-settings.xml ~/settings.xml -"/tmp/$USER/plume-scripts/git-clone-related" typetools guava +"$SCRIPTDIR/.plume-scripts/git-clone-related" eisop guava cd ../guava if [ "$TRAVIS" = "true" ] ; then @@ -26,4 +29,15 @@ fi ## This command works locally, but on Azure it fails with timouts while downloading Maven dependencies. # cd guava && time mvn --debug -B package -P checkerframework-local -Dmaven.test.skip=true -Danimal.sniffer.skip=true -cd guava && time mvn --debug -B compile -P checkerframework-local +# Pre-download Maven dependencies. Otherwise there are sometimes timeouts when downloading a Maven dependency. +(cd guava && \ +(timeout 5m mvn -B dependency:resolve-plugins || (sleep 1m && (timeout 5m mvn -B dependency:resolve-plugins || true)))) +# This downloads even more dependencies, but the above seems to be sufficient. +# (cd guava && \ +# (timeout 5m mvn -B dependency:go-offline || (sleep 1m && (timeout 5m mvn -B dependency:go-offline || true)))) + +# Comment about -D flags: the maven.wagon settings should not be relevant to Maven 3.9 and later, but try them anyway. +# (I saw Maven take 30 minutes to download a dependency even with all these flags.) +(cd guava && \ +time mvn --debug -B compile -P checkerframework-local \ + -Dhttp.keepAlive=false -Daether.connector.http.connectionMaxTtl=25 -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120) diff --git a/checker/bin-devel/test-misc.sh b/checker/bin-devel/test-misc.sh index c5fd22fe78e7..8158fca7f3bf 100755 --- a/checker/bin-devel/test-misc.sh +++ b/checker/bin-devel/test-misc.sh @@ -7,29 +7,61 @@ export SHELLOPTS echo "SHELLOPTS=${SHELLOPTS}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh +PLUME_SCRIPTS="$SCRIPTDIR/.plume-scripts" -# Code style and formatting -./gradlew checkBasicStyle checkFormat --console=plain --warning-mode=all --no-daemon +## Checker Framework demos +"$PLUME_SCRIPTS/git-clone-related" eisop checker-framework.demos -q --single-branch --depth 50 +./gradlew :checker:demosTests --console=plain --warning-mode=all -# Run error-prone -./gradlew runErrorProne --console=plain --warning-mode=all --no-daemon +status=0 -# HTML legality -./gradlew htmlValidate --console=plain --warning-mode=all --no-daemon +## Code style and formatting +JAVA_VER=$(java -version 2>&1 | head -1 | cut -d'"' -f2 | sed '/^1\./s///' | cut -d'.' -f1 | sed 's/-ea//') +if [ "${JAVA_VER}" != "8" ] && [ "${JAVA_VER}" != "20" ] ; then + ./gradlew spotlessCheck --console=plain --warning-mode=all +fi +if grep -n -r --exclude-dir=build --exclude-dir=examples --exclude-dir=jtreg --exclude-dir=tests --exclude="*.astub" --exclude="*.tex" '^\(import static \|import .*\*;$\)'; then + echo "Don't use static import or wildcard import" + exit 1 +fi +make -C checker/bin +make -C checker/bin-devel +make -C docs/developer/release check-python-style -# Javadoc documentation -status=0 -./gradlew javadoc --console=plain --warning-mode=all --no-daemon || status=1 -./gradlew javadocPrivate --console=plain --warning-mode=all --no-daemon || status=1 -(./gradlew requireJavadoc --console=plain --warning-mode=all --no-daemon > /tmp/warnings-rjp.txt 2>&1) || true -/tmp/"$USER"/plume-scripts/ci-lint-diff /tmp/warnings-rjp.txt || status=1 -(./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 +## HTML legality +./gradlew htmlValidate --console=plain --warning-mode=all + +## Javadoc documentation +# Try twice in case of network lossage. +(./gradlew javadoc --console=plain --warning-mode=all || (sleep 60 && ./gradlew javadoc --console=plain --warning-mode=all)) || status=1 +./gradlew javadocPrivate --console=plain --warning-mode=all || status=1 +# For refactorings that touch a lot of code that you don't understand, create +# top-level file SKIP-REQUIRE-JAVADOC. Delete it after the pull request is merged. +if [ -f SKIP-REQUIRE-JAVADOC ]; then + echo "Skipping requireJavadoc because file SKIP-REQUIRE-JAVADOC exists." +else + (./gradlew requireJavadoc --console=plain --warning-mode=all > /tmp/warnings-rjp.txt 2>&1) || true + "$PLUME_SCRIPTS"/ci-lint-diff /tmp/warnings-rjp.txt || status=1 + (./gradlew javadocDoclintAll --console=plain --warning-mode=all > /tmp/warnings-jda.txt 2>&1) || true + "$PLUME_SCRIPTS"/ci-lint-diff /tmp/warnings-jda.txt || status=1 +fi if [ $status -ne 0 ]; then exit $status; fi -# User documentation -make -C docs/manual all + +## User documentation +./gradlew manual +git diff --exit-code docs/manual/contributors.tex || \ + (set +x && set +v && + echo "docs/manual/contributors.tex is not up to date." && + echo "If the above suggestion is appropriate, run: make -C docs/manual contributors.tex" && + echo "If the suggestion contains a username rather than a human name, then do all the following:" && + echo " * Update your git configuration by running: git config --global user.name \"YOURFULLNAME\"" && + echo " * Add your name to your GitHub account profile at https://github.com/settings/profile" && + echo " * Make a pull request to add your GitHub ID to" && + echo " https://github.com/eisop-plume-lib/plume-scripts/blob/master/git-authors.sed" && + echo " and remake contributors.tex after that pull request is merged." && + false) diff --git a/checker/bin-devel/test-plume-lib.sh b/checker/bin-devel/test-plume-lib.sh index b79de7d145ba..9d0d0fa07cb4 100755 --- a/checker/bin-devel/test-plume-lib.sh +++ b/checker/bin-devel/test-plume-lib.sh @@ -9,22 +9,24 @@ echo "SHELLOPTS=${SHELLOPTS}" # Optional argument $1 is the group. GROUPARG=$1 echo "GROUPARG=$GROUPARG" -# These are all the Java projects at https://github.com/plume-lib +# These are all the Java projects at https://github.com/eisop-plume-lib as of Dec 2022. if [[ "${GROUPARG}" == "bcel-util" ]]; then PACKAGES=("${GROUPARG}"); fi if [[ "${GROUPARG}" == "bibtex-clean" ]]; then PACKAGES=("${GROUPARG}"); fi if [[ "${GROUPARG}" == "html-pretty-print" ]]; then PACKAGES=("${GROUPARG}"); fi if [[ "${GROUPARG}" == "icalavailable" ]]; then PACKAGES=("${GROUPARG}"); fi +if [[ "${GROUPARG}" == "javadoc-lookup" ]]; then PACKAGES=("${GROUPARG}"); fi if [[ "${GROUPARG}" == "lookup" ]]; then PACKAGES=("${GROUPARG}"); fi if [[ "${GROUPARG}" == "multi-version-control" ]]; then PACKAGES=("${GROUPARG}"); fi if [[ "${GROUPARG}" == "options" ]]; then PACKAGES=("${GROUPARG}"); fi if [[ "${GROUPARG}" == "plume-util" ]]; then PACKAGES=("${GROUPARG}"); fi +if [[ "${GROUPARG}" == "reflection-util" ]]; then PACKAGES=("${GROUPARG}"); fi if [[ "${GROUPARG}" == "require-javadoc" ]]; then PACKAGES=("${GROUPARG}"); fi -if [[ "${GROUPARG}" == "signature-util" ]]; then PACKAGES=("${GROUPARG}"); fi if [[ "${GROUPARG}" == "all" ]] || [[ "${GROUPARG}" == "" ]]; then if java -version 2>&1 | grep version | grep 1.8 ; then - PACKAGES=(bcel-util bibtex-clean html-pretty-print icalavailable lookup multi-version-control options plume-util require-javadoc) + # options master branch does not compile under JDK 8 + PACKAGES=(bcel-util bibtex-clean html-pretty-print icalavailable javadoc-lookup lookup multi-version-control plume-util reflection-util require-javadoc) else - PACKAGES=(bcel-util bibtex-clean html-pretty-print icalavailable lookup multi-version-control options plume-util) + PACKAGES=(bcel-util bibtex-clean html-pretty-print icalavailable javadoc-lookup lookup multi-version-control options plume-util reflection-util require-javadoc) fi fi if [ -z ${PACKAGES+x} ]; then @@ -35,22 +37,27 @@ echo "PACKAGES=" "${PACKAGES[@]}" SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -# In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) -# shellcheck disable=SC1090 -source "$SCRIPTDIR"/build.sh +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh +./gradlew assembleForJavac --console=plain -Dorg.gradle.internal.http.socketTimeout=60000 -Dorg.gradle.internal.http.connectionTimeout=60000 +failing_packages="" echo "PACKAGES=" "${PACKAGES[@]}" for PACKAGE in "${PACKAGES[@]}"; do echo "PACKAGE=${PACKAGE}" PACKAGEDIR="/tmp/${PACKAGE}" rm -rf "${PACKAGEDIR}" - if [[ "${PACKAGE}" == "options" ]] || [[ "${PACKAGE}" == "plume-util" ]]; then - ORGANIZATION="opprop" - else - ORGANIZATION="plume-lib" - fi - "/tmp/$USER/plume-scripts/git-clone-related" "${ORGANIZATION}" "${PACKAGE}" "${PACKAGEDIR}" - echo "About to call ./gradlew --console=plain -PcfLocal assemble" - (cd "${PACKAGEDIR}" && ./gradlew --console=plain -PcfLocal assemble) + "$SCRIPTDIR/.plume-scripts/git-clone-related" eisop-plume-lib "${PACKAGE}" "${PACKAGEDIR}" -q --single-branch --depth 250 + # Uses "compileJava" target instead of "assemble" to avoid the javadoc error "Error fetching URL: + # https://docs.oracle.com/en/java/javase/17/docs/api/" due to network problems. + echo "About to call ./gradlew --console=plain -PcfLocal compileJava" + # Try twice in case of network lossage. + (cd "${PACKAGEDIR}" && (./gradlew --console=plain -PcfLocal compileJava compileTestJava || (sleep 60 && ./gradlew --console=plain -PcfLocal compileJava compileTestJava))) || failing_packages="${failing_packages} ${PACKAGE}" done + +if [ -n "${failing_packages}" ] ; then + echo "Failing packages: ${failing_packages}" + exit 1 +fi diff --git a/checker/bin-devel/test-typecheck-part1.sh b/checker/bin-devel/test-typecheck-part1.sh new file mode 100755 index 000000000000..2c27f7b1523e --- /dev/null +++ b/checker/bin-devel/test-typecheck-part1.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e +set -o verbose +set -o xtrace +export SHELLOPTS +echo "SHELLOPTS=${SHELLOPTS}" + +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh + + +# Pluggable type-checking: run the Checker Framework on itself +./gradlew typecheck-part1 --console=plain --warning-mode=all diff --git a/checker/bin-devel/test-typecheck-part2.sh b/checker/bin-devel/test-typecheck-part2.sh new file mode 100755 index 000000000000..0d512a6ea2b9 --- /dev/null +++ b/checker/bin-devel/test-typecheck-part2.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e +set -o verbose +set -o xtrace +export SHELLOPTS +echo "SHELLOPTS=${SHELLOPTS}" + +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh + + +# Pluggable type-checking: run the Checker Framework on itself +./gradlew typecheck-part2 --console=plain --warning-mode=all diff --git a/checker/bin-devel/test-typecheck.sh b/checker/bin-devel/test-typecheck.sh new file mode 100755 index 000000000000..5afee821003d --- /dev/null +++ b/checker/bin-devel/test-typecheck.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e +set -o verbose +set -o xtrace +export SHELLOPTS +echo "SHELLOPTS=${SHELLOPTS}" + +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +# shellcheck disable=SC1090 # In newer shellcheck than 0.6.0, pass: "-P SCRIPTDIR" (literally) +export ORG_GRADLE_PROJECT_useJdk17Compiler=true +source "$SCRIPTDIR"/clone-related.sh + + +# Pluggable type-checking: run the Checker Framework on itself +./gradlew typecheck --console=plain --warning-mode=all diff --git a/checker/bin-devel/wpi-plumelib/README b/checker/bin-devel/wpi-plumelib/README new file mode 100644 index 000000000000..2a766e285b64 --- /dev/null +++ b/checker/bin-devel/wpi-plumelib/README @@ -0,0 +1,31 @@ +This directory contains the expected result of type-checking projects from +https://github.com/plume-lib/, after whole-program inference. +The script `test-wpi-plumelib.sh` runs the tests. + +If the tests fail, you may need to fix a problem with whole-program +inference, or you might need to edit the `*.expected` files in this +directory. Those files are just like the `typecheck.out` file created by +running wpi.sh, with two permitted additions: + * comments (a line starting with "#") to explain type-checking errors + * blank lines to improve readability + +The script `test-wpi-plumelib.sh` complements the Gradle `wpiManyTest` +task. Here are differences: + * Different projects use a different list of type-checkers. + (wpi-many.sh uses a fixed set of type-checkers for all projects.) + * This uses the HEAD commit. + (wpi-many.sh uses a fixed commit.) + * This checks for expected type-checking errors. + (The Gradle wpiManyTest target requires that there are no errors, so it + skips type systems for which inference does not currently work.) + +The use of the HEAD commit makes these tests brittle. They might fail if: + * The Checker Framework is changed in a way that makes whole-program + inference fail to infer all needed annotations. + * A plume-lib project is changed in a way that exposes a limitation of + whole-program inference. + +A test failure always indicates a limitation of whole-program inference, or +the removal of a limitation. It is unfortunate that commits to a different +repository can make the Checker Framework tests fail, but that is the price +of staying at the head commit. diff --git a/checker/bin-devel/wpi-plumelib/bcel-util.expected b/checker/bin-devel/wpi-plumelib/bcel-util.expected new file mode 100644 index 000000000000..811aa279c6cc --- /dev/null +++ b/checker/bin-devel/wpi-plumelib/bcel-util.expected @@ -0,0 +1,6 @@ +BcelUtil.java:764: error: [argument] incompatible argument for parameter typename of ClassnameAndDimensions.parseFqBinaryName. + Signatures.ClassnameAndDimensions.parseFqBinaryName(classname); + ^ + found : @SignatureUnknown String + required: @FqBinaryName String +1 error diff --git a/checker/bin-devel/wpi-plumelib/bibtex-clean.expected b/checker/bin-devel/wpi-plumelib/bibtex-clean.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/checker/bin-devel/wpi-plumelib/html-pretty-print.expected b/checker/bin-devel/wpi-plumelib/html-pretty-print.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/checker/bin-devel/wpi-plumelib/icalavailable.expected b/checker/bin-devel/wpi-plumelib/icalavailable.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/checker/bin-devel/wpi-plumelib/lookup.expected b/checker/bin-devel/wpi-plumelib/lookup.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/checker/bin-devel/wpi-plumelib/options.expected b/checker/bin-devel/wpi-plumelib/options.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/checker/bin-devel/wpi-plumelib/test-wpi-plumelib.sh b/checker/bin-devel/wpi-plumelib/test-wpi-plumelib.sh new file mode 100755 index 000000000000..a8cb79a3d481 --- /dev/null +++ b/checker/bin-devel/wpi-plumelib/test-wpi-plumelib.sh @@ -0,0 +1,124 @@ +#!/bin/sh + +# Run wpi.sh on plume-lib projects. +# For each project: +# * clone it +# * remove its annotations +# * run WPI to infer annotations +# * type-check the annotated version +# * check that the output of type-checking is the same as the *.expected file in this directory +# Afterward, the inferred annotations can be found in a directory named /tmp/wpi-ajava-XXXXXX . +# The exact directory name is the last directory in the -Ajava= argument in file +# checker-framework/checker/build/wpi-plumelib-tests/PROJECTNAME/dljc-out/typecheck.out . + +# This script is run by `./gradlew wpiPlumeLibTest` at the top level. + +# wpi.sh may exit with non-zero status. +set +e + +# set -o verbose +# set -o xtrace +export SHELLOPTS +# echo "SHELLOPTS=${SHELLOPTS}" + +SCRIPTDIR="$(cd "$(dirname "$0")" && pwd)" +CHECKERFRAMEWORK="$(cd "$(dirname "$0")"/../../.. && pwd)" + +# Do not use a subdirectory of $CHECKERFRAMEWORK because if a project has no +# `settings.gradle` file, Gradle will find one in $CHECKERFRAMEWORK. +TESTDIR=$(mktemp -d "${TMPDIR:-/tmp}"/wpi-plumelib-tests-"$(date +%Y%m%d-%H%M%S)"-XXX) + +# Takes two arguments, an input file (produced by compilation) and an output file. +# Copies the input to the output, removing parts that might differ from run to run. +clean_compile_output() { + in="$1" + out="$2" + + cp -f "$in" "$out" || exit 1 + + # Remove "Running ..." line + sed -i '/^Running /d' "$out" + + # Remove comments starting with "#" and blank lines + sed -i '/^#/d' "$out" + sed -i '/^$/d' "$out" + + # Remove uninteresting output + sed -i '/^warning: \[path\] bad path element /d' "$out" + sed -i '/^warning: \[options\] bootstrap class path not set/d' "$out" + sed -i '/^warning: \[options\] system modules path not set in conjunction with -source 11/d' "$out" + + # Remove warning count because it can differ between JDK 8 and later JDKs due to the bootstrap warning: + sed -i '/^[0-9]* warning/d' "$out" + + # Remove directory names and line numbers + sed -i 's/^[^ ]*\///' "$out" + sed -i 's/:[0-9]+: /: /' "$out" +} + +# Takes two arguments, the project name and the comma-separated list of checkers to run. +test_wpi_plume_lib() { + project="$1" + checkers="$2" + + rm -rf "$project" + # Try twice in case of network lossage + git clone -q --filter=blob:none "https://github.com/plume-lib/$project.git" || (sleep 60 && git clone -q --filter=blob:none "https://github.com/plume-lib/$project.git") + + cd "$project" || (echo "can't run: cd $project" && exit 1) + + java -cp "$CHECKERFRAMEWORK/checker/dist/checker.jar" org.checkerframework.framework.stub.RemoveAnnotationsForInference . || exit 1 + # The project may not build after running RemoveAnnotationsForInference, because some casts + # may become redundant and javac -Xlint:all yields "warning: [cast] redundant cast to ...". + "$CHECKERFRAMEWORK"/checker/bin-devel/.plume-scripts/preplace -- "-Xlint:" "-Xlint:-cast," build.gradle + + echo "test-wpi-plumelib.sh for ${project} about to call wpi.sh at $(date)." + "$CHECKERFRAMEWORK/checker/bin/wpi.sh" -b "-PskipCheckerFramework" -- --checker "$checkers" + echo "test-wpi-plumelib.sh for ${project} returned from wpi.sh at $(date)." + + EXPECTED_FILE="$SCRIPTDIR/$project.expected" + DLJC_OUT_DIR="$TESTDIR/$project/dljc-out" + ACTUAL_FILE="$DLJC_OUT_DIR"/typecheck.out + mkdir -p "$DLJC_OUT_DIR" + touch "${ACTUAL_FILE}" + clean_compile_output "$EXPECTED_FILE" "expected.txt" + clean_compile_output "$ACTUAL_FILE" "actual.txt" + if ! cmp --quiet expected.txt actual.txt ; then + echo "Comparing $EXPECTED_FILE $ACTUAL_FILE in $(pwd)" + diff -u expected.txt actual.txt + if [ -n "$AZURE_HTTP_USER_AGENT" ] || [ -n "$CIRCLE_PR_USERNAME" ] || [ -n "$GITHUB_HEAD_REF" ] || [ "$TRAVIS" = "true" ] ; then + # Running under continuous integration. Output files that may be useful for debugging. + echo "TESTDIR = ${TESTDIR}" + echo "project = ${project}" + echo "DLJC_OUT_DIR = ${DLJC_OUT_DIR}" + rm -f "$DLJC_OUT_DIR"/dljc.cache + more "$DLJC_OUT_DIR"/* + # The string is printed by `tools/wpi.py` in the do_like_javac repository. + AJAVADIR="$(sed -n 's/Directory for generated annotation files: \(.*\)$/\1/p' "$DLJC_OUT_DIR"/dljc-stdout-*)" + echo "AJAVADIR=$AJAVADIR" + find "$AJAVADIR" -type f -print0 | xargs -0 more + # Repeat the actual error, so it appears at the end of the continuous integration log. + echo "Comparing $EXPECTED_FILE $ACTUAL_FILE in $(pwd)" + diff -u expected.txt actual.txt + fi + exit 1 + fi + + cd .. +} + + +mkdir -p "$TESTDIR" +cd "$TESTDIR" || (echo "can't do: cd $TESTDIR" && exit 1) + +# Get the list of checkers from the project's build.gradle file +## TODO: These projects are annotated for additional checkers, like resourceleak. Add to these lists. +test_wpi_plume_lib bcel-util "formatter,interning,lock,nullness,regex,signature" +test_wpi_plume_lib bibtex-clean "formatter,index,interning,lock,nullness,regex,signature" +test_wpi_plume_lib html-pretty-print "formatter,index,interning,lock,nullness,regex,signature" +test_wpi_plume_lib icalavailable "formatter,index,interning,lock,nullness,regex,signature,initializedfields" +test_wpi_plume_lib lookup "formatter,index,interning,lock,nullness,regex,signature" +## Commented out temporarily +# test_wpi_plume_lib options "formatter,index,interning,lock,nullness,regex,signature,initializedfields" + +echo "exiting test-wpi-plumelib.sh" diff --git a/checker/bin/Makefile b/checker/bin/Makefile new file mode 100644 index 000000000000..40e3ecec6ac3 --- /dev/null +++ b/checker/bin/Makefile @@ -0,0 +1,10 @@ +SH_SCRIPTS = $(shell grep -r -l '^\#! \?\(/bin/\|/usr/bin/env \)sh' * | grep -v .git | grep -v "~") +BASH_SCRIPTS = $(shell grep -r -l '^\#! \?\(/bin/\|/usr/bin/env \)bash' * | grep -v .git | grep -v "~") + +shell-script-style: + shellcheck --format=gcc ${SH_SCRIPTS} ${BASH_SCRIPTS} + checkbashisms ${SH_SCRIPTS} + +showvars: + @echo "SH_SCRIPTS=${SH_SCRIPTS}" + @echo "BASH_SCRIPTS=${BASH_SCRIPTS}" diff --git a/checker/bin/README b/checker/bin/README index db90ef46308a..fc6ab1651165 100644 --- a/checker/bin/README +++ b/checker/bin/README @@ -1,24 +1,15 @@ This directory, "checker/bin", contains scripts to run the Checker Framework. +Before using them, you must run `./gradlew assemble` in the parent directory. -javac - Is a shell script that runs the Checker Framework in *nix systems -including Mac OS X. Its invocation is equivalent to -"java -jar checker.jar ". -This script is a drop-in replacement for the script javac provided by the -OpenJDK. +javac - Is a shell script that runs the Checker Framework in Unix systems +including Mac OS X. This script is a drop-in replacement for the script +javac provided by the OpenJDK. javac.bat - Is the equivalent of the javac script for Windows systems. - -The directory "checker/dist/" houses all jars built for the binary distribution. -To build these jars, run "./gradlew assemble" from the "checker-framework" directory. -Afterward, the directory will contain all jars needed to run the -Checker Framework. - -checker-qual.jar - Contains the annotations used by the checkers built into the -Checker Framework - -checker.jar - Contains all the Checker Framework classes including the framework -itself, all built-in checkers, and the annotations found in checker-qual.jar. - -javac.jar - Contains the Type Annotations Compiler -(see https://checkerframework.org/README-jsr308.html) +The other scripts are used for whole-program inference: +infer-and-annotate.sh +query-github.sh +wpi.sh +wpi-many.sh +wpi-summary.sh diff --git a/checker/bin/infer-and-annotate.sh b/checker/bin/infer-and-annotate.sh index 53d5108f93c6..e1adb561fda4 100755 --- a/checker/bin/infer-and-annotate.sh +++ b/checker/bin/infer-and-annotate.sh @@ -20,7 +20,7 @@ # Example of usage: # ./infer-and-annotate.sh "LockChecker,NullnessChecker" \ # plume-util/build/libs/plume-util-all.jar \ -# `find plume-util/src/main/java/ -name "*.java"` +# $(find plume-util/src/main/java/ -name "*.java") # In case of using this script for Android projects, the classpath must include # paths to: android.jar, gen folder, all libs used, source code folder. @@ -133,7 +133,7 @@ infer_and_annotate() { # Runs CF's javac command="$CHECKERBIN/javac -d $TEMP_DIR/ -cp $cp -processor $processor -Ainfer=jaifs -Awarns -Xmaxwarns 10000 ${extra_args[*]} ${java_files[*]}" echo "About to run: ${command}" - if [ $interactive ]; then + if [ "$interactive" ]; then echo "Press any key to run command... " IFS="" read -r _ fi @@ -158,7 +158,7 @@ infer_and_annotate() { # in its header. To avoid this problem, we add the "|| true" below. DIFF_JAIF="$(diff -qr $PREV_ITERATION_DIR $WHOLE_PROGRAM_INFERENCE_DIR || true)" done - if [ ! $debug ]; then + if [ ! "$debug" ]; then clean fi } diff --git a/checker/bin/javac b/checker/bin/javac index 584b4eb1f0ef..551b1596d0ce 100755 --- a/checker/bin/javac +++ b/checker/bin/javac @@ -5,24 +5,35 @@ # to org.checkerframework.framework.util.CheckerMain # -mydir="`dirname $0`" -case `uname -s` in +mydir="$(dirname "$0")" +case $(uname -s) in CYGWIN*) - mydir=`cygpath -m $mydir` + mydir=$(cygpath -m "$mydir") ;; esac ## Preserve quoting and spaces in arguments, which would otherwise be lost ## due to being passed through the shell twice. -# Unset IFS and use newline as arg separator to preserve spaces in args +# Unset IFS and use newline as arg separator to preserve spaces in args. +# shellcheck disable=SC2034 DUALCASE=1 # for MKS: make case statement case-sensitive (6709498) saveIFS="$IFS" nl=' ' for i in "$@" ; do IFS= - args=$args$nl"'"$i"'" + # shellcheck disable=SC2027 + case $i in + "-Xmn"*) jvmargs=$jvmargs$nl"'"$i"'" ;; + "-Xms"*) jvmargs=$jvmargs$nl"'"$i"'" ;; + "-Xmx"*) jvmargs=$jvmargs$nl"'"$i"'" ;; + *) args=$args$nl"'"$i"'" ;; + esac IFS="$saveIFS" done -eval "java" "-jar" "${mydir}"/../dist/checker.jar ${args} +# shellcheck disable=SC2086 +eval "java" \ + ${jvmargs} \ + "-jar" "${mydir}"/../dist/checker.jar \ + ${args} diff --git a/checker/bin/query-github.sh b/checker/bin/query-github.sh new file mode 100755 index 000000000000..752d789a5083 --- /dev/null +++ b/checker/bin/query-github.sh @@ -0,0 +1,122 @@ +#!/bin/sh + +# This script collects a list of projects that match a query from GitHub. + +# inputs: +# +# The file git-personal-access-token must exist in the directory from which +# this script is run, and must be a valid github OAuth token. The token only +# needs the "Access public repositories" permission. +# +# $1 is the query file, which should contain the literal string to use +# as the github search. REQUIRED, no default. +# +# $2 is the number of GitHub search pages. Default 1. Each page contains 10 results. GitHub only +# returns the first 1000 results, so 100 is the maximum useful number of search pages. +# +# This script outputs a list of projects. The underlying GitHub search is for code snippets, and this +# script eliminates duplicates (i.e. different code snippets from the same project are combined into +# a single result for the project), so the number of projects the script outputs will usually be less +# than 10 times the number of pages requested. + +# Set to 1 to enable debug output from this script. +DEBUG=0 + +query_file=$1 +# Number of times to retry a GitHub search query. +query_tries=5 + +if [ -z "${query_file}" ]; then + echo "you must provide a query file as the first argument" + exit 2 +fi + +if [ -z "$2" ]; then + page_count=1 +else + page_count=$2 +fi + +query=$(tr ' ' '+' < "${query_file}") + +mkdir -p "/tmp/$USER" + +## for storing the results before sorting and uniqing them +rm -f "/tmp/$USER/github-query-results-*.txt" +tempfile=$(mktemp "/tmp/$USER/github-query-results-$(date +%Y%m%d-%H%M%S)-XXX.txt") +#trap "rm -f ${tempfile}" 0 2 3 15 + +rm -f "/tmp/$USER/github-hash-results-*.txt" +hashfile=$(mktemp "/tmp/$USER/github-hash-results-$(date +%Y%m%d-%H%M%S)-XXX.txt") +#trap "rm -f ${hashfile}" 0 2 3 15 + +rm -rf "/tmp/$USER/curl-output-*.txt" +curl_output_file=$(mktemp "/tmp/$USER/curl-output-$(date +%Y%m%d-%H%M%S)-XXX.txt") + +# find the repos +for i in $(seq "${page_count}"); do + # GitHub only allows 30 searches per minute, so add a delay to each request. + if [ "${i}" -gt 1 ]; then + sleep 5 + fi + + full_query='https://api.github.com/search/code?q='${query}'&page='${i} + if [ $DEBUG -ne 0 ] ; then + echo "full_query=$full_query" + fi + for tries in $(seq ${query_tries}); do + status_code=$(curl -s \ + -H "Authorization: token $(cat git-personal-access-token)" \ + -H "Accept: application/vnd.github.v3+json" \ + -w "%{http_code}" \ + -o "${curl_output_file}" \ + "${full_query}") + + if [ "${status_code}" -eq 200 ] || [ "${status_code}" -eq 422 ]; then + # Don't retry. + # 200 is success. 422 means too many GitHub requests. + break + elif [ "${tries}" -lt $((query_tries - 1)) ]; then + # Retry. + # Other status codes are failures. Failures are usually due to + # triggering the abuse detection mechanism for sending too many + # requests, so we add a delay when this happens. + sleep 20 + fi + done + + # GitHub only returns the first 1000 results. Requests past this limit + # return 422, so stop making requests. + if [ "${status_code}" -eq 422 ]; then + break; + elif [ "${status_code}" -ne 200 ]; then + echo "GitHub query failed, last response:" + cat "${curl_output_file}" + rm -f "${curl_output_file}" + exit 1 + fi + + grep " \"html_url" < "${curl_output_file}" \ + | grep -v " " \ + | sort -u \ + | cut -d \" -f 4 >> "${tempfile}" +done + +rm -f "${curl_output_file}" + +# Each loop iteration was sorted and unique; this does it for the full result. +sort -u -o "${tempfile}" "${tempfile}" + +while IFS= read -r line +do + repo=$(echo "${line}" | cut -d / -f 5) + owner=$(echo "${line}" | cut -d / -f 4) + hash_query='https://api.github.com/repos/'${owner}'/'${repo}'/commits?per_page=1' + curl -sH "Authorization: token $(cat git-personal-access-token)" \ + "Accept: application/vnd.github.v3+json" \ + "${hash_query}" \ + | grep '^ "sha":' \ + | cut -d \" -f 4 >> "${hashfile}" +done < "${tempfile}" + +paste "${tempfile}" "${hashfile}" diff --git a/checker/bin/wpi-annotation-paths.sh b/checker/bin/wpi-annotation-paths.sh new file mode 100755 index 000000000000..84fee9b48873 --- /dev/null +++ b/checker/bin/wpi-annotation-paths.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +# Given the name of a directory containing the results produced by an +# invocation of wpi-many.sh, output the human-readable paths to the .ajava +# files to stdout. + +# Usage: +# wpi-annotation-paths TARGETDIR + +TARGETDIR="$1" + +if [ "$#" -ne 1 ]; then + echo "Usage: $(basename "$0") TARGETDIR" >&2 + exit 1 +fi + +# First, count the number of WPI log files in the given directory. +WPI_LOG_FILE_COUNT=$(find "${TARGETDIR}"/*-wpi-stdout.log 2> /dev/null | wc -l) + +if [ "$WPI_LOG_FILE_COUNT" -ne 0 ]; then + # WPI log files exist, find the latest annotation files corresponding to + # each of them. + for wpi_log_file in "$TARGETDIR"/*-wpi-stdout.log; + do + echo "Log file: $wpi_log_file" + + # The latest match from grep should be reported, as the latest match + # in the file corresponds to the final (and most precise) set of annotations + # generated by whole-program inference. + FULL_AJAVA_MATCH=$(grep -oE 'Aajava=/[^ ]+' "$wpi_log_file" | tail -1 | tr -d "'") + AJAVA_PATH="" + if [ -z "$FULL_AJAVA_MATCH" ]; then + AJAVA_PATH="No .ajava files generated" + else + AJAVA_PATH=$(echo "$FULL_AJAVA_MATCH" | cut -d "=" -f 2 | tr -d "'" ) + fi + + echo "Annotated file(s): $AJAVA_PATH" + done +else + echo "No WPI log files found in $TARGETDIR" + exit 1 +fi diff --git a/checker/bin/wpi-many.sh b/checker/bin/wpi-many.sh new file mode 100755 index 000000000000..62bf6583e328 --- /dev/null +++ b/checker/bin/wpi-many.sh @@ -0,0 +1,476 @@ +#!/bin/bash + +# This script runs the Checker Framework's whole-program inference on each of a list of projects. + +# For usage and requirements, see the "Whole-program inference" +# section of the Checker Framework manual: +# https://eisop.github.io/cf/manual/#whole-program-inference + +set -eo pipefail +# not set -u, because this script checks variables directly + +DEBUG=0 +# To enable debugging, uncomment the following line. +# DEBUG=1 + +while getopts "o:i:t:g:s" opt; do + case $opt in + o) OUTDIR="$OPTARG" + ;; + i) INLIST="$OPTARG" + ;; + t) TIMEOUT="$OPTARG" + ;; + g) GRADLECACHEDIR="$OPTARG" + ;; + s) SKIP_OR_DELETE_UNUSABLE="skip" + ;; + \?) # the remainder of the arguments will be passed to DLJC directly + ;; + esac +done + +# Make $@ be the arguments that should be passed to dljc. +shift $(( OPTIND - 1 )) + +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +SCRIPTPATH="${SCRIPTDIR}/wpi-many.sh" + +# Report line numbers when the script fails, from +# https://unix.stackexchange.com/a/522815 . +trap 'echo >&2 "Error - exited with status $? at line $LINENO of wpi-many.sh:"; + pr -tn ${SCRIPTPATH} | tail -n+$((LINENO - 3)) | head -n7' ERR + +echo "Starting wpi-many.sh." + +# check required arguments and environment variables: + +# shellcheck disable=SC2153 # testing for JAVA_HOME, not a typo of JAVA8_HOME +if [ "${JAVA_HOME}" = "" ]; then + has_java_home="no" +else + has_java_home="yes" +fi + +# shellcheck disable=SC2153 # testing for JAVA8_HOME, not a typo of JAVA_HOME +if [ "${JAVA8_HOME}" = "" ]; then + has_java8="no" +else + has_java8="yes" +fi + +# shellcheck disable=SC2153 # testing for JAVA11_HOME, not a typo of JAVA_HOME +if [ "${JAVA11_HOME}" = "" ]; then + has_java11="no" +else + has_java11="yes" +fi + +# shellcheck disable=SC2153 # testing for JAVA17_HOME, not a typo of JAVA_HOME +if [ "${JAVA17_HOME}" = "" ]; then + has_java17="no" +else + has_java17="yes" +fi + +# shellcheck disable=SC2153 # testing for JAVA20_HOME, not a typo of JAVA_HOME +if [ "${JAVA20_HOME}" = "" ]; then + has_java20="no" +else + has_java20="yes" +fi + +# shellcheck disable=SC2153 # testing for JAVA21_HOME, not a typo of JAVA_HOME +if [ "${JAVA21_HOME}" = "" ]; then + has_java21="no" +else + has_java21="yes" +fi + +if [ "${has_java_home}" = "yes" ] && [ ! -d "${JAVA_HOME}" ]; then + echo "JAVA_HOME is set to a non-existent directory ${JAVA_HOME}" + exit 1 +fi + +if [ "${has_java_home}" = "yes" ]; then + java_version=$("${JAVA_HOME}"/bin/java -version 2>&1 | head -1 | cut -d'"' -f2 | sed '/^1\./s///' | cut -d'.' -f1 | sed 's/-ea//') + if [ "${has_java8}" = "no" ] && [ "${java_version}" = 8 ]; then + export JAVA8_HOME="${JAVA_HOME}" + has_java8="yes" + fi + if [ "${has_java11}" = "no" ] && [ "${java_version}" = 11 ]; then + export JAVA11_HOME="${JAVA_HOME}" + has_java11="yes" + fi + if [ "${has_java17}" = "no" ] && [ "${java_version}" = 17 ]; then + export JAVA17_HOME="${JAVA_HOME}" + has_java17="yes" + fi + if [ "${has_java20}" = "no" ] && [ "${java_version}" = 20 ]; then + export JAVA20_HOME="${JAVA_HOME}" + has_java20="yes" + fi + if [ "${has_java21}" = "no" ] && [ "${java_version}" = 21 ]; then + export JAVA21_HOME="${JAVA_HOME}" + has_java21="yes" + fi +fi + +if [ "${has_java8}" = "yes" ] && [ ! -d "${JAVA8_HOME}" ]; then + echo "JAVA8_HOME is set to a non-existent directory ${JAVA8_HOME}" + exit 1 +fi + +if [ "${has_java11}" = "yes" ] && [ ! -d "${JAVA11_HOME}" ]; then + echo "JAVA11_HOME is set to a non-existent directory ${JAVA11_HOME}" + exit 1 +fi + +if [ "${has_java17}" = "yes" ] && [ ! -d "${JAVA17_HOME}" ]; then + echo "JAVA17_HOME is set to a non-existent directory ${JAVA17_HOME}" + exit 1 +fi + +if [ "${has_java20}" = "yes" ] && [ ! -d "${JAVA20_HOME}" ]; then + echo "JAVA20_HOME is set to a non-existent directory ${JAVA20_HOME}" + exit 1 +fi + +if [ "${has_java21}" = "yes" ] && [ ! -d "${JAVA21_HOME}" ]; then + echo "JAVA21_HOME is set to a non-existent directory ${JAVA21_HOME}" + exit 1 +fi + +if [ "${has_java8}" = "no" ] && [ "${has_java11}" = "no" ] && [ "${has_java17}" = "no" ] && [ "${has_java20}" = "no" ] && [ "${has_java21}" = "no" ]; then + if [ "${has_java_home}" = "yes" ]; then + echo "Cannot determine Java version from JAVA_HOME" + else + echo "No Java 8, 11, 17, 20, or 21 JDKs found. At least one of JAVA_HOME, JAVA8_HOME, JAVA11_HOME, JAVA17_HOME, or JAVA21_HOME must be set." + fi + echo "JAVA_HOME = ${JAVA_HOME}" + echo "JAVA8_HOME = ${JAVA8_HOME}" + echo "JAVA11_HOME = ${JAVA11_HOME}" + echo "JAVA17_HOME = ${JAVA17_HOME}" + echo "JAVA20_HOME = ${JAVA20_HOME}" + echo "JAVA21_HOME = ${JAVA21_HOME}" + command -v java + java -version + exit 1 +fi + +if [ "${CHECKERFRAMEWORK}" = "" ]; then + echo "CHECKERFRAMEWORK is not set; it must be set to a locally-built Checker Framework. Please clone and build github.com/typetools/checker-framework" + exit 2 +fi + +if [ ! -d "${CHECKERFRAMEWORK}" ]; then + echo "CHECKERFRAMEWORK is set to a non-existent directory ${CHECKERFRAMEWORK}" + exit 2 +fi + +if [ "${OUTDIR}" = "" ]; then + echo "You must specify an output directory using the -o argument." + exit 3 +fi + +if [ "${INLIST}" = "" ]; then + echo "You must specify an input file using the -i argument." + exit 4 +fi + +if [ "${GRADLECACHEDIR}" = "" ]; then + # Assume that each project should use its own gradle cache. This is more expensive, + # but prevents crashes on distributed file systems, such as the UW CSE machines. + GRADLECACHEDIR=".gradle" +fi + +if [ "${SKIP_OR_DELETE_UNUSABLE}" = "" ]; then + SKIP_OR_DELETE_UNUSABLE="delete" +fi + +### Script + +echo "Finished configuring wpi-many.sh. Results will be placed in ${OUTDIR}-results/." + +export PATH="${JAVA_HOME}/bin:${PATH}" + +mkdir -p "${OUTDIR}" +mkdir -p "${OUTDIR}-results" + +cd "${OUTDIR}" || exit 5 + +while IFS='' read -r line || [ "$line" ] +do + # Skip lines that start with "#". + [[ $line = \#* ]] && continue + + # Remove trailing return character if reading from a DOS file. + line="$(echo "$line" | tr -d '\r')" + + REPOHASH=${line} + + REPO=$(echo "${REPOHASH}" | awk '{print $1}') + HASH=$(echo "${REPOHASH}" | awk '{print $2}') + + REPO_NAME=$(echo "${REPO}" | cut -d / -f 5) + REPO_NAME_HASH="${REPO_NAME}-${HASH}" + + if [ "$DEBUG" -eq "1" ]; then + echo "REPOHASH=$REPOHASH" + echo "REPO=$REPO" + echo "HASH=$HASH" + echo "REPO_NAME=$REPO_NAME" + echo "REPO_NAME_HASH=$REPO_NAME_HASH" + echo "pwd=$(pwd)" + fi + + # Use repo name and hash, but not owner. We want + # repos that are different but have the same name to be treated + # as different repos, but forks with the same content to be skipped. + # TODO: consider just using hash, to skip hard forks? + mkdir -p "./${REPO_NAME_HASH}" || (echo "command failed in $(pwd): mkdir -p ./${REPO_NAME_HASH}" && exit 5) + + cd "./${REPO_NAME_HASH}" || (echo "command failed in $(pwd): cd ./${REPO_NAME_HASH}" && exit 5) + + if [ ! -d "${REPO_NAME}" ]; then + # see https://stackoverflow.com/questions/3489173/how-to-clone-git-repository-with-specific-revision-changeset + # for the inspiration for this code + mkdir "./${REPO_NAME}" || (echo "command failed in $(pwd): mkdir ./${REPO_NAME}" && exit 5) + cd "./${REPO_NAME}" || (echo "command failed in $(pwd): cd ./${REPO_NAME}" && exit 5) + git init + git remote add origin "${REPO}" + + # The "GIT_TERMINAL_PROMPT=0" setting prevents git from prompting for + # username/password if the repository no longer exists. + GIT_TERMINAL_PROMPT=0 git fetch origin "${HASH}" + + git reset --hard FETCH_HEAD + + cd .. || exit 5 + # Skip the rest of the loop and move on to the next project + # if the checkout isn't successful. + if [ ! -d "${REPO_NAME}" ]; then + continue + fi + else + rm -rf -- "${REPO_NAME}/dljc-out" + fi + + if [ ! -d "${REPO_NAME}/.git" ]; then + echo "In $(pwd): no directory ${REPO_NAME}/.git" + echo "Listing of ${REPO_NAME}:" + ls -al -- "${REPO_NAME}" + exit 5 + fi + + cd "./${REPO_NAME}" || (echo "command failed in $(pwd): cd ./${REPO_NAME}" && exit 5) + + git checkout "${HASH}" || (echo "command failed in $(pwd): git checkout ${HASH}" && exit 5) + + REPO_FULLPATH=$(pwd) + + cd "${OUTDIR}/${REPO_NAME_HASH}" || exit 5 + + RESULT_LOG="${OUTDIR}-results/${REPO_NAME_HASH}-wpi-stdout.log" + touch "${RESULT_LOG}" + + if [ -f "${REPO_FULLPATH}/.cannot-run-wpi" ]; then + if [ "${SKIP_OR_DELETE_UNUSABLE}" = "skip" ]; then + echo "Skipping ${REPO_NAME_HASH} because it has a .cannot-run-wpi file present," + echo "indicating that an earlier run of WPI failed." + echo "To try again, delete the .cannot-run-wpi file and re-run the script." + fi + # the repo will be deleted later if SKIP_OR_DELETE_UNUSABLE is "delete" + else + # it's important that "${OUTDIR}-results/wpi-out" > "${RESULT_LOG}" + if [ ! -s "${RESULT_LOG}" ] ; then + echo "Files are empty: ${REPO_FULLPATH}/dljc-out/wpi-stdout.log ${RESULT_LOG}" + echo "Listing of ${REPO_FULLPATH}/dljc-out:" + ls -al "${REPO_FULLPATH}/dljc-out" + wpi_status=9999 + fi + TYPECHECK_FILE=${REPO_FULLPATH}/dljc-out/typecheck.out + if [ -f "$TYPECHECK_FILE" ]; then + cp -p "$TYPECHECK_FILE" "${OUTDIR}-results/${REPO_NAME_HASH}-typecheck.out" + if [ "$DEBUG" -eq "1" ]; then + echo "File exists: $TYPECHECK_FILE" + echo "File exists: ${OUTDIR}-results/${REPO_NAME_HASH}-typecheck.out" + fi + else + echo "File does not exist: $TYPECHECK_FILE" + echo "File does not exist: ${OUTDIR}-results/${REPO_NAME_HASH}-typecheck.out" + echo "Listing of ${REPO_FULLPATH}/dljc-out:" + ls -al "${REPO_FULLPATH}/dljc-out" + cat "${REPO_FULLPATH}"/dljc-out/*.log + echo "Start of toplevel.log:" + cat "${REPO_FULLPATH}"/dljc-out/toplevel.log + echo "End of toplevel.log." + echo "Start of wpi-stdout.log:" + cat "${REPO_FULLPATH}"/dljc-out/wpi-stdout.log + echo "End of wpi-stdout.log." + wpi_status=9999 + fi + if [ "$DEBUG" -eq "1" ]; then + echo "RESULT_LOG=${RESULT_LOG}" + echo "TYPECHECK_FILE=${TYPECHECK_FILE}" + ls -l "${TYPECHECK_FILE}" + ls -l "${OUTDIR}-results/${REPO_NAME_HASH}-typecheck.out" + echo "Listing of ${OUTDIR}-results:" + ls -al "${OUTDIR}-results" + fi + if [ ! -s "${RESULT_LOG}" ] ; then + echo "File does not exist: ${RESULT_LOG}" + wpi_status=9999 + fi + if [ ! -e "${OUTDIR}-results/${REPO_NAME_HASH}-typecheck.out" ] ; then + echo "File does not exist: ${OUTDIR}-results/${REPO_NAME_HASH}-typecheck.out" + wpi_status=9999 + fi + if [[ "$wpi_status" != 0 ]]; then + echo "Listing of ${OUTDIR}-results:" + ls -al "${OUTDIR}-results" + echo "==== start of ${OUTDIR}-results/wpi-out; printed because wpi_status=${wpi_status} ====" + cat "${OUTDIR}-results/wpi-out" + echo "==== end of ${OUTDIR}-results/wpi-out ====" + exit 5 + fi + fi + # Avoid interleaved output from different iterations of the loop. + sleep 1 + + cd "${OUTDIR}" || exit 5 + +done < "${INLIST}" + +## This section is here rather than in wpi-summary.sh because counting lines can be moderately expensive. +## wpi-summary.sh is intended to be run while a human waits (unlike this script), so this script +## precomputes as much as it can, to make wpi-summary.sh faster. + +# this command is allowed to fail, because if no projects returned results then none +# of these expressions will match, and we want to enter the special handling for that +# case that appears below +results_available=$(grep -vl -e "no build file found for" \ + -e "dljc could not run the Checker Framework" \ + -e "dljc could not run the build successfully" \ + -e "dljc timed out for" \ + "${OUTDIR}-results/"*.log || true) + +echo "${results_available}" > "${OUTDIR}-results/results_available.txt" +echo "results_available = ${results_available}" + +if [ -z "${results_available}" ]; then + echo "No results are available." + echo "Log files:" + ls "${OUTDIR}-results"/*.log + echo "End of log files." +else + if [[ "$OSTYPE" == "linux-gnu"* ]]; then + listpath=$(mktemp "/tmp/cloc-file-list-$(date +%Y%m%d-%H%M%S)-XXX.txt") + # Compute lines of non-comment, non-blank Java code in the projects whose + # results can be inspected by hand (that is, those that WPI succeeded on). + # Don't match arguments like "-J--add-opens=jdk.compiler/com.sun.tools.java" + # or "--add-opens=jdk.compiler/com.sun.tools.java". + # shellcheck disable=SC2046 + grep -oh "^\S*\.java" $(cat "${OUTDIR}-results/results_available.txt") | sed "s/'//g" | grep -v '^\-J' | grep -v '^\-\-add\-opens' | sort | uniq > "${listpath}" + + if [ ! -s "${listpath}" ] ; then + echo "listpath ${listpath} has size zero" + ls -al "${listpath}" + echo "results_available = ${results_available}" + echo "---------------- start of ${OUTDIR}-results/results_available.txt ----------------" + cat "${OUTDIR}-results/results_available.txt" + echo "---------------- end of ${OUTDIR}-results/results_available.txt ----------------" + echo "---------------- start of names of log files from which results_available.txt was constructed ----------------" + ls -al "${OUTDIR}-results/"*.log + echo "---------------- end of names of log files from which results_available.txt was constructed ----------------" + ## This is too much output; Azure cuts it off. + # echo "---------------- start of log files from which results_available.txt was constructed ----------------" + # cat "${OUTDIR}-results/"*.log + # echo "---------------- end of log files from which results_available.txt was constructed ----------------" + exit 1 + fi + + mkdir -p "${SCRIPTDIR}/.scc" + cd "${SCRIPTDIR}/.scc" || exit 5 + wget -nc "https://github.com/boyter/scc/releases/download/v2.13.0/scc-2.13.0-i386-unknown-linux.zip" \ + || (sleep 60s && wget -nc "https://github.com/boyter/scc/releases/download/v2.13.0/scc-2.13.0-i386-unknown-linux.zip") + unzip -o "scc-2.13.0-i386-unknown-linux.zip" + + # shellcheck disable=SC2046 + if ! "${SCRIPTDIR}/.scc/scc" --output "${OUTDIR}-results/loc.txt" $(< "${listpath}") ; then + echo "Problem in wpi-many.sh while running scc." + echo " listpath = ${listpath}" + echo " generated from ${OUTDIR}-results/results_available.txt" + echo "---------------- start of listpath = ${listpath} ----------------" + cat "${listpath}" + echo "---------------- end of ${listpath} ----------------" + echo "---------------- start of ${OUTDIR}-results/results_available.txt ----------------" + cat "${OUTDIR}-results/results_available.txt" + echo "---------------- end of ${OUTDIR}-results/results_available.txt ----------------" + echo "---------------- start of names of log files from which results_available.txt was constructed ----------------" + ls -al "${OUTDIR}-results/"*.log + echo "---------------- end of names of log files from which results_available.txt was constructed ----------------" + ## This is too much output; Azure cuts it off. + # echo "---------------- start of log files from which results_available.txt was constructed ----------------" + # cat "${OUTDIR}-results/"*.log + # echo "---------------- end of log files from which results_available.txt was constructed ----------------" + exit 1 + fi + rm -f "${listpath}" + else + echo "skipping computation of lines of code because the operating system is not linux: ${OSTYPE}}" + fi +fi + +echo "Exiting wpi-many.sh successfully. Results were placed in ${OUTDIR}-results/." diff --git a/checker/bin/wpi-summary.sh b/checker/bin/wpi-summary.sh new file mode 100755 index 000000000000..ae3ada88563e --- /dev/null +++ b/checker/bin/wpi-summary.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +# This script takes a directory of .log files as input, and produces a summary of the results. +# Use its output to guide your analysis of the results of running ./wpi-many.sh. +# +# This script categorizes projects as: +# * does not have a maven or gradle build file +# * failed to build +# * WPI timed out +# * WPI produced results (reported as "results available"). This +# script makes no attempt to categorize these projects: a human +# should inspect these log files/projects to see the results. + +targetdir=$1 + +number_of_projects=$(find "${targetdir}" -name "*.log" | wc -l) + +no_build_file=$(grep -o "no build file found for" "${targetdir}/"*.log | wc -l) +no_build_file_percent=$(((no_build_file*100)/number_of_projects)) + +# "old" and "new" in the below refer to the two different messages that +# dljc's wpi tool can emit for this kind of failure. At some point while +# running an early set of these experiments, I realized that the original +# message wasn't correct, and fixed it. But, for backwards compatibility, +# this script looks for both messages and combines the counts. +build_failed_old=$(grep -o "dljc could not run the Checker Framework" "${targetdir}/"*.log | wc -l) +build_failed_new=$(grep -o "dljc could not run the build successfully" "${targetdir}/"*.log | wc -l) +build_failed=$((build_failed_old+build_failed_new)) +build_failed_percent=$(((build_failed*100)/number_of_projects)) + +timed_out=$(grep -o "dljc timed out for" "${targetdir}/"*.log | wc -l) +timed_out_percent=$(((timed_out*100)/number_of_projects)) + +echo "number of projects: ${number_of_projects} (100%)" +echo "no maven or gradle build file: ${no_build_file} (~${no_build_file_percent}%)" +echo "build failed: ${build_failed} (~${build_failed_percent}%)" +echo "timed out: ${timed_out} (~${timed_out_percent}%)" +echo "" +echo "timeouts:" +echo "" +grep -l "dljc timed out for" "${targetdir}/"*.log +echo "" + +results_available=$(cat "${targetdir}/results_available.txt") + +echo "results are available for these projects: " +echo "" +echo "${results_available}" | tr ' ' '\n' +echo "" + +if [ -f "${targetdir}/loc.txt" ]; then + echo "LoC of projects with available results:" + + cat "${targetdir}/loc.txt" +else + echo "No LoC count found for projects with available results" +fi diff --git a/checker/bin/wpi.sh b/checker/bin/wpi.sh new file mode 100755 index 000000000000..d486fbd90e39 --- /dev/null +++ b/checker/bin/wpi.sh @@ -0,0 +1,403 @@ +#!/bin/bash + +# This script performs whole-program inference on a project directory. + +# For usage and requirements, see the "Whole-program inference" +# section of the Checker Framework manual: +# https://eisop.github.io/cf/manual/#whole-program-inference + +set -eo pipefail +# not set -u, because this script checks variables directly + +while getopts "d:t:b:g:c:" opt; do + case $opt in + d) DIR="$OPTARG" + ;; + t) TIMEOUT="$OPTARG" + ;; + b) EXTRA_BUILD_ARGS="$OPTARG" + ;; + g) GRADLECACHEDIR="$OPTARG" + ;; + c) BUILD_TARGET="$OPTARG" + ;; + \?) # echo "Invalid option -$OPTARG" >&2 + ;; + esac +done + +# Make $@ be the arguments that should be passed to dljc. +shift $(( OPTIND - 1 )) + +SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +SCRIPTPATH="${SCRIPTDIR}/wpi.sh" + +# Report line numbers when the script fails, from +# https://unix.stackexchange.com/a/522815 . +trap 'echo >&2 "Error - exited with status $? at line $LINENO of wpi.sh:"; + pr -tn ${SCRIPTPATH} | tail -n+$((LINENO - 3)) | head -n7' ERR + +echo "Starting wpi.sh." + +# check required arguments and environment variables: + +if [ "${JAVA_HOME}" = "" ]; then + has_java_home="no" +else + has_java_home="yes" +fi + +# shellcheck disable=SC2153 # testing for JAVA8_HOME, not a typo of JAVA_HOME +if [ "${JAVA8_HOME}" = "" ]; then + has_java8="no" +else + has_java8="yes" +fi + +# shellcheck disable=SC2153 # testing for JAVA11_HOME, not a typo of JAVA_HOME +if [ "${JAVA11_HOME}" = "" ]; then + has_java11="no" +else + has_java11="yes" +fi + +# shellcheck disable=SC2153 # testing for JAVA17_HOME, not a typo of JAVA_HOME +if [ "${JAVA17_HOME}" = "" ]; then + has_java17="no" +else + has_java17="yes" +fi + +# shellcheck disable=SC2153 # testing for JAVA20_HOME, not a typo of JAVA_HOME +if [ "${JAVA20_HOME}" = "" ]; then + has_java20="no" +else + has_java20="yes" +fi + +# shellcheck disable=SC2153 # testing for JAVA21_HOME, not a typo of JAVA_HOME +if [ "${JAVA21_HOME}" = "" ]; then + has_java21="no" +else + has_java21="yes" +fi + +if [ "${has_java_home}" = "yes" ] && [ ! -d "${JAVA_HOME}" ]; then + echo "JAVA_HOME is set to a non-existent directory ${JAVA_HOME}" + exit 1 +fi + +if [ "${has_java_home}" = "yes" ]; then + java_version=$("${JAVA_HOME}"/bin/java -version 2>&1 | head -1 | cut -d'"' -f2 | sed '/^1\./s///' | cut -d'.' -f1 | sed 's/-ea//') + if [ "${has_java8}" = "no" ] && [ "${java_version}" = 8 ]; then + export JAVA8_HOME="${JAVA_HOME}" + has_java8="yes" + fi + if [ "${has_java11}" = "no" ] && [ "${java_version}" = 11 ]; then + export JAVA11_HOME="${JAVA_HOME}" + has_java11="yes" + fi + if [ "${has_java17}" = "no" ] && [ "${java_version}" = 17 ]; then + export JAVA17_HOME="${JAVA_HOME}" + has_java17="yes" + fi + if [ "${has_java20}" = "no" ] && [ "${java_version}" = 20 ]; then + export JAVA20_HOME="${JAVA_HOME}" + has_java20="yes" + fi + if [ "${has_java21}" = "no" ] && [ "${java_version}" = 21 ]; then + export JAVA21_HOME="${JAVA_HOME}" + has_java21="yes" + fi +fi + +if [ "${has_java8}" = "yes" ] && [ ! -d "${JAVA8_HOME}" ]; then + echo "JAVA8_HOME is set to a non-existent directory ${JAVA8_HOME}" + exit 6 +fi + +if [ "${has_java11}" = "yes" ] && [ ! -d "${JAVA11_HOME}" ]; then + echo "JAVA11_HOME is set to a non-existent directory ${JAVA11_HOME}" + exit 7 +fi + +if [ "${has_java17}" = "yes" ] && [ ! -d "${JAVA17_HOME}" ]; then + echo "JAVA17_HOME is set to a non-existent directory ${JAVA17_HOME}" + exit 7 +fi + +if [ "${has_java20}" = "yes" ] && [ ! -d "${JAVA20_HOME}" ]; then + echo "JAVA20_HOME is set to a non-existent directory ${JAVA20_HOME}" + exit 7 +fi + +if [ "${has_java21}" = "yes" ] && [ ! -d "${JAVA21_HOME}" ]; then + echo "JAVA21_HOME is set to a non-existent directory ${JAVA21_HOME}" + exit 7 +fi + +if [ "${has_java8}" = "no" ] && [ "${has_java11}" = "no" ] && [ "${has_java17}" = "no" ] && [ "${has_java20}" = "no" ] && [ "${has_java21}" = "no" ]; then + if [ "${has_java_home}" = "yes" ]; then + echo "Cannot determine Java version from JAVA_HOME" + else + echo "No Java 8, 11, 17, 20, or 21 JDKs found. At least one of JAVA_HOME, JAVA8_HOME, JAVA11_HOME, JAVA17_HOME, or JAVA21_HOME must be set." + fi + echo "JAVA_HOME = ${JAVA_HOME}" + echo "JAVA8_HOME = ${JAVA8_HOME}" + echo "JAVA11_HOME = ${JAVA11_HOME}" + echo "JAVA17_HOME = ${JAVA17_HOME}" + echo "JAVA21_HOME = ${JAVA21_HOME}" + command -v java + java -version + exit 8 +fi + +if [ "${CHECKERFRAMEWORK}" = "" ]; then + echo "CHECKERFRAMEWORK is not set; it must be set to a locally-built Checker Framework. Please clone and build github.com/typetools/checker-framework" + exit 2 +fi + +if [ ! -d "${CHECKERFRAMEWORK}" ]; then + echo "CHECKERFRAMEWORK is set to a non-existent directory ${CHECKERFRAMEWORK}" + exit 9 +fi + +if [ "${DIR}" = "" ]; then + # echo "wpi.sh: no -d argument supplied, using the current directory." + DIR=$(pwd) +fi + +if [ ! -d "${DIR}" ]; then + echo "wpi.sh's -d argument was not a directory: ${DIR}" + exit 4 +fi + +if [ "${EXTRA_BUILD_ARGS}" = "" ]; then + EXTRA_BUILD_ARGS="" +fi + +if [ "${GRADLECACHEDIR}" = "" ]; then + # Assume that each project should use its own gradle cache. This is more expensive, + # but prevents crashes on distributed file systems, such as the UW CSE machines. + GRADLECACHEDIR=".gradle" +fi + +function configure_and_exec_dljc { + + if [ -f build.gradle ]; then + if [ "${BUILD_TARGET}" = "" ]; then + BUILD_TARGET="compileJava" + fi + if [ -f gradlew ]; then + chmod +x gradlew + GRADLE_EXEC="./gradlew" + else + GRADLE_EXEC="gradle" + fi + if [ ! -d "${GRADLECACHEDIR}" ]; then + mkdir "${GRADLECACHEDIR}" + fi + CLEAN_CMD="${GRADLE_EXEC} clean -g ${GRADLECACHEDIR} -Dorg.gradle.java.home=${JAVA_HOME} ${EXTRA_BUILD_ARGS}" + BUILD_CMD="${GRADLE_EXEC} clean ${BUILD_TARGET} -g ${GRADLECACHEDIR} -Dorg.gradle.java.home=${JAVA_HOME} ${EXTRA_BUILD_ARGS}" + elif [ -f pom.xml ]; then + if [ "${BUILD_TARGET}" = "" ]; then + BUILD_TARGET="compile" + fi + if [ -f mvnw ]; then + chmod +x mvnw + MVN_EXEC="./mvnw" + else + MVN_EXEC="mvn" + fi + # if running on Java 8, need /jre at the end of this Maven command + if [ "${JAVA_HOME}" = "${JAVA8_HOME}" ]; then + CLEAN_CMD="${MVN_EXEC} clean -Djava.home=${JAVA_HOME}/jre ${EXTRA_BUILD_ARGS}" + BUILD_CMD="${MVN_EXEC} clean ${BUILD_TARGET} -Djava.home=${JAVA_HOME}/jre ${EXTRA_BUILD_ARGS}" + else + CLEAN_CMD="${MVN_EXEC} clean -Djava.home=${JAVA_HOME} ${EXTRA_BUILD_ARGS}" + BUILD_CMD="${MVN_EXEC} clean ${BUILD_TARGET} -Djava.home=${JAVA_HOME} ${EXTRA_BUILD_ARGS}" + fi + elif [ -f build.xml ]; then + # TODO: test these more thoroughly + if [ "${BUILD_TARGET}" = "" ]; then + BUILD_TARGET="compile" + fi + CLEAN_CMD="ant clean ${EXTRA_BUILD_ARGS}" + BUILD_CMD="ant clean ${BUILD_TARGET} ${EXTRA_BUILD_ARGS}" + else + WPI_RESULTS_AVAILABLE="no build file found for ${REPO_NAME}; not calling DLJC" + echo "${WPI_RESULTS_AVAILABLE}" + return + fi + + if [ "${JAVA_HOME}" = "${JAVA8_HOME}" ]; then + JDK_VERSION_ARG="--jdkVersion 8" + elif [ "${JAVA_HOME}" = "${JAVA11_HOME}" ]; then + JDK_VERSION_ARG="--jdkVersion 11" + elif [ "${JAVA_HOME}" = "${JAVA17_HOME}" ]; then + JDK_VERSION_ARG="--jdkVersion 17" + else + # Default to the latest LTS release. (Probably better to compute the version.) + JDK_VERSION_ARG="--jdkVersion 11" + fi + + # In bash 4.4, ${QUOTED_ARGS} below can be replaced by ${*@Q} . + # (But, this script does not assume that bash is at least version 4.4.) + QUOTED_ARGS=$(printf '%q ' "$@") + + # This command also includes "clean"; I'm not sure why it is necessary. + DLJC_CMD="${DLJC} -t wpi ${JDK_VERSION_ARG} ${QUOTED_ARGS} -- ${BUILD_CMD}" + + if [ ! "${TIMEOUT}" = "" ]; then + TMP="${DLJC_CMD}" + DLJC_CMD="timeout ${TIMEOUT} ${TMP}" + fi + + # Remove old DLJC output. + rm -rf dljc-out + mkdir -p "${DIR}/dljc-out/" + + # Ensure the project is clean before invoking DLJC. + DLJC_CLEAN_STATUS=0 + eval "${CLEAN_CMD}" < /dev/null > /dev/null 2>&1 || DLJC_CLEAN_STATUS=$? + if [[ $DLJC_CLEAN_STATUS -ne 0 ]] ; then + WPI_RESULTS_AVAILABLE="dljc failed to clean with ${JDK_VERSION_ARG}: ${CLEAN_CMD}" + echo "${WPI_RESULTS_AVAILABLE}" + echo "Re-running clean command." + # Cleaning failed. Re-run without piping output to /dev/null. + echo "${CLEAN_CMD}" > "${DIR}/dljc-out/clean-output" + (eval "${CLEAN_CMD}" < /dev/null | tee -a "${DIR}/dljc-out/clean-output") || true + ls -al "${DIR}/dljc-out" + WPI_RESULTS_AVAILABLE="${WPI_RESULTS_AVAILABLE}"$'\n'"$(cat "${DIR}/dljc-out/clean-output")" + return + fi + + mkdir -p "${DIR}/dljc-out/" + dljc_stdout=$(mktemp "${DIR}/dljc-out/dljc-stdout-$(date +%Y%m%d-%H%M%S)-XXX") + + PATH_BACKUP="${PATH}" + export PATH="${JAVA_HOME}/bin:${PATH}" + + # shellcheck disable=SC2129 # recommended syntax was crashing mysteriously in CI + echo "WORKING DIR: $(pwd)" >> "$dljc_stdout" + echo "JAVA_HOME: ${JAVA_HOME}" >> "$dljc_stdout" + echo "PATH: ${PATH}" >> "$dljc_stdout" + echo "DLJC_CMD: ${DLJC_CMD}" >> "$dljc_stdout" + DLJC_STATUS=0 + eval "${DLJC_CMD}" < /dev/null >> "$dljc_stdout" 2>&1 || DLJC_STATUS=$? + + export PATH="${PATH_BACKUP}" + + echo "=== DLJC standard out/err (${dljc_stdout}) follows: ===" + cat "${dljc_stdout}" + echo "=== End of DLJC standard out/err. ===" + + # The wpi.py script in do-like-javac outputs the following text if no build/whole-program-inference directory + # exists, which means that WPI produced no output. When that happens, the reason is usually that the Checker + # Framework crashed, so output the log file for easier debugging. + wpi_no_output_message="No WPI outputs were discovered; it is likely that WPI failed or the Checker Framework crashed" + if [[ $(cat "${dljc_stdout}") == *"${wpi_no_output_message}"* ]]; then + wpi_log_path="${DIR}"/dljc-out/wpi-stdout.log + echo "=== ${wpi_no_output_message}: start of ${wpi_log_path} ===" + cat "${wpi_log_path}" + echo "=== end of ${wpi_log_path} ===" + fi + + if [[ $DLJC_STATUS -eq 124 ]]; then + WPI_RESULTS_AVAILABLE="dljc timed out for ${DIR}" + echo "${WPI_RESULTS_AVAILABLE}" + return + fi + + if [ -f dljc-out/wpi-stdout.log ]; then + # Put, in file `typecheck.out`, everything from the last "Running ..." onwards. + sed -n '/^Running/h;//!H;$!d;x;//p' dljc-out/wpi-stdout.log > dljc-out/typecheck.out + WPI_RESULTS_AVAILABLE="yes" + echo "dljc output is in ${DIR}/dljc-out/" + echo "typecheck output is in ${DIR}/dljc-out/typecheck.out" + echo "stdout is in $dljc_stdout" + else + WPI_RESULTS_AVAILABLE="dljc failed: file ${DIR}/dljc-out/wpi-stdout.log does not exist +dljc output is in ${DIR}/dljc-out/ +stdout is in $dljc_stdout" + echo "${WPI_RESULTS_AVAILABLE}" + fi +} + +#### Check and setup dependencies + +# Clone or update DLJC +if [ "${DLJC}" = "" ]; then + # The user did not set the DLJC environment variable. + DLJC="${SCRIPTDIR}/.do-like-javac/dljc" +else + # The user did set the DLJC environment variable. + if [ ! -f "${DLJC}" ]; then + echo "Failure: DLJC is set to ${DLJC} which is not a file or does not exist." + exit 1 + fi +fi + +#### Main script + +echo "Finished configuring wpi.sh." + +rm -f -- "${DIR}/.cannot-run-wpi" + +cd "${DIR}" || exit 5 + +JAVA_HOME_BACKUP="${JAVA_HOME}" + +# For the first run, use the Java versions in ascending priority order: 8 if +# it's available, otherwise 11, otherwise 17. +if [ "${has_java8}" = "yes" ]; then + export JAVA_HOME="${JAVA8_HOME}" +elif [ "${has_java11}" = "yes" ]; then + export JAVA_HOME="${JAVA11_HOME}" +elif [ "${has_java17}" = "yes" ]; then + export JAVA_HOME="${JAVA17_HOME}" +fi +configure_and_exec_dljc "$@" +echo "First run configure_and_exec_dljc with JAVA_HOME=${JAVA_HOME}: WPI_RESULTS_AVAILABLE=${WPI_RESULTS_AVAILABLE}" + +# If results aren't available after the first run, then re-run with Java 11 if +# it is available and the first run used Java 8 (since Java 8 has the highest priority, +# the first run using Java 8 is equivalent to Java 8 being available). +if [ "${WPI_RESULTS_AVAILABLE}" != "yes" ] && [ "${has_java11}" = "yes" ]; then + if [ "${has_java8}" = "yes" ]; then + export JAVA_HOME="${JAVA11_HOME}" + echo "wpi.sh couldn't build using Java 8; trying Java 11" + configure_and_exec_dljc "$@" + echo "Second run configure_and_exec_dljc with JAVA_HOME=${JAVA_HOME}: WPI_RESULTS_AVAILABLE=${WPI_RESULTS_AVAILABLE}" + fi +fi + +# If results still aren't available, then re-run with Java 17 if it is available +# and the first run used Java 8 or Java 11 (since Java 17 has the lowest priority, +# the first run using Java 8 or Java 11 is equivalent to either of these being +# available). +if [ "${WPI_RESULTS_AVAILABLE}" != "yes" ] && [ "${has_java17}" = "yes" ]; then + if [ "${has_java11}" = "yes" ] || [ "${has_java8}" = "yes" ]; then + export JAVA_HOME="${JAVA17_HOME}" + echo "wpi.sh couldn't build using Java 11 or Java 8; trying Java 17" + configure_and_exec_dljc "$@" + echo "Third run configure_and_exec_dljc with JAVA_HOME=${JAVA_HOME}: WPI_RESULTS_AVAILABLE=${WPI_RESULTS_AVAILABLE}" + fi +fi + +# support wpi-many.sh's ability to delete projects without usable results +# automatically +if [ "${WPI_RESULTS_AVAILABLE}" != "yes" ]; then + echo "wpi.sh: dljc could not run the build successfully: ${WPI_RESULTS_AVAILABLE}" + echo "Check the log files in ${DIR}/dljc-out/ for diagnostics." + echo "${WPI_RESULTS_AVAILABLE}" > "${DIR}/.cannot-run-wpi" +fi + +# reset JAVA_HOME to its initial value, which could be unset +if [ "${has_java_home}" = "yes" ]; then + export JAVA_HOME="${JAVA_HOME_BACKUP}" +else + unset JAVA_HOME +fi + +echo "Exiting wpi.sh successfully; pwd=$(pwd)" diff --git a/checker/build.gradle b/checker/build.gradle index d41ea45475a4..126e12078094 100644 --- a/checker/build.gradle +++ b/checker/build.gradle @@ -1,134 +1,288 @@ +plugins { + id 'java-library' + id 'base' + + // https://github.com/n0mer/gradle-git-properties + // Generates file build/resources/main/git.properties when the `classes` task runs. + id 'com.gorylenko.gradle-git-properties' version '2.4.1' +} + sourceSets { main { + java { + // NO-AFU + exclude '**/org/checkerframework/checker/resourceleak/MustCallInference.java' + } resources { // Stub files, message.properties, etc. srcDirs += ['src/main/java'] + + // NO-AFU + exclude '**/org/checkerframework/checker/resourceleak/MustCallInference.java' } } testannotations } +sourcesJar { + // The resources duplicate content from the src directory. + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +configurations { + implementation.extendsFrom(annotatedGuava) + fatJar { + canBeConsumed = true + canBeResolved = false + } +} + dependencies { - implementation project(':framework') - implementation project(':dataflow') - implementation project(':javacutil') - implementation project(':checker-qual') - // As of 2019/12/16, the version of reflection-util in the Annotation + // Use "api" instead of "implementation" to re-export sub-projects, making + // sure "minimize()" does not remove those classes. + api project(':framework') + + /* NO-AFU + // AFU is an "includedBuild" imported in checker-framework/settings.gradle, so the version number doesn't matter. + // https://docs.gradle.org/current/userguide/composite_builds.html#settings_defined_composite + implementation('org.checkerframework:annotation-file-utilities:*') { + exclude group: 'com.google.errorprone', module: 'javac' + } + */ + + api project(':checker-qual') + api project(':checker-util') + + // External dependencies: + // If you add an external dependency, you must shadow its packages. + // See the comment in ../build.gradle in the shadowJar block. + + // As of 2019-12-16, the version of reflection-util in the Annotation // File Utilities takes priority over this version, in the fat jar // file. :-( So update it and re-build it locally when updating this. - implementation 'org.plumelib:reflection-util:0.2.2' + implementation "org.plumelib:reflection-util:${versions.reflectionUtil}" + implementation "org.plumelib:plume-util:${versions.plumeUtil}" + + // Dependencies added to "shadow" appear as dependencies in Maven Central. + shadow project(':checker-qual') + shadow project(':checker-util') + + // Called Methods Checker AutoValue + Lombok support + testImplementation "com.google.auto.value:auto-value-annotations:${versions.autoValue}" + testImplementation "com.google.auto.value:auto-value:${versions.autoValue}" + testImplementation 'com.ryanharter.auto.value:auto-value-parcel:0.2.9' + testImplementation "org.projectlombok:lombok:${versions.lombok}" + // Called Methods Checker support for detecting misuses of AWS APIs + testImplementation 'com.amazonaws:aws-java-sdk-ec2' + testImplementation 'com.amazonaws:aws-java-sdk-kms' + // The AWS SDK is used for testing the Called Methods Checker. + testImplementation platform('com.amazonaws:aws-java-sdk-bom:1.12.670') + // For the Resource Leak Checker's support for JavaEE. + testImplementation 'javax.servlet:javax.servlet-api:4.0.1' + // For the Resource Leak Checker's support for IOUtils. + testImplementation 'commons-io:commons-io:2.15.1' + // For the Nullness Checker test of junit-assertions.astub in JUnitNull.java + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testImplementation 'org.apiguardian:apiguardian-api:1.1.2' + // For tests that use JSpecify annotations + testImplementation 'org.jspecify:jspecify:0.3.0' + + // Required for checker/tests/index-initializedfields/RequireJavadoc.java + if (JavaVersion.current() == JavaVersion.VERSION_1_8) { + testImplementation 'org.plumelib:options:1.0.6' + } else { + testImplementation 'org.plumelib:options:2.0.3' + } - testImplementation group: 'junit', name: 'junit', version: '4.13' + testImplementation "junit:junit:${versions.junit}" testImplementation project(':framework-test') testImplementation sourceSets.testannotations.output testannotationsImplementation project(':checker-qual') } +// It's not clear why this dependencies exists, but Gradle issues the following warning: +// - Gradle detected a problem with the following location: +// '/Users/smillst/jsr308/checker-framework/.git'. +// Reason: Task ':checker:generateGitProperties' uses this output of task ':installGitHooks' +// without declaring an explicit or implicit dependency. This can lead to incorrect results being +// produced, depending on what order the tasks are executed. Please refer to +// https://docs.gradle.org/7.1/userguide/validation_problems.html#implicit_dependency for more +// details about this problem. +generateGitProperties.dependsOn(':installGitHooks') + jar { manifest { - attributes("Main-Class": "org.checkerframework.framework.util.CheckerMain") - } - doLast { - new File("$projectDir/build/libs/README.txt").text = -"""Do not use file checker-X.Y.Z.jar, which contains only the checker subproject -and lacks other parts of the Checker Framework. -Instead, use checker/dist/checker.jar. -""" + attributes('Main-Class': 'org.checkerframework.framework.util.CheckerMain') } } -task copyJarsToDist(dependsOn: shadowJar, group: 'Build') { - description 'Builds or downloads jars required by CheckerMain and puts them in checker/dist.' +// This task differs from the `assemble` task in that it does not build Javadoc. +// It is useful for those who only want to run `javac`. +// checker.jar is copied to checker/dist/ when it is built by the shadowJar task. +task assembleForJavac(dependsOn: shadowJar, group: 'Build') { + description 'Builds or downloads jars required by CheckerMain and puts them in checker/dist/.' dependsOn project(':checker-qual').tasks.jar doLast { + def checkerQualJarFile = file(project(':checker-qual').tasks.getByName('jar').archiveFile) + if (!checkerQualJarFile.exists()) { + throw new GradleException('File not found: ' + checkerQualJarFile) + } copy { - from file(project(':checker-qual').tasks.getByName("jar").archivePath) + from checkerQualJarFile into "${projectDir}/dist" rename { String fileName -> // remove version number on checker-qual.jar - fileName.replace(fileName, "checker-qual.jar") + fileName.replace(fileName, 'checker-qual.jar') } } + def checkerUtilJarFile = file(project(':checker-util').tasks.getByName('jar').archiveFile) + if (!checkerUtilJarFile.exists()) { + throw new GradleException('File not found: ' + checkerUtilJarFile) + } copy { + from checkerUtilJarFile + into "${projectDir}/dist" + rename { String fileName -> + // remove version number on checker-util.jar + fileName.replace(fileName, 'checker-util.jar') + } + } + + copy { + // This is required to *run* the Checker Framework on JDK 8. from configurations.javacJar into "${projectDir}/dist" rename { String fileName -> - fileName.replace(fileName, "javac.jar") + fileName.replace(fileName, 'javac.jar') } } } } -assemble.dependsOn copyJarsToDist - -task printPlumeUtilJarPath { - description "Print the path to plume-util.jar" - doFirst { println project.configurations.compile.find { it.name.startsWith("plume-util") } } -} +assemble.dependsOn assembleForJavac +assemble.dependsOn(':getDoLikeJavac') -task allSourcesJar(type: Jar) { +task allSourcesJar(type: Jar, group: 'Build') { description 'Creates a sources jar that includes sources for all Checker Framework classes in checker.jar' destinationDirectory = file("${projectDir}/dist") - archiveFileName = "checker-source.jar" + archiveFileName = 'checker-source.jar' + archiveClassifier = 'sources' from (sourceSets.main.java, project(':framework').sourceSets.main.allJava, - project(':dataflow').sourceSets.main.allJava, project(':javacutil').sourceSets.main.allJava) + project(':dataflow').sourceSets.main.allJava, project(':javacutil').sourceSets.main.allJava, + project(':checker-qual').sourceSets.main.allJava, project(':checker-util').sourceSets.main.allJava) } -task allJavadocJar(type: Jar) { - description 'Creates javadoc jar include Javadoc for all of the framework' +task allJavadocJar(type: Jar, group: 'Build') { + description 'Creates javadoc jar including Javadoc for all of the Checker Framework' dependsOn rootProject.tasks.allJavadoc destinationDirectory = file("${projectDir}/dist") - archiveFileName = "checker-javadoc.jar" + archiveFileName = 'checker-javadoc.jar' + archiveClassifier = 'javadoc' from rootProject.tasks.allJavadoc.destinationDir } +// Shadowing Test Sources and Dependencies +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +task checkerJar(type: ShadowJar, dependsOn: compileJava, group: 'Build') { + description "Builds checker-${project.version}.jar with all dependencies except checker-qual and checker-util." + includeEmptyDirs = false + + from shadowJar.source + configurations = shadowJar.configurations + // To see what files are incorporated into the shadow jar file: + // doLast { println sourceSets.main.runtimeClasspath.asPath } + manifest { + attributes('Main-Class': 'org.checkerframework.framework.util.CheckerMain') + } + exclude 'org/checkerframework/**/qual/*' + exclude 'org/checkerframework/checker/**/util/*' + relocators = shadowJar.getRelocators() +} + +jar { + dependsOn(checkerJar) + // Never build the skinny jar. + onlyIf {false} + archiveClassifier = 'skinny' +} shadowJar { - description 'Creates the "fat" checker.jar in dist/.' - destinationDirectory = file("${projectDir}/dist") - archiveFileName = "checker.jar" + description 'Creates checker-VERSION-all.jar and copies it to dist/checker.jar.' // To see what files are incorporated into the shadow jar file: // doFirst { println sourceSets.main.runtimeClasspath.asPath } + archiveClassifier = 'all' + doLast{ + copy { + from archiveFile.get() + into file("${projectDir}/dist") + rename 'checker.*', 'checker.jar' + } + } + + minimize() } + artifacts { // Don't add this here or else the Javadoc and the sources jar is built during the assemble task. // archives allJavadocJar // archives allSourcesJar archives shadowJar + archives checkerJar + + fatJar(shadowJar) } clean { delete "${projectDir}/dist" + delete 'tests/calledmethods-delomboked' + delete('tests/ainfer-testchecker/annotated') + delete('tests/ainfer-testchecker/inference-output') + delete('tests/ainfer-nullness/annotated') + delete('tests/ainfer-nullness/inference-output') + delete('tests/ainfer-index/annotated') + delete('tests/ainfer-index/inference-output') + delete('tests/ainfer-resourceleak/annotated') + delete('tests/ainfer-resourceleak/inference-output') } +clean.doLast { + while (buildDir.exists()) { + sleep(10000) // wait 10 seconds + buildDir.deleteDir() + } +} + + // Add non-junit tests -createCheckTypeTask(project.name, 'org.checkerframework.checker.compilermsgs.CompilerMessagesChecker', "CompilerMessages") +createCheckTypeTask(project.name, 'CompilerMessages', + 'org.checkerframework.checker.compilermsgs.CompilerMessagesChecker') checkCompilerMessages { doFirst { options.compilerArgs += [ - '-Apropfiles=' + sourceSets.main.resources.filter { file -> file.name.equals('messages.properties') }.asPath + ":" - + project(':framework').sourceSets.main.resources.filter { file -> file.name.equals('messages.properties') }.asPath + '-Apropfiles=' + sourceSets.main.resources.filter { file -> file.name.equals('messages.properties') }.asPath + File.pathSeparator + + project(':framework').sourceSets.main.resources.filter { file -> file.name.equals('messages.properties') }.asPath ] } } -task nullnessExtraTests(type: Exec, dependsOn: copyJarsToDist, group: 'Verification') { +task nullnessExtraTests(type: Exec, dependsOn: assembleForJavac, group: 'Verification') { description 'Run extra tests for the Nullness Checker.' executable 'make' - environment JAVAC: "${projectDir}/bin/javac", JAVAP: 'javap' + environment JAVAC: "${projectDir}/bin/javac -AnoJreVersionCheck", JAVAP: 'javap' args = ['-C', 'tests/nullness-extra/'] } -task commandLineTests(type: Exec, dependsOn: copyJarsToDist, group: 'Verification') { +task commandLineTests(type: Exec, dependsOn: assembleForJavac, group: 'Verification') { description 'Run tests that need a special command line.' executable 'make' - environment JAVAC: "${projectDir}/bin/javac" + environment JAVAC: "${projectDir}/bin/javac -AnoJreVersionCheck" args = ['-C', 'tests/command-line/'] } -task tutorialTests(dependsOn: copyJarsToDist, group: 'Verification') { +task tutorialTests(dependsOn: assembleForJavac, group: 'Verification') { description 'Test that the tutorial is working as expected.' doLast { ant.ant(dir: "${rootDir}/docs/tutorial/tests", useNativeBasedir: 'true', inheritAll: 'false') { @@ -137,48 +291,66 @@ task tutorialTests(dependsOn: copyJarsToDist, group: 'Verification') { } } -task exampleTests(type: Exec, dependsOn: copyJarsToDist, group: 'Verification') { +task exampleTests(type: Exec, dependsOn: assembleForJavac, group: 'Verification') { description 'Run tests for the example programs.' executable 'make' - environment JAVAC: "${projectDir}/bin/javac" + environment JAVAC: "${projectDir}/bin/javac -AnoJreVersionCheck" args = ['-C', '../docs/examples'] } -task demosTests(dependsOn: copyJarsToDist, group: 'Verification') { +task demosTests(dependsOn: assembleForJavac, group: 'Verification') { description 'Test that the demos are working as expected.' doLast { - if (JavaVersion.current() == JavaVersion.VERSION_1_8) { - File demosDir = new File(projectDir, '../../checker-framework.demos'); - if (!demosDir.exists()) { - exec { - workingDir file(demosDir.toString() + '/../') - executable 'git' - args = ['clone', 'https://github.com/typetools/checker-framework.demos.git'] - } - } else { - exec { - workingDir demosDir - executable 'git' - args = ['pull', 'https://github.com/typetools/checker-framework.demos.git'] - ignoreExitValue = true - } + File demosDir = new File(projectDir, '../../checker-framework.demos'); + if (!demosDir.exists()) { + exec { + workingDir file(demosDir.toString() + '/../') + executable 'git' + args = [ + 'clone', + '--depth', + '1', + 'https://github.com/eisop/checker-framework.demos.git' + ] } - ant.properties.put('checker.lib', file("${projectDir}/dist/checker.jar").absolutePath) - ant.ant(dir: demosDir.toString()) } else { - println("Skipping demosTests because they only work with Java 8.") + exec { + workingDir demosDir + executable 'git' + args = [ + 'pull', + 'https://github.com/eisop/checker-framework.demos.git' + ] + ignoreExitValue = true + } } + ant.properties.put('checker.lib', file("${projectDir}/dist/checker.jar").absolutePath) + ant.ant(dir: demosDir.toString()) } } task allNullnessTests(type: Test, group: 'Verification') { - description 'Run all Junit tests for the Nullness Checker.' + description 'Run all JUnit tests for the Nullness Checker.' include '**/Nullness*.class' } -// These are tests that should only be run with JDK 11. +task allCalledMethodsTests(type: Test, group: 'Verification') { + description 'Run all JUnit tests for the Called Methods Checker.' + include '**/CalledMethods*.class' + if (!skipDelombok) { + dependsOn 'delombok' + } +} + +task allResourceLeakTests(type: Test, group: 'Verification') { + description 'Run all JUnit tests for the Resource Leak Checker.' + include '**/ResourceLeak*.class' + include '**/MustCall*.class' +} + +// These are tests that should only be run with JDK 11+. task jtregJdk11Tests(dependsOn: ':downloadJtreg', group: 'Verification') { - description 'Run the jtreg tests made for JDK 11.' + description 'Run the jtreg tests made for JDK 11+.' dependsOn('compileJava') dependsOn('compileTestJava') dependsOn('shadowJar') @@ -186,45 +358,773 @@ task jtregJdk11Tests(dependsOn: ':downloadJtreg', group: 'Verification') { String jtregOutput = "${buildDir}/jtregJdk11" String name = 'all' doLast { - if(isJava8) { - println "This test is only run with JDK 11." + if (isJava8) { + println 'This test is only run with JDK 11+.' return; } exec { executable "${jtregHome}/bin/jtreg" args = [ - "-dir:${projectDir}/jtregJdk11", - "-workDir:${jtregOutput}/${name}/work", - "-reportDir:${jtregOutput}/${name}/report", - "-verbose:summary", - "-javacoptions:-g", - "-keywords:!ignore", - "-samevm", - "-javacoptions:-classpath ${tasks.shadowJar.archiveFile.get()}:${sourceSets.test.output.asPath}", - "-vmoptions:-classpath ${tasks.shadowJar.archiveFile.get()}:${sourceSets.test.output.asPath}", - "-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", - "-javacoptions:-classpath ${sourceSets.testannotations.output.asPath}", - // Location of jtreg tests - '.' + "-dir:${projectDir}/jtregJdk11", + "-workDir:${jtregOutput}/${name}/work", + "-reportDir:${jtregOutput}/${name}/report", + '-verbose:summary', + '-javacoptions:-g', + '-keywords:!ignore', + '-samevm', + "-javacoptions:-classpath ${tasks.shadowJar.archiveFile.get()}:${sourceSets.test.output.asPath}", + "-vmoptions:-classpath ${tasks.shadowJar.archiveFile.get()}:${sourceSets.test.output.asPath}", + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', + '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED', + "-javacoptions:-classpath ${sourceSets.testannotations.output.asPath}", + // Location of jtreg tests + '.' ] } + } +} + +task delombok { + description 'Delomboks the source code tree in tests/calledmethods-lombok' + + def srcDelomboked = 'tests/calledmethods-delomboked' + def srcJava = 'tests/calledmethods-lombok' + + inputs.files file(srcJava) + outputs.dir file(srcDelomboked) + + // Because there are Checker Framework annotations in the test source. + dependsOn project(':checker-qual').tasks.jar + + doLast { + if(!skipDelombok) { + def collection = files(configurations.testCompileClasspath) + ant.taskdef(name: 'delombok', classname: 'lombok.delombok.ant.Tasks$Delombok', + classpath: collection.asPath) + ant.delombok(from: srcJava, to: srcDelomboked, classpath: collection.asPath) + } + } +} +if (skipDelombok) { + delombok.enabled = false +} else { + tasks.test.dependsOn('delombok') +} +/// +/// Tests of the -Ainfer command-line argument. These are not whole-program inference tests. +/// + +test { + useJUnit { + // These are run in task ainferTest. + excludeCategories 'org.checkerframework.checker.test.junit.ainferrunners.AinferTestCheckerJaifsGenerationTest' + excludeCategories 'org.checkerframework.checker.test.junit.ainferrunners.AinferTestCheckerStubsGenerationTest' + excludeCategories 'org.checkerframework.checker.test.junit.ainferrunners.AinferTestCheckerAjavaGenerationTest' + excludeCategories 'org.checkerframework.checker.test.junit.ainferrunners.AinferNullnessJaifsGenerationTest' + excludeCategories 'org.checkerframework.checker.test.junit.ainferrunners.AinferNullnessAjavaGenerationTest' + excludeCategories 'org.checkerframework.checker.test.junit.ainferrunners.AinferIndexAjavaGenerationTest' + excludeCategories 'org.checkerframework.checker.test.junit.ainferrunners.AinferResourceLeakAjavaGenerationTest' } } -task deployArtifactsToLocalRepo(dependsOn: shadowJar) { - description 'Deploys checker.jar to the local Maven repository' +task ainferTestCheckerGenerateStubs(type: Test) { + description 'Internal task. Users should run ainferTestCheckerStubTest instead. This type-checks the ainfer-testchecker files with -Ainfer=stubs to generate stub files.' + + dependsOn(compileTestJava) + doFirst { + delete('tests/ainfer-testchecker/annotated') + delete("${buildDir}/ainfer-testchecker/") + } + outputs.upToDateWhen { false } + include '**/AinferTestCheckerStubsGenerationTest.class' + testLogging { + // Always run the tests + outputs.upToDateWhen { false } + + // Show the found unexpected diagnostics and the expected diagnostics not found. + exceptionFormat 'full' + events 'passed', 'skipped', 'failed' + } + doLast { - mvnDeployToLocalRepo("${projectDir}/dist/checker.jar", "${pomFiles}/checker-pom.xml") + copyNonannotatedToAnnotatedDirectory('ainfer-testchecker') + // The stub file format doesn't support annotations on anonymous inner classes, so + // this test also expects errors on these tests that expect annotations to be inferred + // inside anonymous classes. + delete('tests/ainfer-testchecker/annotated/UsesAnonymous.java') + delete('tests/ainfer-testchecker/annotated/AnonymousClassWithField.java') + + // This test outputs a warning about records. + delete('tests/ainfer-testchecker/annotated/all-systems/java17/Issue6069.java') + + // This test causes an error when its corresponding stub file is read, because the test + // contains an annotation definition. The stub file parser does not support reading + // files that define annotations; this test can be reinstated if the stub parser + // is extended to support annotation definitions. + delete('tests/ainfer-testchecker/annotated/all-systems/Issue4083.java') + + copy { + from file('tests/ainfer-testchecker/non-annotated/UsesAnonymous.java') + into file('tests/ainfer-testchecker/annotated') + } + } +} + +task ainferTestCheckerValidateStubs(type: Test) { + description 'Internal task. Users should run ainferTestCheckerStubTest instead. This type-checks the ainfer-testchecker tests using the stub files generated by ainferTestCheckerGenerateStubs.' + + dependsOn(ainferTestCheckerGenerateStubs) + outputs.upToDateWhen { false } + include '**/AinferTestCheckerStubsValidationTest.class' + testLogging { + // Always run the tests + outputs.upToDateWhen { false } + + // Show the found unexpected diagnostics and the expected diagnostics not found. + exceptionFormat 'full' + events 'passed', 'skipped', 'failed' + } +} + +task ainferTestCheckerGenerateAjava(type: Test) { + description 'Internal task. Users should run ainferTestCheckerAjavaTest instead. This type-checks the ainfer-testchecker files with -Ainfer=ajava to generate ajava files.' + + dependsOn(compileTestJava) + doFirst { + delete('tests/ainfer-testchecker/annotated') + delete("${buildDir}/ainfer-testchecker/") + } + outputs.upToDateWhen { false } + include '**/AinferTestCheckerAjavaGenerationTest.class' + testLogging { + // Always run the tests + outputs.upToDateWhen { false } + + // Show the found unexpected diagnostics and the expected diagnostics not found. + exceptionFormat 'full' + events 'passed', 'skipped', 'failed' + } + + doLast { + copyNonannotatedToAnnotatedDirectory('ainfer-testchecker') + + // AinferTestCheckerAjavaValidationTest fails with "warning: (purity.methodref)", whenever + // there is a user-defined generic interface, and a variable of that type is assigned a + // method reference. + delete('tests/ainfer-testchecker/annotated/all-systems/java8/memberref/Issue946.java') + delete('tests/ainfer-testchecker/annotated/all-systems/java8/memberref/Receivers.java') + + // This test must be deleted, because otherwise an error about a missing type in an + // ajava file is issued. The test itself shouldn't be run as an all-systems test while testing + // WPI; see the copy in the non-annotated WPI tests for an explanation. + delete('tests/ainfer-testchecker/annotated/all-systems/java8/memberref/Purity.java') + + // There is some kind of bad interaction between the purity checker's inference mode + // and method references to constructors: every one of them in this test causes a + // purity.methodref warning during validation. This problem only occurs for ajava-based + // inference because the relevant purity annotations that seem to trigger it are on + // inner classes, which stubs cannot annotate. + // TODO: investigate the cause of this error in the Purity checker, fix it, and then reinstate this test. + delete('tests/ainfer-testchecker/annotated/all-systems/java8/memberref/MemberReferences.java') } } -task deployArtifactsToSonatype { - description 'Deploys checker.jar (and their sources/javadoc jars) to the Sonatype repository' - description 'Deploys checker.jar (and their sources/javadoc jars) to the Sonatype repository' - dependsOn (shadowJar, 'allSourcesJar', 'allJavadocJar') +task ainferTestCheckerValidateAjava(type: Test) { + description 'Internal task. Users should run ainferTestCheckerAjavaTest instead. This re-type-checks the ainfer-testchecker files using the ajava files generated by ainferTestCheckerGenerateAjava' + + dependsOn(ainferTestCheckerGenerateAjava) + outputs.upToDateWhen { false } + include '**/AinferTestCheckerAjavaValidationTest.class' + testLogging { + // Always run the tests + outputs.upToDateWhen { false } + + // Show the found unexpected diagnostics and the expected diagnostics not found. + exceptionFormat 'full' + events 'passed', 'skipped', 'failed' + } +} + +// Copies directories as needed by WPI tests. +// Formal parameter testdir is, for example, "ainfer-testchecker". +// Does work in directory "tests/${testdir}/". +// 1. Copies whole-program inference test source code from the non-annotated/ to the annotated/ directory. +// 2. Copies WPI output, such as .jaif or .stub files, to the inferference-output/ directory. +void copyNonannotatedToAnnotatedDirectory(String testdir) { + // Copying all test files to another directory, removing all expected errors that should not + // occur after inserting inferred annotations from .jaif files. + copy { + from files("tests/${testdir}/non-annotated") + into file("tests/${testdir}/annotated") + filter { String line -> + (line.contains('// :: error:') + // Don't remove unchecked cast warnings, because they're genuinely expected in some all-systems + // tests, such as GenericsCasts.java. + || (line.contains('// :: warning:') && !line.contains('// :: warning: [unchecked]'))) + ? null : line + } + } + // The only file for which expected errors are maintained is ExpectedErrors.java, so we copy it over. + delete("tests/${testdir}/annotated/ExpectedErrors.java") + copy { + from file("tests/${testdir}/non-annotated/ExpectedErrors.java") + into file("tests/${testdir}/annotated") + } + + delete("tests/${testdir}/inference-output") + file('build/whole-program-inference').renameTo(file("tests/${testdir}/inference-output")) +} + +// This task is similar to the ainferTestCheckerJaifTest task below, but it doesn't +// run the insert-annotations-to-source tool. Instead, it tests the -Ainfer=stubs feature +// and the -AmergeStubsWithSource feature to do WPI using stub files. +task ainferTestCheckerStubTest(dependsOn: 'shadowJar', group: 'Verification') { + description 'Run tests for whole-program inference using stub files' + dependsOn(ainferTestCheckerValidateStubs) + outputs.upToDateWhen { false } +} + +// Like ainferTestCheckerStubTest, but with ajava files instead +task ainferTestCheckerAjavaTest(dependsOn: 'shadowJar', group: 'Verification') { + description 'Run tests for whole-program inference using ajava files' + dependsOn(ainferTestCheckerValidateAjava) + outputs.upToDateWhen { false } +} + +task ainferTestCheckerGenerateJaifs(type: Test) { + description 'Internal task. Users should run ainferTestCheckerJaifTest instead. This type-checks the ainfer-testchecker files with -Ainfer=jaifs to generate .jaif files' + + dependsOn(compileTestJava) + dependsOn(':checker-qual:jar') // For the Value Checker annotations. + doFirst { + delete('tests/ainfer-testchecker/annotated') + } + outputs.upToDateWhen { false } + include '**/AinferTestCheckerJaifsGenerationTest.class' + testLogging { + // Always run the tests + outputs.upToDateWhen { false } + + // Show the found unexpected diagnostics and expected diagnostics not found. + exceptionFormat 'full' + events 'passed', 'skipped', 'failed' + } + doLast { - mvnSignAndDeployMultipleToSonatype("${projectDir}/dist/checker.jar", allSourcesJar.archiveFile.get().toString(), allJavadocJar.archiveFile.get().toString(), "${pomFiles}/checker-pom.xml") + copyNonannotatedToAnnotatedDirectory('ainfer-testchecker') + + // JAIF-based WPI fails these tests, which were added for stub-based WPI. + // See issue here: https://github.com/typetools/checker-framework/issues/3009 + delete('tests/ainfer-testchecker/annotated/ConflictingAnnotationsTest.java') + delete('tests/ainfer-testchecker/annotated/MultiDimensionalArrays.java') + + // JAIF-based WPI also fails this test. It used to pass, but the test was changed + // in a way that exposed a bug in the Annotation File Utilities: the AFU + // places annotations incorrectly on qualified types. In this test, a failure occurs because + // the AFU prints "@Annotation Outer.Inner this", rather than "Outer.@Annotation Inner this" + // (see an explanation of the syntax here: + // https://eisop.github.io/cf/manual/#common-problems-non-typechecking). + // TODO: fix this bug in the AFU, then reinstate this test. + delete('tests/ainfer-testchecker/annotated/OverriddenMethodsTest.java') + + // JAIF-based WPI fails this test, too, because the insertion of a declaration annotation + // onto a field with a multi-part type (e.g. Outer.Inner) doesn't appear to be supported by the AFU. + delete('tests/ainfer-testchecker/annotated/InnerClassFieldDeclAnno.java') + + // Inserting annotations from .jaif files in-place. + String jaifsDir = 'tests/ainfer-testchecker/inference-output'; + List jaifs = fileTree(jaifsDir).matching { + include '*.jaif' + }.asList() + if (jaifs.isEmpty()) { + throw new GradleException("no .jaif files found in ${jaifsDir}") + } + String javasDir = 'tests/ainfer-testchecker/annotated/'; + List javas = fileTree(javasDir).matching { + include '*.java' + }.asList() + if (javas.isEmpty()) { + throw new GradleException("no .java files found in ${javasDir}") + } + + def checkerQualJarFile = file(project(':checker-qual').tasks.getByName('jar').archiveFile) + exec { + executable "${afu}/scripts/insert-annotations-to-source" + // Script argument -cp must precede Java program argument -i. + // checker-qual is needed for Constant Value Checker annotations. + // Note that "/" works on Windows as well as on Linux. + args = [ + '-cp', + "${sourceSets.test.output.asPath}:${checkerQualJarFile}:" + file('tests/build/testclasses') + ] + args += ['-i'] + for (File jaif : jaifs) { + args += [jaif.toString()] + } + for (File javaFile : javas) { + args += [javaFile.toString()] + } + } } } + +task ainferTestCheckerValidateJaifs(type: Test) { + description 'Internal task. Users should run ainferTestCheckerJaifTest instead. This type-checks the ainfer-testchecker files using the .jaif files generated by ainferTestCheckerGenerateJaifs' + + dependsOn(ainferTestCheckerGenerateJaifs) + outputs.upToDateWhen { false } + include '**/AinferTestCheckerJaifsValidationTest.class' + testLogging { + // Always run the tests + outputs.upToDateWhen { false } + + // Show the found unexpected diagnostics and expected diagnostics not found. + exceptionFormat 'full' + events 'passed', 'skipped', 'failed' + } +} + +task ainferTestCheckerJaifTest(dependsOn: 'shadowJar', group: 'Verification') { + description 'Run tests for whole-program inference using .jaif files' + dependsOn(ainferTestCheckerValidateJaifs) + outputs.upToDateWhen { false } +} + +task ainferIndexGenerateAjava(type: Test) { + description 'Internal task. Users should run ainferIndexAjavaTest instead. This type-checks the ainfer-index files with -Ainfer=ajava to generate ajava files.' + + dependsOn(compileTestJava) + doFirst { + delete('tests/ainfer-index/annotated') + delete("${buildDir}/ainfer-index/") + } + outputs.upToDateWhen { false } + include '**/AinferIndexAjavaGenerationTest.class' + testLogging { + // Always run the tests + outputs.upToDateWhen { false } + + // Show the found unexpected diagnostics and the expected diagnostics not found. + exceptionFormat 'full' + events 'passed', 'skipped', 'failed' + } + + doLast { + copyNonannotatedToAnnotatedDirectory('ainfer-index') + } +} + +task ainferIndexValidateAjava(type: Test) { + description 'Internal task. Users should run ainferIndexAjavaTest instead. This re-type-checks the ainfer-index files using the ajava files generated by ainferIndexGenerateAjava' + + dependsOn(ainferIndexGenerateAjava) + outputs.upToDateWhen { false } + include '**/AinferIndexAjavaValidationTest.class' + testLogging { + // Always run the tests + outputs.upToDateWhen { false } + + // Show the found unexpected diagnostics and the expected diagnostics not found. + exceptionFormat 'full' + events 'passed', 'skipped', 'failed' + } +} + +task ainferIndexAjavaTest(dependsOn: 'shadowJar', group: 'Verification') { + description 'Run tests for whole-program inference using ajava files and the Index Checker' + dependsOn(ainferIndexValidateAjava) + outputs.upToDateWhen { false } +} + +task ainferNullnessGenerateAjava(type: Test) { + description 'Internal task. Users should run ainferNullnessAjavaTest instead. This type-checks the ainfer-nullness files with -Ainfer=ajava to generate ajava files.' + + dependsOn(compileTestJava) + doFirst { + delete('tests/ainfer-nullness/annotated') + delete("${buildDir}/ainfer-nullness/") + } + outputs.upToDateWhen { false } + include '**/AinferNullnessAjavaGenerationTest.class' + testLogging { + // Always run the tests + outputs.upToDateWhen { false } + + // Show the found unexpected diagnostics and the expected diagnostics not found. + exceptionFormat 'full' + events 'passed', 'skipped', 'failed' + } + + doLast { + copyNonannotatedToAnnotatedDirectory('ainfer-nullness') + } +} + +task ainferNullnessValidateAjava(type: Test) { + description 'Internal task. Users should run ainferNullnessAjavaTest instead. This re-type-checks the ainfer-nullness files using the ajava files generated by ainferNullnessGenerateAjava' + + dependsOn(ainferNullnessGenerateAjava) + outputs.upToDateWhen { false } + include '**/AinferNullnessAjavaValidationTest.class' + testLogging { + // Always run the tests + outputs.upToDateWhen { false } + + // Show the found unexpected diagnostics and the expected diagnostics not found. + exceptionFormat 'full' + events 'passed', 'skipped', 'failed' + } +} + +task ainferNullnessAjavaTest(dependsOn: 'shadowJar', group: 'Verification') { + description 'Run tests for whole-program inference using ajava files and the Nullness Checker' + dependsOn(ainferNullnessValidateAjava) + outputs.upToDateWhen { false } +} + +task ainferResourceLeakGenerateAjava(type: Test) { + description 'Internal task. Users should run ainferResourceLeakAjavaTest instead. This type-checks the ainfer-index files with -Ainfer=ajava to generate ajava files.' + + dependsOn(compileTestJava) + doFirst { + delete('tests/ainfer-resourceleak/annotated') + delete("${buildDir}/ainfer-resourceleak/") + } + outputs.upToDateWhen { false } + include '**/AinferResourceLeakAjavaGenerationTest.class' + testLogging { + // Always run the tests + outputs.upToDateWhen { false } + + // Show the found unexpected diagnostics and the expected diagnostics not found. + exceptionFormat 'full' + events 'passed', 'skipped', 'failed' + } + + doLast { + copyNonannotatedToAnnotatedDirectory('ainfer-resourceleak') + } +} + +task ainferResourceLeakValidateAjava(type: Test) { + description 'Internal task. Users should run ainferResourceLeakAjavaTest instead. This re-type-checks the ainfer-resourceleak files using the ajava files generated by ainferResourceLeakGenerateAjava' + + dependsOn(ainferResourceLeakGenerateAjava) + outputs.upToDateWhen { false } + include '**/AinferResourceLeakAjavaValidationTest.class' + testLogging { + // Always run the tests + outputs.upToDateWhen { false } + + // Show the found unexpected diagnostics and the expected diagnostics not found. + exceptionFormat 'full' + events 'passed', 'skipped', 'failed' + } +} + +task ainferResourceLeakAjavaTest(dependsOn: 'shadowJar', group: 'Verification') { + description 'Run tests for whole-program inference using ajava files and the Resource Leak Checker' + dependsOn(ainferResourceLeakValidateAjava) + outputs.upToDateWhen { false } +} + +task ainferNullnessGenerateJaifs(type: Test) { + description 'Internal task. Users should run ainferNullnessJaifTest instead. This type-checks the ainfer-nullness files with -Ainfer=jaifs to generate .jaif files' + + dependsOn(compileTestJava) + doFirst { + delete('tests/ainfer-nullness/annotated') + } + outputs.upToDateWhen { false } + include '**/AinferNullnessJaifsGenerationTest.class' + testLogging { + // Always run the tests + outputs.upToDateWhen { false } + + // Show the found unexpected diagnostics and expected diagnostics not found. + exceptionFormat 'full' + events 'passed', 'skipped', 'failed' + } + + doLast { + copyNonannotatedToAnnotatedDirectory('ainfer-nullness') + + // JAIF-based WPI does not infer annotations on uses of type variables correctly. + delete('tests/ainfer-nullness/annotated/TwoCtorGenericAbstract.java') + delete('tests/ainfer-nullness/annotated/TypeVarReturnAnnotated.java') + + // Inserting annotations from .jaif files in-place. + String jaifsDir = 'tests/ainfer-nullness/inference-output'; + List jaifs = fileTree(jaifsDir).matching { + include '*.jaif' + }.asList() + if (jaifs.isEmpty()) { + throw new GradleException("no .jaif files found in ${jaifsDir}") + } + String javasDir = 'tests/ainfer-nullness/annotated/'; + List javas = fileTree(javasDir).matching { + include '*.java' + }.asList() + if (javas.isEmpty()) { + throw new GradleException("no .java files found in ${javasDir}") + } + def checkerQualJarFile = file(project(':checker-qual').tasks.getByName('jar').archiveFile) + exec { + executable "${afu}/scripts/insert-annotations-to-source" + // Script argument -cp must precede Java program argument -i. + // Note that "/" works on Windows as well as on Linux. + args = [ + '-cp', + "${sourceSets.test.output.asPath}:${checkerQualJarFile}:" + file('tests/build/testclasses') + ] + args += ['-i'] + for (File jaif : jaifs) { + args += [jaif.toString()] + } + for (File javaFile : javas) { + args += [javaFile.toString()] + } + } + } +} + +task ainferNullnessValidateJaifs(type: Test) { + description 'Internal task. Users should run ainferNullnessJaifTest instead. This re-type-checks the ainfer-nullness files using the .jaif files generated by ainferNullnessGenerateJaifs' + + dependsOn(ainferNullnessGenerateJaifs) + outputs.upToDateWhen { false } + include '**/AinferNullnessJaifsValidationTest.class' + testLogging { + // Always run the tests + outputs.upToDateWhen { false } + + // Show the found unexpected diagnostics and expected diagnostics not found. + exceptionFormat 'full' + events 'passed', 'skipped', 'failed' + } +} + +task ainferNullnessJaifTest(dependsOn: 'shadowJar', group: 'Verification') { + description 'Run tests for whole-program inference using .jaif files' + dependsOn(ainferNullnessValidateJaifs) + outputs.upToDateWhen { false } +} + + +// Empty task that just runs both the jaif and stub WPI tests. +// It is run as part of the inferenceTests task. +task ainferTest(group: 'Verification') { + description 'Run tests for all whole program inference modes.' + dependsOn('ainferTestCheckerJaifTest') + dependsOn('ainferTestCheckerStubTest') + dependsOn('ainferTestCheckerAjavaTest') + dependsOn('ainferNullnessJaifTest') + dependsOn('ainferNullnessAjavaTest') + dependsOn('ainferIndexAjavaTest') + dependsOn('ainferResourceLeakAjavaTest') +} + +/// +/// Whole-program inference tests +/// + +// This is run as part of the inferenceTests task. +task wpiManyTest(group: 'Verification') { + description 'Tests the wpi-many.sh script (and indirectly the wpi.sh script). Requires an Internet connection.' + dependsOn(assembleForJavac) + dependsOn(':getDoLikeJavac') + // This test must always be re-run when requested. + outputs.upToDateWhen { false } + + doFirst { + delete("${project.projectDir}/build/wpi-many-tests-results/") + // wpi-many.sh is run in skip mode so that logs are preserved, but + // we don't actually want to skip previously-failing tests when we + // re-run the tests locally. + delete fileTree("${project.projectDir}/build/wpi-many-tests") { + include '**/.cannot-run-wpi' + } + } + + doLast { + // Run wpi-many.sh + def typecheckFilesDir = "${project.projectDir}/build/wpi-many-tests-results/" + try { + exec { + environment CHECKERFRAMEWORK: "${projectDir}/.." + commandLine 'bin/wpi-many.sh', + '-i', "${project.projectDir}/tests/wpi-many/testin.txt", + '-o', "${project.projectDir}/build/wpi-many-tests", + '-s', + '--', + '--checker', 'nullness,interning,lock,regex,signature,calledmethods,resourceleak', + '--extraJavacArgs=-AenableWpiForRlc' + } + } catch (Exception e) { + println('Failure: Running wpi-many.sh failed with a non-zero exit code.') + File wpiOut = new File("${typecheckFilesDir}/wpi-out") + if (wpiOut.exists()) { + println("========= Start of output from last run of wpi.sh (${typecheckFilesDir}/wpi-out): ========") + exec { + commandLine 'cat', "${typecheckFilesDir}/wpi-out" + } + println("========= End of output from last run of wpi.sh (${typecheckFilesDir}/wpi-out): ========") + } else { + println("========= File ${typecheckFilesDir}/wpi-out does not exist. ========") + } + throw e + } + + // collect the logs from running WPI + def typecheckFiles = fileTree(typecheckFilesDir).matching { + include '**/*-typecheck.out' + } + def testinLines = file("${project.projectDir}/tests/wpi-many/testin.txt").text.readLines() + testinLines.removeIf { it.startsWith('#') } + def expectedTypecheckFileCount = testinLines.size() + def actualTypecheckFileCount = typecheckFiles.size() + if (actualTypecheckFileCount != expectedTypecheckFileCount) { + println("Failure: Too few *-typecheck.out files in ${typecheckFilesDir}: " + + "found ${actualTypecheckFileCount} but expected ${expectedTypecheckFileCount}.") + println("========= Found in ${typecheckFilesDir} ========") + exec { + commandLine 'ls', '-al', "${typecheckFilesDir}" + } + println("========= Expected in ${typecheckFilesDir} ========") + exec { + commandLine 'cat', "${project.projectDir}/tests/wpi-many/testin.txt" + } + println("========= Start of output from last run of wpi.sh (${typecheckFilesDir}/wpi-out): ========") + exec { + commandLine 'cat', "${typecheckFilesDir}/wpi-out" + } + println("========= End of output from last run of wpi.sh (${typecheckFilesDir}/wpi-out): ========") + def logFiles = fileTree(typecheckFilesDir).matching { + include '**/*.log' + } + logFiles.visit { FileVisitDetails details -> + def filename = "${typecheckFilesDir}" + details.getName() + println("======== start of contents of ${filename} ========") + details.getFile().eachLine { line -> println(line) } + println("======== end of contents of ${filename} ========") + } + // If any of these files are present, their contents should be an error + // message that might indicate what went wrong. Even their presenence, + // however, is intereseting (even if they are empty). + def cannotRunWpiFiles = fileTree(typecheckFilesDir).matching { + include '**/.cannot-run-wpi' + } + cannotRunWpiFiles.visit { FileVisitDetails details -> + def filename = "${typecheckFilesDir}" + details.getName() + println("======== start of contents of ${filename} ========") + details.getFile().eachLine { line -> println(line) } + println("======== end of contents of ${filename} ========") + } + throw new GradleException("Failure: Too few *-typecheck.out files in ${typecheckFilesDir}: " + + "found ${actualTypecheckFileCount} but expected ${expectedTypecheckFileCount}.") + } + + // check that WPI causes the expected builds to succeed + typecheckFiles.visit { FileVisitDetails details -> + def filename = "${project.projectDir}/build/wpi-many-tests-results/" + details.getName() + def file = details.getFile() + if (file.length() == 0) { + throw new GradleException('Failure: WPI produced empty typecheck file ' + filename) + } + file.eachLine { line -> + if ( + // Ignore the line that WPI echoes with the javac command being run. + line.startsWith('Running ') + // Warnings about bad path elements aren't related to WPI and are ignored. + || line.startsWith('warning: [path]') + // Ignore bootstrap classpath warning: + || line.startsWith('warning: [options] bootstrap') + // Ignore the warnings about --add-opens arguments to the JVM + || line.contains('warning: [options] --add-opens has no effect at compile time') + // Ignore the summary line that reports the total number of warnings (which can be single or plural). + || line.endsWith(' warning') + || line.endsWith(' warnings') + || line.startsWith('warning: No processor claimed any of these annotations: ')) { + return; + } + if (!line.trim().equals('')) { + println("======== start of contents of ${filename} ========") + details.getFile().eachLine { l -> println(l) } + println("======== end of contents of ${filename} ========") + throw new GradleException('Failure: WPI scripts produced an unexpected output in ' + filename + '. ' + + 'Failing line is the following: ' + line) + } + } + } + } +} + +// This is run as part of the inferenceTests task. +task wpiPlumeLibTest(group: 'Verification') { + description 'Tests whole-program inference on the plume-lib projects. Requires an Internet connection.' + dependsOn(assembleForJavac) + dependsOn(':getDoLikeJavac') + + // This test must always be re-run when requested. + outputs.upToDateWhen { false } + + doLast { + exec { + commandLine 'bin-devel/wpi-plumelib/test-wpi-plumelib.sh' + ignoreExitValue = false + } + } +} + +apply from: rootProject.file('gradle-mvn-push.gradle') + +/** Adds information to the publication for uploading to Maven repositories. */ +final checkerPom(publication) { + sharedPublicationConfiguration(publication) + // Don't use publication.from components.java which would publish the skinny jar as checker.jar. + publication.pom { + name = 'Checker Framework' + description = 'The Checker Framework enhances Java\'s type system to\n' + + 'make it more powerful and useful. This lets software developers\n' + + 'detect and prevent errors in their Java programs.\n' + + 'The Checker Framework includes compiler plug-ins ("checkers")\n' + + 'that find bugs or verify their absence. It also permits you to\n' + + 'write your own compiler plug-ins.' + licenses { + license { + name = 'GNU General Public License, version 2 (GPL2), with the classpath exception' + url = 'http://www.gnu.org/software/classpath/license.html' + distribution = 'repo' + } + } + } +} + +publishing { + publications { + checker(MavenPublication) { + project.shadow.component it + // reset the artifacts because of project.shadow.component changes + // the classifier from 'all' to '' because of + // https://github.com/johnrengelman/shadow/issues/860 + artifacts = [shadowJar] + checkerPom it + artifact checkerJar + artifact allSourcesJar + artifact allJavadocJar + } + } +} + +signing { + sign publishing.publications.checker +} diff --git a/checker/jtreg/SymbolNotFoundErrors.java b/checker/jtreg/SymbolNotFoundErrors.java index ab4959bf68d4..8d869c33cbcf 100644 --- a/checker/jtreg/SymbolNotFoundErrors.java +++ b/checker/jtreg/SymbolNotFoundErrors.java @@ -2,11 +2,8 @@ * @test * @summary Test for expected number of error messages. * @compile/fail/ref=SymbolNotFoundErrors.out -XDrawDiagnostics SymbolNotFoundErrors.java - * @compile/fail/ref=SymbolNotFoundErrors.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker SymbolNotFoundErrors.java + * @compile/fail/ref=SymbolNotFoundErrors2.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker SymbolNotFoundErrors.java */ public class SymbolNotFoundErrors { - // We only expect one error message for the unknown symbol. - // However, we receive three. - // https://github.com/typetools/checker-framework/issues/94 CCC f; } diff --git a/checker/jtreg/SymbolNotFoundErrors.out b/checker/jtreg/SymbolNotFoundErrors.out index 8f717fc9822c..ac8c69adde0e 100644 --- a/checker/jtreg/SymbolNotFoundErrors.out +++ b/checker/jtreg/SymbolNotFoundErrors.out @@ -1,2 +1,2 @@ -SymbolNotFoundErrors.java:11:5: compiler.err.cant.resolve.location: kindname.class, CCC, , , (compiler.misc.location: kindname.class, SymbolNotFoundErrors, null) +SymbolNotFoundErrors.java:8:5: compiler.err.cant.resolve.location: kindname.class, CCC, , , (compiler.misc.location: kindname.class, SymbolNotFoundErrors, null) 1 error diff --git a/checker/jtreg/SymbolNotFoundErrors2.out b/checker/jtreg/SymbolNotFoundErrors2.out new file mode 100644 index 000000000000..ac8c69adde0e --- /dev/null +++ b/checker/jtreg/SymbolNotFoundErrors2.out @@ -0,0 +1,2 @@ +SymbolNotFoundErrors.java:8:5: compiler.err.cant.resolve.location: kindname.class, CCC, , , (compiler.misc.location: kindname.class, SymbolNotFoundErrors, null) +1 error diff --git a/checker/jtreg/index/UnneededSuppressionsTest.java b/checker/jtreg/index/UnneededSuppressionsTest.java index 8f3e4b4d3fa9..fe71afccaafd 100644 --- a/checker/jtreg/index/UnneededSuppressionsTest.java +++ b/checker/jtreg/index/UnneededSuppressionsTest.java @@ -7,7 +7,6 @@ import org.checkerframework.checker.index.qual.NonNegative; -@SuppressWarnings("index") public class UnneededSuppressionsTest { void method(@NonNegative int i) { @@ -36,4 +35,10 @@ void method5() {} @SuppressWarnings("purity") void method6() {} + + @SuppressWarnings("index:foo.bar.baz") + void method7() {} + + @SuppressWarnings("allcheckers:purity.not.deterministic.call") + void method8() {} } diff --git a/checker/jtreg/index/UnneededSuppressionsTest.out b/checker/jtreg/index/UnneededSuppressionsTest.out index 157be3076a59..4664de2895c1 100644 --- a/checker/jtreg/index/UnneededSuppressionsTest.out +++ b/checker/jtreg/index/UnneededSuppressionsTest.out @@ -1,6 +1,5 @@ -UnneededSuppressionsTest.java:10:1: compiler.warn.proc.messager: [unneeded.suppression] warning suppression "index" is not used by IndexChecker -UnneededSuppressionsTest.java:23:5: compiler.warn.proc.messager: [unneeded.suppression] warning suppression "lowerbound" is not used by IndexChecker -UnneededSuppressionsTest.java:25:9: compiler.warn.proc.messager: [unneeded.suppression] warning suppression "upperbound:assignment.type.incompatible" is not used by IndexChecker -UnneededSuppressionsTest.java:34:5: compiler.warn.proc.messager: [unneeded.suppression] warning suppression "purity.not.deterministic.call" is not used by IndexChecker -UnneededSuppressionsTest.java:37:5: compiler.warn.proc.messager: [unneeded.suppression] warning suppression "purity" is not used by IndexChecker -5 warnings +UnneededSuppressionsTest.java:22:5: compiler.warn.proc.messager: [unneeded.suppression] warning suppression "lowerbound" is not used by IndexChecker +UnneededSuppressionsTest.java:24:9: compiler.warn.proc.messager: [unneeded.suppression] warning suppression "upperbound:assignment.type.incompatible" is not used by IndexChecker +UnneededSuppressionsTest.java:39:5: compiler.warn.proc.messager: [unneeded.suppression] warning suppression "index:foo.bar.baz" is not used by IndexChecker +UnneededSuppressionsTest.java:42:5: compiler.warn.proc.messager: [unneeded.suppression] warning suppression "allcheckers:purity.not.deterministic.call" is not used by IndexChecker +4 warnings diff --git a/checker/jtreg/index/ValueStubDriver.java b/checker/jtreg/index/ValueStubDriver.java index b279289165b4..2af0c02273f6 100644 --- a/checker/jtreg/index/ValueStubDriver.java +++ b/checker/jtreg/index/ValueStubDriver.java @@ -6,4 +6,4 @@ * @compile -XDrawDiagnostics -processor org.checkerframework.checker.index.IndexChecker valuestub/UseTest.java */ -class ValueStubDriver {} +public class ValueStubDriver {} diff --git a/checker/jtreg/issue1133/Main.java b/checker/jtreg/issue1133/Main.java index af7217768e39..0e5956315733 100644 --- a/checker/jtreg/issue1133/Main.java +++ b/checker/jtreg/issue1133/Main.java @@ -3,7 +3,7 @@ * @summary Ensure that an invalid annotation doesn't crash the * Checker Framework. * - * @compile/fail/ref=error.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker ClassA.java ClassB.java - * @compile/fail/ref=error.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker ClassB.java ClassA.java + * @compile/fail/ref=errorAB.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker ClassA.java ClassB.java + * @compile/fail/ref=errorBA.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker ClassB.java ClassA.java */ -class Main {} +public class Main {} diff --git a/checker/jtreg/issue1133/error.out b/checker/jtreg/issue1133/errorAB.out similarity index 100% rename from checker/jtreg/issue1133/error.out rename to checker/jtreg/issue1133/errorAB.out diff --git a/checker/jtreg/issue1133/errorBA.out b/checker/jtreg/issue1133/errorBA.out new file mode 100644 index 000000000000..ca7f6b4c3a53 --- /dev/null +++ b/checker/jtreg/issue1133/errorBA.out @@ -0,0 +1,2 @@ +ClassB.java:3:17: compiler.err.cant.resolve.location: kindname.class, Nullable, , , (compiler.misc.location: kindname.class, ClassB, null) +1 error diff --git a/checker/jtreg/issue469/Main.java b/checker/jtreg/issue469/Main.java index 1b3e720314e0..763d08faccc6 100644 --- a/checker/jtreg/issue469/Main.java +++ b/checker/jtreg/issue469/Main.java @@ -12,4 +12,4 @@ * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker advancedcrash/LetItCrash.java advancedcrash/CrashyInterface.java advancedcrash/SomeInterface.java * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker advancedcrash/LetItCrash.java advancedcrash/SomeInterface.java advancedcrash/CrashyInterface.java */ -class Main {} +public class Main {} diff --git a/checker/jtreg/issue469/advancedcrash/LetItCrash.java b/checker/jtreg/issue469/advancedcrash/LetItCrash.java index 6a95256b9080..14cebb2e3e5c 100644 --- a/checker/jtreg/issue469/advancedcrash/LetItCrash.java +++ b/checker/jtreg/issue469/advancedcrash/LetItCrash.java @@ -1,6 +1,6 @@ package advancedcrash; -class LetItCrash implements SomeInterface { +public class LetItCrash implements SomeInterface { Integer longer = 0; diff --git a/checker/jtreg/issue469/simplecrash/LetItCrash.java b/checker/jtreg/issue469/simplecrash/LetItCrash.java index f090eaac3fa8..6ef150ad5d70 100644 --- a/checker/jtreg/issue469/simplecrash/LetItCrash.java +++ b/checker/jtreg/issue469/simplecrash/LetItCrash.java @@ -1,6 +1,6 @@ package simplecrash; -class LetItCrash implements CrashyInterface { +public class LetItCrash implements CrashyInterface { private Long longer = 0L; @Override diff --git a/checker/jtreg/issue469/simplecrash/SomeRandomClass.java b/checker/jtreg/issue469/simplecrash/SomeRandomClass.java index a949061c5e79..a28dbf5bbd94 100644 --- a/checker/jtreg/issue469/simplecrash/SomeRandomClass.java +++ b/checker/jtreg/issue469/simplecrash/SomeRandomClass.java @@ -1,6 +1,6 @@ package simplecrash; -class SomeRandomClass { +public class SomeRandomClass { void randomStuff(LetItCrash letItCrash) { letItCrash.makeItLongerAndCrash(); } diff --git a/checker/jtreg/multiplecheckers/NullnessInterning.java b/checker/jtreg/multiplecheckers/NullnessInterning.java index 50621cb228ba..faf6529c90af 100644 --- a/checker/jtreg/multiplecheckers/NullnessInterning.java +++ b/checker/jtreg/multiplecheckers/NullnessInterning.java @@ -15,7 +15,7 @@ * @compile/ref=NullnessInterning4.out -XDrawDiagnostics -Anomsgtext -processor org.checkerframework.checker.nullness.NullnessChecker,org.checkerframework.checker.interning.InterningChecker NullnessInterning.java -Awarns -Aorg.checkerframework.common.basetype.BaseTypeChecker_skipDefs=NullnessInterning */ -class NullnessInterning { +public class NullnessInterning { Object f = null; void m(Object p) { diff --git a/checker/jtreg/multiplecheckers/NullnessInterning1.out b/checker/jtreg/multiplecheckers/NullnessInterning1.out index 1057f4e5a5d3..c19fba61f3e4 100644 --- a/checker/jtreg/multiplecheckers/NullnessInterning1.out +++ b/checker/jtreg/multiplecheckers/NullnessInterning1.out @@ -1,4 +1,4 @@ NullnessInterning.java:19:16: compiler.warn.proc.messager: (assignment.type.incompatible) NullnessInterning.java:22:13: compiler.warn.proc.messager: (not.interned) NullnessInterning.java:22:18: compiler.warn.proc.messager: (not.interned) -3 warnings \ No newline at end of file +3 warnings diff --git a/checker/jtreg/multipleexecutions/Main.java b/checker/jtreg/multipleexecutions/Main.java index 2b303018c093..a4aa9dac6f59 100644 --- a/checker/jtreg/multipleexecutions/Main.java +++ b/checker/jtreg/multipleexecutions/Main.java @@ -11,12 +11,14 @@ * https://groups.google.com/d/msg/checker-framework-dev/FvWmCxB8OpE/Cgp1DsPwnWwJ */ +import org.checkerframework.checker.regex.RegexChecker; + import java.io.File; import java.util.Arrays; + import javax.tools.JavaCompiler; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; -import org.checkerframework.checker.regex.RegexChecker; public class Main { diff --git a/checker/jtreg/multipleexecutions/Test.java b/checker/jtreg/multipleexecutions/Test.java index 94ecf3196d19..5322099b53d6 100644 --- a/checker/jtreg/multipleexecutions/Test.java +++ b/checker/jtreg/multipleexecutions/Test.java @@ -1,5 +1,5 @@ -import org.checkerframework.checker.regex.RegexUtil; import org.checkerframework.checker.regex.qual.Regex; +import org.checkerframework.checker.regex.util.RegexUtil; public class Test { void foo(String simple) { diff --git a/checker/jtreg/nullness/Issue1438.java b/checker/jtreg/nullness/Issue1438.java index b05c628a043f..f90e6af852b6 100644 --- a/checker/jtreg/nullness/Issue1438.java +++ b/checker/jtreg/nullness/Issue1438.java @@ -2,13 +2,15 @@ * @test * @summary Test case for issue #1438: https://github.com/typetools/checker-framework/issues/1438 * - * @compile/fail/timeout=60 -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Alint Issue1438.java + * @compile/fail/timeout=90 -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Alint Issue1438.java */ import java.util.HashMap; import java.util.Map; public class Issue1438 { + // Do not initialize these variables. The Nullness Checker is supposed to issue an error about + // uninitialized fields. static Integer v0; static Integer v1; static Integer v2; diff --git a/checker/jtreg/nullness/Issue1438b.java b/checker/jtreg/nullness/Issue1438b.java new file mode 100644 index 000000000000..4892527c3dba --- /dev/null +++ b/checker/jtreg/nullness/Issue1438b.java @@ -0,0 +1,2018 @@ +/* + * @test + * @summary Test case for issue #1438: https://github.com/typetools/checker-framework/issues/1438 + * + * @compile/fail/timeout=110 -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Alint Issue1438b.java + */ + +import java.util.HashMap; +import java.util.Map; + +public class Issue1438b { + // Do not initialize these variables. The Nullness Checker is supposed to issue an error about + // uninitialized fields. + Integer v0; + Integer v1; + Integer v2; + Integer v3; + Integer v4; + Integer v5; + Integer v6; + Integer v7; + Integer v8; + Integer v9; + Integer v10; + Integer v11; + Integer v12; + Integer v13; + Integer v14; + Integer v15; + Integer v16; + Integer v17; + Integer v18; + Integer v19; + Integer v20; + Integer v21; + Integer v22; + Integer v23; + Integer v24; + Integer v25; + Integer v26; + Integer v27; + Integer v28; + Integer v29; + Integer v30; + Integer v31; + Integer v32; + Integer v33; + Integer v34; + Integer v35; + Integer v36; + Integer v37; + Integer v38; + Integer v39; + Integer v40; + Integer v41; + Integer v42; + Integer v43; + Integer v44; + Integer v45; + Integer v46; + Integer v47; + Integer v48; + Integer v49; + Integer v50; + Integer v51; + Integer v52; + Integer v53; + Integer v54; + Integer v55; + Integer v56; + Integer v57; + Integer v58; + Integer v59; + Integer v60; + Integer v61; + Integer v62; + Integer v63; + Integer v64; + Integer v65; + Integer v66; + Integer v67; + Integer v68; + Integer v69; + Integer v70; + Integer v71; + Integer v72; + Integer v73; + Integer v74; + Integer v75; + Integer v76; + Integer v77; + Integer v78; + Integer v79; + Integer v80; + Integer v81; + Integer v82; + Integer v83; + Integer v84; + Integer v85; + Integer v86; + Integer v87; + Integer v88; + Integer v89; + Integer v90; + Integer v91; + Integer v92; + Integer v93; + Integer v94; + Integer v95; + Integer v96; + Integer v97; + Integer v98; + Integer v99; + Integer v100; + Integer v101; + Integer v102; + Integer v103; + Integer v104; + Integer v105; + Integer v106; + Integer v107; + Integer v108; + Integer v109; + Integer v110; + Integer v111; + Integer v112; + Integer v113; + Integer v114; + Integer v115; + Integer v116; + Integer v117; + Integer v118; + Integer v119; + Integer v120; + Integer v121; + Integer v122; + Integer v123; + Integer v124; + Integer v125; + Integer v126; + Integer v127; + Integer v128; + Integer v129; + Integer v130; + Integer v131; + Integer v132; + Integer v133; + Integer v134; + Integer v135; + Integer v136; + Integer v137; + Integer v138; + Integer v139; + Integer v140; + Integer v141; + Integer v142; + Integer v143; + Integer v144; + Integer v145; + Integer v146; + Integer v147; + Integer v148; + Integer v149; + Integer v150; + Integer v151; + Integer v152; + Integer v153; + Integer v154; + Integer v155; + Integer v156; + Integer v157; + Integer v158; + Integer v159; + Integer v160; + Integer v161; + Integer v162; + Integer v163; + Integer v164; + Integer v165; + Integer v166; + Integer v167; + Integer v168; + Integer v169; + Integer v170; + Integer v171; + Integer v172; + Integer v173; + Integer v174; + Integer v175; + Integer v176; + Integer v177; + Integer v178; + Integer v179; + Integer v180; + Integer v181; + Integer v182; + Integer v183; + Integer v184; + Integer v185; + Integer v186; + Integer v187; + Integer v188; + Integer v189; + Integer v190; + Integer v191; + Integer v192; + Integer v193; + Integer v194; + Integer v195; + Integer v196; + Integer v197; + Integer v198; + Integer v199; + Integer v200; + Integer v201; + Integer v202; + Integer v203; + Integer v204; + Integer v205; + Integer v206; + Integer v207; + Integer v208; + Integer v209; + Integer v210; + Integer v211; + Integer v212; + Integer v213; + Integer v214; + Integer v215; + Integer v216; + Integer v217; + Integer v218; + Integer v219; + Integer v220; + Integer v221; + Integer v222; + Integer v223; + Integer v224; + Integer v225; + Integer v226; + Integer v227; + Integer v228; + Integer v229; + Integer v230; + Integer v231; + Integer v232; + Integer v233; + Integer v234; + Integer v235; + Integer v236; + Integer v237; + Integer v238; + Integer v239; + Integer v240; + Integer v241; + Integer v242; + Integer v243; + Integer v244; + Integer v245; + Integer v246; + Integer v247; + Integer v248; + Integer v249; + Integer v250; + Integer v251; + Integer v252; + Integer v253; + Integer v254; + Integer v255; + Integer v256; + Integer v257; + Integer v258; + Integer v259; + Integer v260; + Integer v261; + Integer v262; + Integer v263; + Integer v264; + Integer v265; + Integer v266; + Integer v267; + Integer v268; + Integer v269; + Integer v270; + Integer v271; + Integer v272; + Integer v273; + Integer v274; + Integer v275; + Integer v276; + Integer v277; + Integer v278; + Integer v279; + Integer v280; + Integer v281; + Integer v282; + Integer v283; + Integer v284; + Integer v285; + Integer v286; + Integer v287; + Integer v288; + Integer v289; + Integer v290; + Integer v291; + Integer v292; + Integer v293; + Integer v294; + Integer v295; + Integer v296; + Integer v297; + Integer v298; + Integer v299; + Integer v300; + Integer v301; + Integer v302; + Integer v303; + Integer v304; + Integer v305; + Integer v306; + Integer v307; + Integer v308; + Integer v309; + Integer v310; + Integer v311; + Integer v312; + Integer v313; + Integer v314; + Integer v315; + Integer v316; + Integer v317; + Integer v318; + Integer v319; + Integer v320; + Integer v321; + Integer v322; + Integer v323; + Integer v324; + Integer v325; + Integer v326; + Integer v327; + Integer v328; + Integer v329; + Integer v330; + Integer v331; + Integer v332; + Integer v333; + Integer v334; + Integer v335; + Integer v336; + Integer v337; + Integer v338; + Integer v339; + Integer v340; + Integer v341; + Integer v342; + Integer v343; + Integer v344; + Integer v345; + Integer v346; + Integer v347; + Integer v348; + Integer v349; + Integer v350; + Integer v351; + Integer v352; + Integer v353; + Integer v354; + Integer v355; + Integer v356; + Integer v357; + Integer v358; + Integer v359; + Integer v360; + Integer v361; + Integer v362; + Integer v363; + Integer v364; + Integer v365; + Integer v366; + Integer v367; + Integer v368; + Integer v369; + Integer v370; + Integer v371; + Integer v372; + Integer v373; + Integer v374; + Integer v375; + Integer v376; + Integer v377; + Integer v378; + Integer v379; + Integer v380; + Integer v381; + Integer v382; + Integer v383; + Integer v384; + Integer v385; + Integer v386; + Integer v387; + Integer v388; + Integer v389; + Integer v390; + Integer v391; + Integer v392; + Integer v393; + Integer v394; + Integer v395; + Integer v396; + Integer v397; + Integer v398; + Integer v399; + Integer v400; + Integer v401; + Integer v402; + Integer v403; + Integer v404; + Integer v405; + Integer v406; + Integer v407; + Integer v408; + Integer v409; + Integer v410; + Integer v411; + Integer v412; + Integer v413; + Integer v414; + Integer v415; + Integer v416; + Integer v417; + Integer v418; + Integer v419; + Integer v420; + Integer v421; + Integer v422; + Integer v423; + Integer v424; + Integer v425; + Integer v426; + Integer v427; + Integer v428; + Integer v429; + Integer v430; + Integer v431; + Integer v432; + Integer v433; + Integer v434; + Integer v435; + Integer v436; + Integer v437; + Integer v438; + Integer v439; + Integer v440; + Integer v441; + Integer v442; + Integer v443; + Integer v444; + Integer v445; + Integer v446; + Integer v447; + Integer v448; + Integer v449; + Integer v450; + Integer v451; + Integer v452; + Integer v453; + Integer v454; + Integer v455; + Integer v456; + Integer v457; + Integer v458; + Integer v459; + Integer v460; + Integer v461; + Integer v462; + Integer v463; + Integer v464; + Integer v465; + Integer v466; + Integer v467; + Integer v468; + Integer v469; + Integer v470; + Integer v471; + Integer v472; + Integer v473; + Integer v474; + Integer v475; + Integer v476; + Integer v477; + Integer v478; + Integer v479; + Integer v480; + Integer v481; + Integer v482; + Integer v483; + Integer v484; + Integer v485; + Integer v486; + Integer v487; + Integer v488; + Integer v489; + Integer v490; + Integer v491; + Integer v492; + Integer v493; + Integer v494; + Integer v495; + Integer v496; + Integer v497; + Integer v498; + Integer v499; + Integer v500; + Integer v501; + Integer v502; + Integer v503; + Integer v504; + Integer v505; + Integer v506; + Integer v507; + Integer v508; + Integer v509; + Integer v510; + Integer v511; + Integer v512; + Integer v513; + Integer v514; + Integer v515; + Integer v516; + Integer v517; + Integer v518; + Integer v519; + Integer v520; + Integer v521; + Integer v522; + Integer v523; + Integer v524; + Integer v525; + Integer v526; + Integer v527; + Integer v528; + Integer v529; + Integer v530; + Integer v531; + Integer v532; + Integer v533; + Integer v534; + Integer v535; + Integer v536; + Integer v537; + Integer v538; + Integer v539; + Integer v540; + Integer v541; + Integer v542; + Integer v543; + Integer v544; + Integer v545; + Integer v546; + Integer v547; + Integer v548; + Integer v549; + Integer v550; + Integer v551; + Integer v552; + Integer v553; + Integer v554; + Integer v555; + Integer v556; + Integer v557; + Integer v558; + Integer v559; + Integer v560; + Integer v561; + Integer v562; + Integer v563; + Integer v564; + Integer v565; + Integer v566; + Integer v567; + Integer v568; + Integer v569; + Integer v570; + Integer v571; + Integer v572; + Integer v573; + Integer v574; + Integer v575; + Integer v576; + Integer v577; + Integer v578; + Integer v579; + Integer v580; + Integer v581; + Integer v582; + Integer v583; + Integer v584; + Integer v585; + Integer v586; + Integer v587; + Integer v588; + Integer v589; + Integer v590; + Integer v591; + Integer v592; + Integer v593; + Integer v594; + Integer v595; + Integer v596; + Integer v597; + Integer v598; + Integer v599; + Integer v600; + Integer v601; + Integer v602; + Integer v603; + Integer v604; + Integer v605; + Integer v606; + Integer v607; + Integer v608; + Integer v609; + Integer v610; + Integer v611; + Integer v612; + Integer v613; + Integer v614; + Integer v615; + Integer v616; + Integer v617; + Integer v618; + Integer v619; + Integer v620; + Integer v621; + Integer v622; + Integer v623; + Integer v624; + Integer v625; + Integer v626; + Integer v627; + Integer v628; + Integer v629; + Integer v630; + Integer v631; + Integer v632; + Integer v633; + Integer v634; + Integer v635; + Integer v636; + Integer v637; + Integer v638; + Integer v639; + Integer v640; + Integer v641; + Integer v642; + Integer v643; + Integer v644; + Integer v645; + Integer v646; + Integer v647; + Integer v648; + Integer v649; + Integer v650; + Integer v651; + Integer v652; + Integer v653; + Integer v654; + Integer v655; + Integer v656; + Integer v657; + Integer v658; + Integer v659; + Integer v660; + Integer v661; + Integer v662; + Integer v663; + Integer v664; + Integer v665; + Integer v666; + Integer v667; + Integer v668; + Integer v669; + Integer v670; + Integer v671; + Integer v672; + Integer v673; + Integer v674; + Integer v675; + Integer v676; + Integer v677; + Integer v678; + Integer v679; + Integer v680; + Integer v681; + Integer v682; + Integer v683; + Integer v684; + Integer v685; + Integer v686; + Integer v687; + Integer v688; + Integer v689; + Integer v690; + Integer v691; + Integer v692; + Integer v693; + Integer v694; + Integer v695; + Integer v696; + Integer v697; + Integer v698; + Integer v699; + Integer v700; + Integer v701; + Integer v702; + Integer v703; + Integer v704; + Integer v705; + Integer v706; + Integer v707; + Integer v708; + Integer v709; + Integer v710; + Integer v711; + Integer v712; + Integer v713; + Integer v714; + Integer v715; + Integer v716; + Integer v717; + Integer v718; + Integer v719; + Integer v720; + Integer v721; + Integer v722; + Integer v723; + Integer v724; + Integer v725; + Integer v726; + Integer v727; + Integer v728; + Integer v729; + Integer v730; + Integer v731; + Integer v732; + Integer v733; + Integer v734; + Integer v735; + Integer v736; + Integer v737; + Integer v738; + Integer v739; + Integer v740; + Integer v741; + Integer v742; + Integer v743; + Integer v744; + Integer v745; + Integer v746; + Integer v747; + Integer v748; + Integer v749; + Integer v750; + Integer v751; + Integer v752; + Integer v753; + Integer v754; + Integer v755; + Integer v756; + Integer v757; + Integer v758; + Integer v759; + Integer v760; + Integer v761; + Integer v762; + Integer v763; + Integer v764; + Integer v765; + Integer v766; + Integer v767; + Integer v768; + Integer v769; + Integer v770; + Integer v771; + Integer v772; + Integer v773; + Integer v774; + Integer v775; + Integer v776; + Integer v777; + Integer v778; + Integer v779; + Integer v780; + Integer v781; + Integer v782; + Integer v783; + Integer v784; + Integer v785; + Integer v786; + Integer v787; + Integer v788; + Integer v789; + Integer v790; + Integer v791; + Integer v792; + Integer v793; + Integer v794; + Integer v795; + Integer v796; + Integer v797; + Integer v798; + Integer v799; + Integer v800; + Integer v801; + Integer v802; + Integer v803; + Integer v804; + Integer v805; + Integer v806; + Integer v807; + Integer v808; + Integer v809; + Integer v810; + Integer v811; + Integer v812; + Integer v813; + Integer v814; + Integer v815; + Integer v816; + Integer v817; + Integer v818; + Integer v819; + Integer v820; + Integer v821; + Integer v822; + Integer v823; + Integer v824; + Integer v825; + Integer v826; + Integer v827; + Integer v828; + Integer v829; + Integer v830; + Integer v831; + Integer v832; + Integer v833; + Integer v834; + Integer v835; + Integer v836; + Integer v837; + Integer v838; + Integer v839; + Integer v840; + Integer v841; + Integer v842; + Integer v843; + Integer v844; + Integer v845; + Integer v846; + Integer v847; + Integer v848; + Integer v849; + Integer v850; + Integer v851; + Integer v852; + Integer v853; + Integer v854; + Integer v855; + Integer v856; + Integer v857; + Integer v858; + Integer v859; + Integer v860; + Integer v861; + Integer v862; + Integer v863; + Integer v864; + Integer v865; + Integer v866; + Integer v867; + Integer v868; + Integer v869; + Integer v870; + Integer v871; + Integer v872; + Integer v873; + Integer v874; + Integer v875; + Integer v876; + Integer v877; + Integer v878; + Integer v879; + Integer v880; + Integer v881; + Integer v882; + Integer v883; + Integer v884; + Integer v885; + Integer v886; + Integer v887; + Integer v888; + Integer v889; + Integer v890; + Integer v891; + Integer v892; + Integer v893; + Integer v894; + Integer v895; + Integer v896; + Integer v897; + Integer v898; + Integer v899; + Integer v900; + Integer v901; + Integer v902; + Integer v903; + Integer v904; + Integer v905; + Integer v906; + Integer v907; + Integer v908; + Integer v909; + Integer v910; + Integer v911; + Integer v912; + Integer v913; + Integer v914; + Integer v915; + Integer v916; + Integer v917; + Integer v918; + Integer v919; + Integer v920; + Integer v921; + Integer v922; + Integer v923; + Integer v924; + Integer v925; + Integer v926; + Integer v927; + Integer v928; + Integer v929; + Integer v930; + Integer v931; + Integer v932; + Integer v933; + Integer v934; + Integer v935; + Integer v936; + Integer v937; + Integer v938; + Integer v939; + Integer v940; + Integer v941; + Integer v942; + Integer v943; + Integer v944; + Integer v945; + Integer v946; + Integer v947; + Integer v948; + Integer v949; + Integer v950; + Integer v951; + Integer v952; + Integer v953; + Integer v954; + Integer v955; + Integer v956; + Integer v957; + Integer v958; + Integer v959; + Integer v960; + Integer v961; + Integer v962; + Integer v963; + Integer v964; + Integer v965; + Integer v966; + Integer v967; + Integer v968; + Integer v969; + Integer v970; + Integer v971; + Integer v972; + Integer v973; + Integer v974; + Integer v975; + Integer v976; + Integer v977; + Integer v978; + Integer v979; + Integer v980; + Integer v981; + Integer v982; + Integer v983; + Integer v984; + Integer v985; + Integer v986; + Integer v987; + Integer v988; + Integer v989; + Integer v990; + Integer v991; + Integer v992; + Integer v993; + Integer v994; + Integer v995; + Integer v996; + Integer v997; + Integer v998; + Integer v999; + + Issue1438b() { + Map map = new HashMap<>(); + map.put("k0", v0); + map.put("k1", v1); + map.put("k2", v2); + map.put("k3", v3); + map.put("k4", v4); + map.put("k5", v5); + map.put("k6", v6); + map.put("k7", v7); + map.put("k8", v8); + map.put("k9", v9); + map.put("k10", v10); + map.put("k11", v11); + map.put("k12", v12); + map.put("k13", v13); + map.put("k14", v14); + map.put("k15", v15); + map.put("k16", v16); + map.put("k17", v17); + map.put("k18", v18); + map.put("k19", v19); + map.put("k20", v20); + map.put("k21", v21); + map.put("k22", v22); + map.put("k23", v23); + map.put("k24", v24); + map.put("k25", v25); + map.put("k26", v26); + map.put("k27", v27); + map.put("k28", v28); + map.put("k29", v29); + map.put("k30", v30); + map.put("k31", v31); + map.put("k32", v32); + map.put("k33", v33); + map.put("k34", v34); + map.put("k35", v35); + map.put("k36", v36); + map.put("k37", v37); + map.put("k38", v38); + map.put("k39", v39); + map.put("k40", v40); + map.put("k41", v41); + map.put("k42", v42); + map.put("k43", v43); + map.put("k44", v44); + map.put("k45", v45); + map.put("k46", v46); + map.put("k47", v47); + map.put("k48", v48); + map.put("k49", v49); + map.put("k50", v50); + map.put("k51", v51); + map.put("k52", v52); + map.put("k53", v53); + map.put("k54", v54); + map.put("k55", v55); + map.put("k56", v56); + map.put("k57", v57); + map.put("k58", v58); + map.put("k59", v59); + map.put("k60", v60); + map.put("k61", v61); + map.put("k62", v62); + map.put("k63", v63); + map.put("k64", v64); + map.put("k65", v65); + map.put("k66", v66); + map.put("k67", v67); + map.put("k68", v68); + map.put("k69", v69); + map.put("k70", v70); + map.put("k71", v71); + map.put("k72", v72); + map.put("k73", v73); + map.put("k74", v74); + map.put("k75", v75); + map.put("k76", v76); + map.put("k77", v77); + map.put("k78", v78); + map.put("k79", v79); + map.put("k80", v80); + map.put("k81", v81); + map.put("k82", v82); + map.put("k83", v83); + map.put("k84", v84); + map.put("k85", v85); + map.put("k86", v86); + map.put("k87", v87); + map.put("k88", v88); + map.put("k89", v89); + map.put("k90", v90); + map.put("k91", v91); + map.put("k92", v92); + map.put("k93", v93); + map.put("k94", v94); + map.put("k95", v95); + map.put("k96", v96); + map.put("k97", v97); + map.put("k98", v98); + map.put("k99", v99); + map.put("k100", v100); + map.put("k101", v101); + map.put("k102", v102); + map.put("k103", v103); + map.put("k104", v104); + map.put("k105", v105); + map.put("k106", v106); + map.put("k107", v107); + map.put("k108", v108); + map.put("k109", v109); + map.put("k110", v110); + map.put("k111", v111); + map.put("k112", v112); + map.put("k113", v113); + map.put("k114", v114); + map.put("k115", v115); + map.put("k116", v116); + map.put("k117", v117); + map.put("k118", v118); + map.put("k119", v119); + map.put("k120", v120); + map.put("k121", v121); + map.put("k122", v122); + map.put("k123", v123); + map.put("k124", v124); + map.put("k125", v125); + map.put("k126", v126); + map.put("k127", v127); + map.put("k128", v128); + map.put("k129", v129); + map.put("k130", v130); + map.put("k131", v131); + map.put("k132", v132); + map.put("k133", v133); + map.put("k134", v134); + map.put("k135", v135); + map.put("k136", v136); + map.put("k137", v137); + map.put("k138", v138); + map.put("k139", v139); + map.put("k140", v140); + map.put("k141", v141); + map.put("k142", v142); + map.put("k143", v143); + map.put("k144", v144); + map.put("k145", v145); + map.put("k146", v146); + map.put("k147", v147); + map.put("k148", v148); + map.put("k149", v149); + map.put("k150", v150); + map.put("k151", v151); + map.put("k152", v152); + map.put("k153", v153); + map.put("k154", v154); + map.put("k155", v155); + map.put("k156", v156); + map.put("k157", v157); + map.put("k158", v158); + map.put("k159", v159); + map.put("k160", v160); + map.put("k161", v161); + map.put("k162", v162); + map.put("k163", v163); + map.put("k164", v164); + map.put("k165", v165); + map.put("k166", v166); + map.put("k167", v167); + map.put("k168", v168); + map.put("k169", v169); + map.put("k170", v170); + map.put("k171", v171); + map.put("k172", v172); + map.put("k173", v173); + map.put("k174", v174); + map.put("k175", v175); + map.put("k176", v176); + map.put("k177", v177); + map.put("k178", v178); + map.put("k179", v179); + map.put("k180", v180); + map.put("k181", v181); + map.put("k182", v182); + map.put("k183", v183); + map.put("k184", v184); + map.put("k185", v185); + map.put("k186", v186); + map.put("k187", v187); + map.put("k188", v188); + map.put("k189", v189); + map.put("k190", v190); + map.put("k191", v191); + map.put("k192", v192); + map.put("k193", v193); + map.put("k194", v194); + map.put("k195", v195); + map.put("k196", v196); + map.put("k197", v197); + map.put("k198", v198); + map.put("k199", v199); + map.put("k200", v200); + map.put("k201", v201); + map.put("k202", v202); + map.put("k203", v203); + map.put("k204", v204); + map.put("k205", v205); + map.put("k206", v206); + map.put("k207", v207); + map.put("k208", v208); + map.put("k209", v209); + map.put("k210", v210); + map.put("k211", v211); + map.put("k212", v212); + map.put("k213", v213); + map.put("k214", v214); + map.put("k215", v215); + map.put("k216", v216); + map.put("k217", v217); + map.put("k218", v218); + map.put("k219", v219); + map.put("k220", v220); + map.put("k221", v221); + map.put("k222", v222); + map.put("k223", v223); + map.put("k224", v224); + map.put("k225", v225); + map.put("k226", v226); + map.put("k227", v227); + map.put("k228", v228); + map.put("k229", v229); + map.put("k230", v230); + map.put("k231", v231); + map.put("k232", v232); + map.put("k233", v233); + map.put("k234", v234); + map.put("k235", v235); + map.put("k236", v236); + map.put("k237", v237); + map.put("k238", v238); + map.put("k239", v239); + map.put("k240", v240); + map.put("k241", v241); + map.put("k242", v242); + map.put("k243", v243); + map.put("k244", v244); + map.put("k245", v245); + map.put("k246", v246); + map.put("k247", v247); + map.put("k248", v248); + map.put("k249", v249); + map.put("k250", v250); + map.put("k251", v251); + map.put("k252", v252); + map.put("k253", v253); + map.put("k254", v254); + map.put("k255", v255); + map.put("k256", v256); + map.put("k257", v257); + map.put("k258", v258); + map.put("k259", v259); + map.put("k260", v260); + map.put("k261", v261); + map.put("k262", v262); + map.put("k263", v263); + map.put("k264", v264); + map.put("k265", v265); + map.put("k266", v266); + map.put("k267", v267); + map.put("k268", v268); + map.put("k269", v269); + map.put("k270", v270); + map.put("k271", v271); + map.put("k272", v272); + map.put("k273", v273); + map.put("k274", v274); + map.put("k275", v275); + map.put("k276", v276); + map.put("k277", v277); + map.put("k278", v278); + map.put("k279", v279); + map.put("k280", v280); + map.put("k281", v281); + map.put("k282", v282); + map.put("k283", v283); + map.put("k284", v284); + map.put("k285", v285); + map.put("k286", v286); + map.put("k287", v287); + map.put("k288", v288); + map.put("k289", v289); + map.put("k290", v290); + map.put("k291", v291); + map.put("k292", v292); + map.put("k293", v293); + map.put("k294", v294); + map.put("k295", v295); + map.put("k296", v296); + map.put("k297", v297); + map.put("k298", v298); + map.put("k299", v299); + map.put("k300", v300); + map.put("k301", v301); + map.put("k302", v302); + map.put("k303", v303); + map.put("k304", v304); + map.put("k305", v305); + map.put("k306", v306); + map.put("k307", v307); + map.put("k308", v308); + map.put("k309", v309); + map.put("k310", v310); + map.put("k311", v311); + map.put("k312", v312); + map.put("k313", v313); + map.put("k314", v314); + map.put("k315", v315); + map.put("k316", v316); + map.put("k317", v317); + map.put("k318", v318); + map.put("k319", v319); + map.put("k320", v320); + map.put("k321", v321); + map.put("k322", v322); + map.put("k323", v323); + map.put("k324", v324); + map.put("k325", v325); + map.put("k326", v326); + map.put("k327", v327); + map.put("k328", v328); + map.put("k329", v329); + map.put("k330", v330); + map.put("k331", v331); + map.put("k332", v332); + map.put("k333", v333); + map.put("k334", v334); + map.put("k335", v335); + map.put("k336", v336); + map.put("k337", v337); + map.put("k338", v338); + map.put("k339", v339); + map.put("k340", v340); + map.put("k341", v341); + map.put("k342", v342); + map.put("k343", v343); + map.put("k344", v344); + map.put("k345", v345); + map.put("k346", v346); + map.put("k347", v347); + map.put("k348", v348); + map.put("k349", v349); + map.put("k350", v350); + map.put("k351", v351); + map.put("k352", v352); + map.put("k353", v353); + map.put("k354", v354); + map.put("k355", v355); + map.put("k356", v356); + map.put("k357", v357); + map.put("k358", v358); + map.put("k359", v359); + map.put("k360", v360); + map.put("k361", v361); + map.put("k362", v362); + map.put("k363", v363); + map.put("k364", v364); + map.put("k365", v365); + map.put("k366", v366); + map.put("k367", v367); + map.put("k368", v368); + map.put("k369", v369); + map.put("k370", v370); + map.put("k371", v371); + map.put("k372", v372); + map.put("k373", v373); + map.put("k374", v374); + map.put("k375", v375); + map.put("k376", v376); + map.put("k377", v377); + map.put("k378", v378); + map.put("k379", v379); + map.put("k380", v380); + map.put("k381", v381); + map.put("k382", v382); + map.put("k383", v383); + map.put("k384", v384); + map.put("k385", v385); + map.put("k386", v386); + map.put("k387", v387); + map.put("k388", v388); + map.put("k389", v389); + map.put("k390", v390); + map.put("k391", v391); + map.put("k392", v392); + map.put("k393", v393); + map.put("k394", v394); + map.put("k395", v395); + map.put("k396", v396); + map.put("k397", v397); + map.put("k398", v398); + map.put("k399", v399); + map.put("k400", v400); + map.put("k401", v401); + map.put("k402", v402); + map.put("k403", v403); + map.put("k404", v404); + map.put("k405", v405); + map.put("k406", v406); + map.put("k407", v407); + map.put("k408", v408); + map.put("k409", v409); + map.put("k410", v410); + map.put("k411", v411); + map.put("k412", v412); + map.put("k413", v413); + map.put("k414", v414); + map.put("k415", v415); + map.put("k416", v416); + map.put("k417", v417); + map.put("k418", v418); + map.put("k419", v419); + map.put("k420", v420); + map.put("k421", v421); + map.put("k422", v422); + map.put("k423", v423); + map.put("k424", v424); + map.put("k425", v425); + map.put("k426", v426); + map.put("k427", v427); + map.put("k428", v428); + map.put("k429", v429); + map.put("k430", v430); + map.put("k431", v431); + map.put("k432", v432); + map.put("k433", v433); + map.put("k434", v434); + map.put("k435", v435); + map.put("k436", v436); + map.put("k437", v437); + map.put("k438", v438); + map.put("k439", v439); + map.put("k440", v440); + map.put("k441", v441); + map.put("k442", v442); + map.put("k443", v443); + map.put("k444", v444); + map.put("k445", v445); + map.put("k446", v446); + map.put("k447", v447); + map.put("k448", v448); + map.put("k449", v449); + map.put("k450", v450); + map.put("k451", v451); + map.put("k452", v452); + map.put("k453", v453); + map.put("k454", v454); + map.put("k455", v455); + map.put("k456", v456); + map.put("k457", v457); + map.put("k458", v458); + map.put("k459", v459); + map.put("k460", v460); + map.put("k461", v461); + map.put("k462", v462); + map.put("k463", v463); + map.put("k464", v464); + map.put("k465", v465); + map.put("k466", v466); + map.put("k467", v467); + map.put("k468", v468); + map.put("k469", v469); + map.put("k470", v470); + map.put("k471", v471); + map.put("k472", v472); + map.put("k473", v473); + map.put("k474", v474); + map.put("k475", v475); + map.put("k476", v476); + map.put("k477", v477); + map.put("k478", v478); + map.put("k479", v479); + map.put("k480", v480); + map.put("k481", v481); + map.put("k482", v482); + map.put("k483", v483); + map.put("k484", v484); + map.put("k485", v485); + map.put("k486", v486); + map.put("k487", v487); + map.put("k488", v488); + map.put("k489", v489); + map.put("k490", v490); + map.put("k491", v491); + map.put("k492", v492); + map.put("k493", v493); + map.put("k494", v494); + map.put("k495", v495); + map.put("k496", v496); + map.put("k497", v497); + map.put("k498", v498); + map.put("k499", v499); + map.put("k500", v500); + map.put("k501", v501); + map.put("k502", v502); + map.put("k503", v503); + map.put("k504", v504); + map.put("k505", v505); + map.put("k506", v506); + map.put("k507", v507); + map.put("k508", v508); + map.put("k509", v509); + map.put("k510", v510); + map.put("k511", v511); + map.put("k512", v512); + map.put("k513", v513); + map.put("k514", v514); + map.put("k515", v515); + map.put("k516", v516); + map.put("k517", v517); + map.put("k518", v518); + map.put("k519", v519); + map.put("k520", v520); + map.put("k521", v521); + map.put("k522", v522); + map.put("k523", v523); + map.put("k524", v524); + map.put("k525", v525); + map.put("k526", v526); + map.put("k527", v527); + map.put("k528", v528); + map.put("k529", v529); + map.put("k530", v530); + map.put("k531", v531); + map.put("k532", v532); + map.put("k533", v533); + map.put("k534", v534); + map.put("k535", v535); + map.put("k536", v536); + map.put("k537", v537); + map.put("k538", v538); + map.put("k539", v539); + map.put("k540", v540); + map.put("k541", v541); + map.put("k542", v542); + map.put("k543", v543); + map.put("k544", v544); + map.put("k545", v545); + map.put("k546", v546); + map.put("k547", v547); + map.put("k548", v548); + map.put("k549", v549); + map.put("k550", v550); + map.put("k551", v551); + map.put("k552", v552); + map.put("k553", v553); + map.put("k554", v554); + map.put("k555", v555); + map.put("k556", v556); + map.put("k557", v557); + map.put("k558", v558); + map.put("k559", v559); + map.put("k560", v560); + map.put("k561", v561); + map.put("k562", v562); + map.put("k563", v563); + map.put("k564", v564); + map.put("k565", v565); + map.put("k566", v566); + map.put("k567", v567); + map.put("k568", v568); + map.put("k569", v569); + map.put("k570", v570); + map.put("k571", v571); + map.put("k572", v572); + map.put("k573", v573); + map.put("k574", v574); + map.put("k575", v575); + map.put("k576", v576); + map.put("k577", v577); + map.put("k578", v578); + map.put("k579", v579); + map.put("k580", v580); + map.put("k581", v581); + map.put("k582", v582); + map.put("k583", v583); + map.put("k584", v584); + map.put("k585", v585); + map.put("k586", v586); + map.put("k587", v587); + map.put("k588", v588); + map.put("k589", v589); + map.put("k590", v590); + map.put("k591", v591); + map.put("k592", v592); + map.put("k593", v593); + map.put("k594", v594); + map.put("k595", v595); + map.put("k596", v596); + map.put("k597", v597); + map.put("k598", v598); + map.put("k599", v599); + map.put("k600", v600); + map.put("k601", v601); + map.put("k602", v602); + map.put("k603", v603); + map.put("k604", v604); + map.put("k605", v605); + map.put("k606", v606); + map.put("k607", v607); + map.put("k608", v608); + map.put("k609", v609); + map.put("k610", v610); + map.put("k611", v611); + map.put("k612", v612); + map.put("k613", v613); + map.put("k614", v614); + map.put("k615", v615); + map.put("k616", v616); + map.put("k617", v617); + map.put("k618", v618); + map.put("k619", v619); + map.put("k620", v620); + map.put("k621", v621); + map.put("k622", v622); + map.put("k623", v623); + map.put("k624", v624); + map.put("k625", v625); + map.put("k626", v626); + map.put("k627", v627); + map.put("k628", v628); + map.put("k629", v629); + map.put("k630", v630); + map.put("k631", v631); + map.put("k632", v632); + map.put("k633", v633); + map.put("k634", v634); + map.put("k635", v635); + map.put("k636", v636); + map.put("k637", v637); + map.put("k638", v638); + map.put("k639", v639); + map.put("k640", v640); + map.put("k641", v641); + map.put("k642", v642); + map.put("k643", v643); + map.put("k644", v644); + map.put("k645", v645); + map.put("k646", v646); + map.put("k647", v647); + map.put("k648", v648); + map.put("k649", v649); + map.put("k650", v650); + map.put("k651", v651); + map.put("k652", v652); + map.put("k653", v653); + map.put("k654", v654); + map.put("k655", v655); + map.put("k656", v656); + map.put("k657", v657); + map.put("k658", v658); + map.put("k659", v659); + map.put("k660", v660); + map.put("k661", v661); + map.put("k662", v662); + map.put("k663", v663); + map.put("k664", v664); + map.put("k665", v665); + map.put("k666", v666); + map.put("k667", v667); + map.put("k668", v668); + map.put("k669", v669); + map.put("k670", v670); + map.put("k671", v671); + map.put("k672", v672); + map.put("k673", v673); + map.put("k674", v674); + map.put("k675", v675); + map.put("k676", v676); + map.put("k677", v677); + map.put("k678", v678); + map.put("k679", v679); + map.put("k680", v680); + map.put("k681", v681); + map.put("k682", v682); + map.put("k683", v683); + map.put("k684", v684); + map.put("k685", v685); + map.put("k686", v686); + map.put("k687", v687); + map.put("k688", v688); + map.put("k689", v689); + map.put("k690", v690); + map.put("k691", v691); + map.put("k692", v692); + map.put("k693", v693); + map.put("k694", v694); + map.put("k695", v695); + map.put("k696", v696); + map.put("k697", v697); + map.put("k698", v698); + map.put("k699", v699); + map.put("k700", v700); + map.put("k701", v701); + map.put("k702", v702); + map.put("k703", v703); + map.put("k704", v704); + map.put("k705", v705); + map.put("k706", v706); + map.put("k707", v707); + map.put("k708", v708); + map.put("k709", v709); + map.put("k710", v710); + map.put("k711", v711); + map.put("k712", v712); + map.put("k713", v713); + map.put("k714", v714); + map.put("k715", v715); + map.put("k716", v716); + map.put("k717", v717); + map.put("k718", v718); + map.put("k719", v719); + map.put("k720", v720); + map.put("k721", v721); + map.put("k722", v722); + map.put("k723", v723); + map.put("k724", v724); + map.put("k725", v725); + map.put("k726", v726); + map.put("k727", v727); + map.put("k728", v728); + map.put("k729", v729); + map.put("k730", v730); + map.put("k731", v731); + map.put("k732", v732); + map.put("k733", v733); + map.put("k734", v734); + map.put("k735", v735); + map.put("k736", v736); + map.put("k737", v737); + map.put("k738", v738); + map.put("k739", v739); + map.put("k740", v740); + map.put("k741", v741); + map.put("k742", v742); + map.put("k743", v743); + map.put("k744", v744); + map.put("k745", v745); + map.put("k746", v746); + map.put("k747", v747); + map.put("k748", v748); + map.put("k749", v749); + map.put("k750", v750); + map.put("k751", v751); + map.put("k752", v752); + map.put("k753", v753); + map.put("k754", v754); + map.put("k755", v755); + map.put("k756", v756); + map.put("k757", v757); + map.put("k758", v758); + map.put("k759", v759); + map.put("k760", v760); + map.put("k761", v761); + map.put("k762", v762); + map.put("k763", v763); + map.put("k764", v764); + map.put("k765", v765); + map.put("k766", v766); + map.put("k767", v767); + map.put("k768", v768); + map.put("k769", v769); + map.put("k770", v770); + map.put("k771", v771); + map.put("k772", v772); + map.put("k773", v773); + map.put("k774", v774); + map.put("k775", v775); + map.put("k776", v776); + map.put("k777", v777); + map.put("k778", v778); + map.put("k779", v779); + map.put("k780", v780); + map.put("k781", v781); + map.put("k782", v782); + map.put("k783", v783); + map.put("k784", v784); + map.put("k785", v785); + map.put("k786", v786); + map.put("k787", v787); + map.put("k788", v788); + map.put("k789", v789); + map.put("k790", v790); + map.put("k791", v791); + map.put("k792", v792); + map.put("k793", v793); + map.put("k794", v794); + map.put("k795", v795); + map.put("k796", v796); + map.put("k797", v797); + map.put("k798", v798); + map.put("k799", v799); + map.put("k800", v800); + map.put("k801", v801); + map.put("k802", v802); + map.put("k803", v803); + map.put("k804", v804); + map.put("k805", v805); + map.put("k806", v806); + map.put("k807", v807); + map.put("k808", v808); + map.put("k809", v809); + map.put("k810", v810); + map.put("k811", v811); + map.put("k812", v812); + map.put("k813", v813); + map.put("k814", v814); + map.put("k815", v815); + map.put("k816", v816); + map.put("k817", v817); + map.put("k818", v818); + map.put("k819", v819); + map.put("k820", v820); + map.put("k821", v821); + map.put("k822", v822); + map.put("k823", v823); + map.put("k824", v824); + map.put("k825", v825); + map.put("k826", v826); + map.put("k827", v827); + map.put("k828", v828); + map.put("k829", v829); + map.put("k830", v830); + map.put("k831", v831); + map.put("k832", v832); + map.put("k833", v833); + map.put("k834", v834); + map.put("k835", v835); + map.put("k836", v836); + map.put("k837", v837); + map.put("k838", v838); + map.put("k839", v839); + map.put("k840", v840); + map.put("k841", v841); + map.put("k842", v842); + map.put("k843", v843); + map.put("k844", v844); + map.put("k845", v845); + map.put("k846", v846); + map.put("k847", v847); + map.put("k848", v848); + map.put("k849", v849); + map.put("k850", v850); + map.put("k851", v851); + map.put("k852", v852); + map.put("k853", v853); + map.put("k854", v854); + map.put("k855", v855); + map.put("k856", v856); + map.put("k857", v857); + map.put("k858", v858); + map.put("k859", v859); + map.put("k860", v860); + map.put("k861", v861); + map.put("k862", v862); + map.put("k863", v863); + map.put("k864", v864); + map.put("k865", v865); + map.put("k866", v866); + map.put("k867", v867); + map.put("k868", v868); + map.put("k869", v869); + map.put("k870", v870); + map.put("k871", v871); + map.put("k872", v872); + map.put("k873", v873); + map.put("k874", v874); + map.put("k875", v875); + map.put("k876", v876); + map.put("k877", v877); + map.put("k878", v878); + map.put("k879", v879); + map.put("k880", v880); + map.put("k881", v881); + map.put("k882", v882); + map.put("k883", v883); + map.put("k884", v884); + map.put("k885", v885); + map.put("k886", v886); + map.put("k887", v887); + map.put("k888", v888); + map.put("k889", v889); + map.put("k890", v890); + map.put("k891", v891); + map.put("k892", v892); + map.put("k893", v893); + map.put("k894", v894); + map.put("k895", v895); + map.put("k896", v896); + map.put("k897", v897); + map.put("k898", v898); + map.put("k899", v899); + map.put("k900", v900); + map.put("k901", v901); + map.put("k902", v902); + map.put("k903", v903); + map.put("k904", v904); + map.put("k905", v905); + map.put("k906", v906); + map.put("k907", v907); + map.put("k908", v908); + map.put("k909", v909); + map.put("k910", v910); + map.put("k911", v911); + map.put("k912", v912); + map.put("k913", v913); + map.put("k914", v914); + map.put("k915", v915); + map.put("k916", v916); + map.put("k917", v917); + map.put("k918", v918); + map.put("k919", v919); + map.put("k920", v920); + map.put("k921", v921); + map.put("k922", v922); + map.put("k923", v923); + map.put("k924", v924); + map.put("k925", v925); + map.put("k926", v926); + map.put("k927", v927); + map.put("k928", v928); + map.put("k929", v929); + map.put("k930", v930); + map.put("k931", v931); + map.put("k932", v932); + map.put("k933", v933); + map.put("k934", v934); + map.put("k935", v935); + map.put("k936", v936); + map.put("k937", v937); + map.put("k938", v938); + map.put("k939", v939); + map.put("k940", v940); + map.put("k941", v941); + map.put("k942", v942); + map.put("k943", v943); + map.put("k944", v944); + map.put("k945", v945); + map.put("k946", v946); + map.put("k947", v947); + map.put("k948", v948); + map.put("k949", v949); + map.put("k950", v950); + map.put("k951", v951); + map.put("k952", v952); + map.put("k953", v953); + map.put("k954", v954); + map.put("k955", v955); + map.put("k956", v956); + map.put("k957", v957); + map.put("k958", v958); + map.put("k959", v959); + map.put("k960", v960); + map.put("k961", v961); + map.put("k962", v962); + map.put("k963", v963); + map.put("k964", v964); + map.put("k965", v965); + map.put("k966", v966); + map.put("k967", v967); + map.put("k968", v968); + map.put("k969", v969); + map.put("k970", v970); + map.put("k971", v971); + map.put("k972", v972); + map.put("k973", v973); + map.put("k974", v974); + map.put("k975", v975); + map.put("k976", v976); + map.put("k977", v977); + map.put("k978", v978); + map.put("k979", v979); + map.put("k980", v980); + map.put("k981", v981); + map.put("k982", v982); + map.put("k983", v983); + map.put("k984", v984); + map.put("k985", v985); + map.put("k986", v986); + map.put("k987", v987); + map.put("k988", v988); + map.put("k989", v989); + map.put("k990", v990); + map.put("k991", v991); + map.put("k992", v992); + map.put("k993", v993); + map.put("k994", v994); + map.put("k995", v995); + map.put("k996", v996); + map.put("k997", v997); + map.put("k998", v998); + map.put("k999", v999); + } +} diff --git a/checker/jtreg/nullness/Issue1438c.java b/checker/jtreg/nullness/Issue1438c.java new file mode 100644 index 000000000000..07b379d31987 --- /dev/null +++ b/checker/jtreg/nullness/Issue1438c.java @@ -0,0 +1,2018 @@ +/* + * @test + * @summary Test case for issue #1438: https://github.com/typetools/checker-framework/issues/1438 + * + * @compile/fail/timeout=50 -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Alint Issue1438c.java + */ + +import java.util.HashMap; +import java.util.Map; + +public class Issue1438c { + // Do not initialize these variables. The Nullness Checker is supposed to issue an error about + // uninitialized fields. + Integer v0; + Integer v1; + Integer v2; + Integer v3; + Integer v4; + Integer v5; + Integer v6; + Integer v7; + Integer v8; + Integer v9; + Integer v10; + Integer v11; + Integer v12; + Integer v13; + Integer v14; + Integer v15; + Integer v16; + Integer v17; + Integer v18; + Integer v19; + Integer v20; + Integer v21; + Integer v22; + Integer v23; + Integer v24; + Integer v25; + Integer v26; + Integer v27; + Integer v28; + Integer v29; + Integer v30; + Integer v31; + Integer v32; + Integer v33; + Integer v34; + Integer v35; + Integer v36; + Integer v37; + Integer v38; + Integer v39; + Integer v40; + Integer v41; + Integer v42; + Integer v43; + Integer v44; + Integer v45; + Integer v46; + Integer v47; + Integer v48; + Integer v49; + Integer v50; + Integer v51; + Integer v52; + Integer v53; + Integer v54; + Integer v55; + Integer v56; + Integer v57; + Integer v58; + Integer v59; + Integer v60; + Integer v61; + Integer v62; + Integer v63; + Integer v64; + Integer v65; + Integer v66; + Integer v67; + Integer v68; + Integer v69; + Integer v70; + Integer v71; + Integer v72; + Integer v73; + Integer v74; + Integer v75; + Integer v76; + Integer v77; + Integer v78; + Integer v79; + Integer v80; + Integer v81; + Integer v82; + Integer v83; + Integer v84; + Integer v85; + Integer v86; + Integer v87; + Integer v88; + Integer v89; + Integer v90; + Integer v91; + Integer v92; + Integer v93; + Integer v94; + Integer v95; + Integer v96; + Integer v97; + Integer v98; + Integer v99; + Integer v100; + Integer v101; + Integer v102; + Integer v103; + Integer v104; + Integer v105; + Integer v106; + Integer v107; + Integer v108; + Integer v109; + Integer v110; + Integer v111; + Integer v112; + Integer v113; + Integer v114; + Integer v115; + Integer v116; + Integer v117; + Integer v118; + Integer v119; + Integer v120; + Integer v121; + Integer v122; + Integer v123; + Integer v124; + Integer v125; + Integer v126; + Integer v127; + Integer v128; + Integer v129; + Integer v130; + Integer v131; + Integer v132; + Integer v133; + Integer v134; + Integer v135; + Integer v136; + Integer v137; + Integer v138; + Integer v139; + Integer v140; + Integer v141; + Integer v142; + Integer v143; + Integer v144; + Integer v145; + Integer v146; + Integer v147; + Integer v148; + Integer v149; + Integer v150; + Integer v151; + Integer v152; + Integer v153; + Integer v154; + Integer v155; + Integer v156; + Integer v157; + Integer v158; + Integer v159; + Integer v160; + Integer v161; + Integer v162; + Integer v163; + Integer v164; + Integer v165; + Integer v166; + Integer v167; + Integer v168; + Integer v169; + Integer v170; + Integer v171; + Integer v172; + Integer v173; + Integer v174; + Integer v175; + Integer v176; + Integer v177; + Integer v178; + Integer v179; + Integer v180; + Integer v181; + Integer v182; + Integer v183; + Integer v184; + Integer v185; + Integer v186; + Integer v187; + Integer v188; + Integer v189; + Integer v190; + Integer v191; + Integer v192; + Integer v193; + Integer v194; + Integer v195; + Integer v196; + Integer v197; + Integer v198; + Integer v199; + Integer v200; + Integer v201; + Integer v202; + Integer v203; + Integer v204; + Integer v205; + Integer v206; + Integer v207; + Integer v208; + Integer v209; + Integer v210; + Integer v211; + Integer v212; + Integer v213; + Integer v214; + Integer v215; + Integer v216; + Integer v217; + Integer v218; + Integer v219; + Integer v220; + Integer v221; + Integer v222; + Integer v223; + Integer v224; + Integer v225; + Integer v226; + Integer v227; + Integer v228; + Integer v229; + Integer v230; + Integer v231; + Integer v232; + Integer v233; + Integer v234; + Integer v235; + Integer v236; + Integer v237; + Integer v238; + Integer v239; + Integer v240; + Integer v241; + Integer v242; + Integer v243; + Integer v244; + Integer v245; + Integer v246; + Integer v247; + Integer v248; + Integer v249; + Integer v250; + Integer v251; + Integer v252; + Integer v253; + Integer v254; + Integer v255; + Integer v256; + Integer v257; + Integer v258; + Integer v259; + Integer v260; + Integer v261; + Integer v262; + Integer v263; + Integer v264; + Integer v265; + Integer v266; + Integer v267; + Integer v268; + Integer v269; + Integer v270; + Integer v271; + Integer v272; + Integer v273; + Integer v274; + Integer v275; + Integer v276; + Integer v277; + Integer v278; + Integer v279; + Integer v280; + Integer v281; + Integer v282; + Integer v283; + Integer v284; + Integer v285; + Integer v286; + Integer v287; + Integer v288; + Integer v289; + Integer v290; + Integer v291; + Integer v292; + Integer v293; + Integer v294; + Integer v295; + Integer v296; + Integer v297; + Integer v298; + Integer v299; + Integer v300; + Integer v301; + Integer v302; + Integer v303; + Integer v304; + Integer v305; + Integer v306; + Integer v307; + Integer v308; + Integer v309; + Integer v310; + Integer v311; + Integer v312; + Integer v313; + Integer v314; + Integer v315; + Integer v316; + Integer v317; + Integer v318; + Integer v319; + Integer v320; + Integer v321; + Integer v322; + Integer v323; + Integer v324; + Integer v325; + Integer v326; + Integer v327; + Integer v328; + Integer v329; + Integer v330; + Integer v331; + Integer v332; + Integer v333; + Integer v334; + Integer v335; + Integer v336; + Integer v337; + Integer v338; + Integer v339; + Integer v340; + Integer v341; + Integer v342; + Integer v343; + Integer v344; + Integer v345; + Integer v346; + Integer v347; + Integer v348; + Integer v349; + Integer v350; + Integer v351; + Integer v352; + Integer v353; + Integer v354; + Integer v355; + Integer v356; + Integer v357; + Integer v358; + Integer v359; + Integer v360; + Integer v361; + Integer v362; + Integer v363; + Integer v364; + Integer v365; + Integer v366; + Integer v367; + Integer v368; + Integer v369; + Integer v370; + Integer v371; + Integer v372; + Integer v373; + Integer v374; + Integer v375; + Integer v376; + Integer v377; + Integer v378; + Integer v379; + Integer v380; + Integer v381; + Integer v382; + Integer v383; + Integer v384; + Integer v385; + Integer v386; + Integer v387; + Integer v388; + Integer v389; + Integer v390; + Integer v391; + Integer v392; + Integer v393; + Integer v394; + Integer v395; + Integer v396; + Integer v397; + Integer v398; + Integer v399; + Integer v400; + Integer v401; + Integer v402; + Integer v403; + Integer v404; + Integer v405; + Integer v406; + Integer v407; + Integer v408; + Integer v409; + Integer v410; + Integer v411; + Integer v412; + Integer v413; + Integer v414; + Integer v415; + Integer v416; + Integer v417; + Integer v418; + Integer v419; + Integer v420; + Integer v421; + Integer v422; + Integer v423; + Integer v424; + Integer v425; + Integer v426; + Integer v427; + Integer v428; + Integer v429; + Integer v430; + Integer v431; + Integer v432; + Integer v433; + Integer v434; + Integer v435; + Integer v436; + Integer v437; + Integer v438; + Integer v439; + Integer v440; + Integer v441; + Integer v442; + Integer v443; + Integer v444; + Integer v445; + Integer v446; + Integer v447; + Integer v448; + Integer v449; + Integer v450; + Integer v451; + Integer v452; + Integer v453; + Integer v454; + Integer v455; + Integer v456; + Integer v457; + Integer v458; + Integer v459; + Integer v460; + Integer v461; + Integer v462; + Integer v463; + Integer v464; + Integer v465; + Integer v466; + Integer v467; + Integer v468; + Integer v469; + Integer v470; + Integer v471; + Integer v472; + Integer v473; + Integer v474; + Integer v475; + Integer v476; + Integer v477; + Integer v478; + Integer v479; + Integer v480; + Integer v481; + Integer v482; + Integer v483; + Integer v484; + Integer v485; + Integer v486; + Integer v487; + Integer v488; + Integer v489; + Integer v490; + Integer v491; + Integer v492; + Integer v493; + Integer v494; + Integer v495; + Integer v496; + Integer v497; + Integer v498; + Integer v499; + Integer v500; + Integer v501; + Integer v502; + Integer v503; + Integer v504; + Integer v505; + Integer v506; + Integer v507; + Integer v508; + Integer v509; + Integer v510; + Integer v511; + Integer v512; + Integer v513; + Integer v514; + Integer v515; + Integer v516; + Integer v517; + Integer v518; + Integer v519; + Integer v520; + Integer v521; + Integer v522; + Integer v523; + Integer v524; + Integer v525; + Integer v526; + Integer v527; + Integer v528; + Integer v529; + Integer v530; + Integer v531; + Integer v532; + Integer v533; + Integer v534; + Integer v535; + Integer v536; + Integer v537; + Integer v538; + Integer v539; + Integer v540; + Integer v541; + Integer v542; + Integer v543; + Integer v544; + Integer v545; + Integer v546; + Integer v547; + Integer v548; + Integer v549; + Integer v550; + Integer v551; + Integer v552; + Integer v553; + Integer v554; + Integer v555; + Integer v556; + Integer v557; + Integer v558; + Integer v559; + Integer v560; + Integer v561; + Integer v562; + Integer v563; + Integer v564; + Integer v565; + Integer v566; + Integer v567; + Integer v568; + Integer v569; + Integer v570; + Integer v571; + Integer v572; + Integer v573; + Integer v574; + Integer v575; + Integer v576; + Integer v577; + Integer v578; + Integer v579; + Integer v580; + Integer v581; + Integer v582; + Integer v583; + Integer v584; + Integer v585; + Integer v586; + Integer v587; + Integer v588; + Integer v589; + Integer v590; + Integer v591; + Integer v592; + Integer v593; + Integer v594; + Integer v595; + Integer v596; + Integer v597; + Integer v598; + Integer v599; + Integer v600; + Integer v601; + Integer v602; + Integer v603; + Integer v604; + Integer v605; + Integer v606; + Integer v607; + Integer v608; + Integer v609; + Integer v610; + Integer v611; + Integer v612; + Integer v613; + Integer v614; + Integer v615; + Integer v616; + Integer v617; + Integer v618; + Integer v619; + Integer v620; + Integer v621; + Integer v622; + Integer v623; + Integer v624; + Integer v625; + Integer v626; + Integer v627; + Integer v628; + Integer v629; + Integer v630; + Integer v631; + Integer v632; + Integer v633; + Integer v634; + Integer v635; + Integer v636; + Integer v637; + Integer v638; + Integer v639; + Integer v640; + Integer v641; + Integer v642; + Integer v643; + Integer v644; + Integer v645; + Integer v646; + Integer v647; + Integer v648; + Integer v649; + Integer v650; + Integer v651; + Integer v652; + Integer v653; + Integer v654; + Integer v655; + Integer v656; + Integer v657; + Integer v658; + Integer v659; + Integer v660; + Integer v661; + Integer v662; + Integer v663; + Integer v664; + Integer v665; + Integer v666; + Integer v667; + Integer v668; + Integer v669; + Integer v670; + Integer v671; + Integer v672; + Integer v673; + Integer v674; + Integer v675; + Integer v676; + Integer v677; + Integer v678; + Integer v679; + Integer v680; + Integer v681; + Integer v682; + Integer v683; + Integer v684; + Integer v685; + Integer v686; + Integer v687; + Integer v688; + Integer v689; + Integer v690; + Integer v691; + Integer v692; + Integer v693; + Integer v694; + Integer v695; + Integer v696; + Integer v697; + Integer v698; + Integer v699; + Integer v700; + Integer v701; + Integer v702; + Integer v703; + Integer v704; + Integer v705; + Integer v706; + Integer v707; + Integer v708; + Integer v709; + Integer v710; + Integer v711; + Integer v712; + Integer v713; + Integer v714; + Integer v715; + Integer v716; + Integer v717; + Integer v718; + Integer v719; + Integer v720; + Integer v721; + Integer v722; + Integer v723; + Integer v724; + Integer v725; + Integer v726; + Integer v727; + Integer v728; + Integer v729; + Integer v730; + Integer v731; + Integer v732; + Integer v733; + Integer v734; + Integer v735; + Integer v736; + Integer v737; + Integer v738; + Integer v739; + Integer v740; + Integer v741; + Integer v742; + Integer v743; + Integer v744; + Integer v745; + Integer v746; + Integer v747; + Integer v748; + Integer v749; + Integer v750; + Integer v751; + Integer v752; + Integer v753; + Integer v754; + Integer v755; + Integer v756; + Integer v757; + Integer v758; + Integer v759; + Integer v760; + Integer v761; + Integer v762; + Integer v763; + Integer v764; + Integer v765; + Integer v766; + Integer v767; + Integer v768; + Integer v769; + Integer v770; + Integer v771; + Integer v772; + Integer v773; + Integer v774; + Integer v775; + Integer v776; + Integer v777; + Integer v778; + Integer v779; + Integer v780; + Integer v781; + Integer v782; + Integer v783; + Integer v784; + Integer v785; + Integer v786; + Integer v787; + Integer v788; + Integer v789; + Integer v790; + Integer v791; + Integer v792; + Integer v793; + Integer v794; + Integer v795; + Integer v796; + Integer v797; + Integer v798; + Integer v799; + Integer v800; + Integer v801; + Integer v802; + Integer v803; + Integer v804; + Integer v805; + Integer v806; + Integer v807; + Integer v808; + Integer v809; + Integer v810; + Integer v811; + Integer v812; + Integer v813; + Integer v814; + Integer v815; + Integer v816; + Integer v817; + Integer v818; + Integer v819; + Integer v820; + Integer v821; + Integer v822; + Integer v823; + Integer v824; + Integer v825; + Integer v826; + Integer v827; + Integer v828; + Integer v829; + Integer v830; + Integer v831; + Integer v832; + Integer v833; + Integer v834; + Integer v835; + Integer v836; + Integer v837; + Integer v838; + Integer v839; + Integer v840; + Integer v841; + Integer v842; + Integer v843; + Integer v844; + Integer v845; + Integer v846; + Integer v847; + Integer v848; + Integer v849; + Integer v850; + Integer v851; + Integer v852; + Integer v853; + Integer v854; + Integer v855; + Integer v856; + Integer v857; + Integer v858; + Integer v859; + Integer v860; + Integer v861; + Integer v862; + Integer v863; + Integer v864; + Integer v865; + Integer v866; + Integer v867; + Integer v868; + Integer v869; + Integer v870; + Integer v871; + Integer v872; + Integer v873; + Integer v874; + Integer v875; + Integer v876; + Integer v877; + Integer v878; + Integer v879; + Integer v880; + Integer v881; + Integer v882; + Integer v883; + Integer v884; + Integer v885; + Integer v886; + Integer v887; + Integer v888; + Integer v889; + Integer v890; + Integer v891; + Integer v892; + Integer v893; + Integer v894; + Integer v895; + Integer v896; + Integer v897; + Integer v898; + Integer v899; + Integer v900; + Integer v901; + Integer v902; + Integer v903; + Integer v904; + Integer v905; + Integer v906; + Integer v907; + Integer v908; + Integer v909; + Integer v910; + Integer v911; + Integer v912; + Integer v913; + Integer v914; + Integer v915; + Integer v916; + Integer v917; + Integer v918; + Integer v919; + Integer v920; + Integer v921; + Integer v922; + Integer v923; + Integer v924; + Integer v925; + Integer v926; + Integer v927; + Integer v928; + Integer v929; + Integer v930; + Integer v931; + Integer v932; + Integer v933; + Integer v934; + Integer v935; + Integer v936; + Integer v937; + Integer v938; + Integer v939; + Integer v940; + Integer v941; + Integer v942; + Integer v943; + Integer v944; + Integer v945; + Integer v946; + Integer v947; + Integer v948; + Integer v949; + Integer v950; + Integer v951; + Integer v952; + Integer v953; + Integer v954; + Integer v955; + Integer v956; + Integer v957; + Integer v958; + Integer v959; + Integer v960; + Integer v961; + Integer v962; + Integer v963; + Integer v964; + Integer v965; + Integer v966; + Integer v967; + Integer v968; + Integer v969; + Integer v970; + Integer v971; + Integer v972; + Integer v973; + Integer v974; + Integer v975; + Integer v976; + Integer v977; + Integer v978; + Integer v979; + Integer v980; + Integer v981; + Integer v982; + Integer v983; + Integer v984; + Integer v985; + Integer v986; + Integer v987; + Integer v988; + Integer v989; + Integer v990; + Integer v991; + Integer v992; + Integer v993; + Integer v994; + Integer v995; + Integer v996; + Integer v997; + Integer v998; + Integer v999; + + void foo() { + Map map = new HashMap<>(); + map.put("k0", v0); + map.put("k1", v1); + map.put("k2", v2); + map.put("k3", v3); + map.put("k4", v4); + map.put("k5", v5); + map.put("k6", v6); + map.put("k7", v7); + map.put("k8", v8); + map.put("k9", v9); + map.put("k10", v10); + map.put("k11", v11); + map.put("k12", v12); + map.put("k13", v13); + map.put("k14", v14); + map.put("k15", v15); + map.put("k16", v16); + map.put("k17", v17); + map.put("k18", v18); + map.put("k19", v19); + map.put("k20", v20); + map.put("k21", v21); + map.put("k22", v22); + map.put("k23", v23); + map.put("k24", v24); + map.put("k25", v25); + map.put("k26", v26); + map.put("k27", v27); + map.put("k28", v28); + map.put("k29", v29); + map.put("k30", v30); + map.put("k31", v31); + map.put("k32", v32); + map.put("k33", v33); + map.put("k34", v34); + map.put("k35", v35); + map.put("k36", v36); + map.put("k37", v37); + map.put("k38", v38); + map.put("k39", v39); + map.put("k40", v40); + map.put("k41", v41); + map.put("k42", v42); + map.put("k43", v43); + map.put("k44", v44); + map.put("k45", v45); + map.put("k46", v46); + map.put("k47", v47); + map.put("k48", v48); + map.put("k49", v49); + map.put("k50", v50); + map.put("k51", v51); + map.put("k52", v52); + map.put("k53", v53); + map.put("k54", v54); + map.put("k55", v55); + map.put("k56", v56); + map.put("k57", v57); + map.put("k58", v58); + map.put("k59", v59); + map.put("k60", v60); + map.put("k61", v61); + map.put("k62", v62); + map.put("k63", v63); + map.put("k64", v64); + map.put("k65", v65); + map.put("k66", v66); + map.put("k67", v67); + map.put("k68", v68); + map.put("k69", v69); + map.put("k70", v70); + map.put("k71", v71); + map.put("k72", v72); + map.put("k73", v73); + map.put("k74", v74); + map.put("k75", v75); + map.put("k76", v76); + map.put("k77", v77); + map.put("k78", v78); + map.put("k79", v79); + map.put("k80", v80); + map.put("k81", v81); + map.put("k82", v82); + map.put("k83", v83); + map.put("k84", v84); + map.put("k85", v85); + map.put("k86", v86); + map.put("k87", v87); + map.put("k88", v88); + map.put("k89", v89); + map.put("k90", v90); + map.put("k91", v91); + map.put("k92", v92); + map.put("k93", v93); + map.put("k94", v94); + map.put("k95", v95); + map.put("k96", v96); + map.put("k97", v97); + map.put("k98", v98); + map.put("k99", v99); + map.put("k100", v100); + map.put("k101", v101); + map.put("k102", v102); + map.put("k103", v103); + map.put("k104", v104); + map.put("k105", v105); + map.put("k106", v106); + map.put("k107", v107); + map.put("k108", v108); + map.put("k109", v109); + map.put("k110", v110); + map.put("k111", v111); + map.put("k112", v112); + map.put("k113", v113); + map.put("k114", v114); + map.put("k115", v115); + map.put("k116", v116); + map.put("k117", v117); + map.put("k118", v118); + map.put("k119", v119); + map.put("k120", v120); + map.put("k121", v121); + map.put("k122", v122); + map.put("k123", v123); + map.put("k124", v124); + map.put("k125", v125); + map.put("k126", v126); + map.put("k127", v127); + map.put("k128", v128); + map.put("k129", v129); + map.put("k130", v130); + map.put("k131", v131); + map.put("k132", v132); + map.put("k133", v133); + map.put("k134", v134); + map.put("k135", v135); + map.put("k136", v136); + map.put("k137", v137); + map.put("k138", v138); + map.put("k139", v139); + map.put("k140", v140); + map.put("k141", v141); + map.put("k142", v142); + map.put("k143", v143); + map.put("k144", v144); + map.put("k145", v145); + map.put("k146", v146); + map.put("k147", v147); + map.put("k148", v148); + map.put("k149", v149); + map.put("k150", v150); + map.put("k151", v151); + map.put("k152", v152); + map.put("k153", v153); + map.put("k154", v154); + map.put("k155", v155); + map.put("k156", v156); + map.put("k157", v157); + map.put("k158", v158); + map.put("k159", v159); + map.put("k160", v160); + map.put("k161", v161); + map.put("k162", v162); + map.put("k163", v163); + map.put("k164", v164); + map.put("k165", v165); + map.put("k166", v166); + map.put("k167", v167); + map.put("k168", v168); + map.put("k169", v169); + map.put("k170", v170); + map.put("k171", v171); + map.put("k172", v172); + map.put("k173", v173); + map.put("k174", v174); + map.put("k175", v175); + map.put("k176", v176); + map.put("k177", v177); + map.put("k178", v178); + map.put("k179", v179); + map.put("k180", v180); + map.put("k181", v181); + map.put("k182", v182); + map.put("k183", v183); + map.put("k184", v184); + map.put("k185", v185); + map.put("k186", v186); + map.put("k187", v187); + map.put("k188", v188); + map.put("k189", v189); + map.put("k190", v190); + map.put("k191", v191); + map.put("k192", v192); + map.put("k193", v193); + map.put("k194", v194); + map.put("k195", v195); + map.put("k196", v196); + map.put("k197", v197); + map.put("k198", v198); + map.put("k199", v199); + map.put("k200", v200); + map.put("k201", v201); + map.put("k202", v202); + map.put("k203", v203); + map.put("k204", v204); + map.put("k205", v205); + map.put("k206", v206); + map.put("k207", v207); + map.put("k208", v208); + map.put("k209", v209); + map.put("k210", v210); + map.put("k211", v211); + map.put("k212", v212); + map.put("k213", v213); + map.put("k214", v214); + map.put("k215", v215); + map.put("k216", v216); + map.put("k217", v217); + map.put("k218", v218); + map.put("k219", v219); + map.put("k220", v220); + map.put("k221", v221); + map.put("k222", v222); + map.put("k223", v223); + map.put("k224", v224); + map.put("k225", v225); + map.put("k226", v226); + map.put("k227", v227); + map.put("k228", v228); + map.put("k229", v229); + map.put("k230", v230); + map.put("k231", v231); + map.put("k232", v232); + map.put("k233", v233); + map.put("k234", v234); + map.put("k235", v235); + map.put("k236", v236); + map.put("k237", v237); + map.put("k238", v238); + map.put("k239", v239); + map.put("k240", v240); + map.put("k241", v241); + map.put("k242", v242); + map.put("k243", v243); + map.put("k244", v244); + map.put("k245", v245); + map.put("k246", v246); + map.put("k247", v247); + map.put("k248", v248); + map.put("k249", v249); + map.put("k250", v250); + map.put("k251", v251); + map.put("k252", v252); + map.put("k253", v253); + map.put("k254", v254); + map.put("k255", v255); + map.put("k256", v256); + map.put("k257", v257); + map.put("k258", v258); + map.put("k259", v259); + map.put("k260", v260); + map.put("k261", v261); + map.put("k262", v262); + map.put("k263", v263); + map.put("k264", v264); + map.put("k265", v265); + map.put("k266", v266); + map.put("k267", v267); + map.put("k268", v268); + map.put("k269", v269); + map.put("k270", v270); + map.put("k271", v271); + map.put("k272", v272); + map.put("k273", v273); + map.put("k274", v274); + map.put("k275", v275); + map.put("k276", v276); + map.put("k277", v277); + map.put("k278", v278); + map.put("k279", v279); + map.put("k280", v280); + map.put("k281", v281); + map.put("k282", v282); + map.put("k283", v283); + map.put("k284", v284); + map.put("k285", v285); + map.put("k286", v286); + map.put("k287", v287); + map.put("k288", v288); + map.put("k289", v289); + map.put("k290", v290); + map.put("k291", v291); + map.put("k292", v292); + map.put("k293", v293); + map.put("k294", v294); + map.put("k295", v295); + map.put("k296", v296); + map.put("k297", v297); + map.put("k298", v298); + map.put("k299", v299); + map.put("k300", v300); + map.put("k301", v301); + map.put("k302", v302); + map.put("k303", v303); + map.put("k304", v304); + map.put("k305", v305); + map.put("k306", v306); + map.put("k307", v307); + map.put("k308", v308); + map.put("k309", v309); + map.put("k310", v310); + map.put("k311", v311); + map.put("k312", v312); + map.put("k313", v313); + map.put("k314", v314); + map.put("k315", v315); + map.put("k316", v316); + map.put("k317", v317); + map.put("k318", v318); + map.put("k319", v319); + map.put("k320", v320); + map.put("k321", v321); + map.put("k322", v322); + map.put("k323", v323); + map.put("k324", v324); + map.put("k325", v325); + map.put("k326", v326); + map.put("k327", v327); + map.put("k328", v328); + map.put("k329", v329); + map.put("k330", v330); + map.put("k331", v331); + map.put("k332", v332); + map.put("k333", v333); + map.put("k334", v334); + map.put("k335", v335); + map.put("k336", v336); + map.put("k337", v337); + map.put("k338", v338); + map.put("k339", v339); + map.put("k340", v340); + map.put("k341", v341); + map.put("k342", v342); + map.put("k343", v343); + map.put("k344", v344); + map.put("k345", v345); + map.put("k346", v346); + map.put("k347", v347); + map.put("k348", v348); + map.put("k349", v349); + map.put("k350", v350); + map.put("k351", v351); + map.put("k352", v352); + map.put("k353", v353); + map.put("k354", v354); + map.put("k355", v355); + map.put("k356", v356); + map.put("k357", v357); + map.put("k358", v358); + map.put("k359", v359); + map.put("k360", v360); + map.put("k361", v361); + map.put("k362", v362); + map.put("k363", v363); + map.put("k364", v364); + map.put("k365", v365); + map.put("k366", v366); + map.put("k367", v367); + map.put("k368", v368); + map.put("k369", v369); + map.put("k370", v370); + map.put("k371", v371); + map.put("k372", v372); + map.put("k373", v373); + map.put("k374", v374); + map.put("k375", v375); + map.put("k376", v376); + map.put("k377", v377); + map.put("k378", v378); + map.put("k379", v379); + map.put("k380", v380); + map.put("k381", v381); + map.put("k382", v382); + map.put("k383", v383); + map.put("k384", v384); + map.put("k385", v385); + map.put("k386", v386); + map.put("k387", v387); + map.put("k388", v388); + map.put("k389", v389); + map.put("k390", v390); + map.put("k391", v391); + map.put("k392", v392); + map.put("k393", v393); + map.put("k394", v394); + map.put("k395", v395); + map.put("k396", v396); + map.put("k397", v397); + map.put("k398", v398); + map.put("k399", v399); + map.put("k400", v400); + map.put("k401", v401); + map.put("k402", v402); + map.put("k403", v403); + map.put("k404", v404); + map.put("k405", v405); + map.put("k406", v406); + map.put("k407", v407); + map.put("k408", v408); + map.put("k409", v409); + map.put("k410", v410); + map.put("k411", v411); + map.put("k412", v412); + map.put("k413", v413); + map.put("k414", v414); + map.put("k415", v415); + map.put("k416", v416); + map.put("k417", v417); + map.put("k418", v418); + map.put("k419", v419); + map.put("k420", v420); + map.put("k421", v421); + map.put("k422", v422); + map.put("k423", v423); + map.put("k424", v424); + map.put("k425", v425); + map.put("k426", v426); + map.put("k427", v427); + map.put("k428", v428); + map.put("k429", v429); + map.put("k430", v430); + map.put("k431", v431); + map.put("k432", v432); + map.put("k433", v433); + map.put("k434", v434); + map.put("k435", v435); + map.put("k436", v436); + map.put("k437", v437); + map.put("k438", v438); + map.put("k439", v439); + map.put("k440", v440); + map.put("k441", v441); + map.put("k442", v442); + map.put("k443", v443); + map.put("k444", v444); + map.put("k445", v445); + map.put("k446", v446); + map.put("k447", v447); + map.put("k448", v448); + map.put("k449", v449); + map.put("k450", v450); + map.put("k451", v451); + map.put("k452", v452); + map.put("k453", v453); + map.put("k454", v454); + map.put("k455", v455); + map.put("k456", v456); + map.put("k457", v457); + map.put("k458", v458); + map.put("k459", v459); + map.put("k460", v460); + map.put("k461", v461); + map.put("k462", v462); + map.put("k463", v463); + map.put("k464", v464); + map.put("k465", v465); + map.put("k466", v466); + map.put("k467", v467); + map.put("k468", v468); + map.put("k469", v469); + map.put("k470", v470); + map.put("k471", v471); + map.put("k472", v472); + map.put("k473", v473); + map.put("k474", v474); + map.put("k475", v475); + map.put("k476", v476); + map.put("k477", v477); + map.put("k478", v478); + map.put("k479", v479); + map.put("k480", v480); + map.put("k481", v481); + map.put("k482", v482); + map.put("k483", v483); + map.put("k484", v484); + map.put("k485", v485); + map.put("k486", v486); + map.put("k487", v487); + map.put("k488", v488); + map.put("k489", v489); + map.put("k490", v490); + map.put("k491", v491); + map.put("k492", v492); + map.put("k493", v493); + map.put("k494", v494); + map.put("k495", v495); + map.put("k496", v496); + map.put("k497", v497); + map.put("k498", v498); + map.put("k499", v499); + map.put("k500", v500); + map.put("k501", v501); + map.put("k502", v502); + map.put("k503", v503); + map.put("k504", v504); + map.put("k505", v505); + map.put("k506", v506); + map.put("k507", v507); + map.put("k508", v508); + map.put("k509", v509); + map.put("k510", v510); + map.put("k511", v511); + map.put("k512", v512); + map.put("k513", v513); + map.put("k514", v514); + map.put("k515", v515); + map.put("k516", v516); + map.put("k517", v517); + map.put("k518", v518); + map.put("k519", v519); + map.put("k520", v520); + map.put("k521", v521); + map.put("k522", v522); + map.put("k523", v523); + map.put("k524", v524); + map.put("k525", v525); + map.put("k526", v526); + map.put("k527", v527); + map.put("k528", v528); + map.put("k529", v529); + map.put("k530", v530); + map.put("k531", v531); + map.put("k532", v532); + map.put("k533", v533); + map.put("k534", v534); + map.put("k535", v535); + map.put("k536", v536); + map.put("k537", v537); + map.put("k538", v538); + map.put("k539", v539); + map.put("k540", v540); + map.put("k541", v541); + map.put("k542", v542); + map.put("k543", v543); + map.put("k544", v544); + map.put("k545", v545); + map.put("k546", v546); + map.put("k547", v547); + map.put("k548", v548); + map.put("k549", v549); + map.put("k550", v550); + map.put("k551", v551); + map.put("k552", v552); + map.put("k553", v553); + map.put("k554", v554); + map.put("k555", v555); + map.put("k556", v556); + map.put("k557", v557); + map.put("k558", v558); + map.put("k559", v559); + map.put("k560", v560); + map.put("k561", v561); + map.put("k562", v562); + map.put("k563", v563); + map.put("k564", v564); + map.put("k565", v565); + map.put("k566", v566); + map.put("k567", v567); + map.put("k568", v568); + map.put("k569", v569); + map.put("k570", v570); + map.put("k571", v571); + map.put("k572", v572); + map.put("k573", v573); + map.put("k574", v574); + map.put("k575", v575); + map.put("k576", v576); + map.put("k577", v577); + map.put("k578", v578); + map.put("k579", v579); + map.put("k580", v580); + map.put("k581", v581); + map.put("k582", v582); + map.put("k583", v583); + map.put("k584", v584); + map.put("k585", v585); + map.put("k586", v586); + map.put("k587", v587); + map.put("k588", v588); + map.put("k589", v589); + map.put("k590", v590); + map.put("k591", v591); + map.put("k592", v592); + map.put("k593", v593); + map.put("k594", v594); + map.put("k595", v595); + map.put("k596", v596); + map.put("k597", v597); + map.put("k598", v598); + map.put("k599", v599); + map.put("k600", v600); + map.put("k601", v601); + map.put("k602", v602); + map.put("k603", v603); + map.put("k604", v604); + map.put("k605", v605); + map.put("k606", v606); + map.put("k607", v607); + map.put("k608", v608); + map.put("k609", v609); + map.put("k610", v610); + map.put("k611", v611); + map.put("k612", v612); + map.put("k613", v613); + map.put("k614", v614); + map.put("k615", v615); + map.put("k616", v616); + map.put("k617", v617); + map.put("k618", v618); + map.put("k619", v619); + map.put("k620", v620); + map.put("k621", v621); + map.put("k622", v622); + map.put("k623", v623); + map.put("k624", v624); + map.put("k625", v625); + map.put("k626", v626); + map.put("k627", v627); + map.put("k628", v628); + map.put("k629", v629); + map.put("k630", v630); + map.put("k631", v631); + map.put("k632", v632); + map.put("k633", v633); + map.put("k634", v634); + map.put("k635", v635); + map.put("k636", v636); + map.put("k637", v637); + map.put("k638", v638); + map.put("k639", v639); + map.put("k640", v640); + map.put("k641", v641); + map.put("k642", v642); + map.put("k643", v643); + map.put("k644", v644); + map.put("k645", v645); + map.put("k646", v646); + map.put("k647", v647); + map.put("k648", v648); + map.put("k649", v649); + map.put("k650", v650); + map.put("k651", v651); + map.put("k652", v652); + map.put("k653", v653); + map.put("k654", v654); + map.put("k655", v655); + map.put("k656", v656); + map.put("k657", v657); + map.put("k658", v658); + map.put("k659", v659); + map.put("k660", v660); + map.put("k661", v661); + map.put("k662", v662); + map.put("k663", v663); + map.put("k664", v664); + map.put("k665", v665); + map.put("k666", v666); + map.put("k667", v667); + map.put("k668", v668); + map.put("k669", v669); + map.put("k670", v670); + map.put("k671", v671); + map.put("k672", v672); + map.put("k673", v673); + map.put("k674", v674); + map.put("k675", v675); + map.put("k676", v676); + map.put("k677", v677); + map.put("k678", v678); + map.put("k679", v679); + map.put("k680", v680); + map.put("k681", v681); + map.put("k682", v682); + map.put("k683", v683); + map.put("k684", v684); + map.put("k685", v685); + map.put("k686", v686); + map.put("k687", v687); + map.put("k688", v688); + map.put("k689", v689); + map.put("k690", v690); + map.put("k691", v691); + map.put("k692", v692); + map.put("k693", v693); + map.put("k694", v694); + map.put("k695", v695); + map.put("k696", v696); + map.put("k697", v697); + map.put("k698", v698); + map.put("k699", v699); + map.put("k700", v700); + map.put("k701", v701); + map.put("k702", v702); + map.put("k703", v703); + map.put("k704", v704); + map.put("k705", v705); + map.put("k706", v706); + map.put("k707", v707); + map.put("k708", v708); + map.put("k709", v709); + map.put("k710", v710); + map.put("k711", v711); + map.put("k712", v712); + map.put("k713", v713); + map.put("k714", v714); + map.put("k715", v715); + map.put("k716", v716); + map.put("k717", v717); + map.put("k718", v718); + map.put("k719", v719); + map.put("k720", v720); + map.put("k721", v721); + map.put("k722", v722); + map.put("k723", v723); + map.put("k724", v724); + map.put("k725", v725); + map.put("k726", v726); + map.put("k727", v727); + map.put("k728", v728); + map.put("k729", v729); + map.put("k730", v730); + map.put("k731", v731); + map.put("k732", v732); + map.put("k733", v733); + map.put("k734", v734); + map.put("k735", v735); + map.put("k736", v736); + map.put("k737", v737); + map.put("k738", v738); + map.put("k739", v739); + map.put("k740", v740); + map.put("k741", v741); + map.put("k742", v742); + map.put("k743", v743); + map.put("k744", v744); + map.put("k745", v745); + map.put("k746", v746); + map.put("k747", v747); + map.put("k748", v748); + map.put("k749", v749); + map.put("k750", v750); + map.put("k751", v751); + map.put("k752", v752); + map.put("k753", v753); + map.put("k754", v754); + map.put("k755", v755); + map.put("k756", v756); + map.put("k757", v757); + map.put("k758", v758); + map.put("k759", v759); + map.put("k760", v760); + map.put("k761", v761); + map.put("k762", v762); + map.put("k763", v763); + map.put("k764", v764); + map.put("k765", v765); + map.put("k766", v766); + map.put("k767", v767); + map.put("k768", v768); + map.put("k769", v769); + map.put("k770", v770); + map.put("k771", v771); + map.put("k772", v772); + map.put("k773", v773); + map.put("k774", v774); + map.put("k775", v775); + map.put("k776", v776); + map.put("k777", v777); + map.put("k778", v778); + map.put("k779", v779); + map.put("k780", v780); + map.put("k781", v781); + map.put("k782", v782); + map.put("k783", v783); + map.put("k784", v784); + map.put("k785", v785); + map.put("k786", v786); + map.put("k787", v787); + map.put("k788", v788); + map.put("k789", v789); + map.put("k790", v790); + map.put("k791", v791); + map.put("k792", v792); + map.put("k793", v793); + map.put("k794", v794); + map.put("k795", v795); + map.put("k796", v796); + map.put("k797", v797); + map.put("k798", v798); + map.put("k799", v799); + map.put("k800", v800); + map.put("k801", v801); + map.put("k802", v802); + map.put("k803", v803); + map.put("k804", v804); + map.put("k805", v805); + map.put("k806", v806); + map.put("k807", v807); + map.put("k808", v808); + map.put("k809", v809); + map.put("k810", v810); + map.put("k811", v811); + map.put("k812", v812); + map.put("k813", v813); + map.put("k814", v814); + map.put("k815", v815); + map.put("k816", v816); + map.put("k817", v817); + map.put("k818", v818); + map.put("k819", v819); + map.put("k820", v820); + map.put("k821", v821); + map.put("k822", v822); + map.put("k823", v823); + map.put("k824", v824); + map.put("k825", v825); + map.put("k826", v826); + map.put("k827", v827); + map.put("k828", v828); + map.put("k829", v829); + map.put("k830", v830); + map.put("k831", v831); + map.put("k832", v832); + map.put("k833", v833); + map.put("k834", v834); + map.put("k835", v835); + map.put("k836", v836); + map.put("k837", v837); + map.put("k838", v838); + map.put("k839", v839); + map.put("k840", v840); + map.put("k841", v841); + map.put("k842", v842); + map.put("k843", v843); + map.put("k844", v844); + map.put("k845", v845); + map.put("k846", v846); + map.put("k847", v847); + map.put("k848", v848); + map.put("k849", v849); + map.put("k850", v850); + map.put("k851", v851); + map.put("k852", v852); + map.put("k853", v853); + map.put("k854", v854); + map.put("k855", v855); + map.put("k856", v856); + map.put("k857", v857); + map.put("k858", v858); + map.put("k859", v859); + map.put("k860", v860); + map.put("k861", v861); + map.put("k862", v862); + map.put("k863", v863); + map.put("k864", v864); + map.put("k865", v865); + map.put("k866", v866); + map.put("k867", v867); + map.put("k868", v868); + map.put("k869", v869); + map.put("k870", v870); + map.put("k871", v871); + map.put("k872", v872); + map.put("k873", v873); + map.put("k874", v874); + map.put("k875", v875); + map.put("k876", v876); + map.put("k877", v877); + map.put("k878", v878); + map.put("k879", v879); + map.put("k880", v880); + map.put("k881", v881); + map.put("k882", v882); + map.put("k883", v883); + map.put("k884", v884); + map.put("k885", v885); + map.put("k886", v886); + map.put("k887", v887); + map.put("k888", v888); + map.put("k889", v889); + map.put("k890", v890); + map.put("k891", v891); + map.put("k892", v892); + map.put("k893", v893); + map.put("k894", v894); + map.put("k895", v895); + map.put("k896", v896); + map.put("k897", v897); + map.put("k898", v898); + map.put("k899", v899); + map.put("k900", v900); + map.put("k901", v901); + map.put("k902", v902); + map.put("k903", v903); + map.put("k904", v904); + map.put("k905", v905); + map.put("k906", v906); + map.put("k907", v907); + map.put("k908", v908); + map.put("k909", v909); + map.put("k910", v910); + map.put("k911", v911); + map.put("k912", v912); + map.put("k913", v913); + map.put("k914", v914); + map.put("k915", v915); + map.put("k916", v916); + map.put("k917", v917); + map.put("k918", v918); + map.put("k919", v919); + map.put("k920", v920); + map.put("k921", v921); + map.put("k922", v922); + map.put("k923", v923); + map.put("k924", v924); + map.put("k925", v925); + map.put("k926", v926); + map.put("k927", v927); + map.put("k928", v928); + map.put("k929", v929); + map.put("k930", v930); + map.put("k931", v931); + map.put("k932", v932); + map.put("k933", v933); + map.put("k934", v934); + map.put("k935", v935); + map.put("k936", v936); + map.put("k937", v937); + map.put("k938", v938); + map.put("k939", v939); + map.put("k940", v940); + map.put("k941", v941); + map.put("k942", v942); + map.put("k943", v943); + map.put("k944", v944); + map.put("k945", v945); + map.put("k946", v946); + map.put("k947", v947); + map.put("k948", v948); + map.put("k949", v949); + map.put("k950", v950); + map.put("k951", v951); + map.put("k952", v952); + map.put("k953", v953); + map.put("k954", v954); + map.put("k955", v955); + map.put("k956", v956); + map.put("k957", v957); + map.put("k958", v958); + map.put("k959", v959); + map.put("k960", v960); + map.put("k961", v961); + map.put("k962", v962); + map.put("k963", v963); + map.put("k964", v964); + map.put("k965", v965); + map.put("k966", v966); + map.put("k967", v967); + map.put("k968", v968); + map.put("k969", v969); + map.put("k970", v970); + map.put("k971", v971); + map.put("k972", v972); + map.put("k973", v973); + map.put("k974", v974); + map.put("k975", v975); + map.put("k976", v976); + map.put("k977", v977); + map.put("k978", v978); + map.put("k979", v979); + map.put("k980", v980); + map.put("k981", v981); + map.put("k982", v982); + map.put("k983", v983); + map.put("k984", v984); + map.put("k985", v985); + map.put("k986", v986); + map.put("k987", v987); + map.put("k988", v988); + map.put("k989", v989); + map.put("k990", v990); + map.put("k991", v991); + map.put("k992", v992); + map.put("k993", v993); + map.put("k994", v994); + map.put("k995", v995); + map.put("k996", v996); + map.put("k997", v997); + map.put("k998", v998); + map.put("k999", v999); + } +} diff --git a/checker/jtreg/nullness/Issue347-con.out b/checker/jtreg/nullness/Issue347-con.out index 833f06db43f4..644fabd8daed 100644 --- a/checker/jtreg/nullness/Issue347-con.out +++ b/checker/jtreg/nullness/Issue347-con.out @@ -1,2 +1,2 @@ -Issue347.java:33:9: compiler.err.proc.messager: [dereference.of.nullable] dereference of possibly-null reference nble -1 error \ No newline at end of file +Issue347.java:32:9: compiler.err.proc.messager: [dereference.of.nullable] dereference of possibly-null reference nble +1 error diff --git a/checker/jtreg/nullness/Issue347.java b/checker/jtreg/nullness/Issue347.java index 0e77c70c242f..29692279f035 100644 --- a/checker/jtreg/nullness/Issue347.java +++ b/checker/jtreg/nullness/Issue347.java @@ -9,7 +9,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; -class Issue347 { +public class Issue347 { @MonotonicNonNull String mono; @@ -19,9 +19,8 @@ void testMono() { if (mono == null) { return; } - // The object referenced by mono might change, but - // it can't become null again, even in concurrent - // semantics. + // The object referenced by mono might change, but it can't become null again, even in + // concurrent semantics. mono.toString(); } diff --git a/checker/jtreg/nullness/Issue373-err.out b/checker/jtreg/nullness/Issue373-err.out index 0d7c7495a85d..bce55df40f17 100644 --- a/checker/jtreg/nullness/Issue373-err.out +++ b/checker/jtreg/nullness/Issue373-err.out @@ -1,8 +1,8 @@ Issue373.java:16:15: compiler.err.proc.messager: [override.return.invalid] Incompatible return type. -Method - Set> entrySet(Issue373 this) in Issue373 -cannot override - Set> entrySet(AbstractMap this) in java.util.AbstractMap found : Set> required: Set> +Consequence: method in Issue373 + Set> entrySet(Issue373 this) +cannot override method in AbstractMap + Set> entrySet(AbstractMap this) 1 error diff --git a/checker/jtreg/nullness/Issue373-warn.out b/checker/jtreg/nullness/Issue373-warn.out index 3fc702dee339..dc38acb20ed8 100644 --- a/checker/jtreg/nullness/Issue373-warn.out +++ b/checker/jtreg/nullness/Issue373-warn.out @@ -1,8 +1,8 @@ Issue373.java:16:15: compiler.warn.proc.messager: [override.return.invalid] Incompatible return type. -Method - Set> entrySet(Issue373 this) in Issue373 -cannot override - Set> entrySet(AbstractMap this) in java.util.AbstractMap found : Set> required: Set> +Consequence: method in Issue373 + Set> entrySet(Issue373 this) +cannot override method in AbstractMap + Set> entrySet(AbstractMap this) 1 warning diff --git a/checker/jtreg/nullness/Issue373.java b/checker/jtreg/nullness/Issue373.java index e2034bc3f5df..a11a6697dead 100644 --- a/checker/jtreg/nullness/Issue373.java +++ b/checker/jtreg/nullness/Issue373.java @@ -11,7 +11,7 @@ import java.util.Map; import java.util.Set; -class Issue373 extends AbstractMap { +public class Issue373 extends AbstractMap { @Override public Set> entrySet() { return Collections.emptySet(); diff --git a/checker/jtreg/nullness/Issue4948.java b/checker/jtreg/nullness/Issue4948.java new file mode 100644 index 000000000000..4c33e0568436 --- /dev/null +++ b/checker/jtreg/nullness/Issue4948.java @@ -0,0 +1,1225 @@ +/* + * @test + * @summary Test case for issue #4948: https://github.com/typetools/checker-framework/issues/4948 + * + * @compile/timeout=30 -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Alint Issue4948.java + */ +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.PolyNull; + +import java.util.stream.Stream; + +@SuppressWarnings("nullness") // Check for timeout. +public class Issue4948 { + + static class MyClass { + + // No type argumment inference. + static MyClass of2(@Nullable Object[]... p) { + throw new RuntimeException(); + } + + // Poly qualifiers + static MyClass<@PolyNull Object[]> of3(@PolyNull Object[]... p) { + throw new RuntimeException(); + } + } + + static Stream parseTest() { + return Stream.of( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[1], + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } + + static Stream parseTest2() { + return Stream.of( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } + + static Stream parseTest4() { + return Stream.of( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } + + static MyClass parseTest11() { + return MyClass.of2( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } + + static MyClass parseTestB4() { + return MyClass.of3( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}, + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } + + static Stream parseTestB9() { + return Stream.of( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } + + static Stream parseTestB10() { + return Stream.of( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } + + static Stream parseTestB11() { + return Stream.of( + new Object[] {"12:15:24", 125}, + new Object[] {"3:27", 5456}, + new Object[] {"-1:15:4:00", 54665453}, + new Object[] {"124:7:56:00", 45}, + new Object[] {"10", null}, + new Object[] {"10:74:00:00", null}, + new Object[] {"10:00:60:00", null}, + new Object[] {"10:00:00:05", null}, + new Object[] {"12#15:24", null}, + new Object[] {"12:15#24", null}, + new Object[] {"12:15:24#10", null}, + new Object[] {"x12:15:24", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"12:15:24x", null}, + new Object[] {"#####", -2}, + new Object[] {"*****", -1}, + new Object[] {"<<<<<", Integer.MIN_VALUE}, + new Object[] {">>>>>", Integer.MAX_VALUE}); + } +} diff --git a/checker/jtreg/nullness/Issue6373.java b/checker/jtreg/nullness/Issue6373.java new file mode 100644 index 000000000000..1c17141251ae --- /dev/null +++ b/checker/jtreg/nullness/Issue6373.java @@ -0,0 +1,139 @@ +/* + * @test + * @summary Test case for issue #6373: https://github.com/typetools/checker-framework/issues/6373 + * + * @requires jdk.version.major >= 10 + * @compile/timeout=80 -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker Issue6373.java + */ +public class Issue6373 { + + abstract static class C1< + C extends C1, + Q extends C2, + B extends C3, + D extends C4, + CR extends C5> + extends C6 {} + + static class C6 {} + + abstract static class C2< + C extends C1, + Q extends C2, + B extends C3, + D extends C4, + RT extends C5> + implements C7 {} + + abstract static class C3< + C extends C1, + Q extends C2, + B extends C3, + D extends C4, + R extends C5> {} + + abstract static class C4< + C extends C1, + Q extends C2, + B extends C3, + D extends C4, + R extends C5> { + interface I {} + } + + abstract static class C5> implements C7 {} + + interface C7 {} + + abstract static class C8< + C extends C1, + Q extends C2, + B extends C3, + D extends C4, + CR extends C5, + RpT extends C5> { + + public static < + C extends C1, + Q extends C2, + B extends C3, + D extends C4, + CR extends C5, + RpT extends C5> + Builder n(Q q) { + throw new AssertionError(); + } + + abstract static class Builder< + C extends C1, + Q extends C2, + B extends C3, + D extends C4, + CR extends C5, + RpT extends C5> { + + public abstract Builder f(C9 p); + + public C8 b() { + throw new AssertionError(); + } + } + } + + abstract static class C9> {} + + static final class C10 extends C9 {} + + abstract static class C11, B extends C11> {} + + static final class C12 extends C11 { + + public C10 b() { + throw new AssertionError(); + } + } + + static class C13 { + + public static final C12 n() { + return new C12(); + } + + static final class C14 extends C1 {} + + static final class C15 extends C2 {} + + static final class C18 extends C5 {} + + static class C17 extends C4 implements C4.I, C19 {} + + static final class C16 extends C3 { + + public C15 b() { + throw new AssertionError(); + } + } + + static final C16 q() { + throw new AssertionError(); + } + } + + interface C19 {} + + void f() { + var x = C8.n(C13.q().b()).f(C13.n().b()).b(); + var y = x; + x = y; + x = y; + x = y; + } + + void g() { + Object x = C8.n(C13.q().b()).f(C13.n().b()).b(); + Object y = x; + x = y; + x = y; + x = y; + } +} diff --git a/checker/jtreg/nullness/PersistUtil.java b/checker/jtreg/nullness/PersistUtil.java index 8fa8884f6214..1ce20637f246 100644 --- a/checker/jtreg/nullness/PersistUtil.java +++ b/checker/jtreg/nullness/PersistUtil.java @@ -2,23 +2,27 @@ // is added to the invocation of the compiler! // TODO: add a @Processor method-annotation to parameterize -/** - * This class has auxiliary methods to compile a class and return its classfile. It is used by - * defaultPersists/Driver and inheritDeclAnnoPersist/Driver. - */ import com.sun.tools.classfile.ClassFile; + import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; +import java.io.UncheckedIOException; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.StringJoiner; +/** + * This class has auxiliary methods to compile a class and return its classfile. It is used by + * defaultPersists/Driver and inheritDeclAnnoPersist/Driver. + */ public class PersistUtil { public static String testClassOf(Method m) { @@ -38,9 +42,9 @@ public static ClassFile compileAndReturn(String fullFile, String testClass) thro public static File writeTestFile(String fullFile) throws IOException { File f = new File("Test.java"); - PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(f))); - out.println(fullFile); - out.close(); + try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(f)))) { + out.println(fullFile); + } return f; } @@ -48,8 +52,7 @@ public static File compileTestFile(File f, String testClass) { int rc = com.sun.tools.javac.Main.compile( new String[] { - "-source", - "1.8", + "-AnoJreVersionCheck", "-g", "-processor", "org.checkerframework.checker.nullness.NullnessChecker", @@ -58,14 +61,28 @@ public static File compileTestFile(File f, String testClass) { if (rc != 0) { throw new Error("compilation failed. rc=" + rc); } - String path; - if (f.getParent() != null) { - path = f.getParent(); - } else { - path = ""; + + File result = new File(f.getParent(), testClass + ".class"); + + // This diagnostic code preserves temporary files and prints the paths where they are + // preserved. + if (false) { + try { + File tempDir = new File(System.getProperty("java.io.tmpdir")); + File fCopy = File.createTempFile("FCopy", ".java", tempDir); + File resultCopy = File.createTempFile("FCopy", ".class", tempDir); + // REPLACE_EXISTING is essential in the `Files.copy()` calls because createTempFile + // actually creates a file in addition to returning its name. + Files.copy(f.toPath(), fCopy.toPath(), StandardCopyOption.REPLACE_EXISTING); + Files.copy( + result.toPath(), resultCopy.toPath(), StandardCopyOption.REPLACE_EXISTING); + System.out.printf("comileTestFile: copied to %s %s%n", fCopy, resultCopy); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } - return new File(path + testClass + ".class"); + return result; } public static String wrap(String compact) { diff --git a/checker/jtreg/nullness/UnneededSuppressionsClassPrefixed.java b/checker/jtreg/nullness/UnneededSuppressionsClassPrefixed.java new file mode 100644 index 000000000000..bce171b1e3cf --- /dev/null +++ b/checker/jtreg/nullness/UnneededSuppressionsClassPrefixed.java @@ -0,0 +1,15 @@ +/* + * @test + * @summary Test -AwarnUnneededSuppressions + * + * @compile/ref=UnneededSuppressionsClassPrefixed.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -AwarnUnneededSuppressions UnneededSuppressionsClassPrefixed.java + */ + +@SuppressWarnings("nullness:unneeded.suppression") +class UnneededSuppressionsClassAnnotated { + + @SuppressWarnings("nullness:return") + public String getClassAndUid0() { + return "hello"; + } +} diff --git a/checker/jtreg/nullness/UnneededSuppressionsClassPrefixed.out b/checker/jtreg/nullness/UnneededSuppressionsClassPrefixed.out new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/checker/jtreg/nullness/UnneededSuppressionsClassPrefixedRequirePrefix.java b/checker/jtreg/nullness/UnneededSuppressionsClassPrefixedRequirePrefix.java new file mode 100644 index 000000000000..c3c23a99d988 --- /dev/null +++ b/checker/jtreg/nullness/UnneededSuppressionsClassPrefixedRequirePrefix.java @@ -0,0 +1,15 @@ +/* + * @test + * @summary Test -AwarnUnneededSuppressions + * + * @compile/ref=UnneededSuppressionsClassPrefixedRequirePrefix.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -AwarnUnneededSuppressions -ArequirePrefixInWarningSuppressions UnneededSuppressionsClassPrefixedRequirePrefix.java + */ + +@SuppressWarnings("nullness:unneeded.suppression") +class UnneededSuppressionsClassAnnotated { + + @SuppressWarnings("nullness:return") + public String getClassAndUid0() { + return "hello"; + } +} diff --git a/checker/jtreg/nullness/UnneededSuppressionsClassPrefixedRequirePrefix.out b/checker/jtreg/nullness/UnneededSuppressionsClassPrefixedRequirePrefix.out new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/checker/jtreg/nullness/UnneededSuppressionsClassUnprefixed.java b/checker/jtreg/nullness/UnneededSuppressionsClassUnprefixed.java new file mode 100644 index 000000000000..4aa9fc665123 --- /dev/null +++ b/checker/jtreg/nullness/UnneededSuppressionsClassUnprefixed.java @@ -0,0 +1,15 @@ +/* + * @test + * @summary Test -AwarnUnneededSuppressions + * + * @compile/ref=UnneededSuppressionsClassUnprefixed.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -AwarnUnneededSuppressions UnneededSuppressionsClassUnprefixed.java + */ + +@SuppressWarnings("unneeded.suppression") +class UnneededSuppressionsClassAnnotated { + + @SuppressWarnings("nullness:return.type.incompatible") + public String getClassAndUid0() { + return "hello"; + } +} diff --git a/checker/jtreg/nullness/UnneededSuppressionsClassUnprefixed.out b/checker/jtreg/nullness/UnneededSuppressionsClassUnprefixed.out new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/checker/jtreg/nullness/UnneededSuppressionsClassUnprefixedRequirePrefix.java b/checker/jtreg/nullness/UnneededSuppressionsClassUnprefixedRequirePrefix.java new file mode 100644 index 000000000000..9365586d3544 --- /dev/null +++ b/checker/jtreg/nullness/UnneededSuppressionsClassUnprefixedRequirePrefix.java @@ -0,0 +1,15 @@ +/* + * @test + * @summary Test -AwarnUnneededSuppressions + * + * @compile/ref=UnneededSuppressionsClassUnprefixedRequirePrefix.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -AwarnUnneededSuppressions -ArequirePrefixInWarningSuppressions UnneededSuppressionsClassUnprefixedRequirePrefix.java + */ + +@SuppressWarnings("unneeded.suppression") +class UnneededSuppressionsClassAnnotated { + + @SuppressWarnings("nullness:return.type.incompatible") + public String getClassAndUid0() { + return "hello"; + } +} diff --git a/checker/jtreg/nullness/UnneededSuppressionsClassUnprefixedRequirePrefix.out b/checker/jtreg/nullness/UnneededSuppressionsClassUnprefixedRequirePrefix.out new file mode 100644 index 000000000000..c4eaea84f314 --- /dev/null +++ b/checker/jtreg/nullness/UnneededSuppressionsClassUnprefixedRequirePrefix.out @@ -0,0 +1,2 @@ +UnneededSuppressionsClassUnprefixedRequirePrefix.java:11:5: compiler.warn.proc.messager: [nullness:unneeded.suppression] warning suppression "nullness:return.type.incompatible" is not used by NullnessChecker +1 warning diff --git a/checker/jtreg/nullness/UnneededSuppressionsTest.java b/checker/jtreg/nullness/UnneededSuppressionsTest.java new file mode 100644 index 000000000000..81202e127964 --- /dev/null +++ b/checker/jtreg/nullness/UnneededSuppressionsTest.java @@ -0,0 +1,34 @@ +/* + * @test + * @summary Test -AwarnUnneededSuppressions + * + * @compile/ref=UnneededSuppressionsTest.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -AwarnUnneededSuppressions UnneededSuppressionsTest.java + */ + +class UnneededSuppressionsTest { + + @SuppressWarnings({"nullness:return.type.incompatible"}) + public String getClassAndUid1() { + return "hello"; + } + + @SuppressWarnings({"nullness:return.type.incompatible", "unneeded.suppression"}) + public String getClassAndUid2() { + return "hello"; + } + + @SuppressWarnings({"nullness:return.type.incompatible", "nullness:unneeded.suppression"}) + public String getClassAndUid3() { + return "hello"; + } + + @SuppressWarnings({"unneeded.suppression", "nullness:return.type.incompatible"}) + public String getClassAndUid5() { + return "hello"; + } + + @SuppressWarnings({"nullness:unneeded.suppression", "nullness:return.type.incompatible"}) + public String getClassAndUid6() { + return "hello"; + } +} diff --git a/checker/jtreg/nullness/UnneededSuppressionsTest.out b/checker/jtreg/nullness/UnneededSuppressionsTest.out new file mode 100644 index 000000000000..e6b8d69ed4e0 --- /dev/null +++ b/checker/jtreg/nullness/UnneededSuppressionsTest.out @@ -0,0 +1,2 @@ +UnneededSuppressionsTest.java:10:5: compiler.warn.proc.messager: [unneeded.suppression] warning suppression "nullness:return.type.incompatible" is not used by NullnessChecker +1 warning diff --git a/checker/jtreg/nullness/UnneededSuppressionsTestRequirePrefix.java b/checker/jtreg/nullness/UnneededSuppressionsTestRequirePrefix.java new file mode 100644 index 000000000000..a526a029f259 --- /dev/null +++ b/checker/jtreg/nullness/UnneededSuppressionsTestRequirePrefix.java @@ -0,0 +1,37 @@ +/* + * @test + * @summary Test -AwarnUnneededSuppressions + * + * @compile/ref=UnneededSuppressionsTestRequirePrefix.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -AwarnUnneededSuppressions -ArequirePrefixInWarningSuppressions UnneededSuppressionsTestRequirePrefix.java + */ + +class UnneededSuppressionsTest { + + @SuppressWarnings({"nullness:return.type.incompatible"}) + public String getClassAndUid1() { + return "hello"; + } + + @SuppressWarnings({"nullness:return.type.incompatible", "unneeded.suppression"}) + public String getClassAndUid2() { + return "hello"; + } + + @SuppressWarnings({"nullness:return.type.incompatible", "nullness:unneeded.suppression"}) + public String getClassAndUid3() { + return "hello"; + } + + @SuppressWarnings({ + "unneeded.suppression.type.incompatible", + "nullness:return.type.incompatible" + }) + public String getClassAndUid5() { + return "hello"; + } + + @SuppressWarnings({"nullness:unneeded.suppression", "nullness:return.type.incompatible"}) + public String getClassAndUid6() { + return "hello"; + } +} diff --git a/checker/jtreg/nullness/UnneededSuppressionsTestRequirePrefix.out b/checker/jtreg/nullness/UnneededSuppressionsTestRequirePrefix.out new file mode 100644 index 000000000000..6fab81e209fe --- /dev/null +++ b/checker/jtreg/nullness/UnneededSuppressionsTestRequirePrefix.out @@ -0,0 +1,4 @@ +UnneededSuppressionsTestRequirePrefix.java:10:5: compiler.warn.proc.messager: [nullness:unneeded.suppression] warning suppression "nullness:return.type.incompatible" is not used by NullnessChecker +UnneededSuppressionsTestRequirePrefix.java:15:5: compiler.warn.proc.messager: [nullness:unneeded.suppression] warning suppression "nullness:return.type.incompatible" is not used by NullnessChecker +UnneededSuppressionsTestRequirePrefix.java:25:5: compiler.warn.proc.messager: [nullness:unneeded.suppression] warning suppression "nullness:return.type.incompatible" is not used by NullnessChecker +3 warnings diff --git a/checker/jtreg/nullness/annotationsOnExtends/Other.java b/checker/jtreg/nullness/annotationsOnExtends/Other.java index cca0222e1349..f0cc1b66948b 100644 --- a/checker/jtreg/nullness/annotationsOnExtends/Other.java +++ b/checker/jtreg/nullness/annotationsOnExtends/Other.java @@ -6,7 +6,7 @@ * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker Other.java Test.java Test2.java */ -class Other { +public class Other { void foo() { Test other = null; Test2 other2 = null; diff --git a/checker/jtreg/nullness/annotationsOnExtends/Test.java b/checker/jtreg/nullness/annotationsOnExtends/Test.java index 6c52b6ff38f6..6fd8099f6964 100644 --- a/checker/jtreg/nullness/annotationsOnExtends/Test.java +++ b/checker/jtreg/nullness/annotationsOnExtends/Test.java @@ -1,3 +1,3 @@ import java.io.Serializable; -class Test implements Serializable {} +public class Test implements Serializable {} diff --git a/checker/jtreg/nullness/annotationsOnExtends/Test2.java b/checker/jtreg/nullness/annotationsOnExtends/Test2.java index ede2457b4616..68e0782c524c 100644 --- a/checker/jtreg/nullness/annotationsOnExtends/Test2.java +++ b/checker/jtreg/nullness/annotationsOnExtends/Test2.java @@ -1 +1 @@ -class Test2 extends Object {} +public class Test2 extends Object {} diff --git a/checker/jtreg/nullness/constructor-initialization/DefaultConstructor.out b/checker/jtreg/nullness/constructor-initialization/DefaultConstructor.out index 1de7b5f4716d..71b260dfc017 100644 --- a/checker/jtreg/nullness/constructor-initialization/DefaultConstructor.out +++ b/checker/jtreg/nullness/constructor-initialization/DefaultConstructor.out @@ -1,2 +1,2 @@ -DefaultConstructor.java:9:8: compiler.err.proc.messager: [initialization.fields.uninitialized] the constructor does not initialize fields: nullObject -1 error \ No newline at end of file +DefaultConstructor.java:10:12: compiler.err.proc.messager: [initialization.field.uninitialized] the default constructor does not initialize field nullObject +1 error diff --git a/checker/jtreg/nullness/constructor-initialization/NonDefaultConstructor.java b/checker/jtreg/nullness/constructor-initialization/NonDefaultConstructor.java index 248a33ad6f6c..43023cae6b37 100644 --- a/checker/jtreg/nullness/constructor-initialization/NonDefaultConstructor.java +++ b/checker/jtreg/nullness/constructor-initialization/NonDefaultConstructor.java @@ -6,7 +6,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -class NonDefaultConstructor { +public class NonDefaultConstructor { Object nonNull = 4; Object nullObject; @MonotonicNonNull Object lazyField; diff --git a/checker/jtreg/nullness/defaultsPersist/Classes.java b/checker/jtreg/nullness/defaultsPersist/Classes.java index 4c5d9f8fa044..3064a45e285e 100644 --- a/checker/jtreg/nullness/defaultsPersist/Classes.java +++ b/checker/jtreg/nullness/defaultsPersist/Classes.java @@ -71,6 +71,16 @@ public String extendsDefault3() { type = CLASS_TYPE_PARAMETER_BOUND, paramIndex = 0, boundIndex = 0), + // Annotations on the implicit constructor, which the test ignores. + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/NonNull", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/initialization/qual/UnderInitialization", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/UnknownKeyFor", + // type = METHOD_RETURN), }) public String typeParams1() { return "class Test {}"; @@ -104,6 +114,16 @@ public String typeParams1() { type = CLASS_TYPE_PARAMETER_BOUND, paramIndex = 0, boundIndex = 0), + // Annotations on the implicit constructor, which the test ignores. + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/NonNull", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/initialization/qual/UnderInitialization", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/UnknownKeyFor", + // type = METHOD_RETURN), }) public String typeParams2() { return "class Test {}"; @@ -137,6 +157,16 @@ public String typeParams2() { type = CLASS_TYPE_PARAMETER_BOUND, paramIndex = 0, boundIndex = 1), + // Annotations on the implicit constructor, which the test ignores. + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/NonNull", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/initialization/qual/UnderInitialization", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/UnknownKeyFor", + // type = METHOD_RETURN), }) public String typeParams3() { return "class Test> {}"; @@ -197,6 +227,16 @@ public String typeParams3() { type = CLASS_TYPE_PARAMETER_BOUND, paramIndex = 1, boundIndex = 1), + // Annotations on the implicit constructor, which the test ignores. + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/NonNull", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/initialization/qual/UnderInitialization", + // type = METHOD_RETURN), + // @TADescription( + // annotation = "org/checkerframework/checker/nullness/qual/UnknownKeyFor", + // type = METHOD_RETURN), }) public String typeParams4() { return "class Test> {}"; diff --git a/checker/jtreg/nullness/defaultsPersist/Driver.java b/checker/jtreg/nullness/defaultsPersist/Driver.java index 663555f2ff91..cd45901ec970 100644 --- a/checker/jtreg/nullness/defaultsPersist/Driver.java +++ b/checker/jtreg/nullness/defaultsPersist/Driver.java @@ -8,6 +8,7 @@ import com.sun.tools.classfile.ClassFile; import com.sun.tools.classfile.TypeAnnotation; import com.sun.tools.classfile.TypeAnnotation.TargetType; + import java.io.PrintStream; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -16,14 +17,14 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; -import org.checkerframework.javacutil.Pair; public class Driver { private static final PrintStream out = System.out; + // The argument is in the format expected by Class.forName(). public static void main(String[] args) throws Exception { - if (args.length == 0 || args.length > 1) { + if (args.length != 1) { throw new IllegalArgumentException("Usage: java Driver "); } String name = args[0]; @@ -38,7 +39,7 @@ protected void runDriver(Object object) throws Exception { // Find methods for (Method method : clazz.getMethods()) { - List> expected = expectedOf(method); + List expected = expectedOf(method); if (expected == null) { continue; } @@ -52,8 +53,17 @@ protected void runDriver(Object object) throws Exception { String compact = (String) method.invoke(object); String fullFile = PersistUtil.wrap(compact); ClassFile cf = PersistUtil.compileAndReturn(fullFile, testClass); - List actual = ReferenceInfoUtil.extendedAnnotationsOf(cf); - ReferenceInfoUtil.compare(expected, actual, cf); + boolean ignoreConstructors = !clazz.getName().equals("Constructors"); + List actual = + ReferenceInfoUtil.extendedAnnotationsOf(cf, ignoreConstructors); + String diagnostic = + String.join( + "; ", + "Tests for " + clazz.getName(), + "compact=" + compact, + "fullFile=" + fullFile, + "testClass=" + testClass); + ReferenceInfoUtil.compare(expected, actual, cf, diagnostic); out.println("PASSED: " + method.getName()); ++passed; } catch (Throwable e) { @@ -74,7 +84,7 @@ protected void runDriver(Object object) throws Exception { } } - private List> expectedOf(Method m) { + private List expectedOf(Method m) { TADescription ta = m.getAnnotation(TADescription.class); TADescriptions tas = m.getAnnotation(TADescriptions.class); @@ -82,7 +92,7 @@ private List> expectedOf(Method m) { return null; } - List> result = new ArrayList<>(); + List result = new ArrayList<>(); if (ta != null) { result.add(expectedOf(ta)); @@ -97,7 +107,7 @@ private List> expectedOf(Method m) { return result; } - private Pair expectedOf(TADescription d) { + private AnnoPosPair expectedOf(TADescription d) { String annoName = d.annotation(); TypeAnnotation.Position p = new TypeAnnotation.Position(); @@ -132,7 +142,7 @@ private Pair expectedOf(TADescription d) { wrapIntArray(d.genericLocation())); } - return Pair.of(annoName, p); + return AnnoPosPair.of(annoName, p); } private List wrapIntArray(int[] ints) { @@ -146,6 +156,37 @@ private List wrapIntArray(int[] ints) { public static final int NOT_SET = -888; } +/** A pair of an annotation name and a position. */ +class AnnoPosPair { + /** The first element of the pair. */ + public final String first; + + /** The second element of the pair. */ + public final TypeAnnotation.Position second; + + /** + * Creates a new immutable pair. Clients should use {@link #of}. + * + * @param first the first element of the pair + * @param second the second element of the pair + */ + private AnnoPosPair(String first, TypeAnnotation.Position second) { + this.first = first; + this.second = second; + } + + /** + * Creates a new immutable pair. + * + * @param first first argument + * @param second second argument + * @return a pair of the values (first, second) + */ + public static AnnoPosPair of(String first, TypeAnnotation.Position second) { + return new AnnoPosPair(first, second); + } +} + @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface TADescription { diff --git a/checker/jtreg/nullness/defaultsPersist/Fields.java b/checker/jtreg/nullness/defaultsPersist/Fields.java index a3499ab51312..95ecc80b88bc 100644 --- a/checker/jtreg/nullness/defaultsPersist/Fields.java +++ b/checker/jtreg/nullness/defaultsPersist/Fields.java @@ -243,6 +243,7 @@ public String wildcards1() { genericLocation = {3, 0, 2, 0, 3, 0, 0, 0}), }) public String wildcards2() { - return "java.util.List> f = new java.util.ArrayList<>();"; + return "java.util.List> f = new" + + " java.util.ArrayList<>();"; } } diff --git a/checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java b/checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java index 1743208fbc72..d7044639bb01 100644 --- a/checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java +++ b/checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java @@ -1,6 +1,6 @@ // Keep somewhat in sync with // langtools/test/tools/javac/annotations/typeAnnotations/referenceinfos/ReferenceInfoUtil.java -// Adapted to handled the same type qualifier appearing multiple times. +// Adapted to handle the same type qualifier appearing multiple times. import com.sun.tools.classfile.Attribute; import com.sun.tools.classfile.ClassFile; @@ -11,24 +11,37 @@ import com.sun.tools.classfile.Method; import com.sun.tools.classfile.RuntimeTypeAnnotations_attribute; import com.sun.tools.classfile.TypeAnnotation; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.checkerframework.javacutil.Pair; -import org.checkerframework.javacutil.PluginUtil; public class ReferenceInfoUtil { public static final int IGNORE_VALUE = -321; - public static List extendedAnnotationsOf(ClassFile cf) { + /** If true, don't collect annotations on constructors. */ + boolean ignoreConstructors; + + /** + * Creates a new ReferenceInfoUtil. + * + * @param ignoreConstructors if true, don't collect annotations on constructor + */ + public ReferenceInfoUtil(boolean ignoreConstructors) { + this.ignoreConstructors = ignoreConstructors; + } + + public static List extendedAnnotationsOf( + ClassFile cf, boolean ignoreConstructors) { + ReferenceInfoUtil riu = new ReferenceInfoUtil(ignoreConstructors); List annos = new ArrayList<>(); - findAnnotations(cf, annos); + riu.findAnnotations(cf, annos); return annos; } /////////////////// Extract type annotations ////////////////// - private static void findAnnotations(ClassFile cf, List annos) { + private void findAnnotations(ClassFile cf, List annos) { findAnnotations(cf, Attribute.RuntimeVisibleTypeAnnotations, annos); findAnnotations(cf, Attribute.RuntimeInvisibleTypeAnnotations, annos); @@ -36,6 +49,19 @@ private static void findAnnotations(ClassFile cf, List annos) { findAnnotations(cf, f, annos); } for (Method m : cf.methods) { + String methodName; + try { + methodName = m.getName(cf.constant_pool); + } catch (Exception e) { + throw new Error(e); + } + // This method, `findAnnotations`, aims to extract annotations from one method. + // In JDK 17, constructors are included in ClassFile.methods(); in JDK 11, they are not. + // Therefore, this if statement is required in JDK 17, and has no effect in JDK 11. + if (ignoreConstructors && methodName.equals("")) { + continue; + } + findAnnotations(cf, m, annos); } } @@ -159,7 +185,8 @@ && areEquals(p1.type_index, p2.type_index) public static String positionCompareStr( TypeAnnotation.Position p1, TypeAnnotation.Position p2) { - return PluginUtil.joinLines( + return String.join( + System.lineSeparator(), "type = " + p1.type + ", " + p2.type, "offset = " + p1.offset + ", " + p2.offset, "lvarOffset = " + p1.lvarOffset + ", " + p2.lvarOffset, @@ -194,22 +221,30 @@ private static TypeAnnotation findAnnotation( } public static boolean compare( - List> expectedAnnos, + List expectedAnnos, List actualAnnos, - ClassFile cf) + ClassFile cf, + String diagnostic) throws InvalidIndex, UnexpectedEntry { if (actualAnnos.size() != expectedAnnos.size()) { throw new ComparisonException( - "Wrong number of annotations", expectedAnnos, actualAnnos); + "Wrong number of annotations in " + cf + "; " + diagnostic, + expectedAnnos, + actualAnnos); } - for (Pair e : expectedAnnos) { + for (AnnoPosPair e : expectedAnnos) { String aName = e.first; TypeAnnotation.Position expected = e.second; TypeAnnotation actual = findAnnotation(aName, expected, actualAnnos, cf); if (actual == null) { throw new ComparisonException( - "Expected annotation not found: " + aName + " position: " + expected, + "Expected annotation not found: " + + aName + + " position: " + + expected + + "; " + + diagnostic, expectedAnnos, actualAnnos); } @@ -221,27 +256,19 @@ public static boolean compare( class ComparisonException extends RuntimeException { private static final long serialVersionUID = -3930499712333815821L; - public final List> expected; + public final List expected; public final List found; public ComparisonException( - String message, - List> expected, - List found) { + String message, List expected, List found) { super(message); this.expected = expected; this.found = found; } public String toString() { - return PluginUtil.joinLines( - super.toString(), - "\tExpected: " - + expected.size() - + " annotations; but found: " - + found.size() - + " annotations", - " Expected: " + expected, - " Found: " + found); + return String.format( + "%s%n Expected (%d): %s%s Found (%d): %s", + super.toString(), expected.size(), expected, found.size(), found); } } diff --git a/checker/jtreg/nullness/eisop673/Lib.java b/checker/jtreg/nullness/eisop673/Lib.java new file mode 100644 index 000000000000..42810b179d80 --- /dev/null +++ b/checker/jtreg/nullness/eisop673/Lib.java @@ -0,0 +1,5 @@ +import org.jetbrains.annotations.Nullable; + +interface Lib { + @Nullable String[] get(); +} diff --git a/checker/jtreg/nullness/eisop673/Lib7.class b/checker/jtreg/nullness/eisop673/Lib7.class new file mode 100644 index 000000000000..7363a0642c7d Binary files /dev/null and b/checker/jtreg/nullness/eisop673/Lib7.class differ diff --git a/checker/jtreg/nullness/eisop673/Lib7.source b/checker/jtreg/nullness/eisop673/Lib7.source new file mode 100644 index 000000000000..5f8f35e68789 --- /dev/null +++ b/checker/jtreg/nullness/eisop673/Lib7.source @@ -0,0 +1,5 @@ +import org.jetbrains.annotations.Nullable; + +interface Lib7 { + @Nullable String[] get(); +} diff --git a/checker/jtreg/nullness/eisop673/User.java b/checker/jtreg/nullness/eisop673/User.java new file mode 100644 index 000000000000..20e4b80c61eb --- /dev/null +++ b/checker/jtreg/nullness/eisop673/User.java @@ -0,0 +1,16 @@ +/* + * @test + * @summary Test case for EISOP issue #673: https://github.com/eisop/checker-framework/issues/673 + * + * `Lib7.class` is the result of compiling `Lib7.source` (with a `.java` extension) with a Java 7 + * compiler (or with `--release 7` for Java <= 19 compiler). + * + * @compile Lib.java + * @compile/fail/ref=User.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker User.java + */ +class User { + void go(Lib lib, Lib7 lib7) { + String[] b = lib.get(); + String[] b7 = lib7.get(); + } +} diff --git a/checker/jtreg/nullness/eisop673/User.out b/checker/jtreg/nullness/eisop673/User.out new file mode 100644 index 000000000000..7e87c3618017 --- /dev/null +++ b/checker/jtreg/nullness/eisop673/User.out @@ -0,0 +1,4 @@ +User.java:13:29: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. +found : @Nullable String @Nullable [] +required: @NonNull String @Nullable [] +1 error diff --git a/checker/jtreg/nullness/inheritDeclAnnoPersist/Driver.java b/checker/jtreg/nullness/inheritDeclAnnoPersist/Driver.java index f8e2f0d8dba9..604a470ac7e7 100644 --- a/checker/jtreg/nullness/inheritDeclAnnoPersist/Driver.java +++ b/checker/jtreg/nullness/inheritDeclAnnoPersist/Driver.java @@ -3,6 +3,7 @@ import com.sun.tools.classfile.Annotation; import com.sun.tools.classfile.ClassFile; + import java.io.PrintStream; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -16,8 +17,9 @@ public class Driver { private static final PrintStream out = System.out; + // The argument is in the format expected by Class.forName(). public static void main(String[] args) throws Exception { - if (args.length == 0 || args.length > 1) { + if (args.length != 1) { throw new IllegalArgumentException("Usage: java Driver "); } String name = args[0]; @@ -47,7 +49,14 @@ protected void runDriver(Object object) throws Exception { String fullFile = PersistUtil.wrap(compact); ClassFile cf = PersistUtil.compileAndReturn(fullFile, testClass); List actual = ReferenceInfoUtil.extendedAnnotationsOf(cf); - ReferenceInfoUtil.compare(expected, actual, cf); + String diagnostic = + String.join( + "; ", + "Tests for " + clazz.getName(), + "compact=" + compact, + "fullFile=" + fullFile, + "testClass=" + testClass); + ReferenceInfoUtil.compare(expected, actual, cf, diagnostic); out.println("PASSED: " + method.getName()); ++passed; } catch (Throwable e) { diff --git a/checker/jtreg/nullness/inheritDeclAnnoPersist/Extends.java b/checker/jtreg/nullness/inheritDeclAnnoPersist/Extends.java index f895a3027aac..224feb2b133b 100644 --- a/checker/jtreg/nullness/inheritDeclAnnoPersist/Extends.java +++ b/checker/jtreg/nullness/inheritDeclAnnoPersist/Extends.java @@ -1,5 +1,3 @@ -import org.checkerframework.javacutil.PluginUtil; - /* * @test * @summary Test that inherited declaration annotations are stored in bytecode. @@ -24,8 +22,7 @@ public String m2() { } // Issue 342 - // We do not want that behavior with related annotations. @Pure should - // override @SideEffectFree. + // We do not want that behavior with related annotations. @Pure should override @SideEffectFree. @ADescriptions({ @ADescription(annotation = "org/checkerframework/dataflow/qual/Pure"), @ADescription(annotation = "org/checkerframework/dataflow/qual/SideEffectFree") @@ -37,7 +34,10 @@ public String m3() { class TestWrapper { public static String wrap(String... method) { - return PluginUtil.joinLines( - "class Test extends Super {", PluginUtil.joinLines(method), "}"); + return "class Test extends Super {" + + System.lineSeparator() + + String.join(System.lineSeparator(), method) + + System.lineSeparator() + + "}"; } } diff --git a/checker/jtreg/nullness/inheritDeclAnnoPersist/Implements.java b/checker/jtreg/nullness/inheritDeclAnnoPersist/Implements.java index 247916f2b7ed..096f49b32232 100644 --- a/checker/jtreg/nullness/inheritDeclAnnoPersist/Implements.java +++ b/checker/jtreg/nullness/inheritDeclAnnoPersist/Implements.java @@ -1,5 +1,3 @@ -import org.checkerframework.javacutil.PluginUtil; - /* * @test * @summary Test that inherited declaration annotations are stored in bytecode. @@ -23,7 +21,10 @@ public String m1() { class TestWrapper { public static String wrap(String... method) { - return PluginUtil.joinLines( - "class Test extends AbstractClass {", PluginUtil.joinLines(method), "}"); + return String.join( + System.lineSeparator(), + "class Test extends AbstractClass {", + String.join(System.lineSeparator(), method), + "}"); } } diff --git a/checker/jtreg/nullness/inheritDeclAnnoPersist/ReferenceInfoUtil.java b/checker/jtreg/nullness/inheritDeclAnnoPersist/ReferenceInfoUtil.java index 02c9c8256e00..4dac1fbbf44e 100644 --- a/checker/jtreg/nullness/inheritDeclAnnoPersist/ReferenceInfoUtil.java +++ b/checker/jtreg/nullness/inheritDeclAnnoPersist/ReferenceInfoUtil.java @@ -1,6 +1,6 @@ // Keep somewhat in sync with // langtools/test/tools/javac/annotations/typeAnnotations/referenceinfos/ReferenceInfoUtil.java -// Adapted to handled the same type qualifier appearing multiple times. +// Adapted to handle the same type qualifier appearing multiple times. import com.sun.tools.classfile.Annotation; import com.sun.tools.classfile.Attribute; @@ -9,10 +9,10 @@ import com.sun.tools.classfile.ConstantPool.UnexpectedEntry; import com.sun.tools.classfile.Method; import com.sun.tools.classfile.RuntimeAnnotations_attribute; + import java.util.ArrayList; import java.util.List; import java.util.StringJoiner; -import org.checkerframework.javacutil.PluginUtil; public class ReferenceInfoUtil { @@ -64,17 +64,20 @@ private static Annotation findAnnotation( } public static boolean compare( - List expectedAnnos, List actualAnnos, ClassFile cf) + List expectedAnnos, + List actualAnnos, + ClassFile cf, + String diagnostic) throws InvalidIndex, UnexpectedEntry { if (actualAnnos.size() != expectedAnnos.size()) { throw new ComparisonException( - "Wrong number of annotations", expectedAnnos, actualAnnos, cf); + "Wrong number of annotations; " + diagnostic, expectedAnnos, actualAnnos, cf); } for (String annoName : expectedAnnos) { Annotation anno = findAnnotation(annoName, actualAnnos, cf); if (anno == null) { throw new ComparisonException( - "Expected annotation not found: " + annoName, + "Expected annotation not found: " + annoName + "; " + diagnostic, expectedAnnos, actualAnnos, cf); @@ -123,7 +126,8 @@ public String toString() { throw new RuntimeException(e); } } - return PluginUtil.joinLines( + return String.join( + System.lineSeparator(), super.toString(), "\tExpected: " + expected.size() diff --git a/checker/jtreg/nullness/inheritDeclAnnoPersist/Super.java b/checker/jtreg/nullness/inheritDeclAnnoPersist/Super.java index 7acb37b8cc56..2ab7a324c59c 100644 --- a/checker/jtreg/nullness/inheritDeclAnnoPersist/Super.java +++ b/checker/jtreg/nullness/inheritDeclAnnoPersist/Super.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.dataflow.qual.SideEffectFree; -class Super { +public class Super { Object f; Object g; Object h; diff --git a/checker/jtreg/nullness/issue12/BinaryDefaultTest.java b/checker/jtreg/nullness/issue12/BinaryDefaultTest.java index 25d0a305f4ac..42408204f7ce 100644 --- a/checker/jtreg/nullness/issue12/BinaryDefaultTest.java +++ b/checker/jtreg/nullness/issue12/BinaryDefaultTest.java @@ -10,7 +10,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -class BinaryDefaultTest { +public class BinaryDefaultTest { void test1(@NonNull BinaryDefaultTestInterface bar, @Nullable BinaryDefaultTestInterface bar2) { @Nullable BinaryDefaultTestBinary foo = BinaryDefaultTestBinary.foo(bar); @Nullable BinaryDefaultTestBinary baz = BinaryDefaultTestBinary.foo(bar2); diff --git a/checker/jtreg/nullness/issue12/BinaryDefaultTestWithStub.java b/checker/jtreg/nullness/issue12/BinaryDefaultTestWithStub.java index 0eee2c843a98..9bf213bc4d61 100644 --- a/checker/jtreg/nullness/issue12/BinaryDefaultTestWithStub.java +++ b/checker/jtreg/nullness/issue12/BinaryDefaultTestWithStub.java @@ -12,7 +12,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -class BinaryDefaultTestWithStub { +public class BinaryDefaultTestWithStub { void test1(@NonNull BinaryDefaultTestInterface bar, @Nullable BinaryDefaultTestInterface bar2) { @Nullable BinaryDefaultTestBinary foo = BinaryDefaultTestBinary.foo(bar); @Nullable BinaryDefaultTestBinary baz = BinaryDefaultTestBinary.foo(bar2); diff --git a/checker/jtreg/nullness/issue141/CharStreams.java b/checker/jtreg/nullness/issue141/CharStreams.java index 6dafa32eee88..75a7c761efab 100644 --- a/checker/jtreg/nullness/issue141/CharStreams.java +++ b/checker/jtreg/nullness/issue141/CharStreams.java @@ -1,4 +1,4 @@ -class CharStreams { +public class CharStreams { static void copy( InputSupplier from, OutputSupplier to) {} } diff --git a/checker/jtreg/nullness/issue141/Driver.java b/checker/jtreg/nullness/issue141/Driver.java index 91db31d3bde8..e1bf7ecdf5e5 100644 --- a/checker/jtreg/nullness/issue141/Driver.java +++ b/checker/jtreg/nullness/issue141/Driver.java @@ -9,4 +9,4 @@ * @compile -XDrawDiagnostics -processor org.checkerframework.checker.regex.RegexChecker Files.java * */ -class Driver {} +public class Driver {} diff --git a/checker/jtreg/nullness/issue1582/FlowExprParseError.out b/checker/jtreg/nullness/issue1582/FlowExprParseError.out deleted file mode 100644 index 0ef57527625f..000000000000 --- a/checker/jtreg/nullness/issue1582/FlowExprParseError.out +++ /dev/null @@ -1,2 +0,0 @@ -FlowExprParseError.java:8:29: compiler.err.proc.messager: (flowexpr.parse.error.postcondition) -1 error diff --git a/checker/jtreg/nullness/issue1582/Foo.out b/checker/jtreg/nullness/issue1582/Foo.out index 4523ed1dd6b7..aabfce39ffab 100644 --- a/checker/jtreg/nullness/issue1582/Foo.out +++ b/checker/jtreg/nullness/issue1582/Foo.out @@ -1,2 +1,2 @@ -Foo.java:14:20: compiler.err.proc.messager: (flowexpr.parse.error) +Foo.java:15:20: compiler.err.proc.messager: (flowexpr.parse.error) 1 error diff --git a/checker/jtreg/nullness/issue1582/JavaExpressionParseError.out b/checker/jtreg/nullness/issue1582/JavaExpressionParseError.out new file mode 100644 index 000000000000..e198d486cdb8 --- /dev/null +++ b/checker/jtreg/nullness/issue1582/JavaExpressionParseError.out @@ -0,0 +1,2 @@ +JavaExpressionParseError.java:8:29: compiler.err.proc.messager: (flowexpr.parse.error.postcondition) +1 error diff --git a/checker/jtreg/nullness/issue1582/Main.java b/checker/jtreg/nullness/issue1582/Main.java index c0df823bef92..65e38f6c3f66 100644 --- a/checker/jtreg/nullness/issue1582/Main.java +++ b/checker/jtreg/nullness/issue1582/Main.java @@ -6,8 +6,8 @@ * * @compile/fail/ref=Foo.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext foo/Foo.java * @compile -XDrawDiagnostics foo/Foo.java - * @compile/fail/ref=FlowExprParseError.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext mainrepropkg/FlowExprParseError.java - * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext mainrepropkg/FlowExprParseError.java -AsuppressWarnings=flowexpr.parse.error.postcondition + * @compile/fail/ref=JavaExpressionParseError.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext mainrepropkg/JavaExpressionParseError.java + * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext mainrepropkg/JavaExpressionParseError.java -AsuppressWarnings=flowexpr.parse.error.postcondition * */ diff --git a/checker/jtreg/nullness/issue1582/foo/Foo.java b/checker/jtreg/nullness/issue1582/foo/Foo.java index 48fa91808376..cfff2d56fbcd 100644 --- a/checker/jtreg/nullness/issue1582/foo/Foo.java +++ b/checker/jtreg/nullness/issue1582/foo/Foo.java @@ -2,6 +2,7 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.Pure; public class Foo { @@ -15,6 +16,7 @@ public boolean hasTheObject() { return false; } + @Pure public @Nullable Object getTheObject() { return null; } diff --git a/checker/jtreg/nullness/issue1582/mainrepropkg/FlowExprParseError.java b/checker/jtreg/nullness/issue1582/mainrepropkg/FlowExprParseError.java deleted file mode 100644 index b177ad2bc150..000000000000 --- a/checker/jtreg/nullness/issue1582/mainrepropkg/FlowExprParseError.java +++ /dev/null @@ -1,12 +0,0 @@ -package mainrepropkg; - -import foo.Foo; - -public class FlowExprParseError { - - public void printAThing(Foo foo) { - if (foo.hasTheObject()) { - System.out.println("Print false: " + foo.getTheObject().equals(new Object())); - } - } -} diff --git a/checker/jtreg/nullness/issue1582/mainrepropkg/JavaExpressionParseError.java b/checker/jtreg/nullness/issue1582/mainrepropkg/JavaExpressionParseError.java new file mode 100644 index 000000000000..700386c01078 --- /dev/null +++ b/checker/jtreg/nullness/issue1582/mainrepropkg/JavaExpressionParseError.java @@ -0,0 +1,12 @@ +package mainrepropkg; + +import foo.Foo; + +public class JavaExpressionParseError { + + public void printAThing(Foo foo) { + if (foo.hasTheObject()) { + System.out.println("Print false: " + foo.getTheObject().equals(new Object())); + } + } +} diff --git a/checker/jtreg/nullness/issue1929/Issue1929-notrust.out b/checker/jtreg/nullness/issue1929/Issue1929-notrust.out index a868da756aa5..8f71e1d1b5e0 100644 --- a/checker/jtreg/nullness/issue1929/Issue1929-notrust.out +++ b/checker/jtreg/nullness/issue1929/Issue1929-notrust.out @@ -1,10 +1,10 @@ -Issue1929.java:21:25: compiler.err.proc.messager: [return.type.incompatible] incompatible types in return. -type of expression: @Initialized @Nullable String @Initialized @NonNull [] -method return type: @Initialized @NonNull String @Initialized @NonNull [] -Issue1929.java:21:25: compiler.warn.proc.messager: [toArray.nullable.elements.not.newarray] call of toArray on collection of non-null elements yields an array of possibly-null elements; omit the argument to toArray or make it an explicit array constructor -Issue1929.java:29:25: compiler.err.proc.messager: [return.type.incompatible] incompatible types in return. -type of expression: @Initialized @Nullable String @Initialized @NonNull [] -method return type: @Initialized @NonNull String @Initialized @NonNull [] -Issue1929.java:29:25: compiler.warn.proc.messager: [toArray.nullable.elements.not.newarray] call of toArray on collection of non-null elements yields an array of possibly-null elements; omit the argument to toArray or make it an explicit array constructor +Issue1929.java:22:25: compiler.err.proc.messager: [return.type.incompatible] incompatible types in return. +type of expression: @Nullable String @NonNull [] +method return type: @NonNull String @NonNull [] +Issue1929.java:22:25: compiler.warn.proc.messager: [toarray.nullable.elements.not.newarray] call of toArray on collection of non-null elements yields an array of possibly-null elements; omit the argument to toArray or make it an explicit array constructor +Issue1929.java:30:25: compiler.err.proc.messager: [return.type.incompatible] incompatible types in return. +type of expression: @Nullable String @NonNull [] +method return type: @NonNull String @NonNull [] +Issue1929.java:30:25: compiler.warn.proc.messager: [toarray.nullable.elements.not.newarray] call of toArray on collection of non-null elements yields an array of possibly-null elements; omit the argument to toArray or make it an explicit array constructor 2 errors 2 warnings diff --git a/checker/jtreg/nullness/issue1929/Issue1929-trust.out b/checker/jtreg/nullness/issue1929/Issue1929-trust.out index ccb85c683852..b1b2db210316 100644 --- a/checker/jtreg/nullness/issue1929/Issue1929-trust.out +++ b/checker/jtreg/nullness/issue1929/Issue1929-trust.out @@ -1,6 +1,6 @@ -Issue1929.java:29:25: compiler.err.proc.messager: [return.type.incompatible] incompatible types in return. -type of expression: @Initialized @Nullable String @Initialized @NonNull [] -method return type: @Initialized @NonNull String @Initialized @NonNull [] -Issue1929.java:29:25: compiler.warn.proc.messager: [toArray.nullable.elements.not.newarray] call of toArray on collection of non-null elements yields an array of possibly-null elements; omit the argument to toArray or make it an explicit array constructor +Issue1929.java:30:25: compiler.err.proc.messager: [return.type.incompatible] incompatible types in return. +type of expression: @Nullable String @NonNull [] +method return type: @NonNull String @NonNull [] +Issue1929.java:30:25: compiler.warn.proc.messager: [toarray.nullable.elements.not.newarray] call of toArray on collection of non-null elements yields an array of possibly-null elements; omit the argument to toArray or make it an explicit array constructor 1 error 1 warning diff --git a/checker/jtreg/nullness/issue1929/Issue1929.java b/checker/jtreg/nullness/issue1929/Issue1929.java index 06c0bcab655f..6482d71e7a5c 100644 --- a/checker/jtreg/nullness/issue1929/Issue1929.java +++ b/checker/jtreg/nullness/issue1929/Issue1929.java @@ -6,9 +6,10 @@ * @compile/fail/ref=Issue1929-trust.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Alint=trustArrayLenZero Issue1929.java */ -import java.util.Collection; import org.checkerframework.common.value.qual.ArrayLen; +import java.util.Collection; + public class Issue1929 { String[] works1(Collection c) { @@ -24,7 +25,7 @@ String[] fails2(Collection c) { private static final String[] EMPTY_STRING_ARRAY_3 = new String[0]; String[] fails3(Collection c) { - // We don't determine field types from initializition expressions. + // We don't determine field types from initialization expressions. // :: error: (return.type.incompatible) return c.toArray(EMPTY_STRING_ARRAY_3); } diff --git a/checker/jtreg/nullness/issue1958/SupplierDefs.java b/checker/jtreg/nullness/issue1958/SupplierDefs.java index 475366cb872b..4a15a8e54218 100644 --- a/checker/jtreg/nullness/issue1958/SupplierDefs.java +++ b/checker/jtreg/nullness/issue1958/SupplierDefs.java @@ -6,9 +6,10 @@ * @compile/fail/ref=NPE2Test.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker NPE2Test.java -Anomsgtext */ -import java.util.function.Supplier; import org.checkerframework.checker.nullness.qual.*; +import java.util.function.Supplier; + public class SupplierDefs { public abstract static class Supplier { public abstract R get(); diff --git a/checker/jtreg/nullness/issue2173/README b/checker/jtreg/nullness/issue2173/README index 82e80a56358a..4f99b133563b 100644 --- a/checker/jtreg/nullness/issue2173/README +++ b/checker/jtreg/nullness/issue2173/README @@ -5,3 +5,10 @@ the annotation from a lambda to a method parameter type argument, but the parameter does not have a type argument. This bug has been fixed in Java 9, but bytecode generated by Java 8 can still be read by a checker, so this tests that it won't crash a checker. +ImporterManager.java.tmp is the source for the .class files in this folder; +it uses the .tmp extension to ensure javac doesn't use the source file instead +of the bytecode. +View.out is the expected output. We are running the Nullness Checker, which in turn +runs the KeyFor and NullnessNoInit Checkers as subcheckers. We thus have three +warnings because every checker complains about the unexpected type argument +annotation. diff --git a/checker/jtreg/nullness/issue2173/View.out b/checker/jtreg/nullness/issue2173/View.out index 7edd091e483d..0d64584fc03d 100644 --- a/checker/jtreg/nullness/issue2173/View.out +++ b/checker/jtreg/nullness/issue2173/View.out @@ -1,5 +1,4 @@ - compiler.warn.proc.messager: (invalid.annotation.location.bytecode) - compiler.warn.proc.messager: (invalid.annotation.location.bytecode) - compiler.warn.proc.messager: (invalid.annotation.location.bytecode) -- compiler.warn.proc.messager: (invalid.annotation.location.bytecode) -4 warnings +3 warnings diff --git a/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.1.out b/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.1.out index 28a8a98e4e14..780ca14382b0 100644 --- a/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.1.out +++ b/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.1.out @@ -1,10 +1,10 @@ RequireCheckerPrefix.java:19:29: compiler.err.proc.messager: [nullness:assignment.type.incompatible] incompatible types in assignment. -found : @Initialized @Nullable Object -required: @UnknownInitialization @NonNull Object +found : @Nullable Object +required: @NonNull Object RequireCheckerPrefix.java:24:29: compiler.err.proc.messager: [nullness:assignment.type.incompatible] incompatible types in assignment. -found : @Initialized @Nullable Object -required: @UnknownInitialization @NonNull Object +found : @Nullable Object +required: @NonNull Object RequireCheckerPrefix.java:27:29: compiler.err.proc.messager: [nullness:assignment.type.incompatible] incompatible types in assignment. -found : @Initialized @Nullable Object -required: @UnknownInitialization @NonNull Object +found : @Nullable Object +required: @NonNull Object 3 errors diff --git a/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.2.out b/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.2.out index a0fd79e2deab..36322441456a 100644 --- a/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.2.out +++ b/checker/jtreg/nullness/issue2318/RequireCheckerPrefix.2.out @@ -1,4 +1,4 @@ RequireCheckerPrefix.java:19:29: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. -found : @Initialized @Nullable Object -required: @UnknownInitialization @NonNull Object +found : @Nullable Object +required: @NonNull Object 1 error diff --git a/checker/jtreg/nullness/issue257/ClientBuilder.java b/checker/jtreg/nullness/issue257/ClientBuilder.java index 528115bf457f..829d324cebcc 100644 --- a/checker/jtreg/nullness/issue257/ClientBuilder.java +++ b/checker/jtreg/nullness/issue257/ClientBuilder.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; -class ClientBuilder> { +public class ClientBuilder> { static @NonNull ClientBuilder newBuilder() { return new BuilderImpl(); diff --git a/checker/jtreg/nullness/issue257/Module.java b/checker/jtreg/nullness/issue257/Module.java index 4a27ad52a56d..400c94390a5b 100644 --- a/checker/jtreg/nullness/issue257/Module.java +++ b/checker/jtreg/nullness/issue257/Module.java @@ -8,7 +8,7 @@ * * @compile -XDrawDiagnostics ClientBuilder.java */ -class Module { +public class Module { void buildClient() { ClientBuilder builder = ClientBuilder.newBuilder().setThing().setThing(); } diff --git a/checker/jtreg/nullness/issue257/Small.java b/checker/jtreg/nullness/issue257/Small.java index 6ca932042c67..cd4b6bea4877 100644 --- a/checker/jtreg/nullness/issue257/Small.java +++ b/checker/jtreg/nullness/issue257/Small.java @@ -7,7 +7,7 @@ class Gen> { } } -class Small { +public class Small { void buildGen() { Gen> builder = Gen.newBuilder(); } diff --git a/checker/jtreg/nullness/issue3700/Client.java b/checker/jtreg/nullness/issue3700/Client.java new file mode 100644 index 000000000000..0dd05d60d5a6 --- /dev/null +++ b/checker/jtreg/nullness/issue3700/Client.java @@ -0,0 +1,4 @@ +public class Client { + + TimeUnitRange t = TimeUnitRange.YEAR; +} diff --git a/checker/jtreg/nullness/issue3700/Client.out b/checker/jtreg/nullness/issue3700/Client.out new file mode 100644 index 000000000000..248618f6aa95 --- /dev/null +++ b/checker/jtreg/nullness/issue3700/Client.out @@ -0,0 +1,2 @@ +warning: TimeUnitRange.astub:(line 3,col 1): TimeUnitRange is an enum, but stub file declared it as class TimeUnitRange { public TimeUnitRange of(@Nullable String endUnit); } +1 warning diff --git a/checker/jtreg/nullness/issue3700/TimeUnitRange.astub b/checker/jtreg/nullness/issue3700/TimeUnitRange.astub new file mode 100644 index 000000000000..293b48861b1d --- /dev/null +++ b/checker/jtreg/nullness/issue3700/TimeUnitRange.astub @@ -0,0 +1,5 @@ +import org.checkerframework.checker.nullness.qual.*; + +class TimeUnitRange { + public TimeUnitRange of(@Nullable String endUnit); +} diff --git a/checker/jtreg/nullness/issue3700/TimeUnitRange.java b/checker/jtreg/nullness/issue3700/TimeUnitRange.java new file mode 100644 index 000000000000..cc4e5f0f9afb --- /dev/null +++ b/checker/jtreg/nullness/issue3700/TimeUnitRange.java @@ -0,0 +1,16 @@ +/* + * @test + * @summary Test case for Issue #3700 https://github.com/typetools/checker-framework/issues/3700 + * @compile -XDrawDiagnostics -Xlint:unchecked TimeUnitRange.java + * @compile/ref=Client.out -processor org.checkerframework.checker.nullness.NullnessChecker Client.java -Astubs=TimeUnitRange.astub -implicit:none -Anomsgtext + */ + +public enum TimeUnitRange { + YEAR, + YEAR_TO_MONTH, + MONTH; + + public static TimeUnitRange of(Object endUnit) { + throw new Error("body is irrelevant"); + } +} diff --git a/checker/jtreg/nullness/issue380/DA.java b/checker/jtreg/nullness/issue380/DA.java index 809f777d98f8..8676c10dbcae 100644 --- a/checker/jtreg/nullness/issue380/DA.java +++ b/checker/jtreg/nullness/issue380/DA.java @@ -1,4 +1,4 @@ -class DA { +public class DA { @Decl(flag = true) void foo() {} } diff --git a/checker/jtreg/nullness/issue380/DB.java b/checker/jtreg/nullness/issue380/DB.java index 393d18415078..98b982598b05 100644 --- a/checker/jtreg/nullness/issue380/DB.java +++ b/checker/jtreg/nullness/issue380/DB.java @@ -1,3 +1,3 @@ -class DB extends DA { +public class DB extends DA { void foo() {} } diff --git a/checker/jtreg/nullness/issue380/Driver.java b/checker/jtreg/nullness/issue380/Driver.java index 60c4ef06c51a..7cb884db3852 100644 --- a/checker/jtreg/nullness/issue380/Driver.java +++ b/checker/jtreg/nullness/issue380/Driver.java @@ -7,4 +7,4 @@ * * */ -class Driver {} +public class Driver {} diff --git a/checker/jtreg/nullness/issue6053/Main.java b/checker/jtreg/nullness/issue6053/Main.java new file mode 100644 index 000000000000..5ea27ddacb98 --- /dev/null +++ b/checker/jtreg/nullness/issue6053/Main.java @@ -0,0 +1,9 @@ +/* + * @test + * @summary Test case for typetools issue #6053: https://github.com/typetools/checker-framework/issues/6053 + * + * @compile/ref=Main.out -XDrawDiagnostics -Anomsgtext -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=stubs.jar Main.java + */ +public class Main { + public static void main(String[] args) {} +} diff --git a/checker/jtreg/nullness/issue6053/Main.out b/checker/jtreg/nullness/issue6053/Main.out new file mode 100644 index 000000000000..f22b87370aea --- /dev/null +++ b/checker/jtreg/nullness/issue6053/Main.out @@ -0,0 +1,2 @@ +- compiler.warn.proc.messager: stubs.jar!Stubs.astub: Parse problem: java.lang.AssertionError: A reference was unexpectedly null. +1 warning diff --git a/checker/jtreg/nullness/issue6053/Stubs.astub b/checker/jtreg/nullness/issue6053/Stubs.astub new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/checker/jtreg/nullness/issue6053/stubs.jar b/checker/jtreg/nullness/issue6053/stubs.jar new file mode 100644 index 000000000000..3ab4d37ea5b1 Binary files /dev/null and b/checker/jtreg/nullness/issue6053/stubs.jar differ diff --git a/checker/jtreg/nullness/issue6374/Lib.java b/checker/jtreg/nullness/issue6374/Lib.java new file mode 100644 index 000000000000..dea871dac3ad --- /dev/null +++ b/checker/jtreg/nullness/issue6374/Lib.java @@ -0,0 +1,17 @@ +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jmlspecs.annotation.NonNull; + +@SuppressWarnings("unchecked") // ignore heap pollution +class Lib { + // element type inferred, array non-null + static void none(T... o) {} + + // element type inferred, array non-null + static void decl(@NonNull T... o) {} + + // element type nullable, array non-null + static void type(@Nullable T... o) {} + + // element type nullable, array nullable + static void typenn(@Nullable T @Nullable ... o) {} +} diff --git a/checker/jtreg/nullness/issue6374/User.java b/checker/jtreg/nullness/issue6374/User.java new file mode 100644 index 000000000000..ca0de77a7c27 --- /dev/null +++ b/checker/jtreg/nullness/issue6374/User.java @@ -0,0 +1,20 @@ +/* + * @test + * @summary Test case for typetools issue #6374: https://github.com/typetools/checker-framework/issues/6374 + * + * @compile Lib.java + * @compile/fail/ref=User.out -XDrawDiagnostics -Anomsgtext -processor org.checkerframework.checker.nullness.NullnessChecker User.java + */ +// Also see checker/tests/nullness/generics/Issue6374.java +class User { + void go() { + Lib.decl("", null); + // :: error: (argument.type.incompatible) + Lib.decl((Object[]) null); + Lib.type("", null); + // :: error: (argument.type.incompatible) + Lib.type((Object[]) null); + Lib.typenn("", null); + Lib.typenn((Object[]) null); + } +} diff --git a/checker/jtreg/nullness/issue6374/User.out b/checker/jtreg/nullness/issue6374/User.out new file mode 100644 index 000000000000..a80316facf47 --- /dev/null +++ b/checker/jtreg/nullness/issue6374/User.out @@ -0,0 +1,3 @@ +User.java:13:18: compiler.err.proc.messager: (argument.type.incompatible) +User.java:16:18: compiler.err.proc.messager: (argument.type.incompatible) +2 errors diff --git a/checker/jtreg/nullness/issue790/Class1.java b/checker/jtreg/nullness/issue790/Class1.java index 932d3d6adbb5..10ac7ec761c0 100644 --- a/checker/jtreg/nullness/issue790/Class1.java +++ b/checker/jtreg/nullness/issue790/Class1.java @@ -2,9 +2,9 @@ * @test * @summary Test for Issue #790 * - * @compile/fail/ref=expected.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext Class1.java Class2.java + * @compile/fail/ref=expected12.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext Class1.java Class2.java * - * @compile/fail/ref=expected.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext Class2.java Class1.java + * @compile/fail/ref=expected21.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext Class2.java Class1.java * */ public class Class1 { diff --git a/checker/jtreg/nullness/issue790/expected.out b/checker/jtreg/nullness/issue790/expected12.out similarity index 100% rename from checker/jtreg/nullness/issue790/expected.out rename to checker/jtreg/nullness/issue790/expected12.out diff --git a/checker/jtreg/nullness/issue790/expected21.out b/checker/jtreg/nullness/issue790/expected21.out new file mode 100644 index 000000000000..dfdffebd6441 --- /dev/null +++ b/checker/jtreg/nullness/issue790/expected21.out @@ -0,0 +1,3 @@ +Class1.java:12:15: compiler.err.cant.ref.before.ctor.called: this +Class1.java:12:9: compiler.err.cant.apply.symbol: kindname.constructor, Object, compiler.misc.no.args, Class1, kindname.class, java.lang.Object, (compiler.misc.arg.length.mismatch) +2 errors diff --git a/checker/jtreg/nullness/issue820/AnonymousClass.java b/checker/jtreg/nullness/issue820/AnonymousClass.java index 8d6acb61becb..cafe4c2bb596 100644 --- a/checker/jtreg/nullness/issue820/AnonymousClass.java +++ b/checker/jtreg/nullness/issue820/AnonymousClass.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; -class AnonymousClass { +public class AnonymousClass { @NonNull Object error = null; void method() { diff --git a/checker/jtreg/nullness/issue820/Error.java b/checker/jtreg/nullness/issue820/Error.java index f9a21d5c9361..05c981269a17 100644 --- a/checker/jtreg/nullness/issue820/Error.java +++ b/checker/jtreg/nullness/issue820/Error.java @@ -9,6 +9,6 @@ * */ -class Error { +public class Error { @NonNull Object o = null; } diff --git a/checker/jtreg/nullness/issue824/Class1.astub b/checker/jtreg/nullness/issue824/Class1.astub index 58b898949ea0..30c08d8ce27b 100644 --- a/checker/jtreg/nullness/issue824/Class1.astub +++ b/checker/jtreg/nullness/issue824/Class1.astub @@ -1,7 +1,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; - class Class1 { +class Class1 { public T methodTypeParam(T t); public void classTypeParam(Q e); diff --git a/checker/jtreg/nullness/issue824/Class2.java b/checker/jtreg/nullness/issue824/Class2.java index 4bdcfb37a6c5..f29ad11bf894 100644 --- a/checker/jtreg/nullness/issue824/Class2.java +++ b/checker/jtreg/nullness/issue824/Class2.java @@ -5,7 +5,7 @@ * the issue was filed. So, this test case has been changed so that * annotations on type variable bounds in stub files is still tested. * @compile -XDrawDiagnostics -Xlint:unchecked ../issue824lib/Class1.java - * @compile/fail/ref=Class2.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext Class2.java -Astubs=Class1.astub -AstubWarnIfNotFound + * @compile/fail/ref=Class2.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext Class2.java -Astubs=Class1.astub * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext Class2.java */ diff --git a/checker/jtreg/nullness/issue824lib/First.java b/checker/jtreg/nullness/issue824lib/First.java index ecf80eca4509..c92b9a241331 100644 --- a/checker/jtreg/nullness/issue824lib/First.java +++ b/checker/jtreg/nullness/issue824lib/First.java @@ -1,4 +1,4 @@ -class First { +public class First { public interface Supplier {} public interface Callable {} diff --git a/checker/jtreg/nullness/preciseErrorMsg/LocateArtificialTree.java b/checker/jtreg/nullness/preciseErrorMsg/LocateArtificialTree.java new file mode 100644 index 000000000000..5860c6dc2fef --- /dev/null +++ b/checker/jtreg/nullness/preciseErrorMsg/LocateArtificialTree.java @@ -0,0 +1,22 @@ +/* + * @test + * @summary + * Test case for eisop issue 244 + * https://github.com/eisop/checker-framework/issues/244 + * + * @compile/fail/ref=LocateArtificialTree.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext LocateArtificialTree.java + * + */ + +import org.checkerframework.checker.nullness.qual.*; + +import java.util.List; +import java.util.function.Consumer; + +public class LocateArtificialTree { + @NonNull class A {} + + void foo() { + Consumer> c = a -> {}; + } +} diff --git a/checker/jtreg/nullness/preciseErrorMsg/LocateArtificialTree.out b/checker/jtreg/nullness/preciseErrorMsg/LocateArtificialTree.out new file mode 100644 index 000000000000..9d80ade6b8da --- /dev/null +++ b/checker/jtreg/nullness/preciseErrorMsg/LocateArtificialTree.out @@ -0,0 +1,3 @@ +LocateArtificialTree.java:20:23: compiler.err.proc.messager: (type.invalid.annotations.on.use) +LocateArtificialTree.java:20:41: compiler.err.proc.messager: (type.invalid.annotations.on.use) +2 errors diff --git a/checker/jtreg/nullness/stub-arg/EisopIssue608-bad.astub b/checker/jtreg/nullness/stub-arg/EisopIssue608-bad.astub new file mode 100644 index 000000000000..c64ee6c441bd --- /dev/null +++ b/checker/jtreg/nullness/stub-arg/EisopIssue608-bad.astub @@ -0,0 +1,7 @@ +package java.util; + +import org.checkerframework.checker.nullness.qual.Nullable; + +public interface List extends Bad { + boolean contains(@Nullable Object o); +} diff --git a/checker/jtreg/nullness/stub-arg/EisopIssue608-badstub.out b/checker/jtreg/nullness/stub-arg/EisopIssue608-badstub.out new file mode 100644 index 000000000000..ff2c69dc5aa5 --- /dev/null +++ b/checker/jtreg/nullness/stub-arg/EisopIssue608-badstub.out @@ -0,0 +1,5 @@ +- compiler.warn.proc.messager: EisopIssue608-bad.astub:(line 5,col 1): direct supertype Bad not found +- compiler.warn.proc.messager: EisopIssue608-bad.astub:(line 5,col 1): stub file does not match bytecode: could not find direct superclass Bad from type List +- compiler.err.warnings.and.werror +1 error +2 warnings diff --git a/checker/jtreg/nullness/stub-arg/EisopIssue608-nostub.out b/checker/jtreg/nullness/stub-arg/EisopIssue608-nostub.out new file mode 100644 index 000000000000..0133b78320ea --- /dev/null +++ b/checker/jtreg/nullness/stub-arg/EisopIssue608-nostub.out @@ -0,0 +1,2 @@ +EisopIssue608.java:13:27: compiler.err.proc.messager: (argument.type.incompatible) +1 error diff --git a/checker/jtreg/nullness/stub-arg/EisopIssue608.java b/checker/jtreg/nullness/stub-arg/EisopIssue608.java new file mode 100644 index 000000000000..9e26a807659c --- /dev/null +++ b/checker/jtreg/nullness/stub-arg/EisopIssue608.java @@ -0,0 +1,15 @@ +/* + * @test + * @summary Test case for EISOP issue #608. + * + * @compile/fail/ref=EisopIssue608-nostub.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Werror EisopIssue608.java + * @compile/fail/ref=EisopIssue608-badstub.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Werror -Astubs=EisopIssue608-bad.astub EisopIssue608.java + * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Werror -AstubNoWarnIfNotFound -Astubs=collection-object-parameters-may-be-null.astub EisopIssue608.java + */ +import java.util.List; + +public class EisopIssue608 { + boolean go(List l) { + return l.contains(null); + } +} diff --git a/checker/jtreg/nullness/stub-typeparams/Box.java b/checker/jtreg/nullness/stub-typeparams/Box.java new file mode 100644 index 000000000000..cd8ebb7cbdd4 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/Box.java @@ -0,0 +1,10 @@ +// This class is not compiled with the Nullness Checker, +// so that no annotations are stored in bytecode. +public class Box { + static Box of(S in) { + // Implementation doesn't matter. + return null; + } + + static void consume(Box producer) {} +} diff --git a/checker/jtreg/nullness/stub-typeparams/NullableBox.java b/checker/jtreg/nullness/stub-typeparams/NullableBox.java new file mode 100644 index 000000000000..b2f8d3b06cff --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/NullableBox.java @@ -0,0 +1,15 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +// This class is not compiled with the Nullness Checker, +// so that only explicit annotations are stored in bytecode. +public class NullableBox { + static NullableBox of(S in) { + // Implementation doesn't matter. + return null; + } + + static void consume(NullableBox producer) {} + + static void nonnull(NullableBox producer) {} +} diff --git a/checker/jtreg/nullness/stub-typeparams/StubTypeParamsClass.java b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsClass.java new file mode 100644 index 000000000000..13b8d046341c --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsClass.java @@ -0,0 +1,15 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +/* + * @test + * @summary Test class type parameters in stub files. + * + * @compile -XDrawDiagnostics -Xlint:unchecked Box.java + * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=box-none.astub StubTypeParamsClass.java + * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=box-nullable.astub StubTypeParamsClass.java + * @compile/fail/ref=StubTypeParamsClass.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=box-object.astub -Werror StubTypeParamsClass.java + * @compile/fail/ref=StubTypeParamsClass.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=box-nonnull.astub -Werror StubTypeParamsClass.java + */ +public class StubTypeParamsClass { + @Nullable Box<@Nullable Object> f; +} diff --git a/checker/jtreg/nullness/stub-typeparams/StubTypeParamsClass.out b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsClass.out new file mode 100644 index 000000000000..896c38fdd980 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsClass.out @@ -0,0 +1,2 @@ +StubTypeParamsClass.java:14:19: compiler.err.proc.messager: (type.argument.type.incompatible) +1 error diff --git a/checker/jtreg/nullness/stub-typeparams/StubTypeParamsClassNbl.java b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsClassNbl.java new file mode 100644 index 000000000000..8b0b88b1e55f --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsClassNbl.java @@ -0,0 +1,15 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +/* + * @test + * @summary Test class type parameters in stub files. + * + * @compile -XDrawDiagnostics -Xlint:unchecked NullableBox.java + * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-none.astub StubTypeParamsClassNbl.java + * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-nullable.astub StubTypeParamsClassNbl.java + * @compile/fail/ref=StubTypeParamsClassNbl.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-object.astub -Werror StubTypeParamsClassNbl.java + * @compile/fail/ref=StubTypeParamsClassNbl.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-nonnull.astub -Werror StubTypeParamsClassNbl.java + */ +public class StubTypeParamsClassNbl { + @Nullable NullableBox<@Nullable Object> f; +} diff --git a/checker/jtreg/nullness/stub-typeparams/StubTypeParamsClassNbl.out b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsClassNbl.out new file mode 100644 index 000000000000..7cbd72da969b --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsClassNbl.out @@ -0,0 +1,2 @@ +StubTypeParamsClassNbl.java:14:27: compiler.err.proc.messager: (type.argument.type.incompatible) +1 error diff --git a/checker/jtreg/nullness/stub-typeparams/StubTypeParamsMeth.java b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsMeth.java new file mode 100644 index 000000000000..0bd86b94d98e --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsMeth.java @@ -0,0 +1,15 @@ +/* + * @test + * @summary Test method type parameters in stub files. + * + * @compile -XDrawDiagnostics -Xlint:unchecked Box.java + * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=box-none.astub StubTypeParamsMeth.java + * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=box-nullable.astub StubTypeParamsMeth.java + * @compile/fail/ref=StubTypeParamsMeth.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=box-object.astub -Werror StubTypeParamsMeth.java + * @compile/fail/ref=StubTypeParamsMeth.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=box-nonnull.astub -Werror StubTypeParamsMeth.java + */ +public class StubTypeParamsMeth { + void use() { + Box.of(null); + } +} diff --git a/checker/jtreg/nullness/stub-typeparams/StubTypeParamsMeth.out b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsMeth.out new file mode 100644 index 000000000000..2249ca515c56 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsMeth.out @@ -0,0 +1,2 @@ +StubTypeParamsMeth.java:13:15: compiler.err.proc.messager: (type.argument.type.incompatible) +1 error diff --git a/checker/jtreg/nullness/stub-typeparams/StubTypeParamsMethNbl.java b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsMethNbl.java new file mode 100644 index 000000000000..62605bc5e6d9 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsMethNbl.java @@ -0,0 +1,15 @@ +/* + * @test + * @summary Test method type parameters in stub files. + * + * @compile -XDrawDiagnostics -Xlint:unchecked NullableBox.java + * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-none.astub StubTypeParamsMethNbl.java + * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-nullable.astub StubTypeParamsMethNbl.java + * @compile/fail/ref=StubTypeParamsMethNbl.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-object.astub -Werror StubTypeParamsMethNbl.java + * @compile/fail/ref=StubTypeParamsMethNbl.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-nonnull.astub -Werror StubTypeParamsMethNbl.java + */ +public class StubTypeParamsMethNbl { + void use() { + NullableBox.of(null); + } +} diff --git a/checker/jtreg/nullness/stub-typeparams/StubTypeParamsMethNbl.out b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsMethNbl.out new file mode 100644 index 000000000000..2114c010a608 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/StubTypeParamsMethNbl.out @@ -0,0 +1,2 @@ +StubTypeParamsMethNbl.java:13:23: compiler.err.proc.messager: (type.argument.type.incompatible) +1 error diff --git a/checker/jtreg/nullness/stub-typeparams/StubWildcards.java b/checker/jtreg/nullness/stub-typeparams/StubWildcards.java new file mode 100644 index 000000000000..591c18e9c967 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/StubWildcards.java @@ -0,0 +1,17 @@ +/* + * @test + * @summary Test wildcards in stub files. + * + * @compile -XDrawDiagnostics -Xlint:unchecked Box.java + * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=box-none.astub StubWildcards.java + * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=box-nullable.astub StubWildcards.java + * @compile/fail/ref=StubWildcards.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=box-object.astub -Werror StubWildcards.java + * @compile/fail/ref=StubWildcards.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=box-nonnull.astub -Werror StubWildcards.java + */ +import org.checkerframework.checker.nullness.qual.Nullable; + +public class StubWildcards { + void use(Box<@Nullable String> bs) { + Box.consume(bs); + } +} diff --git a/checker/jtreg/nullness/stub-typeparams/StubWildcards.out b/checker/jtreg/nullness/stub-typeparams/StubWildcards.out new file mode 100644 index 000000000000..0f3fc6341898 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/StubWildcards.out @@ -0,0 +1,3 @@ +StubWildcards.java:14:18: compiler.err.proc.messager: (type.argument.type.incompatible) +StubWildcards.java:15:21: compiler.err.proc.messager: (argument.type.incompatible) +2 errors diff --git a/checker/jtreg/nullness/stub-typeparams/StubWildcardsNbl.java b/checker/jtreg/nullness/stub-typeparams/StubWildcardsNbl.java new file mode 100644 index 000000000000..ea3810028a17 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/StubWildcardsNbl.java @@ -0,0 +1,17 @@ +/* + * @test + * @summary Test wildcards in stub files. + * + * @compile -XDrawDiagnostics -Xlint:unchecked NullableBox.java + * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-none.astub StubWildcardsNbl.java + * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-nullable.astub StubWildcardsNbl.java + * @compile/fail/ref=StubWildcardsNbl.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-object.astub -Werror StubWildcardsNbl.java + * @compile/fail/ref=StubWildcardsNbl.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-nonnull.astub -Werror StubWildcardsNbl.java + */ +import org.checkerframework.checker.nullness.qual.Nullable; + +public class StubWildcardsNbl { + void use(NullableBox<@Nullable String> bs) { + NullableBox.consume(bs); + } +} diff --git a/checker/jtreg/nullness/stub-typeparams/StubWildcardsNbl.out b/checker/jtreg/nullness/stub-typeparams/StubWildcardsNbl.out new file mode 100644 index 000000000000..05853c5a7307 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/StubWildcardsNbl.out @@ -0,0 +1,3 @@ +StubWildcardsNbl.java:14:26: compiler.err.proc.messager: (type.argument.type.incompatible) +StubWildcardsNbl.java:15:29: compiler.err.proc.messager: (argument.type.incompatible) +2 errors diff --git a/checker/jtreg/nullness/stub-typeparams/StubWildcardsNon.java b/checker/jtreg/nullness/stub-typeparams/StubWildcardsNon.java new file mode 100644 index 000000000000..304c3241dcba --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/StubWildcardsNon.java @@ -0,0 +1,17 @@ +/* + * @test + * @summary Test wildcards in stub files. + * + * @compile -XDrawDiagnostics -Xlint:unchecked NullableBox.java + * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-none.astub StubWildcardsNon.java + * @compile -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-nullable.astub StubWildcardsNon.java + * @compile/fail/ref=StubWildcardsNon.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-object.astub -Werror StubWildcardsNon.java + * @compile/fail/ref=StubWildcardsNon.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=nullablebox-nonnull.astub -Werror StubWildcardsNon.java + */ +import org.checkerframework.checker.nullness.qual.Nullable; + +public class StubWildcardsNon { + void use(NullableBox<@Nullable String> bs) { + NullableBox.nonnull(bs); + } +} diff --git a/checker/jtreg/nullness/stub-typeparams/StubWildcardsNon.out b/checker/jtreg/nullness/stub-typeparams/StubWildcardsNon.out new file mode 100644 index 000000000000..aeabd38a0a9e --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/StubWildcardsNon.out @@ -0,0 +1,3 @@ +StubWildcardsNon.java:14:26: compiler.err.proc.messager: (type.argument.type.incompatible) +StubWildcardsNon.java:15:29: compiler.err.proc.messager: (argument.type.incompatible) +2 errors diff --git a/checker/jtreg/nullness/stub-typeparams/box-none.astub b/checker/jtreg/nullness/stub-typeparams/box-none.astub new file mode 100644 index 000000000000..e12444d66b59 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/box-none.astub @@ -0,0 +1,7 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// T corresponds to "T extends @Nullable Object" +class Box { + static Box of(S in); + static void consume(Box producer); +} diff --git a/checker/jtreg/nullness/stub-typeparams/box-nonnull.astub b/checker/jtreg/nullness/stub-typeparams/box-nonnull.astub new file mode 100644 index 000000000000..d820fc858335 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/box-nonnull.astub @@ -0,0 +1,6 @@ +import org.checkerframework.checker.nullness.qual.NonNull; + +class Box { + static Box of(S in); + static void consume(Box producer); +} diff --git a/checker/jtreg/nullness/stub-typeparams/box-nullable.astub b/checker/jtreg/nullness/stub-typeparams/box-nullable.astub new file mode 100644 index 000000000000..e20f81c441d4 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/box-nullable.astub @@ -0,0 +1,6 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +class Box { + static Box of(S in); + static void consume(Box producer); +} diff --git a/checker/jtreg/nullness/stub-typeparams/box-object.astub b/checker/jtreg/nullness/stub-typeparams/box-object.astub new file mode 100644 index 000000000000..fb267b77d348 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/box-object.astub @@ -0,0 +1,7 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// Explicit bound corresponds to "T extends @NonNull Object" +class Box { + static Box of(S in); + static void consume(Box producer); +} diff --git a/checker/jtreg/nullness/stub-typeparams/nullablebox-none.astub b/checker/jtreg/nullness/stub-typeparams/nullablebox-none.astub new file mode 100644 index 000000000000..891e00fcdc51 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/nullablebox-none.astub @@ -0,0 +1,8 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// T corresponds to "T extends @Nullable Object" +class NullableBox { + static NullableBox of(S in); + static void consume(NullableBox producer); + static void nonnull(NullableBox producer); +} diff --git a/checker/jtreg/nullness/stub-typeparams/nullablebox-nonnull.astub b/checker/jtreg/nullness/stub-typeparams/nullablebox-nonnull.astub new file mode 100644 index 000000000000..7193bb96ea2c --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/nullablebox-nonnull.astub @@ -0,0 +1,7 @@ +import org.checkerframework.checker.nullness.qual.NonNull; + +class NullableBox { + static NullableBox of(S in); + static void consume(NullableBox producer); + static void nonnull(NullableBox producer); +} diff --git a/checker/jtreg/nullness/stub-typeparams/nullablebox-nullable.astub b/checker/jtreg/nullness/stub-typeparams/nullablebox-nullable.astub new file mode 100644 index 000000000000..6f54e292d5c8 --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/nullablebox-nullable.astub @@ -0,0 +1,7 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +class NullableBox { + static NullableBox of(S in); + static void consume(NullableBox producer); + static void nonnull(NullableBox producer); +} diff --git a/checker/jtreg/nullness/stub-typeparams/nullablebox-object.astub b/checker/jtreg/nullness/stub-typeparams/nullablebox-object.astub new file mode 100644 index 000000000000..8aec736a0f1f --- /dev/null +++ b/checker/jtreg/nullness/stub-typeparams/nullablebox-object.astub @@ -0,0 +1,8 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// Explicit bound corresponds to "T extends @NonNull Object" +class NullableBox { + static NullableBox of(S in); + static void consume(NullableBox producer); + static void nonnull(NullableBox producer); +} diff --git a/checker/jtreg/nullness/stub-warnings/Binary.java b/checker/jtreg/nullness/stub-warnings/Binary.java index 0b1d01071e18..3e5037c12cee 100644 --- a/checker/jtreg/nullness/stub-warnings/Binary.java +++ b/checker/jtreg/nullness/stub-warnings/Binary.java @@ -1,10 +1,11 @@ // This class is not compiled with the Nullness Checker, // so that only explicit annotations are stored in bytecode. -import javax.annotation.Nullable; import org.checkerframework.checker.nullness.qual.NonNull; -class Binary { +import javax.annotation.Nullable; + +public class Binary { @Nullable Object foo() { return null; } diff --git a/checker/jtreg/nullness/stub-warnings/StubWarnings.java b/checker/jtreg/nullness/stub-warnings/StubWarnings.java index 7055fe315c13..9f6b53a8cf74 100644 --- a/checker/jtreg/nullness/stub-warnings/StubWarnings.java +++ b/checker/jtreg/nullness/stub-warnings/StubWarnings.java @@ -1,6 +1,6 @@ /* * @test - * @summary Test warnings for redundant stub specifications. + * @summary Test warnings for redundant stub file specifications. * * @compile -XDrawDiagnostics -Xlint:unchecked Binary.java * @compile/fail/ref=StubWarnings.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=binary.astub -AstubWarnIfRedundantWithBytecode -AstubWarnIfOverwritesBytecode -Werror StubWarnings.java diff --git a/checker/jtreg/nullness/stub-warnings/StubWarnings.out b/checker/jtreg/nullness/stub-warnings/StubWarnings.out index d6823b0b9480..705d938e1a9d 100644 --- a/checker/jtreg/nullness/stub-warnings/StubWarnings.out +++ b/checker/jtreg/nullness/stub-warnings/StubWarnings.out @@ -1,5 +1,5 @@ -- compiler.warn.proc.messager: StubParser: in file binary.astub at line 6: redundant stub file specification for: Binary.foo() -- compiler.warn.proc.messager: StubParser: in file binary.astub at line 8: redundant stub file specification for: Binary.bar(java.lang.Object) +- compiler.warn.proc.messager: binary.astub:(line 6,col 5): stub file specification is same as bytecode for Binary.foo() +- compiler.warn.proc.messager: binary.astub:(line 8,col 5): stub file specification is same as bytecode for Binary.bar(java.lang.Object) - compiler.err.warnings.and.werror 1 error 2 warnings diff --git a/checker/jtreg/rawtypes/RawTypeFail.java b/checker/jtreg/rawtypes/RawTypeFail.java index 68f80a35c9d1..ba9aecc53cdc 100644 --- a/checker/jtreg/rawtypes/RawTypeFail.java +++ b/checker/jtreg/rawtypes/RawTypeFail.java @@ -10,7 +10,7 @@ import java.util.HashMap; import java.util.Map; -class RawTypeFail { +public class RawTypeFail { Map mr = new HashMap(); Map mc = mr; Map mc2 = new HashMap(); diff --git a/checker/jtreg/showPrefix/ShowPrefixTest.java b/checker/jtreg/showPrefix/ShowPrefixTest.java new file mode 100644 index 000000000000..b7a2eeed9689 --- /dev/null +++ b/checker/jtreg/showPrefix/ShowPrefixTest.java @@ -0,0 +1,14 @@ +/* + * @test + * + * @compile/fail/ref=ShowPrefixTest.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker ShowPrefixTest.java -AshowPrefixInWarningMessages + */ + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class ShowPrefixTest { + @NonNull Object foo(@Nullable Object nble) { + return nble; + } +} diff --git a/checker/jtreg/showPrefix/ShowPrefixTest.out b/checker/jtreg/showPrefix/ShowPrefixTest.out new file mode 100644 index 000000000000..27e6a4423577 --- /dev/null +++ b/checker/jtreg/showPrefix/ShowPrefixTest.out @@ -0,0 +1,4 @@ +ShowPrefixTest.java:12:16: compiler.err.proc.messager: [nullness:return.type.incompatible] incompatible types in return. +type of expression: @Nullable Object +method return type: @NonNull Object +1 error diff --git a/checker/jtreg/sortwarnings/ErrorOrders.java b/checker/jtreg/sortwarnings/ErrorOrders.java index 53c36bcc8a32..f2f8dec12f0b 100644 --- a/checker/jtreg/sortwarnings/ErrorOrders.java +++ b/checker/jtreg/sortwarnings/ErrorOrders.java @@ -13,6 +13,8 @@ void test2(int i, int[] a) { a[i] = 2; } + // Ignore the test suite's usage of qualifiers in illegal locations. + @SuppressWarnings("type.invalid.annotations.on.location") int test4( @GTENegativeOne @UpperBoundBottom int p1, @UpperBoundBottom @GTENegativeOne int p2, @@ -45,6 +47,8 @@ void test2(int i, int[] a) { a[i] = 2; } + // Ignore the test suite's usage of qualifiers in illegal locations. + @SuppressWarnings("type.invalid.annotations.on.location") int test4( @GTENegativeOne @UpperBoundBottom int p1, @UpperBoundBottom @GTENegativeOne int p2, diff --git a/checker/jtreg/sortwarnings/ErrorOrders.out b/checker/jtreg/sortwarnings/ErrorOrders.out index ceddf61d07bd..d4fd6ff341f2 100644 --- a/checker/jtreg/sortwarnings/ErrorOrders.out +++ b/checker/jtreg/sortwarnings/ErrorOrders.out @@ -1,140 +1,140 @@ -OrderOfCheckers.java:12:99: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. +OrderOfCheckers.java:14:99: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. found : @UnknownVal int @UnknownVal [] -required: @UnknownVal int [] -OrderOfCheckers.java:12:99: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. -found : @SearchIndexUnknown int @SearchIndexUnknown [] -required: @SearchIndexBottom int @SearchIndexUnknown [] -OrderOfCheckers.java:12:99: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. -found : @SameLenUnknown int @SameLen("y") [] -required: @SameLenUnknown int @SameLenBottom [] -OrderOfCheckers.java:12:99: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. -found : @LowerBoundUnknown int @LowerBoundUnknown [] -required: @GTENegativeOne int @LowerBoundUnknown [] -OrderOfCheckers.java:12:99: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. -found : @UpperBoundUnknown int @UpperBoundUnknown [] -required: @UpperBoundBottom int @UpperBoundUnknown [] +required: @UnknownVal int @BottomVal [] +OrderOfCheckers.java:14:99: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. +found : int [] +required: @SearchIndexBottom int [] +OrderOfCheckers.java:14:99: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. +found : int @SameLen("y") [] +required: int @SameLenBottom [] +OrderOfCheckers.java:14:99: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. +found : int [] +required: @GTENegativeOne int [] +OrderOfCheckers.java:14:99: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. +found : int [] +required: @UpperBoundBottom int [] ErrorOrders.java:13:11: compiler.err.proc.messager: [array.access.unsafe.low] Potentially unsafe array access: the index could be negative. -found : @LowerBoundUnknown int +found : int required: an integer >= 0 (@NonNegative or @Positive) ErrorOrders.java:13:11: compiler.err.proc.messager: [array.access.unsafe.high] Potentially unsafe array access: the index could be larger than the array's bound -found : @UpperBoundUnknown int +found : int required: @IndexFor("a") or @LTLengthOf("a") -- an integer less than a's length -ErrorOrders.java:23:33: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. -found : @UpperBoundUnknown int -required: @LTLengthOf(value="p2") int -ErrorOrders.java:24: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] -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. -found : @LowerBoundUnknown int +ErrorOrders.java:25:33: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. +found : @UpperBoundLiteral(0) int +required: @LTLengthOf("p2") int +ErrorOrders.java:26: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 the expression did not parse. Error message: Encountered unexpected token: "isn" ] +ErrorOrders.java:26:55: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. +found : @LTLengthOf("p2") int +required: @LTLengthOf("[error for expression: This isn't an expression; error: Invalid 'This isn't an expression' because the expression did not parse. Error message: Encountered unexpected token: "isn" ]") int +ErrorOrders.java:31:15: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of ErrorOrders.test4. +found : int required: @GTENegativeOne int -ErrorOrders.java:29:15: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @UpperBoundUnknown int +ErrorOrders.java:31:15: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of ErrorOrders.test4. +found : int required: @UpperBoundBottom int -ErrorOrders.java:29:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @LowerBoundUnknown int +ErrorOrders.java:31:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of ErrorOrders.test4. +found : int required: @GTENegativeOne int -ErrorOrders.java:29:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @UpperBoundUnknown int +ErrorOrders.java:31:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of ErrorOrders.test4. +found : int required: @UpperBoundBottom int -ErrorOrders.java:29:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @LowerBoundUnknown int +ErrorOrders.java:31:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of ErrorOrders.test4. +found : int required: @GTENegativeOne int -ErrorOrders.java:29:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @UpperBoundUnknown int +ErrorOrders.java:31:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of ErrorOrders.test4. +found : int required: @UpperBoundBottom int -ErrorOrders.java:29:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @LowerBoundUnknown int +ErrorOrders.java:31:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of ErrorOrders.test4. +found : int required: @GTENegativeOne int -ErrorOrders.java:29:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @UpperBoundUnknown int +ErrorOrders.java:31:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of ErrorOrders.test4. +found : int required: @UpperBoundBottom int -ErrorOrders.java:29:33: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:31:33: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p3 of ErrorOrders.test4. found : @UnknownVal int @UnknownVal [] -required: @UnknownVal int [] -ErrorOrders.java:29:37: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @SameLenUnknown int @SameLen("p4") [] -required: @SameLenUnknown int @SameLenBottom [] -ErrorOrders.java:29:41: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +required: @UnknownVal int @BottomVal [] +ErrorOrders.java:31:37: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p4 of ErrorOrders.test4. +found : int @SameLen("p4") [] +required: int @SameLenBottom [] +ErrorOrders.java:31:41: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p5 of ErrorOrders.test4. found : @UnknownVal int -required: int -ErrorOrders.java:29:46: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +required: @BottomVal int +ErrorOrders.java:31:46: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p3 of ErrorOrders.test4. found : @UnknownVal int @UnknownVal [] -required: @UnknownVal int [] -ErrorOrders.java:29:50: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @SameLenUnknown int @SameLen("p4") [] -required: @SameLenUnknown int @SameLenBottom [] -ErrorOrders.java:29:54: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +required: @UnknownVal int @BottomVal [] +ErrorOrders.java:31:50: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p4 of ErrorOrders.test4. +found : int @SameLen("p4") [] +required: int @SameLenBottom [] +ErrorOrders.java:31:54: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p5 of ErrorOrders.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] -ErrorOrders.java:33:55: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. -found : @UpperBoundUnknown 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:36:15: compiler.err.proc.messager: [array.access.unsafe.low] Potentially unsafe array access: the index could be negative. -found : @LowerBoundUnknown int +required: @BottomVal int +ErrorOrders.java:35: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 the expression did not parse. Error message: Encountered unexpected token: "isn" ] +ErrorOrders.java:35:55: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. +found : @UpperBoundLiteral(0) int +required: @LTLengthOf("[error for expression: This isn't an expression; error: Invalid 'This isn't an expression' because the expression did not parse. Error message: Encountered unexpected token: "isn" ]") int +ErrorOrders.java:38:15: compiler.err.proc.messager: [array.access.unsafe.low] Potentially unsafe array access: the index could be negative. +found : int required: an integer >= 0 (@NonNegative or @Positive) -ErrorOrders.java:36:15: compiler.err.proc.messager: [array.access.unsafe.high] Potentially unsafe array access: the index could be larger than the array's bound -found : @UpperBoundUnknown int +ErrorOrders.java:38:15: compiler.err.proc.messager: [array.access.unsafe.high] Potentially unsafe array access: the index could be larger than the array's bound +found : int required: @IndexFor("a") or @LTLengthOf("a") -- an integer less than a's length -ErrorOrders.java:42:43: 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] -ErrorOrders.java:42:51: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. -found : @UpperBoundUnknown 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:45:11: compiler.err.proc.messager: [array.access.unsafe.low] Potentially unsafe array access: the index could be negative. -found : @LowerBoundUnknown int +ErrorOrders.java:44:43: 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 the expression did not parse. Error message: Encountered unexpected token: "isn" ] +ErrorOrders.java:44:51: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. +found : @UpperBoundLiteral(0) int +required: @LTLengthOf("[error for expression: This isn't an expression; error: Invalid 'This isn't an expression' because the expression did not parse. Error message: Encountered unexpected token: "isn" ]") int +ErrorOrders.java:47:11: compiler.err.proc.messager: [array.access.unsafe.low] Potentially unsafe array access: the index could be negative. +found : int required: an integer >= 0 (@NonNegative or @Positive) -ErrorOrders.java:45:11: compiler.err.proc.messager: [array.access.unsafe.high] Potentially unsafe array access: the index could be larger than the array's bound -found : @UpperBoundUnknown int +ErrorOrders.java:47:11: compiler.err.proc.messager: [array.access.unsafe.high] Potentially unsafe array access: the index could be larger than the array's bound +found : int required: @IndexFor("a") or @LTLengthOf("a") -- an integer less than a's length -ErrorOrders.java:55:33: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. -found : @UpperBoundUnknown int -required: @LTLengthOf(value="p2") int -ErrorOrders.java:56: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] -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. -found : @LowerBoundUnknown int +ErrorOrders.java:59:33: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. +found : @UpperBoundLiteral(0) int +required: @LTLengthOf("p2") int +ErrorOrders.java:60: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 the expression did not parse. Error message: Encountered unexpected token: "isn" ] +ErrorOrders.java:60:55: compiler.err.proc.messager: [assignment.type.incompatible] incompatible types in assignment. +found : @LTLengthOf("p2") int +required: @LTLengthOf("[error for expression: This isn't an expression; error: Invalid 'This isn't an expression' because the expression did not parse. Error message: Encountered unexpected token: "isn" ]") int +ErrorOrders.java:65:15: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of InSameCompilationUnit.test4. +found : int required: @GTENegativeOne int -ErrorOrders.java:61:15: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @UpperBoundUnknown int +ErrorOrders.java:65:15: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of InSameCompilationUnit.test4. +found : int required: @UpperBoundBottom int -ErrorOrders.java:61:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @LowerBoundUnknown int +ErrorOrders.java:65:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of InSameCompilationUnit.test4. +found : int required: @GTENegativeOne int -ErrorOrders.java:61:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @UpperBoundUnknown int +ErrorOrders.java:65:24: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of InSameCompilationUnit.test4. +found : int required: @UpperBoundBottom int -ErrorOrders.java:61:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @LowerBoundUnknown int +ErrorOrders.java:65:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of InSameCompilationUnit.test4. +found : int required: @GTENegativeOne int -ErrorOrders.java:61:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @UpperBoundUnknown int +ErrorOrders.java:65:25: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p1 of InSameCompilationUnit.test4. +found : int required: @UpperBoundBottom int -ErrorOrders.java:61:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @LowerBoundUnknown int +ErrorOrders.java:65:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of InSameCompilationUnit.test4. +found : int required: @GTENegativeOne int -ErrorOrders.java:61:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @UpperBoundUnknown int +ErrorOrders.java:65:29: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p2 of InSameCompilationUnit.test4. +found : int required: @UpperBoundBottom int -ErrorOrders.java:61:33: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +ErrorOrders.java:65:33: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p3 of InSameCompilationUnit.test4. found : @UnknownVal int @UnknownVal [] -required: @UnknownVal int [] -ErrorOrders.java:61:37: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @SameLenUnknown int @SameLen("p4") [] -required: @SameLenUnknown int @SameLenBottom [] -ErrorOrders.java:61:41: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +required: @UnknownVal int @BottomVal [] +ErrorOrders.java:65:37: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p4 of InSameCompilationUnit.test4. +found : int @SameLen("p4") [] +required: int @SameLenBottom [] +ErrorOrders.java:65:41: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p5 of InSameCompilationUnit.test4. found : @UnknownVal int -required: int -ErrorOrders.java:61:46: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +required: @BottomVal int +ErrorOrders.java:65:46: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p3 of InSameCompilationUnit.test4. found : @UnknownVal int @UnknownVal [] -required: @UnknownVal int [] -ErrorOrders.java:61:50: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. -found : @SameLenUnknown int @SameLen("p4") [] -required: @SameLenUnknown int @SameLenBottom [] -ErrorOrders.java:61:54: compiler.err.proc.messager: [argument.type.incompatible] incompatible types in argument. +required: @UnknownVal int @BottomVal [] +ErrorOrders.java:65:50: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p4 of InSameCompilationUnit.test4. +found : int @SameLen("p4") [] +required: int @SameLenBottom [] +ErrorOrders.java:65:54: compiler.err.proc.messager: [argument.type.incompatible] incompatible argument for parameter p5 of InSameCompilationUnit.test4. found : @UnknownVal int -required: int +required: @BottomVal int 49 errors diff --git a/checker/jtreg/sortwarnings/Main.java b/checker/jtreg/sortwarnings/Main.java index 7a192198bd97..42f9a8192163 100644 --- a/checker/jtreg/sortwarnings/Main.java +++ b/checker/jtreg/sortwarnings/Main.java @@ -10,4 +10,4 @@ * @compile/fail/ref=OrderOfCheckers.out -XDrawDiagnostics -processor org.checkerframework.checker.index.IndexChecker OrderOfCheckers.java -AshowSuppressWarningsStrings */ -class Main {} +public class Main {} diff --git a/checker/jtreg/sortwarnings/OrderOfCheckers.java b/checker/jtreg/sortwarnings/OrderOfCheckers.java index 3f59a3c40d0d..b1c2df2104d2 100644 --- a/checker/jtreg/sortwarnings/OrderOfCheckers.java +++ b/checker/jtreg/sortwarnings/OrderOfCheckers.java @@ -8,6 +8,8 @@ /** This class tests that errors issued on the same tree are sorted by checker. */ public class OrderOfCheckers { + // Ignore the test suite's usage of qualifiers in illegal locations. + @SuppressWarnings("type.invalid.annotations.on.location") void test(int[] y) { @GTENegativeOne @UpperBoundBottom @SearchIndexBottom int @BottomVal @SameLenBottom [] x = y; } diff --git a/checker/jtreg/sortwarnings/OrderOfCheckers.out b/checker/jtreg/sortwarnings/OrderOfCheckers.out index 16406c27447b..479526be3c27 100644 --- a/checker/jtreg/sortwarnings/OrderOfCheckers.out +++ b/checker/jtreg/sortwarnings/OrderOfCheckers.out @@ -1,16 +1,16 @@ -OrderOfCheckers.java:12:99: compiler.err.proc.messager: [[value, allcheckers]:assignment.type.incompatible] incompatible types in assignment. +OrderOfCheckers.java:14:99: compiler.err.proc.messager: [[value, allcheckers]:assignment.type.incompatible] incompatible types in assignment. found : @UnknownVal int @UnknownVal [] -required: @UnknownVal int [] -OrderOfCheckers.java:12:99: compiler.err.proc.messager: [[index, searchindex, allcheckers]:assignment.type.incompatible] incompatible types in assignment. -found : @SearchIndexUnknown int @SearchIndexUnknown [] -required: @SearchIndexBottom int @SearchIndexUnknown [] -OrderOfCheckers.java:12:99: compiler.err.proc.messager: [[index, samelen, allcheckers]:assignment.type.incompatible] incompatible types in assignment. -found : @SameLenUnknown int @SameLen("y") [] -required: @SameLenUnknown int @SameLenBottom [] -OrderOfCheckers.java:12:99: compiler.err.proc.messager: [[index, lowerbound, allcheckers]:assignment.type.incompatible] incompatible types in assignment. -found : @LowerBoundUnknown int @LowerBoundUnknown [] -required: @GTENegativeOne int @LowerBoundUnknown [] -OrderOfCheckers.java:12:99: compiler.err.proc.messager: [[index, upperbound, allcheckers]:assignment.type.incompatible] incompatible types in assignment. -found : @UpperBoundUnknown int @UpperBoundUnknown [] -required: @UpperBoundBottom int @UpperBoundUnknown [] +required: @UnknownVal int @BottomVal [] +OrderOfCheckers.java:14:99: compiler.err.proc.messager: [[index, searchindex, allcheckers]:assignment.type.incompatible] incompatible types in assignment. +found : int [] +required: @SearchIndexBottom int [] +OrderOfCheckers.java:14:99: compiler.err.proc.messager: [[index, samelen, allcheckers]:assignment.type.incompatible] incompatible types in assignment. +found : int @SameLen("y") [] +required: int @SameLenBottom [] +OrderOfCheckers.java:14:99: compiler.err.proc.messager: [[index, lowerbound, allcheckers]:assignment.type.incompatible] incompatible types in assignment. +found : int [] +required: @GTENegativeOne int [] +OrderOfCheckers.java:14:99: compiler.err.proc.messager: [[index, upperbound, allcheckers]:assignment.type.incompatible] incompatible types in assignment. +found : int [] +required: @UpperBoundBottom int [] 5 errors diff --git a/checker/jtreg/stubs/annotatedFor/Test.astub b/checker/jtreg/stubs/annotatedFor/Test.astub new file mode 100644 index 000000000000..29c7e1d7bf9e --- /dev/null +++ b/checker/jtreg/stubs/annotatedFor/Test.astub @@ -0,0 +1,13 @@ +package annotatedforlib; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.AnnotatedFor; + +public class Test { + @AnnotatedFor("tainting") + public void method1(@Nullable T t){} + @AnnotatedFor("nullness") + public void method2(T t){} + @AnnotatedFor("nullness") + public Object method3() {} +} diff --git a/checker/jtreg/stubs/annotatedFor/UseTest.java b/checker/jtreg/stubs/annotatedFor/UseTest.java new file mode 100644 index 000000000000..a01dba5b82c2 --- /dev/null +++ b/checker/jtreg/stubs/annotatedFor/UseTest.java @@ -0,0 +1,23 @@ +/* + * @test + * @summary Test AnnotatedFor in stub files + * @compile -XDrawDiagnostics -Xlint:unchecked ../annotatedForLib/Test.java + * @compile/fail/ref=WithStub.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext UseTest.java -Astubs=Test.astub -Werror + * @compile/fail/ref=WithStub.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext UseTest.java -Astubs=Test.astub -Werror -AuseConservativeDefaultsForUncheckedCode=bytecode + * @compile/fail/ref=WithoutStub.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext UseTest.java -Werror + * @compile/fail/ref=WithoutStubConservative.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext UseTest.java -Werror -AuseConservativeDefaultsForUncheckedCode=bytecode + */ + +package annotatedfor; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import annotatedforlib.Test; + +public class UseTest { + void test(Test test) { + test.method1(null); + test.method2(null); + @NonNull Object o = test.method3(); + } +} diff --git a/checker/jtreg/stubs/annotatedFor/WithStub.out b/checker/jtreg/stubs/annotatedFor/WithStub.out new file mode 100644 index 000000000000..0c7b6e396a5d --- /dev/null +++ b/checker/jtreg/stubs/annotatedFor/WithStub.out @@ -0,0 +1,3 @@ +UseTest.java:19:22: compiler.err.proc.messager: (argument.type.incompatible) +UseTest.java:20:22: compiler.err.proc.messager: (argument.type.incompatible) +2 errors diff --git a/checker/jtreg/stubs/annotatedFor/WithoutStub.out b/checker/jtreg/stubs/annotatedFor/WithoutStub.out new file mode 100644 index 000000000000..34459869bcbd --- /dev/null +++ b/checker/jtreg/stubs/annotatedFor/WithoutStub.out @@ -0,0 +1,2 @@ +UseTest.java:19:22: compiler.err.proc.messager: (argument.type.incompatible) +1 error diff --git a/checker/jtreg/stubs/annotatedFor/WithoutStubConservative.out b/checker/jtreg/stubs/annotatedFor/WithoutStubConservative.out new file mode 100644 index 000000000000..1d82d5d7a14b --- /dev/null +++ b/checker/jtreg/stubs/annotatedFor/WithoutStubConservative.out @@ -0,0 +1,3 @@ +UseTest.java:19:22: compiler.err.proc.messager: (argument.type.incompatible) +UseTest.java:21:41: compiler.err.proc.messager: (assignment.type.incompatible) +2 errors diff --git a/checker/jtreg/stubs/annotatedForLib/Test.java b/checker/jtreg/stubs/annotatedForLib/Test.java new file mode 100644 index 000000000000..931ab715f481 --- /dev/null +++ b/checker/jtreg/stubs/annotatedForLib/Test.java @@ -0,0 +1,13 @@ +package annotatedforlib; + +import org.checkerframework.checker.nullness.qual.Nullable; + +public class Test { + public void method1(T t) {} + + public void method2(@Nullable T t) {} + + public Object method3() { + return ""; + } +} diff --git a/checker/jtreg/stubs/defaultqualinstub/Main.java b/checker/jtreg/stubs/defaultqualinstub/Main.java index 67d792e393a1..d8e881f4cdff 100644 --- a/checker/jtreg/stubs/defaultqualinstub/Main.java +++ b/checker/jtreg/stubs/defaultqualinstub/Main.java @@ -2,7 +2,7 @@ * @test * @summary Test that the DefaultQualifier in an astub works. * @library . - * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=defaults.astub pck/Defaults.java -AstubWarnIfNotFound -Werror + * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=defaults.astub pck/Defaults.java -Werror */ -class Main {} +public class Main {} diff --git a/checker/jtreg/stubs/defaultqualinstub/default-on-class/List.java b/checker/jtreg/stubs/defaultqualinstub/default-on-class/List.java new file mode 100644 index 000000000000..56ccb10f9c4c --- /dev/null +++ b/checker/jtreg/stubs/defaultqualinstub/default-on-class/List.java @@ -0,0 +1,3 @@ +public abstract class List { + abstract void retainAll(List other); +} diff --git a/checker/jtreg/stubs/defaultqualinstub/default-on-class/MutableList.java b/checker/jtreg/stubs/defaultqualinstub/default-on-class/MutableList.java new file mode 100644 index 000000000000..a4a6b8377bd3 --- /dev/null +++ b/checker/jtreg/stubs/defaultqualinstub/default-on-class/MutableList.java @@ -0,0 +1,4 @@ +public abstract class MutableList extends List { + @Override + abstract void retainAll(List other); +} diff --git a/checker/jtreg/stubs/defaultqualinstub/default-on-class/Test.java b/checker/jtreg/stubs/defaultqualinstub/default-on-class/Test.java new file mode 100644 index 000000000000..6b3689ef26f5 --- /dev/null +++ b/checker/jtreg/stubs/defaultqualinstub/default-on-class/Test.java @@ -0,0 +1,22 @@ +import org.checkerframework.checker.nullness.qual.*; + +/* + * @test + * @summary Defaults applied on class will not be inherited. + * + * @compile -XDrawDiagnostics -Xlint:unchecked List.java + * @compile -XDrawDiagnostics -Xlint:unchecked MutableList.java + * @compile/fail/ref=test.out -XDrawDiagnostics -Xlint:unchecked -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext -Astubs=list.astub -Werror Test.java + */ + +public class Test { + // l1 has type List + // mutableList has type MutableList + void foo(List l1, MutableList mutableList) { + // retainAll only accepts List with non-null elements + l1.retainAll(mutableList); + + // l2 has type List + List l2 = mutableList; + } +} diff --git a/checker/jtreg/stubs/defaultqualinstub/default-on-class/list.astub b/checker/jtreg/stubs/defaultqualinstub/default-on-class/list.astub new file mode 100644 index 000000000000..86e3ede54314 --- /dev/null +++ b/checker/jtreg/stubs/defaultqualinstub/default-on-class/list.astub @@ -0,0 +1,8 @@ +import org.checkerframework.framework.qual.DefaultQualifier; +import org.checkerframework.framework.qual.TypeUseLocation; +import org.checkerframework.checker.nullness.qual.NonNull; + +@DefaultQualifier(value = NonNull.class, locations = TypeUseLocation.UPPER_BOUND) +public class List { + abstract void retainAll(List other); +} diff --git a/checker/jtreg/stubs/defaultqualinstub/default-on-class/test.out b/checker/jtreg/stubs/defaultqualinstub/default-on-class/test.out new file mode 100644 index 000000000000..7561b6e9ae20 --- /dev/null +++ b/checker/jtreg/stubs/defaultqualinstub/default-on-class/test.out @@ -0,0 +1,3 @@ +Test.java:17:22: compiler.err.proc.messager: (argument.type.incompatible) +Test.java:20:22: compiler.err.proc.messager: (assignment.type.incompatible) +2 errors diff --git a/checker/jtreg/stubs/defaultqualinstub/pck/Defaults.java b/checker/jtreg/stubs/defaultqualinstub/pck/Defaults.java index 442574d66ac5..32e338c728a5 100644 --- a/checker/jtreg/stubs/defaultqualinstub/pck/Defaults.java +++ b/checker/jtreg/stubs/defaultqualinstub/pck/Defaults.java @@ -1,6 +1,6 @@ package pck; -class Defaults { +public class Defaults { Object o; void test() { diff --git a/checker/jtreg/stubs/fakeoverrides/DefineClasses.astub b/checker/jtreg/stubs/fakeoverrides/DefineClasses.astub new file mode 100644 index 000000000000..a01a70485571 --- /dev/null +++ b/checker/jtreg/stubs/fakeoverrides/DefineClasses.astub @@ -0,0 +1,12 @@ +package fakeoverrides; +import org.checkerframework.checker.tainting.qual.Untainted; + +class SuperClass implements SuperInterface { + // fake override: + @Untainted int m(); +} + +interface SubInterface extends SuperInterface { + // fake override: + int m(); +} diff --git a/checker/jtreg/stubs/fakeoverrides/DefineClasses.java b/checker/jtreg/stubs/fakeoverrides/DefineClasses.java new file mode 100644 index 000000000000..8894a01278ce --- /dev/null +++ b/checker/jtreg/stubs/fakeoverrides/DefineClasses.java @@ -0,0 +1,19 @@ +package fakeoverrides; + +public class DefineClasses {} + +interface SuperInterface { + default int m() { + return 0; + } +} + +class SuperClass implements SuperInterface { + // fake override: + // @Untainted int m(); +} + +interface SubInterface extends SuperInterface { + // fake override: + // int m(); +} diff --git a/checker/jtreg/stubs/fakeoverrides/Use.java b/checker/jtreg/stubs/fakeoverrides/Use.java new file mode 100644 index 000000000000..cf1f8bba4785 --- /dev/null +++ b/checker/jtreg/stubs/fakeoverrides/Use.java @@ -0,0 +1,19 @@ +package fakeoverrides; + +import org.checkerframework.checker.tainting.qual.Untainted; + +/* + * @test + * @summary Test case for multiple fake overrides applying to a callsite. + * + * @compile -XDrawDiagnostics DefineClasses.java + * @compile -XDrawDiagnostics -processor org.checkerframework.checker.tainting.TaintingChecker -Astubs=DefineClasses.astub -AstubWarnIfNotFound -Werror Use.java + */ +// TODO: Issue error SuperClass and SubInterface have conflicting fake overrides +// See https://github.com/typetools/checker-framework/issues/2724 +public class Use extends SuperClass implements SubInterface { + void use(Use d) { + // Ok, because the fake override in SuperClasses is taken over the one in SubInterface. + @Untainted int i = d.m(); + } +} diff --git a/checker/jtreg/stubs/general/Driver.java b/checker/jtreg/stubs/general/Driver.java index ebf512d9f4af..44b018fdba8f 100644 --- a/checker/jtreg/stubs/general/Driver.java +++ b/checker/jtreg/stubs/general/Driver.java @@ -2,10 +2,10 @@ * @test * @summary Test that java.lang annotations can be used. * @library . - * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=MyStub.astub Driver.java -AstubWarnIfNotFound -Werror + * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=MyStub.astub Driver.java -Werror */ -class Driver { +public class Driver { void test() { Object o = null; String v = String.valueOf(o); diff --git a/checker/jtreg/stubs/general/Driver2.java b/checker/jtreg/stubs/general/Driver2.java new file mode 100644 index 000000000000..4ca1a6d98e02 --- /dev/null +++ b/checker/jtreg/stubs/general/Driver2.java @@ -0,0 +1,9 @@ +/* + * @test + * @summary Test that a stub file can have no package, but have an annotation on the class. + * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=NoPackage.astub Driver2.java -Werror + */ + +public class Driver2 { + void test() {} +} diff --git a/checker/jtreg/stubs/general/NoPackage.astub b/checker/jtreg/stubs/general/NoPackage.astub new file mode 100644 index 000000000000..5e70f59c1954 --- /dev/null +++ b/checker/jtreg/stubs/general/NoPackage.astub @@ -0,0 +1,7 @@ +// No package statement. + +// Prevent a warning about a stub file having no import statements. +import java.lang.SuppressWarnings; + +@SuppressWarnings("") // Any annotation. +class Driver {} diff --git a/checker/jtreg/stubs/issue1356/Main.java b/checker/jtreg/stubs/issue1356/Main.java index 37ecab187105..551a004929be 100644 --- a/checker/jtreg/stubs/issue1356/Main.java +++ b/checker/jtreg/stubs/issue1356/Main.java @@ -7,4 +7,4 @@ * @compile -XDrawDiagnostics -processor org.checkerframework.checker.regex.RegexChecker mypackage/UseMyClass.java */ -class Main {} +public class Main {} diff --git a/checker/jtreg/stubs/issue1456/Main.java b/checker/jtreg/stubs/issue1456/Main.java index ec7c9cfc3108..8712dbd13b3d 100644 --- a/checker/jtreg/stubs/issue1456/Main.java +++ b/checker/jtreg/stubs/issue1456/Main.java @@ -7,9 +7,10 @@ */ package issue1456; -import issue1456lib.Lib; import org.checkerframework.checker.tainting.qual.Untainted; +import issue1456lib.Lib; + public class Main { void test(Lib lib) { diff --git a/checker/jtreg/stubs/issue1456/WithStub.out b/checker/jtreg/stubs/issue1456/WithStub.out index 37c1e01deaf8..1ad5923e6306 100644 --- a/checker/jtreg/stubs/issue1456/WithStub.out +++ b/checker/jtreg/stubs/issue1456/WithStub.out @@ -1,2 +1,2 @@ -Main.java:26:17: compiler.err.proc.messager: (type.argument.type.incompatible) +Main.java:27:17: compiler.err.proc.messager: (type.argument.type.incompatible) 1 error diff --git a/checker/jtreg/stubs/issue1456/WithoutStub.out b/checker/jtreg/stubs/issue1456/WithoutStub.out index 6b42bcfdcfc5..c71f3c767459 100644 --- a/checker/jtreg/stubs/issue1456/WithoutStub.out +++ b/checker/jtreg/stubs/issue1456/WithoutStub.out @@ -1,8 +1,8 @@ -Main.java:16:34: compiler.err.proc.messager: (assignment.type.incompatible) -Main.java:17:46: compiler.err.proc.messager: (assignment.type.incompatible) -Main.java:18:35: compiler.err.proc.messager: (assignment.type.incompatible) +Main.java:17:34: compiler.err.proc.messager: (assignment.type.incompatible) +Main.java:18:46: compiler.err.proc.messager: (assignment.type.incompatible) Main.java:19:35: compiler.err.proc.messager: (assignment.type.incompatible) -Main.java:20:33: compiler.err.proc.messager: (assignment.type.incompatible) -Main.java:21:47: compiler.err.proc.messager: (assignment.type.incompatible) -Main.java:22:50: compiler.err.proc.messager: (assignment.type.incompatible) +Main.java:20:35: compiler.err.proc.messager: (assignment.type.incompatible) +Main.java:21:33: compiler.err.proc.messager: (assignment.type.incompatible) +Main.java:22:47: compiler.err.proc.messager: (assignment.type.incompatible) +Main.java:23:50: compiler.err.proc.messager: (assignment.type.incompatible) 7 errors diff --git a/checker/jtreg/stubs/issue1496/ClassAnnotation.astub b/checker/jtreg/stubs/issue1496/ClassAnnotation.astub index a4c77f226642..65bb657f5d43 100644 --- a/checker/jtreg/stubs/issue1496/ClassAnnotation.astub +++ b/checker/jtreg/stubs/issue1496/ClassAnnotation.astub @@ -1,10 +1,10 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.DefaultQualifier; -// This checks that fully-qualified class literals in annotations are properly handled by the StubParser. +// This checks that fully-qualified class literals in annotations are properly handled by the AnnotationFileParser. @DefaultQualifier(org.checkerframework.checker.nullness.qual.NonNull.class) package java.lang; -// This checks that simply-named class literals in annotations are properly handled by the StubParser. +// This checks that simply-named class literals in annotations are properly handled by the AnnotationFileParser. @DefaultQualifier(Nullable.class) package java.util; diff --git a/checker/jtreg/stubs/issue1496/Main.java b/checker/jtreg/stubs/issue1496/Main.java index df9affddfa30..92a2bbcec2a6 100644 --- a/checker/jtreg/stubs/issue1496/Main.java +++ b/checker/jtreg/stubs/issue1496/Main.java @@ -1,7 +1,7 @@ /* * @test * @summary Test case for Issue 1496 https://github.com/typetools/checker-framework/issues/1496 - * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=ClassAnnotation.astub Main.java -Werror -AstubWarnIfNotFound + * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=ClassAnnotation.astub Main.java -Werror */ package issue1496; diff --git a/checker/jtreg/stubs/issue1542/UsesIntRange.java b/checker/jtreg/stubs/issue1542/UsesIntRange.java index f5a2536314b0..d5bfaa01525d 100644 --- a/checker/jtreg/stubs/issue1542/UsesIntRange.java +++ b/checker/jtreg/stubs/issue1542/UsesIntRange.java @@ -2,7 +2,7 @@ import org.checkerframework.common.value.qual.IntRange; -class UsesIntRange { +public class UsesIntRange { void do_things() { @IntRange(from = 3, to = 20000) int x = NeedsIntRange.range(true); @IntRange(from = 3L, to = 20000) int y = NeedsIntRange.range(true); diff --git a/checker/jtreg/stubs/issue1585/Main.java b/checker/jtreg/stubs/issue1585/Main.java index be8db878f8f1..3e4c7086ffc4 100644 --- a/checker/jtreg/stubs/issue1585/Main.java +++ b/checker/jtreg/stubs/issue1585/Main.java @@ -5,8 +5,8 @@ * * ParseError.out and UnknownField.out contain expected warnings. * - * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=UnknownField.astub Main.java - * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=ParseError.astub Main.java + * @compile/ref=UnknownField.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=UnknownField.astub -Anomsgtext Main.java + * @compile/ref=ParseError.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=ParseError.astub -Anomsgtext Main.java */ package issue1585; diff --git a/checker/jtreg/stubs/issue1585/ParseError.out b/checker/jtreg/stubs/issue1585/ParseError.out index 30d3fed09b6f..fd6157b14a8a 100644 --- a/checker/jtreg/stubs/issue1585/ParseError.out +++ b/checker/jtreg/stubs/issue1585/ParseError.out @@ -1,9 +1,2 @@ -warning: StubParser: exception from StubParser.parse for file checker/jtreg/stubs/issue1585/ParseErrors.astub. Encountered problems: Encountered unexpected token: "{" "{" - at line 3, column 17. - - Was expecting one of: - - "," - ">" - "extends" +- compiler.warn.proc.messager: ParseError.astub: (line 3,col 12) Parse error. Found "{", expected one of "," ">" "extends" 1 warning diff --git a/checker/jtreg/stubs/issue1585/UnknownField.out b/checker/jtreg/stubs/issue1585/UnknownField.out index 8df7a68a9f81..d99f595ea8c6 100644 --- a/checker/jtreg/stubs/issue1585/UnknownField.out +++ b/checker/jtreg/stubs/issue1585/UnknownField.out @@ -1,3 +1,4 @@ -warning: StubParser: Field not found: NonNull -warning: StubParser: Annotation expression, NonNull, could not be processed for annotation: @DefaultQualifier(NonNull). -2 warnings +- compiler.warn.proc.messager: UnknownField.astub:(line 5,col 19): static field NonNull is not imported +- compiler.warn.proc.messager: UnknownField.astub:(line 5,col 19): for annotation @DefaultQualifier(NonNull), could not add value=NonNull because variable NonNull not found +- compiler.warn.proc.messager: UnknownField.astub:(line 5,col 1): unknown annotation @DefaultQualifier(NonNull) +3 warnings diff --git a/checker/jtreg/stubs/issue2059/AnnoNotFound.out b/checker/jtreg/stubs/issue2059/AnnoNotFound.out index 1dbde8e859c0..701edd58bb3d 100644 --- a/checker/jtreg/stubs/issue2059/AnnoNotFound.out +++ b/checker/jtreg/stubs/issue2059/AnnoNotFound.out @@ -1,2 +1,2 @@ -- compiler.warn.proc.messager: StubParser: Unknown annotation: @Nullable +- compiler.warn.proc.messager: AnnoNotFound.astub:(line 8,col 3): unknown annotation @Nullable 1 warning diff --git a/checker/jtreg/stubs/issue2059/Main.java b/checker/jtreg/stubs/issue2059/Main.java index f82487eb308c..65cc63757d03 100644 --- a/checker/jtreg/stubs/issue2059/Main.java +++ b/checker/jtreg/stubs/issue2059/Main.java @@ -2,7 +2,7 @@ * @test * @summary Test case for Issue 2059 https://github.com/typetools/checker-framework/issues/2059 * - * @compile/ref=AnnoNotFound.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -AstubWarnIfNotFound -Astubs=AnnoNotFound.astub Main.java + * @compile/ref=AnnoNotFound.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=AnnoNotFound.astub -Anomsgtext Main.java */ package issue2059; diff --git a/checker/jtreg/stubs/issue2147/EnumStubTest.java b/checker/jtreg/stubs/issue2147/EnumStubTest.java index ac02044609ca..79c6974beb40 100644 --- a/checker/jtreg/stubs/issue2147/EnumStubTest.java +++ b/checker/jtreg/stubs/issue2147/EnumStubTest.java @@ -10,7 +10,7 @@ import org.checkerframework.checker.tainting.qual.*; -class EnumStubTest { +public class EnumStubTest { void test() { requireEnum(SampleEnum.FIRST); requireEnum(SampleEnum.SECOND); diff --git a/checker/jtreg/stubs/issue2147/WithStub.out b/checker/jtreg/stubs/issue2147/WithStub.out index f9e01800442a..8e3130cc4832 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 EnumStubTest.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 d543064e4817..e841ecfa4858 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 EnumStubTest.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 EnumStubTest.requireEnum. found : @Tainted SampleEnum required: @Untainted SampleEnum 2 errors diff --git a/checker/jtreg/stubs/sample/Sample.java b/checker/jtreg/stubs/sample/Sample.java index 82fb9d60284b..9b15e7eb156a 100644 --- a/checker/jtreg/stubs/sample/Sample.java +++ b/checker/jtreg/stubs/sample/Sample.java @@ -2,10 +2,10 @@ * @test * @summary Test that the stub files get invoked * @library . - * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=sample.astub Sample.java -AstubWarnIfNotFound -Werror + * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=sample.astub Sample.java -Werror */ -class Sample { +public class Sample { void test() { Object o = null; String v = String.valueOf(o); diff --git a/checker/jtreg/stubs/stub-over-jdk/Issue321.astub b/checker/jtreg/stubs/stub-over-jdk/Issue321.astub new file mode 100644 index 000000000000..ed9b8891cd22 --- /dev/null +++ b/checker/jtreg/stubs/stub-over-jdk/Issue321.astub @@ -0,0 +1,2 @@ +package java.util; +class Optional {} diff --git a/checker/jtreg/stubs/stub-over-jdk/Issue321.java b/checker/jtreg/stubs/stub-over-jdk/Issue321.java new file mode 100644 index 000000000000..ad8fac0360f4 --- /dev/null +++ b/checker/jtreg/stubs/stub-over-jdk/Issue321.java @@ -0,0 +1,14 @@ +/* + * @test + * @summary Test for EISOP Issue #321 + * @compile/fail/ref=Issue321.out -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker Issue321.java -Anomsgtext + * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=Issue321.astub Issue321.java + */ + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Optional; + +interface Issue321 { + @Nullable Optional o(); +} diff --git a/checker/jtreg/stubs/stub-over-jdk/Issue321.out b/checker/jtreg/stubs/stub-over-jdk/Issue321.out new file mode 100644 index 000000000000..2ee6951f4e10 --- /dev/null +++ b/checker/jtreg/stubs/stub-over-jdk/Issue321.out @@ -0,0 +1,2 @@ +Issue321.java:13:32: compiler.err.proc.messager: (type.invalid.annotations.on.use) +1 error diff --git a/checker/jtreg/stubs/wildcards/NonN.java b/checker/jtreg/stubs/wildcards/NonN.java index 92ad4f14f347..3da7420ea46a 100644 --- a/checker/jtreg/stubs/wildcards/NonN.java +++ b/checker/jtreg/stubs/wildcards/NonN.java @@ -1 +1 @@ -class NonN {} +public class NonN {} diff --git a/checker/jtreg/stubs/wildcards/Wildcards.java b/checker/jtreg/stubs/wildcards/Wildcards.java index a87cb169f4b1..79caa4414aa5 100644 --- a/checker/jtreg/stubs/wildcards/Wildcards.java +++ b/checker/jtreg/stubs/wildcards/Wildcards.java @@ -3,10 +3,10 @@ * @summary Test for Issue #1055 * @library . * @compile NonN.java - * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=NonN.astub Wildcards.java -AstubWarnIfNotFound -Werror + * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=NonN.astub Wildcards.java -Werror */ -class Wildcards { +public class Wildcards { NonN f = new NonN(); class LocalNonN {} diff --git a/checker/jtreg/tainting/NewClass.out b/checker/jtreg/tainting/NewClass.out index b589c23b8ae2..aab933afc0bc 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 NewClass.get. found : @Tainted Object required: @Untainted Object 1 error diff --git a/checker/jtreg/tainting/classes/Issue919.java b/checker/jtreg/tainting/classes/Issue919.java index 8f30fa89cf3e..84ffb923cfd7 100644 --- a/checker/jtreg/tainting/classes/Issue919.java +++ b/checker/jtreg/tainting/classes/Issue919.java @@ -1,8 +1,9 @@ package classes; -import classes.Issue919B.InnerClass; import java.util.Set; +import classes.Issue919B.InnerClass; + public class Issue919 { private static void method(Set innerClassSet2) throws Exception { Issue919B.otherMethod(innerClassSet2); diff --git a/checker/jtreg/unboundedWildcards/issue1275/Crash.java b/checker/jtreg/unboundedWildcards/issue1275/Crash.java index 22c4742ce3ea..2f3fddf281f2 100644 --- a/checker/jtreg/unboundedWildcards/issue1275/Crash.java +++ b/checker/jtreg/unboundedWildcards/issue1275/Crash.java @@ -6,7 +6,7 @@ * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext Lib.java * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker -Anomsgtext Crash.java */ -class Crash { +public class Crash { void crash(Sub o) { Sub.SubInner x = o.a().b().b(); o.a().b().b().c(); diff --git a/checker/jtreg/unboundedWildcards/issue1427/T.java b/checker/jtreg/unboundedWildcards/issue1427/T.java index e084bcbef83d..5fcad3ecdc00 100644 --- a/checker/jtreg/unboundedWildcards/issue1427/T.java +++ b/checker/jtreg/unboundedWildcards/issue1427/T.java @@ -6,7 +6,7 @@ * @compile B.java * @compile -XDrawDiagnostics -processor org.checkerframework.checker.tainting.TaintingChecker T.java */ -class Test { +public class T { { Three.c().g().f().build(); } diff --git a/checker/jtreg/unboundedWildcards/issue1428/T.java b/checker/jtreg/unboundedWildcards/issue1428/T.java index d29421ab7efb..e85b0e55fbdb 100644 --- a/checker/jtreg/unboundedWildcards/issue1428/T.java +++ b/checker/jtreg/unboundedWildcards/issue1428/T.java @@ -10,7 +10,7 @@ import java.util.Collections; import java.util.List; -class T { +public class T { public List> f(B b) { return true ? Collections.>emptyList() : b.getItems(); } diff --git a/checker/jtregJdk11/Jdk11Release8.java b/checker/jtregJdk11/Jdk11Release8.java index ef05dc68fe65..ec5d530abd1b 100644 --- a/checker/jtregJdk11/Jdk11Release8.java +++ b/checker/jtregJdk11/Jdk11Release8.java @@ -5,4 +5,4 @@ * @compile -XDrawDiagnostics -processor org.checkerframework.checker.interning.InterningChecker Jdk11Release8.java --release 8 */ -class Jdk11Release8 {} +public class Jdk11Release8 {} diff --git a/checker/jtregJdk11/issue244/MyDialog.java b/checker/jtregJdk11/issue244/MyDialog.java index 266ce7e60767..e91450655fef 100644 --- a/checker/jtregJdk11/issue244/MyDialog.java +++ b/checker/jtregJdk11/issue244/MyDialog.java @@ -2,6 +2,6 @@ * @test * @summary Test for crash * - * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker MyDialog.java -target 1.8 -source 1.8 -nowarn + * @compile -XDrawDiagnostics -processor org.checkerframework.checker.nullness.NullnessChecker MyDialog.java -nowarn */ public class MyDialog extends javax.swing.JDialog {} diff --git a/checker/resources b/checker/resources deleted file mode 120000 index 02aeeb1b16dc..000000000000 --- a/checker/resources +++ /dev/null @@ -1 +0,0 @@ -src/main/resources \ No newline at end of file diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnalysis.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnalysis.java new file mode 100644 index 000000000000..a1085a572841 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnalysis.java @@ -0,0 +1,58 @@ +package org.checkerframework.checker.calledmethods; + +import com.google.common.collect.ImmutableSet; +import com.sun.tools.javac.code.Type; + +import org.checkerframework.checker.signature.qual.CanonicalName; +import org.checkerframework.common.accumulation.AccumulationAnalysis; +import org.checkerframework.common.basetype.BaseTypeChecker; + +import java.util.Set; + +import javax.lang.model.type.TypeMirror; + +/** + * The analysis for the Called Methods Checker. The analysis is specialized to ignore certain + * exception types; see {@link #isIgnoredExceptionType(TypeMirror)}. + */ +public class CalledMethodsAnalysis extends AccumulationAnalysis { + + /** + * The fully-qualified names of the exception types that are ignored by this checker when + * computing dataflow stores. + */ + protected static final Set<@CanonicalName String> ignoredExceptionTypes = + ImmutableSet.of( + // Use the Nullness Checker instead. + NullPointerException.class.getCanonicalName(), + // Ignore run-time errors, which cannot be predicted statically. Doing + // so is unsound in the sense that they could still occur - e.g., the + // program could run out of memory - but if they did, the checker's + // results would be useless anyway. + Error.class.getCanonicalName(), + RuntimeException.class.getCanonicalName()); + + /** + * Creates a new {@code CalledMethodsAnalysis}. + * + * @param checker the checker + * @param factory the factory + */ + protected CalledMethodsAnalysis( + BaseTypeChecker checker, CalledMethodsAnnotatedTypeFactory factory) { + super(checker, factory); + } + + /** + * Ignore exceptional control flow due to ignored exception types. + * + * @param exceptionType exception type + * @return {@code true} if {@code exceptionType} is a member of {@link #ignoredExceptionTypes}, + * {@code false} otherwise + */ + @Override + protected boolean isIgnoredExceptionType(TypeMirror exceptionType) { + return ignoredExceptionTypes.contains( + ((Type) exceptionType).tsym.getQualifiedName().toString()); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java new file mode 100644 index 000000000000..8a310f5a7df9 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsAnnotatedTypeFactory.java @@ -0,0 +1,581 @@ +package org.checkerframework.checker.calledmethods; + +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.Tree; + +import org.checkerframework.checker.calledmethods.builder.AutoValueSupport; +import org.checkerframework.checker.calledmethods.builder.BuilderFrameworkSupport; +import org.checkerframework.checker.calledmethods.builder.LombokSupport; +import org.checkerframework.checker.calledmethods.qual.CalledMethods; +import org.checkerframework.checker.calledmethods.qual.CalledMethodsBottom; +import org.checkerframework.checker.calledmethods.qual.CalledMethodsPredicate; +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsOnException; +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsVarArgs; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.accumulation.AccumulationAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.value.ValueAnnotatedTypeFactory; +import org.checkerframework.common.value.ValueChecker; +import org.checkerframework.common.value.ValueCheckerUtils; +import org.checkerframework.dataflow.analysis.Analysis; +import org.checkerframework.dataflow.analysis.Analysis.BeforeOrAfter; +import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; +import org.checkerframework.framework.type.treeannotator.TreeAnnotator; +import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; +import org.checkerframework.framework.type.typeannotator.TypeAnnotator; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; +import org.checkerframework.javacutil.UserError; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** The annotated type factory for the Called Methods Checker. */ +public class CalledMethodsAnnotatedTypeFactory extends AccumulationAnnotatedTypeFactory { + + /** + * The builder frameworks (such as Lombok and AutoValue) supported by this instance of the + * Called Methods Checker. + */ + private final Collection builderFrameworkSupports; + + /** + * Whether to use the Value Checker as a subchecker to reduce false positives when analyzing + * calls to the AWS SDK. Defaults to false. Controlled by the command-line option {@code + * -AuseValueChecker}. + */ + private final boolean useValueChecker; + + /** + * The {@link java.util.Collections#singletonList} method. It is treated specially by {@link + * #adjustMethodNameUsingValueChecker}. + */ + private final ExecutableElement collectionsSingletonList = + TreeUtils.getMethod("java.util.Collections", "singletonList", 1, getProcessingEnv()); + + /** The {@link CalledMethods#value} element/argument. */ + /*package-private*/ final ExecutableElement calledMethodsValueElement = + TreeUtils.getMethod(CalledMethods.class, "value", 0, processingEnv); + + /** The {@link EnsuresCalledMethodsVarArgs#value} element/argument. */ + /*package-private*/ final ExecutableElement ensuresCalledMethodsVarArgsValueElement = + TreeUtils.getMethod(EnsuresCalledMethodsVarArgs.class, "value", 0, processingEnv); + + /** The {@link EnsuresCalledMethodsOnException#value} element/argument. */ + /*package-private*/ final ExecutableElement ensuresCalledMethodsOnExceptionValueElement = + TreeUtils.getMethod(EnsuresCalledMethodsOnException.class, "value", 0, processingEnv); + + /** The {@link EnsuresCalledMethodsOnException#methods} element/argument. */ + /*package-private*/ final ExecutableElement ensuresCalledMethodsOnExceptionMethodsElement = + TreeUtils.getMethod(EnsuresCalledMethodsOnException.class, "methods", 0, processingEnv); + + /** The {@link EnsuresCalledMethodsOnException.List#value} element/argument. */ + /*package-private*/ final ExecutableElement ensuresCalledMethodsOnExceptionListValueElement = + TreeUtils.getMethod( + EnsuresCalledMethodsOnException.List.class, "value", 0, processingEnv); + + /** + * Create a new CalledMethodsAnnotatedTypeFactory. + * + * @param checker the checker + */ + public CalledMethodsAnnotatedTypeFactory(BaseTypeChecker checker) { + super( + checker, + CalledMethods.class, + CalledMethodsBottom.class, + CalledMethodsPredicate.class); + + this.builderFrameworkSupports = new ArrayList<>(2); + List disabledFrameworks = + checker.getStringsOption( + CalledMethodsChecker.DISABLE_BUILDER_FRAMEWORK_SUPPORTS, ','); + enableFrameworks(disabledFrameworks); + + this.useValueChecker = checker.hasOption(CalledMethodsChecker.USE_VALUE_CHECKER); + + // Lombok generates @CalledMethods annotations using an old package name, + // so we maintain it as an alias. + addAliasedTypeAnnotation( + "org.checkerframework.checker.builder.qual.CalledMethods", + CalledMethods.class, + true); + // Lombok also generates an @NotCalledMethods annotation, which we have no support for. We + // therefore treat it as top. + addAliasedTypeAnnotation( + "org.checkerframework.checker.builder.qual.NotCalledMethods", this.top); + + // Don't call postInit() for subclasses. + if (this.getClass() == CalledMethodsAnnotatedTypeFactory.class) { + this.postInit(); + } + } + + /** + * Enables support for the default builder-generation frameworks, except those listed in the + * disabled builder frameworks parsed from the -AdisableBuilderFrameworkSupport option's + * arguments. Throws a UserError if the user included an unsupported framework in the list of + * frameworks to be disabled. + * + * @param disabledFrameworks the disabled builder frameworks + */ + private void enableFrameworks(List disabledFrameworks) { + boolean enableAutoValueSupport = true; + boolean enableLombokSupport = true; + for (String framework : disabledFrameworks) { + switch (framework) { + case "autovalue": + enableAutoValueSupport = false; + break; + case "lombok": + enableLombokSupport = false; + break; + default: + throw new UserError( + "Unsupported builder framework in -AdisableBuilderFrameworkSupports: " + + framework); + } + } + if (enableAutoValueSupport) { + builderFrameworkSupports.add(new AutoValueSupport(this)); + } + if (enableLombokSupport) { + builderFrameworkSupports.add(new LombokSupport(this)); + } + } + + @Override + protected TreeAnnotator createTreeAnnotator() { + return new ListTreeAnnotator( + super.createTreeAnnotator(), new CalledMethodsTreeAnnotator(this)); + } + + @Override + protected TypeAnnotator createTypeAnnotator() { + return new ListTypeAnnotator( + super.createTypeAnnotator(), new CalledMethodsTypeAnnotator(this)); + } + + @Override + public boolean returnsThis(MethodInvocationTree tree) { + return super.returnsThis(tree) + // Continue to trust but not check the old {@link + // org.checkerframework.checker.builder.qual.ReturnsReceiver} annotation, for + // backwards compatibility. + || this.getDeclAnnotation( + TreeUtils.elementFromUse(tree), + org.checkerframework.checker.builder.qual.ReturnsReceiver.class) + != null; + } + + /** + * Given a tree, returns the name of the method that the tree should be considered as calling. + * Returns "withOwners" if the call sets an "owner", "owner-alias", or "owner-id" filter. + * Returns "withImageIds" if the call sets an "image-ids" filter. + * + *

    Package-private to permit calls from {@link CalledMethodsTransfer}. + * + * @param methodName the name of the method being explicitly called + * @param tree the invocation of the method + * @return "withOwners" or "withImageIds" if the tree is an equivalent filter addition. + * Otherwise, return the first argument. + */ + // This cannot return a Name because filterTreeToMethodName cannot. + public String adjustMethodNameUsingValueChecker(String methodName, MethodInvocationTree tree) { + if (!useValueChecker) { + return methodName; + } + + ExecutableElement invokedMethod = TreeUtils.elementFromUse(tree); + if (!ElementUtils.enclosingTypeElement(invokedMethod) + .getQualifiedName() + .contentEquals("com.amazonaws.services.ec2.model.DescribeImagesRequest")) { + return methodName; + } + + if (methodName.equals("withFilters") || methodName.equals("setFilters")) { + ValueAnnotatedTypeFactory valueATF = getTypeFactoryOfSubchecker(ValueChecker.class); + for (Tree filterTree : tree.getArguments()) { + if (TreeUtils.isMethodInvocation( + filterTree, collectionsSingletonList, getProcessingEnv())) { + // Descend into a call to Collections.singletonList() + filterTree = ((MethodInvocationTree) filterTree).getArguments().get(0); + } + String adjustedMethodName = filterTreeToMethodName(filterTree, valueATF); + if (adjustedMethodName != null) { + return adjustedMethodName; + } + } + } + return methodName; + } + + /** + * Determine the name of the method in DescribeImagesRequest that is equivalent to the Filter in + * the given tree. + * + *

    Returns null unless the argument is one of the following: + * + *

      + *
    • a constructor invocation of the Filter constructor whose first argument is the name, + * such as {@code new Filter("owner").*}, or + *
    • a call to the withName method, such as {@code new Filter().*.withName("owner").*}. + *
    + * + * In those cases, it returns either the argument to the constructor or the argument to the last + * invocation of withName ("owner" in both of the above examples). + * + * @param filterTree the tree that represents the filter (an argument to the withFilters or + * setFilters method) + * @param valueATF the type factory from the Value Checker + * @return the adjusted method name, or null if the method name should not be adjusted + */ + // This cannot return a Name because filterKindToMethodName cannot. + private @Nullable String filterTreeToMethodName( + Tree filterTree, ValueAnnotatedTypeFactory valueATF) { + while (filterTree != null && filterTree.getKind() == Tree.Kind.METHOD_INVOCATION) { + + MethodInvocationTree filterTreeAsMethodInvocation = (MethodInvocationTree) filterTree; + String filterMethodName = TreeUtils.methodName(filterTreeAsMethodInvocation).toString(); + if (filterMethodName.contentEquals("withName") + && filterTreeAsMethodInvocation.getArguments().size() >= 1) { + Tree withNameArgTree = filterTreeAsMethodInvocation.getArguments().get(0); + String withNameArg = + ValueCheckerUtils.getExactStringValue(withNameArgTree, valueATF); + return filterKindToMethodName(withNameArg); + } + // Proceed leftward (toward the receiver) in a fluent call sequence. + filterTree = TreeUtils.getReceiverTree(filterTreeAsMethodInvocation.getMethodSelect()); + } + // The loop has reached the beginning of a fluent sequence of method calls. If the ultimate + // receiver at the beginning of that fluent sequence is a call to the Filter() constructor, + // then use the first argument to the Filter constructor, which is the name of the filter. + if (filterTree == null) { + return null; + } + if (filterTree.getKind() == Tree.Kind.NEW_CLASS) { + ExpressionTree constructorArg = ((NewClassTree) filterTree).getArguments().get(0); + String filterKindName = ValueCheckerUtils.getExactStringValue(constructorArg, valueATF); + if (filterKindName != null) { + return filterKindToMethodName(filterKindName); + } + } + return null; + } + + /** + * Converts from a kind of filter to the name of the corresponding method on a + * DescribeImagesRequest object. + * + * @param filterKind the kind of filter + * @return "withOwners" if filterKind is "owner", "owner-alias", or "owner-id"; "withImageIds" + * if filterKind is "image-id"; null otherwise + */ + private static @Nullable String filterKindToMethodName(String filterKind) { + switch (filterKind) { + case "owner": + case "owner-alias": + case "owner-id": + return "withOwners"; + case "image-id": + return "withImageIds"; + default: + return null; + } + } + + /** + * At a fluent method call (which returns {@code this}), add the method to the type of the + * return value. + */ + private class CalledMethodsTreeAnnotator extends AccumulationTreeAnnotator { + /** + * Creates an instance of this tree annotator for the given type factory. + * + * @param factory the type factory + */ + public CalledMethodsTreeAnnotator(AccumulationAnnotatedTypeFactory factory) { + super(factory); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, AnnotatedTypeMirror type) { + // Accumulate a method call, by adding the method being invoked to the return type. + if (returnsThis(tree)) { + TypeMirror typeMirror = type.getUnderlyingType(); + String methodName = TreeUtils.getMethodName(tree.getMethodSelect()); + methodName = adjustMethodNameUsingValueChecker(methodName, tree); + AnnotationMirror oldAnno = type.getAnnotationInHierarchy(top); + AnnotationMirror newAnno = + qualHierarchy.greatestLowerBoundShallow( + oldAnno, + typeMirror, + createAccumulatorAnnotation(methodName), + typeMirror); + type.replaceAnnotation(newAnno); + } + + // Also do the standard accumulation analysis behavior: copy any accumulation + // annotations from the receiver to the return type. + return super.visitMethodInvocation(tree, type); + } + + @Override + public Void visitNewClass(NewClassTree tree, AnnotatedTypeMirror type) { + for (BuilderFrameworkSupport builderFrameworkSupport : builderFrameworkSupports) { + builderFrameworkSupport.handleConstructor(tree, type); + } + return super.visitNewClass(tree, type); + } + } + + /** + * Adds @CalledMethod annotations for build() methods of AutoValue and Lombok Builders to ensure + * required properties have been set. + */ + private class CalledMethodsTypeAnnotator extends TypeAnnotator { + + /** + * Constructor matching super. + * + * @param atypeFactory the type factory + */ + public CalledMethodsTypeAnnotator(AnnotatedTypeFactory atypeFactory) { + super(atypeFactory); + } + + @Override + public Void visitExecutable(AnnotatedTypeMirror.AnnotatedExecutableType t, Void p) { + ExecutableElement element = t.getElement(); + + TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); + + for (BuilderFrameworkSupport builderFrameworkSupport : builderFrameworkSupports) { + if (builderFrameworkSupport.isToBuilderMethod(element)) { + builderFrameworkSupport.handleToBuilderMethod(t); + } + } + + Element nextEnclosingElement = enclosingElement.getEnclosingElement(); + if (nextEnclosingElement.getKind().isClass()) { + for (BuilderFrameworkSupport builderFrameworkSupport : builderFrameworkSupports) { + if (builderFrameworkSupport.isBuilderBuildMethod(element)) { + builderFrameworkSupport.handleBuilderBuildMethod(t); + } + } + } + + return super.visitExecutable(t, p); + } + } + + @Override + protected CalledMethodsAnalysis createFlowAnalysis() { + return new CalledMethodsAnalysis(checker, this); + } + + /** + * Returns the annotation type mirror for the type of {@code expressionTree} with default + * annotations applied. As types relevant to Called Methods checking are rarely used inside + * generics, this is typically the best choice for type inference. + */ + @Override + public @Nullable AnnotatedTypeMirror getDummyAssignedTo(ExpressionTree expressionTree) { + TypeMirror type = TreeUtils.typeOf(expressionTree); + if (type.getKind() != TypeKind.VOID) { + AnnotatedTypeMirror atm = type(expressionTree); + addDefaultAnnotations(atm); + return atm; + } + return null; + } + + /** + * Fetch the supported builder frameworks that are enabled. + * + * @return a collection of builder frameworks that are enabled in this run of the checker + */ + /*package-private*/ Collection getBuilderFrameworkSupports() { + return builderFrameworkSupports; + } + + /** + * Get the called methods specified by the given {@link CalledMethods} annotation. + * + * @param calledMethodsAnnotation the annotation + * @return the called methods + */ + protected List getCalledMethods(AnnotationMirror calledMethodsAnnotation) { + return AnnotationUtils.getElementValueArray( + calledMethodsAnnotation, + calledMethodsValueElement, + String.class, + Collections.emptyList()); + } + + @Override + protected @Nullable AnnotationMirror createRequiresOrEnsuresQualifier( + String expression, + AnnotationMirror qualifier, + AnnotatedTypeMirror declaredType, + Analysis.BeforeOrAfter preOrPost, + @Nullable List preconds) { + if (preOrPost == BeforeOrAfter.AFTER && isAccumulatorAnnotation(qualifier)) { + List calledMethods = getCalledMethods(qualifier); + if (!calledMethods.isEmpty()) { + return ensuresCMAnno(expression, calledMethods); + } + } + return super.createRequiresOrEnsuresQualifier( + expression, qualifier, declaredType, preOrPost, preconds); + } + + /** + * Returns a {@code @EnsuresCalledMethods("...")} annotation for the given expression. + * + * @param expression the expression to put in the value field of the EnsuresCalledMethods + * annotation + * @param calledMethods the methods that were definitely called on the expression + * @return a {@code @EnsuresCalledMethods("...")} annotation for the given expression + */ + private AnnotationMirror ensuresCMAnno(String expression, List calledMethods) { + return ensuresCMAnno(new String[] {expression}, calledMethods); + } + + /** + * Returns a {@code @EnsuresCalledMethods("...")} annotation for the given expressions. + * + * @param expressions the expressions to put in the value field of the EnsuresCalledMethods + * annotation + * @param calledMethods the methods that were definitely called on the expression + * @return a {@code @EnsuresCalledMethods("...")} annotation for the given expression + */ + private AnnotationMirror ensuresCMAnno(String[] expressions, List calledMethods) { + AnnotationBuilder builder = + new AnnotationBuilder(processingEnv, EnsuresCalledMethods.class); + builder.setValue("value", expressions); + builder.setValue("methods", calledMethods.toArray(new String[calledMethods.size()])); + AnnotationMirror am = builder.build(); + return am; + } + + /** + * Returns true if the checker should ignore exceptional control flow due to the given exception + * type. + * + * @param exceptionType exception type + * @return {@code true} if {@code exceptionType} is a member of {@link + * CalledMethodsAnalysis#ignoredExceptionTypes}, {@code false} otherwise + */ + @Override + public boolean isIgnoredExceptionType(TypeMirror exceptionType) { + if (exceptionType.getKind() == TypeKind.DECLARED) { + return CalledMethodsAnalysis.ignoredExceptionTypes.contains( + TypesUtils.getQualifiedName((DeclaredType) exceptionType)); + } + return false; + } + + /** + * Get the exceptional postconditions for the given method from the {@link + * EnsuresCalledMethodsOnException} annotations on it. + * + * @param methodOrConstructor the method to examine + * @return the exceptional postconditions on the given method; the return value is + * newly-allocated and can be freely modified by callers + */ + public Set getExceptionalPostconditions( + ExecutableElement methodOrConstructor) { + Set result = new LinkedHashSet<>(); + + parseEnsuresCalledMethodOnExceptionListAnnotation( + getDeclAnnotation(methodOrConstructor, EnsuresCalledMethodsOnException.List.class), + result); + + parseEnsuresCalledMethodOnExceptionAnnotation( + getDeclAnnotation(methodOrConstructor, EnsuresCalledMethodsOnException.class), + result); + + return result; + } + + /** + * Helper for {@link #getExceptionalPostconditions(ExecutableElement)} that parses a {@link + * EnsuresCalledMethodsOnException.List} annotation and stores the results in out. + * + * @param annotation the annotation + * @param out the output collection + */ + private void parseEnsuresCalledMethodOnExceptionListAnnotation( + @Nullable AnnotationMirror annotation, + Set out) { + if (annotation == null) { + return; + } + + List annotations = + AnnotationUtils.getElementValueArray( + annotation, + ensuresCalledMethodsOnExceptionListValueElement, + AnnotationMirror.class, + Collections.emptyList()); + + for (AnnotationMirror a : annotations) { + parseEnsuresCalledMethodOnExceptionAnnotation(a, out); + } + } + + /** + * Helper for {@link #getExceptionalPostconditions(ExecutableElement)} that parses a {@link + * EnsuresCalledMethodsOnException} annotation and stores the results in out. + * + * @param annotation the annotation + * @param out the output collection + */ + private void parseEnsuresCalledMethodOnExceptionAnnotation( + @Nullable AnnotationMirror annotation, + Set out) { + if (annotation == null) { + return; + } + + List expressions = + AnnotationUtils.getElementValueArray( + annotation, + ensuresCalledMethodsOnExceptionValueElement, + String.class, + Collections.emptyList()); + List methods = + AnnotationUtils.getElementValueArray( + annotation, + ensuresCalledMethodsOnExceptionMethodsElement, + String.class, + Collections.emptyList()); + + for (String expr : expressions) { + for (String method : methods) { + out.add(new EnsuresCalledMethodOnExceptionContract(expr, method)); + } + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsChecker.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsChecker.java new file mode 100644 index 000000000000..a91368f6f126 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsChecker.java @@ -0,0 +1,122 @@ +package org.checkerframework.checker.calledmethods; + +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.common.accumulation.AccumulationChecker; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.returnsreceiver.ReturnsReceiverChecker; +import org.checkerframework.common.value.ValueChecker; +import org.checkerframework.framework.qual.StubFiles; +import org.checkerframework.framework.source.SupportedOptions; +import org.checkerframework.framework.source.SuppressWarningsPrefix; + +import java.util.Set; + +/** + * The Called Methods Checker tracks the methods that have definitely been called on an object. One + * common use case for the Called Methods Checker is to specify safe combinations of options to + * builder or builder-like interfaces, preventing objects from being instantiated incompletely. + */ +@SuppressWarningsPrefix({ + // Preferred checkername. + "calledmethods", + // Deprecated checkernames, supported for backward compatibility. + "builder", + "object.construction", + "objectconstruction" +}) +@SupportedOptions({ + CalledMethodsChecker.USE_VALUE_CHECKER, + CalledMethodsChecker.COUNT_FRAMEWORK_BUILD_CALLS, + CalledMethodsChecker.DISABLE_BUILDER_FRAMEWORK_SUPPORTS, + CalledMethodsChecker.DISABLE_RETURNS_RECEIVER +}) +@StubFiles({"DescribeImages.astub", "GenerateDataKey.astub"}) +public class CalledMethodsChecker extends AccumulationChecker { + + /** + * If this option is supplied, count the number of analyzed calls to build() in supported + * builder frameworks and print it when analysis is complete. Useful for collecting metrics. + */ + public static final String COUNT_FRAMEWORK_BUILD_CALLS = "countFrameworkBuildCalls"; + + /** + * This option disables the support for (and therefore the automated checking of) code that uses + * the given builder frameworks. Useful when a user **only** wants to enforce specifications on + * custom builder objects (such as the AWS SDK examples). + */ + public static final String DISABLE_BUILDER_FRAMEWORK_SUPPORTS = + "disableBuilderFrameworkSupports"; + + /** + * If this option is supplied, use the Value Checker to reduce false positives when analyzing + * calls to the AWS SDK. + */ + public static final String USE_VALUE_CHECKER = "useValueChecker"; + + /** + * Some use cases for the Called Methods Checker do not involve checking fluent APIs, and in + * those cases disabling the Returns Receiver Checker using this flag will make the Called + * Methods Checker run much faster. + */ + public static final String DISABLE_RETURNS_RECEIVER = "disableReturnsReceiver"; + + /** + * The number of calls to build frameworks supported by this invocation. Incremented only if the + * {@link #COUNT_FRAMEWORK_BUILD_CALLS} command-line option was supplied. + */ + int numBuildCalls = 0; + + /** Never access this boolean directly. Call {@link #isReturnsReceiverDisabled()} instead. */ + private @MonotonicNonNull Boolean returnsReceiverDisabled = null; + + /** + * Was the Returns Receiver Checker disabled on the command line? + * + * @return whether the -AdisableReturnsReceiver option was specified on the command line + */ + private boolean isReturnsReceiverDisabled() { + if (returnsReceiverDisabled == null) { + returnsReceiverDisabled = hasOptionNoSubcheckers(DISABLE_RETURNS_RECEIVER); + } + return returnsReceiverDisabled; + } + + @Override + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); + if (!isReturnsReceiverDisabled()) { + checkers.add(ReturnsReceiverChecker.class); + } + // BaseTypeChecker#hasOption calls this method (so that all subcheckers' options are + // considered), so the processingEnvironment must be checked for options directly. + if (this.processingEnv.getOptions().containsKey(USE_VALUE_CHECKER) + || this.processingEnv + .getOptions() + .containsKey(this.getClass().getSimpleName() + "_" + USE_VALUE_CHECKER)) { + checkers.add(ValueChecker.class); + } + return checkers; + } + + /** + * Check whether the given alias analysis is enabled by this particular accumulation checker. + * + * @param aliasAnalysis the analysis to check + * @return true iff the analysis is enabled + */ + @Override + public boolean isEnabled(AliasAnalysis aliasAnalysis) { + if (aliasAnalysis == AliasAnalysis.RETURNS_RECEIVER) { + return !isReturnsReceiverDisabled(); + } + return super.isEnabled(aliasAnalysis); + } + + @Override + public void typeProcessingOver() { + if (getBooleanOption(COUNT_FRAMEWORK_BUILD_CALLS)) { + System.out.printf("Found %d build() method calls.%n", numBuildCalls); + } + super.typeProcessingOver(); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java new file mode 100644 index 000000000000..d3f2926c6d5a --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsTransfer.java @@ -0,0 +1,364 @@ +package org.checkerframework.checker.calledmethods; + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsVarArgs; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.accumulation.AccumulationStore; +import org.checkerframework.common.accumulation.AccumulationTransfer; +import org.checkerframework.common.accumulation.AccumulationValue; +import org.checkerframework.dataflow.analysis.ConditionalTransferResult; +import org.checkerframework.dataflow.analysis.TransferInput; +import org.checkerframework.dataflow.analysis.TransferResult; +import org.checkerframework.dataflow.cfg.block.ExceptionBlock; +import org.checkerframework.dataflow.cfg.node.ArrayCreationNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.framework.flow.CFAbstractStore; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.util.JavaExpressionParseUtil; +import org.checkerframework.framework.util.StringToJavaExpression; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.plumelib.util.CollectionsPlume; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; + +/** A transfer function that accumulates the names of methods called. */ +public class CalledMethodsTransfer extends AccumulationTransfer { + + /** + * {@link #makeExceptionalStores(MethodInvocationNode, TransferInput)} requires a TransferInput, + * but the actual exceptional stores need to be modified in {@link #accumulate(Node, + * TransferResult, String...)}, which only has access to a TransferResult. So this field is set + * to non-null in {@link #visitMethodInvocation(MethodInvocationNode, TransferInput)} via a call + * to {@link #makeExceptionalStores(MethodInvocationNode, TransferInput)} (which reads the + * CFStores from the TransferInput) before the call to accumulate(); accumulate() can then use + * this field to read the CFStores; and then finally this field is then reset to null afterwards + * to prevent it from being used somewhere it shouldn't be. + */ + private @Nullable Map exceptionalStores; + + /** + * The element for the CalledMethods annotation's value element. Stored in a field in this class + * to prevent the need to cast to CalledMethods ATF every time it's used. + */ + private final ExecutableElement calledMethodsValueElement; + + /** The type mirror for {@link Exception}. */ + protected final TypeMirror javaLangExceptionType; + + /* NO-AFU + * True if -AenableWpiForRlc was passed on the command line. See {@link + * ResourceLeakChecker#ENABLE_WPI_FOR_RLC}. + * + private final boolean enableWpiForRlc; + */ + + /** + * Create a new CalledMethodsTransfer. + * + * @param analysis the analysis + */ + public CalledMethodsTransfer(CalledMethodsAnalysis analysis) { + super(analysis); + calledMethodsValueElement = + ((CalledMethodsAnnotatedTypeFactory) atypeFactory).calledMethodsValueElement; + // enableWpiForRlc = + // atypeFactory.getChecker().hasOption(ResourceLeakChecker.ENABLE_WPI_FOR_RLC); + + ProcessingEnvironment env = atypeFactory.getProcessingEnv(); + javaLangExceptionType = + env.getTypeUtils() + .getDeclaredType(ElementUtils.getTypeElement(env, Exception.class)); + } + + /* NO-AFU + * @param tree a tree + * @return false if Resource Leak Checker is running as one of the upstream checkers and the + * -AenableWpiForRlc flag (see {@link ResourceLeakChecker#ENABLE_WPI_FOR_RLC}) is not passed + * as a command line argument, otherwise returns the result of the super call + */ + /* NO-AFU + @Override + protected boolean shouldPerformWholeProgramInference(Tree tree) { + if (!isWpiEnabledForRLC() + && atypeFactory.getCheckerNames().contains(ResourceLeakChecker.class.getCanonicalName())) { + return false; + } + return super.shouldPerformWholeProgramInference(tree); + } + */ + + /* NO-AFU + * See {@link ResourceLeakChecker#ENABLE_WPI_FOR_RLC}. + * + * @param expressionTree a tree + * @param lhsTree its element + * @return false if Resource Leak Checker is running as one of the upstream checkers and the + * -AenableWpiForRlc flag is not passed as a command line argument, otherwise returns the + * result of the super call + */ + /* NO-AFU + @Override + protected boolean shouldPerformWholeProgramInference(Tree expressionTree, Tree lhsTree) { + if (!isWpiEnabledForRLC() + && atypeFactory.getCheckerNames().contains(ResourceLeakChecker.class.getCanonicalName())) { + return false; + } + return super.shouldPerformWholeProgramInference(expressionTree, lhsTree); + } + */ + + @Override + public TransferResult visitMethodInvocation( + MethodInvocationNode node, TransferInput input) { + exceptionalStores = makeExceptionalStores(node, input); + TransferResult superResult = + super.visitMethodInvocation(node, input); + + ExecutableElement method = TreeUtils.elementFromUse(node.getTree()); + handleEnsuresCalledMethodsVarArgs(node, method, superResult); + handleEnsuresCalledMethodsOnException(node, method, exceptionalStores); + + Node receiver = node.getTarget().getReceiver(); + if (receiver != null) { + String methodName = node.getTarget().getMethod().getSimpleName().toString(); + methodName = + ((CalledMethodsAnnotatedTypeFactory) atypeFactory) + .adjustMethodNameUsingValueChecker(methodName, node.getTree()); + accumulate(receiver, superResult, methodName); + } + TransferResult finalResult = + new ConditionalTransferResult<>( + superResult.getResultValue(), + superResult.getThenStore(), + superResult.getElseStore(), + exceptionalStores); + exceptionalStores = null; + return finalResult; + } + + @Override + public void accumulate( + Node node, + TransferResult result, + String... values) { + super.accumulate(node, result, values); + if (exceptionalStores == null) { + return; + } + + List valuesAsList = Arrays.asList(values); + JavaExpression target = JavaExpression.fromNode(node); + if (CFAbstractStore.canInsertJavaExpression(target)) { + AccumulationValue flowValue = result.getRegularStore().getValue(target); + if (flowValue != null) { + // Dataflow has already recorded information about the target. Integrate it into + // the list of values in the new annotation. + AnnotationMirrorSet flowAnnos = flowValue.getAnnotations(); + assert flowAnnos.size() <= 1; + for (AnnotationMirror anno : flowAnnos) { + if (atypeFactory.isAccumulatorAnnotation(anno)) { + List oldFlowValues = + AnnotationUtils.getElementValueArray( + anno, calledMethodsValueElement, String.class); + // valuesAsList cannot have its length changed -- it is backed by an + // array. getElementValueArray returns a new, modifiable list. + oldFlowValues.addAll(valuesAsList); + valuesAsList = oldFlowValues; + } + } + } + AnnotationMirror newAnno = atypeFactory.createAccumulatorAnnotation(valuesAsList); + exceptionalStores.forEach( + (tm, s) -> + s.replaceValue( + target, + analysis.createSingleAnnotationValue( + newAnno, target.getType()))); + } + } + + /** + * Create a set of stores for the exceptional paths out of the block containing {@code node}. + * This allows propagation, along those paths, of the fact that the method being invoked in + * {@code node} was definitely called. + * + * @param node a method invocation + * @param input the transfer input associated with the method invocation + * @return a map from types to stores. The keys are the same keys used by {@link + * ExceptionBlock#getExceptionalSuccessors()}. The values are copies of the regular store + * from {@code input}. + */ + private Map makeExceptionalStores( + MethodInvocationNode node, TransferInput input) { + if (!(node.getBlock() instanceof ExceptionBlock)) { + // This can happen in some weird (buggy?) cases: + // see https://github.com/typetools/checker-framework/issues/3585 + return Collections.emptyMap(); + } + ExceptionBlock block = (ExceptionBlock) node.getBlock(); + Map result = new LinkedHashMap<>(); + block.getExceptionalSuccessors() + .forEach((tm, b) -> result.put(tm, input.getRegularStore().copy())); + return result; + } + + /** + * Update the types of varargs parameters passed to a method with an {@link + * EnsuresCalledMethodsVarArgs} annotation. This method is a no-op if no such annotation is + * present. + * + * @param node the method invocation node + * @param elt the method being invoked + * @param result the current result + */ + private void handleEnsuresCalledMethodsVarArgs( + MethodInvocationNode node, + ExecutableElement elt, + TransferResult result) { + AnnotationMirror annot = + atypeFactory.getDeclAnnotation(elt, EnsuresCalledMethodsVarArgs.class); + if (annot == null) { + return; + } + List ensuredMethodNames = + AnnotationUtils.getElementValueArray( + annot, + ((CalledMethodsAnnotatedTypeFactory) atypeFactory) + .ensuresCalledMethodsVarArgsValueElement, + String.class); + List parameters = elt.getParameters(); + int varArgsPos = parameters.size() - 1; + Node varArgActual = node.getArguments().get(varArgsPos); + // In the CFG, explicit passing of multiple arguments in the varargs position is represented + // via an ArrayCreationNode. This is the only case we handle for now. + if (varArgActual instanceof ArrayCreationNode) { + ArrayCreationNode arrayCreationNode = (ArrayCreationNode) varArgActual; + // add in the called method to all the vararg arguments + AccumulationStore thenStore = result.getThenStore(); + AccumulationStore elseStore = result.getElseStore(); + for (Node arg : arrayCreationNode.getInitializers()) { + AnnotatedTypeMirror currentType = atypeFactory.getAnnotatedType(arg.getTree()); + AnnotationMirror newType = + getUpdatedCalledMethodsType(currentType, ensuredMethodNames); + if (newType == null) { + continue; + } + + JavaExpression receiverReceiver = JavaExpression.fromNode(arg); + thenStore.insertValue(receiverReceiver, newType); + elseStore.insertValue(receiverReceiver, newType); + } + } + } + + /** + * Update the given exceptionalStores for the {@link + * org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsOnException} annotations + * written on the given method. + * + * @param node a method invocation + * @param method the method being invoked + * @param exceptionalStores the stores to update + */ + private void handleEnsuresCalledMethodsOnException( + MethodInvocationNode node, + ExecutableElement method, + Map exceptionalStores) { + Types types = atypeFactory.getProcessingEnv().getTypeUtils(); + for (EnsuresCalledMethodOnExceptionContract postcond : + ((CalledMethodsAnnotatedTypeFactory) atypeFactory) + .getExceptionalPostconditions(method)) { + JavaExpression e; + try { + e = + StringToJavaExpression.atMethodInvocation( + postcond.getExpression(), + node.getTree(), + atypeFactory.getChecker()); + } catch (JavaExpressionParseUtil.JavaExpressionParseException ex) { + // This parse error will be reported later. For now, we'll skip this malformed + // postcondition and move on to the others. + continue; + } + + // NOTE: this code is a little inefficient; it creates a single-method annotation and + // calls `insertOrRefine` in a loop. Even worse, this code appears within a loop. For + // now we aren't too worried about it, since the number of + // EnsuresCalledMethodsOnException annotations should be small. + AnnotationMirror calledMethod = + atypeFactory.createAccumulatorAnnotation(postcond.getMethod()); + for (Map.Entry successor : + exceptionalStores.entrySet()) { + TypeMirror caughtException = successor.getKey(); + if (types.isSubtype(caughtException, javaLangExceptionType)) { + AccumulationStore resultStore = successor.getValue(); + resultStore.insertOrRefine(e, calledMethod); + } + } + } + } + + /** + * Extract the current called-methods type from {@code currentType}, and then add each element + * of {@code methodNames} to it, and return the result. This method is similar to GLB, but + * should be used when the new methods come from a source other than an {@code CalledMethods} + * annotation. + * + * @param currentType the current type in the called-methods hierarchy + * @param methodNames the names of the new methods to add to the type + * @return the new annotation to be added to the type, or null if the current type cannot be + * converted to an accumulator annotation + */ + private @Nullable AnnotationMirror getUpdatedCalledMethodsType( + AnnotatedTypeMirror currentType, List methodNames) { + AnnotationMirror type; + if (currentType == null || !currentType.hasAnnotationInHierarchy(atypeFactory.top)) { + type = atypeFactory.top; + } else { + type = currentType.getAnnotationInHierarchy(atypeFactory.top); + } + + // Don't attempt to strengthen @CalledMethodsPredicate annotations, because that would + // require reasoning about the predicate itself. Instead, start over from top. + if (AnnotationUtils.areSameByName( + type, "org.checkerframework.checker.calledmethods.qual.CalledMethodsPredicate")) { + type = atypeFactory.top; + } + + if (AnnotationUtils.areSame(type, atypeFactory.bottom)) { + return null; + } + + List currentMethods = + AnnotationUtils.getElementValueArray(type, calledMethodsValueElement, String.class); + List newList = CollectionsPlume.concatenate(currentMethods, methodNames); + + return atypeFactory.createAccumulatorAnnotation(newList); + } + + /* NO-AFU + * Checks if WPI is enabled for the Resource Leak Checker inference. See {@link + * ResourceLeakChecker#ENABLE_WPI_FOR_RLC}. + * + * @return returns true if WPI is enabled for the Resource Leak Checker + * + protected boolean isWpiEnabledForRLC() { + return enableWpiForRlc; + } + */ +} diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsVisitor.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsVisitor.java new file mode 100644 index 000000000000..2a7b83ac74e4 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/CalledMethodsVisitor.java @@ -0,0 +1,167 @@ +package org.checkerframework.checker.calledmethods; + +import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; + +import org.checkerframework.checker.calledmethods.builder.BuilderFrameworkSupport; +import org.checkerframework.checker.calledmethods.qual.CalledMethods; +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsVarArgs; +import org.checkerframework.common.accumulation.AccumulationVisitor; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.framework.flow.CFAbstractStore; +import org.checkerframework.framework.flow.CFAbstractValue; +import org.checkerframework.framework.source.DiagMessage; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.util.JavaExpressionParseUtil; +import org.checkerframework.framework.util.StringToJavaExpression; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.tools.Diagnostic; + +/** + * This visitor implements the custom error message "finalizer.invocation.invalid". It also supports + * counting the number of framework build calls. + */ +public class CalledMethodsVisitor extends AccumulationVisitor { + + /** + * Creates a new CalledMethodsVisitor. + * + * @param checker the type-checker associated with this visitor + */ + public CalledMethodsVisitor(BaseTypeChecker checker) { + super(checker); + } + + /** + * Issue an error at every EnsuresCalledMethodsVarArgs annotation, because using it is unsound. + */ + @Override + public Void visitAnnotation(AnnotationTree tree, Void p) { + AnnotationMirror anno = TreeUtils.annotationFromAnnotationTree(tree); + if (AnnotationUtils.areSameByName( + anno, + "org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsVarArgs")) { + // We can't verify these yet. Emit an error (which will have to be suppressed) for now. + checker.report( + tree, new DiagMessage(Diagnostic.Kind.ERROR, "ensuresvarargs.unverified")); + } + return super.visitAnnotation(tree, p); + } + + @Override + public Void visitMethod(MethodTree tree, Void p) { + ExecutableElement elt = TreeUtils.elementFromDeclaration(tree); + AnnotationMirror ecmva = + atypeFactory.getDeclAnnotation(elt, EnsuresCalledMethodsVarArgs.class); + if (ecmva != null) { + if (!elt.isVarArgs()) { + checker.report( + tree, new DiagMessage(Diagnostic.Kind.ERROR, "ensuresvarargs.invalid")); + } + } + for (EnsuresCalledMethodOnExceptionContract postcond : + ((CalledMethodsAnnotatedTypeFactory) atypeFactory) + .getExceptionalPostconditions(elt)) { + checkExceptionalPostcondition(postcond, tree); + } + return super.visitMethod(tree, p); + } + + /** + * Check if the given postcondition is really ensured by the body of the given method. + * + * @param postcond the postcondition to check + * @param tree the method + */ + protected void checkExceptionalPostcondition( + EnsuresCalledMethodOnExceptionContract postcond, MethodTree tree) { + CFAbstractStore exitStore = atypeFactory.getExceptionalExitStore(tree); + if (exitStore == null) { + // If there is no exceptional exitStore, then the method cannot throw exceptions and + // there is no need to check anything. + return; + } + + JavaExpression e; + try { + e = StringToJavaExpression.atMethodBody(postcond.getExpression(), tree, checker); + } catch (JavaExpressionParseUtil.JavaExpressionParseException ex) { + checker.report(tree, ex.getDiagMessage()); + return; + } + + AnnotationMirror requiredAnno = + atypeFactory.createAccumulatorAnnotation(postcond.getMethod()); + + CFAbstractValue value = exitStore.getValue(e); + AnnotationMirror inferredAnno = null; + if (value != null) { + AnnotationMirrorSet annos = value.getAnnotations(); + inferredAnno = qualHierarchy.findAnnotationInSameHierarchy(annos, requiredAnno); + } + + if (!checkContract(e, requiredAnno, inferredAnno, exitStore)) { + checker.reportError( + tree, + "contracts.exceptional.postcondition.not.satisfied", + tree.getName(), + contractExpressionAndType(postcond.getExpression(), inferredAnno), + contractExpressionAndType(postcond.getExpression(), requiredAnno)); + } + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { + if (checker.getBooleanOption(CalledMethodsChecker.COUNT_FRAMEWORK_BUILD_CALLS)) { + ExecutableElement element = TreeUtils.elementFromUse(tree); + for (BuilderFrameworkSupport builderFrameworkSupport : + ((CalledMethodsAnnotatedTypeFactory) getTypeFactory()) + .getBuilderFrameworkSupports()) { + if (builderFrameworkSupport.isBuilderBuildMethod(element)) { + ((CalledMethodsChecker) checker).numBuildCalls++; + break; + } + } + } + return super.visitMethodInvocation(tree, p); + } + + /** Turns some "method.invocation.invalid" errors into "finalizer.invocation.invalid" errors. */ + @Override + protected void reportMethodInvocabilityError( + MethodInvocationTree tree, AnnotatedTypeMirror found, AnnotatedTypeMirror expected) { + + AnnotationMirror expectedCM = expected.getAnnotation(CalledMethods.class); + if (expectedCM != null) { + AnnotationMirror foundCM = found.getAnnotation(CalledMethods.class); + Set foundMethods = + foundCM == null + ? Collections.emptySet() + : new HashSet<>(atypeFactory.getAccumulatedValues(foundCM)); + List expectedMethods = atypeFactory.getAccumulatedValues(expectedCM); + StringJoiner missingMethods = new StringJoiner(" "); + for (String expectedMethod : expectedMethods) { + if (!foundMethods.contains(expectedMethod)) { + missingMethods.add(expectedMethod + "()"); + } + } + + checker.reportError(tree, "finalizer.invocation.invalid", missingMethods.toString()); + } else { + super.reportMethodInvocabilityError(tree, found, expected); + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/DescribeImages.astub b/checker/src/main/java/org/checkerframework/checker/calledmethods/DescribeImages.astub new file mode 100644 index 000000000000..43f299f82526 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/DescribeImages.astub @@ -0,0 +1,35 @@ +package com.amazonaws.services.ec2; + +import org.checkerframework.checker.calledmethods.qual.*; + +// Interface +class AmazonEC2 { + DescribeImagesResult describeImages( + // The receiver must have its owner, imageId, or executable user set, either via the "wither" or "setter" methods. + @CalledMethodsPredicate("(withOwners || setOwners) || (withImageIds || setImageIds) || (withExecutableUsers || setExecutableUsers)") + DescribeImagesRequest request); +} + +// The main implementation class +class AmazonEC2Client { + DescribeImagesResult describeImages( + // any combination of withX/setX has to be permitted if an owner has been set or an imageId has been set + @CalledMethodsPredicate("(withOwners || setOwners) || (withImageIds || setImageIds) || (withExecutableUsers || setExecutableUsers)") + DescribeImagesRequest request); +} + +// Async interface +class AmazonEC2Async { + Future describeImagesAsync( + // The receiver must have its owner, imageId, or executable user set, either via the "wither" or "setter" methods. + @CalledMethodsPredicate("(withOwners || setOwners) || (withImageIds || setImageIds) || (withExecutableUsers || setExecutableUsers)") + DescribeImagesRequest request); +} + +// The main async implementation class +class AmazonEC2AsyncClient { + Future describeImagesAsync( + // any combination of withX/setX has to be permitted if an owner has been set or an imageId has been set + @CalledMethodsPredicate("(withOwners || setOwners) || (withImageIds || setImageIds) || (withExecutableUsers || setExecutableUsers)") + DescribeImagesRequest request); +} diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/EnsuresCalledMethodOnExceptionContract.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/EnsuresCalledMethodOnExceptionContract.java new file mode 100644 index 000000000000..ab3605ea6312 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/EnsuresCalledMethodOnExceptionContract.java @@ -0,0 +1,66 @@ +package org.checkerframework.checker.calledmethods; + +import java.util.Objects; + +/** + * A postcondition contract that a method calls the given method on the given expression when that + * method throws an exception. + * + *

    Instances of this class are plain old immutable data with no interesting behavior. + * + * @see org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsOnException + */ +// TODO: In the future, this class should be a record. +public class EnsuresCalledMethodOnExceptionContract { + + /** The expression described by this postcondition. */ + private final String expression; + + /** The method this postcondition promises to call. */ + private final String method; + + /** + * Create a new EnsuredCalledMethodOnException. Usually this should be constructed + * from a {@link + * org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsOnException} appearing in + * the source code. + * + * @param expression the expression described by this postcondition + * @param method the method this postcondition promises to call + */ + public EnsuresCalledMethodOnExceptionContract(String expression, String method) { + this.expression = expression; + this.method = method; + } + + /** + * The expression described by this postcondition. + * + * @return the expression described by this postcondition + */ + public String getExpression() { + return expression; + } + + /** + * The method this postcondition promises to call. + * + * @return the method this postcondition promises to call + */ + public String getMethod() { + return method; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof EnsuresCalledMethodOnExceptionContract)) return false; + EnsuresCalledMethodOnExceptionContract that = (EnsuresCalledMethodOnExceptionContract) o; + return expression.equals(that.expression) && method.equals(that.method); + } + + @Override + public int hashCode() { + return Objects.hash(expression, method); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/GenerateDataKey.astub b/checker/src/main/java/org/checkerframework/checker/calledmethods/GenerateDataKey.astub new file mode 100644 index 000000000000..d51e0b6cae7b --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/GenerateDataKey.astub @@ -0,0 +1,11 @@ +package com.amazonaws.services.kms; + +import org.checkerframework.checker.calledmethods.qual.*; + +interface AWSKMS { + // This predicate enforces two properties: + // 1) the number of bytes or the keyspec has been set. This property is enforced soundly. + // 2) both the number of bytes and the keyspec have not been set. This property uses the ! operator, + // so it is best regarded as a bug-finder: what it really proves is that both are not set on all paths. + GenerateDataKeyResult generateDataKey(@CalledMethodsPredicate("(setNumberOfBytes || withNumberOfBytes || setKeySpec || withKeySpec) && !(setNumberOfBytes && setKeySpec) && !(setNumberOfBytes && withKeySpec) && !(withNumberOfBytes && setKeySpec) && !(withNumberOfBytes && withKeySpec)") GenerateDataKeyRequest request); +} diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/AutoValueSupport.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/AutoValueSupport.java new file mode 100644 index 000000000000..f7c52f5f30d6 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/AutoValueSupport.java @@ -0,0 +1,451 @@ +package org.checkerframework.checker.calledmethods.builder; + +import com.sun.source.tree.NewClassTree; + +import org.checkerframework.checker.calledmethods.CalledMethodsAnnotatedTypeFactory; +import org.checkerframework.checker.calledmethods.qual.CalledMethods; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.framework.util.AnnotatedTypes; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; +import org.checkerframework.javacutil.UserError; +import org.plumelib.util.ArraysPlume; + +import java.beans.Introspector; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** + * AutoValue support for the Called Methods Checker. This class adds {@code @}{@link CalledMethods} + * annotations to the code generated by AutoValue. + */ +public class AutoValueSupport implements BuilderFrameworkSupport { + + /** The type factory. */ + private final CalledMethodsAnnotatedTypeFactory atypeFactory; + + /** + * Create a new AutoValueSupport. + * + * @param atypeFactory the typechecker's type factory + */ + public AutoValueSupport(CalledMethodsAnnotatedTypeFactory atypeFactory) { + this.atypeFactory = atypeFactory; + } + + /** + * This method modifies the type of a copy constructor generated by AutoValue to match the type + * of the AutoValue toBuilder method, and has no effect if {@code tree} is a call to any other + * constructor. + * + * @param tree an AST for a constructor call + * @param type type of the call expression + */ + @Override + public void handleConstructor(NewClassTree tree, AnnotatedTypeMirror type) { + ExecutableElement element = TreeUtils.elementFromUse(tree); + TypeMirror superclass = ((TypeElement) element.getEnclosingElement()).getSuperclass(); + + if (superclass.getKind() != TypeKind.NONE + && ElementUtils.hasAnnotation( + TypesUtils.getTypeElement(superclass), + getAutoValuePackageName() + ".AutoValue.Builder") + && element.getParameters().size() > 0) { + handleToBuilderType( + type, + superclass, + (TypeElement) TypesUtils.getTypeElement(superclass).getEnclosingElement()); + } + } + + @Override + public boolean isBuilderBuildMethod(ExecutableElement candidateBuildElement) { + TypeElement builderElement = (TypeElement) candidateBuildElement.getEnclosingElement(); + if (ElementUtils.hasAnnotation( + builderElement, getAutoValuePackageName() + ".AutoValue.Builder")) { + Element classContainingBuilderElement = builderElement.getEnclosingElement(); + if (!ElementUtils.hasAnnotation( + classContainingBuilderElement, getAutoValuePackageName() + ".AutoValue")) { + throw new UserError( + "class " + + classContainingBuilderElement.getSimpleName() + + " is missing @AutoValue annotation"); + } + // it is a build method if it returns the type with the @AutoValue annotation + if (TypesUtils.getTypeElement(candidateBuildElement.getReturnType()) + .equals(classContainingBuilderElement)) { + return true; + } + } + return false; + } + + @Override + public void handleBuilderBuildMethod(AnnotatedExecutableType builderBuildType) { + + ExecutableElement element = builderBuildType.getElement(); + TypeElement builderElement = (TypeElement) element.getEnclosingElement(); + TypeElement autoValueClassElement = (TypeElement) builderElement.getEnclosingElement(); + AnnotationMirror newCalledMethodsAnno = + createCalledMethodsForAutoValueClass(builderElement, autoValueClassElement); + // Only add the new @CalledMethods annotation if there is not already a @CalledMethods + // annotation present. + builderBuildType.getReceiverType().addMissingAnnotation(newCalledMethodsAnno); + } + + @Override + public boolean isToBuilderMethod(ExecutableElement candidateToBuilderElement) { + if (!"toBuilder".equals(candidateToBuilderElement.getSimpleName().toString())) { + return false; + } + + TypeElement candidateClassContainingToBuilder = + (TypeElement) candidateToBuilderElement.getEnclosingElement(); + boolean isAbstractAV = + isAutoValueGenerated(candidateClassContainingToBuilder) + && candidateToBuilderElement.getModifiers().contains(Modifier.ABSTRACT); + TypeMirror superclassOfClassContainingToBuilder = + candidateClassContainingToBuilder.getSuperclass(); + boolean superIsAV = false; + if (superclassOfClassContainingToBuilder.getKind() != TypeKind.NONE) { + superIsAV = + isAutoValueGenerated( + TypesUtils.getTypeElement(superclassOfClassContainingToBuilder)); + } + return superIsAV || isAbstractAV; + } + + @Override + public void handleToBuilderMethod(AnnotatedExecutableType toBuilderType) { + AnnotatedTypeMirror returnType = toBuilderType.getReturnType(); + ExecutableElement toBuilderElement = toBuilderType.getElement(); + TypeElement classContainingToBuilder = (TypeElement) toBuilderElement.getEnclosingElement(); + // Because of the way that the check in #isToBuilderMethod works, if the code reaches this + // point and this condition is false, the other condition MUST be true (otherwise, + // isToBuilderMethod would have returned false). + if (isAutoValueGenerated(classContainingToBuilder) + && toBuilderElement.getModifiers().contains(Modifier.ABSTRACT)) { + handleToBuilderType( + returnType, returnType.getUnderlyingType(), classContainingToBuilder); + } else { + TypeElement superElement = + TypesUtils.getTypeElement(classContainingToBuilder.getSuperclass()); + handleToBuilderType(returnType, returnType.getUnderlyingType(), superElement); + } + } + + /** + * Was the given element generated by AutoValue? + * + * @param element the element to check + * @return true if the element was generated by AutoValue + */ + private boolean isAutoValueGenerated(Element element) { + return ElementUtils.hasAnnotation(element, getAutoValuePackageName() + ".AutoValue"); + } + + /** + * Add, to {@code type}, a CalledMethods annotation with all required methods called. The type + * can be the return type of toBuilder or of the corresponding generated "copy" constructor. + * + * @param type type to update + * @param builderType type of abstract @AutoValue.Builder class + * @param classElement an AutoValue class corresponding to {@code type} + */ + private void handleToBuilderType( + AnnotatedTypeMirror type, TypeMirror builderType, TypeElement classElement) { + TypeElement builderElement = TypesUtils.getTypeElement(builderType); + AnnotationMirror calledMethodsAnno = + createCalledMethodsForAutoValueClass(builderElement, classElement); + type.replaceAnnotation(calledMethodsAnno); + } + + /** + * Create an @CalledMethods annotation for the given AutoValue class and builder. The returned + * annotation contains all the required setters. + * + * @param builderElement the element for the Builder class + * @param classElement the element for the AutoValue class (i.e. the class that is built by the + * builder) + * @return an @CalledMethods annotation representing that all the required setters have been + * called + */ + private AnnotationMirror createCalledMethodsForAutoValueClass( + TypeElement builderElement, TypeElement classElement) { + Set avBuilderSetterNames = getAutoValueBuilderSetterMethodNames(builderElement); + List requiredProperties = + getAutoValueRequiredProperties(classElement, avBuilderSetterNames); + return createCalledMethodsForAutoValueProperties(requiredProperties, avBuilderSetterNames); + } + + /** + * Creates a @CalledMethods annotation for the given property names, converting the names to the + * corresponding setter method name in the Builder. + * + * @param propertyNames the property names + * @param avBuilderSetterNames names of all setters in the builder class + * @return a @CalledMethods annotation that indicates all the given properties have been set + */ + private AnnotationMirror createCalledMethodsForAutoValueProperties( + List propertyNames, Set avBuilderSetterNames) { + List calledMethodNames = + propertyNames.stream() + .map(prop -> autoValuePropToBuilderSetterName(prop, avBuilderSetterNames)) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + return atypeFactory.createAccumulatorAnnotation(calledMethodNames); + } + + /** + * Converts the name of a property (i.e., a field) into the name of its setter. + * + * @param prop the property (i.e., field) name + * @param builderSetterNames names of all methods in the builder class + * @return the name of the setter for prop, or null if it cannot be found + */ + private static @Nullable String autoValuePropToBuilderSetterName( + String prop, Set builderSetterNames) { + String[] possiblePropNames; + if (prop.startsWith("get") && prop.length() > 3 && Character.isUpperCase(prop.charAt(3))) { + possiblePropNames = new String[] {prop, Introspector.decapitalize(prop.substring(3))}; + } else if (prop.startsWith("is") + && prop.length() > 2 + && Character.isUpperCase(prop.charAt(2))) { + possiblePropNames = new String[] {prop, Introspector.decapitalize(prop.substring(2))}; + } else { + possiblePropNames = new String[] {prop}; + } + + for (String propName : possiblePropNames) { + // The setter may be the property name itself, or prefixed by 'set'. + if (builderSetterNames.contains(propName)) { + return propName; + } + String setterName = "set" + BuilderFrameworkSupportUtils.capitalize(propName); + if (builderSetterNames.contains(setterName)) { + return setterName; + } + } + + // Could not find a corresponding setter. This is likely because an AutoValue Extension is + // in use. See https://github.com/kelloggm/object-construction-checker/issues/110 . + // For now we return null, but once that bug is fixed, this should be changed to an + // assertion failure. + return null; + } + + /** + * Computes the required properties of an @AutoValue class. + * + * @param autoValueClassElement the @AutoValue class + * @param avBuilderSetterNames names of all setters in the corresponding AutoValue builder class + * @return a list of required property names + */ + private List getAutoValueRequiredProperties( + TypeElement autoValueClassElement, Set avBuilderSetterNames) { + return getAllAbstractMethods(autoValueClassElement).stream() + .filter(member -> isAutoValueRequiredProperty(member, avBuilderSetterNames)) + .map(e -> e.getSimpleName().toString()) + .collect(Collectors.toList()); + } + + /** Method names for {@link #isAutoValueRequiredProperty} to ignore. */ + private final Set isAutoValueRequiredPropertyIgnored = + new HashSet<>(Arrays.asList("equals", "hashCode", "toString", "", "toBuilder")); + + /** + * Does member represent a required property of an AutoValue class? + * + * @param member a member of an AutoValue class or superclass + * @param avBuilderSetterNames names of all setters in corresponding AutoValue builder class + * @return true if {@code member} is required + */ + private boolean isAutoValueRequiredProperty( + ExecutableElement member, Set avBuilderSetterNames) { + String name = member.getSimpleName().toString(); + // Ignore java.lang.Object overrides, constructors, and toBuilder methods in AutoValue + // classes. + // Strictly speaking, this code should check return types, etc. to handle strange + // overloads and other corner cases. They seem unlikely enough that we are skipping for now. + if (isAutoValueRequiredPropertyIgnored.contains(name)) { + return false; + } + TypeMirror returnType = member.getReturnType(); + if (returnType.getKind() == TypeKind.VOID) { + return false; + } + // shouldn't have a nullable return + boolean hasNullable = + Stream.concat( + atypeFactory + .getElementUtils() + .getAllAnnotationMirrors(member) + .stream(), + returnType.getAnnotationMirrors().stream()) + .anyMatch(anm -> AnnotationUtils.annotationName(anm).endsWith(".Nullable")); + if (hasNullable) { + return false; + } + // if return type of foo() is a Guava Immutable type, not required if there is a + // builder method fooBuilder() + if (BuilderFrameworkSupportUtils.isGuavaImmutableType(returnType) + && avBuilderSetterNames.contains(name + "Builder")) { + return false; + } + // if it's an Optional, the Builder will automatically initialize it + if (isOptional(returnType)) { + return false; + } + // it's required! + return true; + } + + /** + * Classes that AutoValue considers "optional". This list comes from AutoValue's source code. + */ + private static final String[] optionalClassNames = + new String[] { + // Use concatenation to avoid ShadowJar relocate + // "com.google.common.base.Optional", + "com.go".toString() + "ogle.common.base.Optional", + "java.util.Optional", + "java.util.OptionalDouble", + "java.util.OptionalInt", + "java.util.OptionalLong" + }; + + /** + * Returns whether AutoValue considers a type to be "optional". Optional types do not need to be + * set before build is called on a builder. Adapted from AutoValue source code. + * + * @param type some type + * @return true if type is an Optional type + */ + private static boolean isOptional(TypeMirror type) { + if (type.getKind() != TypeKind.DECLARED) { + return false; + } + DeclaredType declaredType = (DeclaredType) type; + TypeElement typeElement = (TypeElement) declaredType.asElement(); + return typeElement.getTypeParameters().size() == declaredType.getTypeArguments().size() + && ArraysPlume.indexOf( + optionalClassNames, typeElement.getQualifiedName().toString()) + != -1; + } + + /** + * Returns names of all setter methods. + * + * @see #isAutoValueBuilderSetter + * @param builderElement the element representing an AutoValue builder + * @return the names of setter methods for the AutoValue builder + */ + private Set getAutoValueBuilderSetterMethodNames(TypeElement builderElement) { + return getAllAbstractMethods(builderElement).stream() + .filter(e -> isAutoValueBuilderSetter(e, builderElement)) + .map(e -> e.getSimpleName().toString()) + .collect(Collectors.toSet()); + } + + /** + * Return true if the given method is a setter for an AutoValue builder; that is, its return + * type is the builder itself or a Guava Immutable type. + * + * @param method a method of a builder or one of its supertypes + * @param builderElement element for the AutoValue builder + * @return true if {@code method} is a setter for the builder + */ + private boolean isAutoValueBuilderSetter(ExecutableElement method, TypeElement builderElement) { + TypeMirror retType = method.getReturnType(); + if (retType.getKind() == TypeKind.TYPEVAR) { + // instantiate the type variable for the Builder class + retType = + AnnotatedTypes.asMemberOf( + atypeFactory.getChecker().getTypeUtils(), + atypeFactory, + atypeFactory.getAnnotatedType(builderElement), + method) + .getReturnType() + .getUnderlyingType(); + } + // Either the return type should be the builder itself, or it should be a Guava immutable + // type. + return BuilderFrameworkSupportUtils.isGuavaImmutableType(retType) + || builderElement.equals(TypesUtils.getTypeElement(retType)); + } + + /** + * Get all the abstract methods for a class. This includes inherited abstract methods that are + * not overridden by the class or a superclass. There is no guarantee that this method will work + * as intended on code that implements an interface (which AutoValue classes are not supposed to + * do: https://github.com/google/auto/blob/master/value/userguide/howto.md#inherit). + * + * @param classElement the class + * @return list of all abstract methods + */ + public List getAllAbstractMethods(TypeElement classElement) { + List supertypes = + ElementUtils.getAllSupertypes(classElement, atypeFactory.getProcessingEnv()); + List abstractMethods = new ArrayList<>(); + Set overriddenMethods = new HashSet<>(); + for (Element t : supertypes) { + for (Element member : t.getEnclosedElements()) { + if (member.getKind() != ElementKind.METHOD) { + continue; + } + Set modifiers = member.getModifiers(); + if (modifiers.contains(Modifier.STATIC)) { + continue; + } + if (modifiers.contains(Modifier.ABSTRACT)) { + // Make sure it's not overridden. This only works because ElementUtils#closure + // returns results in a particular order. + if (!overriddenMethods.contains(member)) { + abstractMethods.add((ExecutableElement) member); + } + } else { + // Exclude any methods that this overrides. + overriddenMethods.addAll( + AnnotatedTypes.overriddenMethods( + atypeFactory.getElementUtils(), + atypeFactory, + (ExecutableElement) member) + .values()); + } + } + } + return abstractMethods; + } + + /** + * Get the qualified name of the package containing AutoValue annotations. This method + * constructs the String dynamically, to ensure it does not get rewritten due to relocation of + * the {@code "com.google"} package during the build process. + * + * @return {@code "com.google.auto.value"} + */ + private String getAutoValuePackageName() { + String com = "com"; + return com + "." + "google.auto.value"; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/BuilderFrameworkSupport.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/BuilderFrameworkSupport.java new file mode 100644 index 000000000000..40aa4a062029 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/BuilderFrameworkSupport.java @@ -0,0 +1,76 @@ +package org.checkerframework.checker.calledmethods.builder; + +import com.sun.source.tree.NewClassTree; + +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; + +import javax.lang.model.element.ExecutableElement; + +/** + * Provides hooks to add CalledMethods annotations to code generated by a builder framework like + * Lombok or AutoValue. If you are adding support to the Called Methods Checker for a new builder + * framework, you should create a subclass of this class and modify the private method {@code + * enableFramework} in {@link + * org.checkerframework.checker.calledmethods.CalledMethodsAnnotatedTypeFactory}. + * + *

    Every method in this class is permitted to do nothing (or always return false). The work that + * each method must do is particular to the builder framework being supported. + */ +public interface BuilderFrameworkSupport { + + /** + * Returns true if a method is a {@code toBuilder} method on a type generated by the builder + * framework. + * + * @param candidateToBuilderElement a method + * @return {@code true} if {@code candidateToBuilderElement} is a {@code toBuilder} method on a + * type generated by the builder framework + */ + boolean isToBuilderMethod(ExecutableElement candidateToBuilderElement); + + /** + * Hook for supporting a builder framework's {@code toBuilder} routine. Typically, the returned + * Builder has had all of its required setters invoked. So, implementations of this method + * should add a {@link org.checkerframework.checker.calledmethods.qual.CalledMethods} annotation + * capturing this fact. + * + * @param toBuilderType the type of a method that is the {@code toBuilder} method (as determined + * by {@link #isToBuilderMethod(ExecutableElement)}) for a type that has an associated + * builder + */ + void handleToBuilderMethod(AnnotatedExecutableType toBuilderType); + + /** + * Returns true if a method is a {@code build} method on a {@code Builder} type for the builder + * framework. + * + * @param candidateBuildElement a method + * @return {@code true} if {@code candidateBuildElement} is a {@code build} method on a {@code + * Builder} type for the builder framework + */ + boolean isBuilderBuildMethod(ExecutableElement candidateBuildElement); + + /** + * Hook for adding annotations to a build() method (i.e. a finalizer) generated by a builder + * framework. + * + *

    For {@code build} methods on {@code Builder} types, implementations of this method should + * determine the required properties and add a corresponding {@link + * org.checkerframework.checker.calledmethods.qual.CalledMethods} annotation to the type of the + * receiver parameter. + * + * @param builderBuildType the type of a method that is the {@code build} method (as determined + * by {@link #isBuilderBuildMethod(ExecutableElement)}) for a builder + */ + void handleBuilderBuildMethod(AnnotatedExecutableType builderBuildType); + + /** + * Hook for adding annotations (e.g., {@code @}{@link + * org.checkerframework.checker.calledmethods.qual.CalledMethods}) to a constructor call. + * + * @param tree a constructor call + * @param type type of the call expression + */ + void handleConstructor(NewClassTree tree, AnnotatedTypeMirror type); +} diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/BuilderFrameworkSupportUtils.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/BuilderFrameworkSupportUtils.java new file mode 100644 index 000000000000..3d98fa883b12 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/BuilderFrameworkSupportUtils.java @@ -0,0 +1,34 @@ +package org.checkerframework.checker.calledmethods.builder; + +import javax.lang.model.type.TypeMirror; + +/** A utility class of static methods used in supporting builder-generation frameworks. */ +public class BuilderFrameworkSupportUtils { + /** This class is non-instantiable. */ + private BuilderFrameworkSupportUtils() { + throw new Error("Do not instantiate"); + } + + /** + * Returns true if the given type is one of the immutable collections defined in + * com.google.common.collect. + * + * @param type a type + * @return true if the type is a Guava immutable collection + */ + public static boolean isGuavaImmutableType(TypeMirror type) { + // Use concatenation to avoid ShadowJar relocate + // "com.google.common.collect.Immutable" + return type.toString().startsWith("com.go".toString() + "ogle.common.collect.Immutable"); + } + + /** + * Capitalizes the first letter of the given string. + * + * @param prop a String + * @return the same String, but with the first character capitalized + */ + public static String capitalize(String prop) { + return Character.toUpperCase(prop.charAt(0)) + prop.substring(1); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/LombokSupport.java b/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/LombokSupport.java new file mode 100644 index 000000000000..9807af5357ff --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/builder/LombokSupport.java @@ -0,0 +1,215 @@ +package org.checkerframework.checker.calledmethods.builder; + +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.VariableTree; + +import org.checkerframework.checker.calledmethods.CalledMethodsAnnotatedTypeFactory; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; + +import java.beans.Introspector; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; + +/** + * Lombok support for the Called Methods Checker. This class adds CalledMethods annotations to the + * code generated by Lombok. + */ +public class LombokSupport implements BuilderFrameworkSupport { + + /** The type factory. */ + private final CalledMethodsAnnotatedTypeFactory atypeFactory; + + /** + * Create a new LombokSupport. + * + * @param atypeFactory the typechecker's type factory + */ + public LombokSupport(CalledMethodsAnnotatedTypeFactory atypeFactory) { + this.atypeFactory = atypeFactory; + } + + // The list is copied from lombok.core.handlers.HandlerUtil. The list cannot be used from that + // class directly because Lombok does not provide class files for its own implementation, to + // prevent itself from being accidentally added to clients' compile classpaths. This design + // decision means that it is impossible to depend directly on Lombok internals. + /** The list of annotations that Lombok treats as non-null. */ + public static final List NONNULL_ANNOTATIONS = + Collections.unmodifiableList( + Arrays.asList( + "android.annotation.NonNull", + "android.support.annotation.NonNull", + "com.sun.istack.internal.NotNull", + "edu.umd.cs.findbugs.annotations.NonNull", + "javax.annotation.Nonnull", + // "javax.validation.constraints.NotNull", // The field might contain a + // null value until it is persisted. + "lombok.NonNull", + "org.checkerframework.checker.nullness.qual.NonNull", + "org.eclipse.jdt.annotation.NonNull", + "org.eclipse.jgit.annotations.NonNull", + "org.jetbrains.annotations.NotNull", + "org.jmlspecs.annotation.NonNull", + "org.netbeans.api.annotations.common.NonNull", + "org.springframework.lang.NonNull")); + + /** + * A map from elements that have a lombok.Builder.Default annotation to the simple property name + * that should be treated as defaulted. + * + *

    This cache is kept because the usual method for checking that an element has been + * defaulted (calling declarationFromElement and examining the resulting VariableTree) only + * works if a corresponding Tree is available (for code that is only available as bytecode, no + * such Tree is available and that method returns null). See the code in {@link + * #getLombokRequiredProperties(Element)} that handles fields. + */ + private final Map defaultedElements = new HashMap<>(2); + + @Override + public boolean isBuilderBuildMethod(ExecutableElement candidateBuildElement) { + TypeElement candidateGeneratedBuilderElement = + (TypeElement) candidateBuildElement.getEnclosingElement(); + + if ((ElementUtils.hasAnnotation(candidateGeneratedBuilderElement, "lombok.Generated") + || ElementUtils.hasAnnotation(candidateBuildElement, "lombok.Generated")) + && candidateGeneratedBuilderElement + .getSimpleName() + .toString() + .endsWith("Builder")) { + return candidateBuildElement.getSimpleName().contentEquals("build"); + } + return false; + } + + @Override + public void handleBuilderBuildMethod(AnnotatedExecutableType builderBuildType) { + ExecutableElement buildElement = builderBuildType.getElement(); + + TypeElement generatedBuilderElement = (TypeElement) buildElement.getEnclosingElement(); + // The class with the @lombok.Builder annotation... + Element annotatedWithBuilderElement = generatedBuilderElement.getEnclosingElement(); + + List requiredProperties = getLombokRequiredProperties(annotatedWithBuilderElement); + AnnotationMirror newCalledMethodsAnno = + atypeFactory.createAccumulatorAnnotation(requiredProperties); + builderBuildType.getReceiverType().addAnnotation(newCalledMethodsAnno); + } + + @Override + public boolean isToBuilderMethod(ExecutableElement candidateToBuilderElement) { + return candidateToBuilderElement.getSimpleName().contentEquals("toBuilder") + && (ElementUtils.hasAnnotation(candidateToBuilderElement, "lombok.Generated") + || ElementUtils.hasAnnotation( + candidateToBuilderElement.getEnclosingElement(), + "lombok.Generated")); + } + + @Override + public void handleToBuilderMethod(AnnotatedExecutableType toBuilderType) { + AnnotatedTypeMirror returnType = toBuilderType.getReturnType(); + ExecutableElement buildElement = toBuilderType.getElement(); + TypeElement generatedBuilderElement = (TypeElement) buildElement.getEnclosingElement(); + handleToBuilderType(returnType, generatedBuilderElement); + } + + /** + * Add, to a type, a CalledMethods annotation that states that all required setters have been + * called. The type can be the return type of toBuilder or of the corresponding generated "copy" + * constructor. + * + * @param type type to update + * @param classElement corresponding AutoValue class + */ + private void handleToBuilderType(AnnotatedTypeMirror type, Element classElement) { + List requiredProperties = getLombokRequiredProperties(classElement); + AnnotationMirror calledMethodsAnno = + atypeFactory.createAccumulatorAnnotation(requiredProperties); + type.replaceAnnotation(calledMethodsAnno); + } + + /** + * Computes the required properties of a @lombok.Builder class, i.e., the names of the fields + * with @lombok.NonNull annotations. + * + * @param lombokClassElement the class with the @lombok.Builder annotation + * @return a list of required property names + */ + private List getLombokRequiredProperties(Element lombokClassElement) { + List requiredPropertyNames = new ArrayList<>(); + List defaultedPropertyNames = new ArrayList<>(); + for (Element member : lombokClassElement.getEnclosedElements()) { + if (member.getKind() == ElementKind.FIELD) { + // Lombok never generates non-null fields with initializers in builders, unless the + // field is annotated with @Default or @Singular, which are handled elsewhere. So, + // this code doesn't need to consider whether the field has or does not have + // initializers. + for (AnnotationMirror anm : + atypeFactory.getElementUtils().getAllAnnotationMirrors(member)) { + if (NONNULL_ANNOTATIONS.contains(AnnotationUtils.annotationName(anm))) { + requiredPropertyNames.add(member.getSimpleName().toString()); + } + } + } else if (member.getKind() == ElementKind.METHOD + && ElementUtils.hasAnnotation(member, "lombok.Generated")) { + String methodName = member.getSimpleName().toString(); + // If a field foo has an @Builder.Default annotation, Lombok always generates a + // method called $default$foo. + if (methodName.startsWith("$default$")) { + String propName = methodName.substring(9); // $default$ has 9 characters + defaultedPropertyNames.add(propName); + } + } else if (member.getKind().isClass() && member.toString().endsWith("Builder")) { + // Note that the test above will fail to catch builders generated by Lombok that + // have custom names using the builderClassName attribute. TODO: find a way to + // handle such builders too. + + // If a field bar has an @Singular annotation, Lombok always generates a method + // called clearBar in the builder class itself. Therefore, search the builder for + // such a method, and extract the appropriate property name to treat as defaulted. + for (Element builderMember : member.getEnclosedElements()) { + if (builderMember.getKind() == ElementKind.METHOD + && ElementUtils.hasAnnotation(builderMember, "lombok.Generated")) { + String methodName = builderMember.getSimpleName().toString(); + if (methodName.startsWith("clear")) { + String propName = + Introspector.decapitalize( + methodName.substring(5)); // clear has 5 characters + defaultedPropertyNames.add(propName); + } + } else if (builderMember.getKind() == ElementKind.FIELD) { + VariableTree variableTree = + (VariableTree) atypeFactory.declarationFromElement(builderMember); + if (variableTree != null && variableTree.getInitializer() != null) { + Name propName = variableTree.getName(); + defaultedPropertyNames.add(propName.toString()); + defaultedElements.put(builderMember, propName); + } else if (defaultedElements.containsKey(builderMember)) { + defaultedPropertyNames.add( + defaultedElements.get(builderMember).toString()); + } + } + } + } + } + requiredPropertyNames.removeAll(defaultedPropertyNames); + return requiredPropertyNames; + } + + @Override + public void handleConstructor(NewClassTree tree, AnnotatedTypeMirror type) { + // do nothing + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/calledmethods/messages.properties b/checker/src/main/java/org/checkerframework/checker/calledmethods/messages.properties new file mode 100644 index 000000000000..9442a0260b6a --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/calledmethods/messages.properties @@ -0,0 +1,4 @@ +finalizer.invocation.invalid=This finalizer cannot be invoked, because the following methods have not been called: %s +ensuresvarargs.unverified=@EnsuresCalledMethodsVarArgs cannot be verified yet. Please check that the implementation of the method actually does call the given methods on the varargs parameters by hand, and then suppress this warning. +ensuresvarargs.invalid=@EnsuresCalledMethodsVarArgs cannot be written on a non-varargs method. +contracts.exceptional.postcondition.not.satisfied=on exception, postcondition of %s is not satisfied.%nfound : %s%nrequired: %s diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/FenumAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/fenum/FenumAnnotatedTypeFactory.java index b79e50b29087..aa84913b0c23 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/FenumAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/fenum/FenumAnnotatedTypeFactory.java @@ -1,35 +1,54 @@ package org.checkerframework.checker.fenum; -import java.lang.annotation.Annotation; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; import org.checkerframework.checker.fenum.qual.Fenum; import org.checkerframework.checker.fenum.qual.FenumBottom; import org.checkerframework.checker.fenum.qual.FenumTop; import org.checkerframework.checker.fenum.qual.FenumUnqualified; import org.checkerframework.checker.fenum.qual.PolyFenum; +import org.checkerframework.checker.initialization.qual.UnderInitialization; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.MostlyNoElementQualifierHierarchy; import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.framework.util.GraphQualifierHierarchy; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory; +import org.checkerframework.framework.util.DefaultQualifierKindHierarchy; +import org.checkerframework.framework.util.QualifierKind; +import org.checkerframework.framework.util.QualifierKindHierarchy; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.UserError; import org.plumelib.reflection.Signatures; +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.util.Elements; + /** The type factory for the Fenum Checker. */ public class FenumAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { - protected AnnotationMirror FENUM_UNQUALIFIED; - protected AnnotationMirror FENUM, FENUM_BOTTOM; + /** AnnotationMirror for {@link FenumUnqualified}. */ + protected final AnnotationMirror FENUM_UNQUALIFIED; + + /** AnnotationMirror for {@link FenumBottom}. */ + protected final AnnotationMirror FENUM_BOTTOM; + /** AnnotationMirror for {@link FenumTop}. */ + protected final AnnotationMirror FENUM_TOP; + + /** + * Create a FenumAnnotatedTypeFactory. + * + * @param checker checker + */ public FenumAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); FENUM_BOTTOM = AnnotationBuilder.fromClass(elements, FenumBottom.class); - FENUM = AnnotationBuilder.fromClass(elements, Fenum.class); FENUM_UNQUALIFIED = AnnotationBuilder.fromClass(elements, FenumUnqualified.class); + FENUM_TOP = AnnotationBuilder.fromClass(elements, FenumTop.class); this.postInit(); } @@ -37,6 +56,8 @@ public FenumAnnotatedTypeFactory(BaseTypeChecker checker) { /** * Copied from SubtypingChecker. Instead of returning an empty set if no "quals" option is * given, we return Fenum as the only qualifier. + * + * @return the supported type qualifiers */ @Override protected Set> createSupportedTypeQualifiers() { @@ -50,25 +71,22 @@ protected Set> createSupportedTypeQualifiers() { PolyFenum.class); // Load externally defined quals given in the -Aquals and/or -AqualDirs options - String qualNames = checker.getOption("quals"); - String qualDirectories = checker.getOption("qualDirs"); // load individually named qualifiers - if (qualNames != null) { - for (String qualName : qualNames.split(",")) { - if (!Signatures.isBinaryName(qualName)) { - throw new UserError( - "Malformed qualifier \"%s\" in -Aquals=%s", qualName, qualNames); - } - qualSet.add(loader.loadExternalAnnotationClass(qualName)); + for (String qualName : checker.getStringsOption("quals", ',')) { + if (!Signatures.isBinaryName(qualName)) { + throw new UserError("Malformed qualifier \"%s\" in -Aquals", qualName); + } + Class annoClass = loader.loadExternalAnnotationClass(qualName); + if (annoClass == null) { + throw new UserError("Cannot load qualifier \"%s\" in -Aquals", qualName); } + qualSet.add(annoClass); } // load directories of qualifiers - if (qualDirectories != null) { - for (String dirName : qualDirectories.split(":")) { - qualSet.addAll(loader.loadExternalAnnotationClassesFromDirectory(dirName)); - } + for (String dirName : checker.getStringsOption("qualDirs", ',')) { + qualSet.addAll(loader.loadExternalAnnotationClassesFromDirectory(dirName)); } // TODO: warn if no qualifiers given? @@ -77,37 +95,82 @@ protected Set> createSupportedTypeQualifiers() { } @Override - public QualifierHierarchy createQualifierHierarchy(MultiGraphFactory factory) { - return new FenumQualifierHierarchy(factory); + protected QualifierHierarchy createQualifierHierarchy() { + return new FenumQualifierHierarchy(getSupportedTypeQualifiers(), elements); } - protected class FenumQualifierHierarchy extends GraphQualifierHierarchy { + /** Fenum qualifier hierarchy. */ + protected class FenumQualifierHierarchy extends MostlyNoElementQualifierHierarchy { + + /** QualifierKind for {@link Fenum} qualifier. */ + private final QualifierKind FENUM_KIND; - /* The user is expected to introduce additional fenum annotations. - * These annotations are declared to be subtypes of FenumTop, using the - * @SubtypeOf annotation. - * However, there is no way to declare that it is a supertype of Bottom. - * Therefore, we use the constructor of GraphQualifierHierarchy that allows - * us to set a dedicated bottom qualifier. + /** + * Creates FenumQualifierHierarchy. + * + * @param qualifierClasses qualifier classes + * @param elements element utils */ - public FenumQualifierHierarchy(MultiGraphFactory factory) { - super(factory, FENUM_BOTTOM); + public FenumQualifierHierarchy( + Collection> qualifierClasses, Elements elements) { + super(qualifierClasses, elements, FenumAnnotatedTypeFactory.this); + this.FENUM_KIND = + this.qualifierKindHierarchy.getQualifierKind(Fenum.class.getCanonicalName()); } @Override - public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { - if (AnnotationUtils.areSameByName(superAnno, FENUM) - && AnnotationUtils.areSameByName(subAnno, FENUM)) { - return AnnotationUtils.areSame(superAnno, subAnno); - } - // Ignore annotation values to ensure that annotation is in supertype map. - if (AnnotationUtils.areSameByName(superAnno, FENUM)) { - superAnno = FENUM; + protected QualifierKindHierarchy createQualifierKindHierarchy( + @UnderInitialization FenumQualifierHierarchy this, + Collection> qualifierClasses) { + return new DefaultQualifierKindHierarchy(qualifierClasses, FenumBottom.class); + } + + @Override + protected boolean isSubtypeWithElements( + AnnotationMirror subAnno, + QualifierKind subKind, + AnnotationMirror superAnno, + QualifierKind superKind) { + return AnnotationUtils.areSame(subAnno, superAnno); + } + + @Override + protected AnnotationMirror leastUpperBoundWithElements( + AnnotationMirror a1, + QualifierKind qualifierKind1, + AnnotationMirror a2, + QualifierKind qualifierKind2, + QualifierKind lubKind) { + if (qualifierKind1 == FENUM_KIND && qualifierKind2 == FENUM_KIND) { + if (AnnotationUtils.areSame(a1, a2)) { + return a1; + } + return FENUM_TOP; + } else if (qualifierKind1 == FENUM_KIND) { + return a1; + } else if (qualifierKind2 == FENUM_KIND) { + return a2; } - if (AnnotationUtils.areSameByName(subAnno, FENUM)) { - subAnno = FENUM; + throw new TypeSystemError( + "Unexpected QualifierKinds %s %s", qualifierKind1, qualifierKind2); + } + + @Override + protected AnnotationMirror greatestLowerBoundWithElements( + AnnotationMirror a1, + QualifierKind qualifierKind1, + AnnotationMirror a2, + QualifierKind qualifierKind2, + QualifierKind glbKind) { + if (qualifierKind1 == FENUM_KIND && qualifierKind2 == FENUM_KIND) { + return FENUM_BOTTOM; + } else if (qualifierKind1 == FENUM_KIND) { + return a2; + } else if (qualifierKind2 == FENUM_KIND) { + return a1; } - return super.isSubtype(subAnno, superAnno); + throw new TypeSystemError( + "Unexpected QualifierKinds %s %s", qualifierKind1, qualifierKind2); } } } diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/FenumChecker.java b/checker/src/main/java/org/checkerframework/checker/fenum/FenumChecker.java index 350fc96648e5..7835b788c73a 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/FenumChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/fenum/FenumChecker.java @@ -1,11 +1,13 @@ package org.checkerframework.checker.fenum; -import java.util.SortedSet; -import javax.annotation.processing.SupportedOptions; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.subtyping.SubtypingChecker; import org.checkerframework.framework.qual.StubFiles; +import java.util.NavigableSet; + +import javax.annotation.processing.SupportedOptions; + /** * The main checker class for the Fake Enum Checker. * @@ -25,15 +27,8 @@ @SupportedOptions({"quals", "qualDirs"}) public class FenumChecker extends BaseTypeChecker { - /* - @Override - public void initChecker() { - super.initChecker(); - } - */ - @Override - public SortedSet getSuppressWarningsPrefixes() { + public NavigableSet getSuppressWarningsPrefixes() { return SubtypingChecker.getSuppressWarningsPrefixes( this.visitor, super.getSuppressWarningsPrefixes()); } diff --git a/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java b/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java index 6b8a38d5afac..5b915fb934cd 100644 --- a/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/fenum/FenumVisitor.java @@ -6,58 +6,66 @@ import com.sun.source.tree.NewClassTree; import com.sun.source.tree.SwitchTree; import com.sun.source.tree.Tree; -import java.util.Collections; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; + import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.framework.type.QualifierHierarchy; +import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.CaseUtils; + +import java.util.List; + +import javax.lang.model.element.ExecutableElement; +/** The visitor for Fenum Checker. */ public class FenumVisitor extends BaseTypeVisitor { + + /** + * Creates a Fenum Visitor + * + * @param checker the checker + */ public FenumVisitor(BaseTypeChecker checker) { super(checker); } @Override - public Void visitBinary(BinaryTree node, Void p) { - if (!TreeUtils.isStringConcatenation(node)) { - // TODO: ignore string concatenations - + public Void visitBinary(BinaryTree tree, Void p) { + if (!TreeUtils.isStringConcatenation(tree)) { // The Fenum Checker is only concerned with primitive types, so just check that // the primary annotations are equivalent. - AnnotatedTypeMirror lhsAtm = atypeFactory.getAnnotatedType(node.getLeftOperand()); - AnnotatedTypeMirror rhsAtm = atypeFactory.getAnnotatedType(node.getRightOperand()); - - Set lhs = lhsAtm.getEffectiveAnnotations(); - Set rhs = rhsAtm.getEffectiveAnnotations(); - QualifierHierarchy qualHierarchy = atypeFactory.getQualifierHierarchy(); - if (!(qualHierarchy.isSubtype(lhs, rhs) || qualHierarchy.isSubtype(rhs, lhs))) { - checker.reportError(node, "binary.type.incompatible", lhsAtm, rhsAtm); + AnnotatedTypeMirror lhs = atypeFactory.getAnnotatedType(tree.getLeftOperand()); + AnnotatedTypeMirror rhs = atypeFactory.getAnnotatedType(tree.getRightOperand()); + + if (!(typeHierarchy.isSubtypeShallowEffective(lhs, rhs) + || typeHierarchy.isSubtypeShallowEffective(rhs, lhs))) { + checker.reportError(tree, "binary.type.incompatible", lhs, rhs); } } - return super.visitBinary(node, p); + return super.visitBinary(tree, p); } @Override - public Void visitSwitch(SwitchTree node, Void p) { - ExpressionTree expr = node.getExpression(); + public Void visitSwitch(SwitchTree tree, Void p) { + ExpressionTree expr = tree.getExpression(); AnnotatedTypeMirror exprType = atypeFactory.getAnnotatedType(expr); - for (CaseTree caseExpr : node.getCases()) { - ExpressionTree realCaseExpr = caseExpr.getExpression(); - if (realCaseExpr != null) { + for (CaseTree caseExpr : tree.getCases()) { + List realCaseExprs = CaseUtils.getExpressions(caseExpr); + // Check all the case options against the switch expression type: + for (ExpressionTree realCaseExpr : realCaseExprs) { AnnotatedTypeMirror caseType = atypeFactory.getAnnotatedType(realCaseExpr); + // There is currently no "switch.type.incompatible" message key, so it is treated + // identically to "type.incompatible". this.commonAssignmentCheck( exprType, caseType, caseExpr, "switch.type.incompatible"); } } - return super.visitSwitch(node, p); + return super.visitSwitch(tree, p); } @Override @@ -73,8 +81,8 @@ protected void checkConstructorResult( } @Override - protected Set getExceptionParameterLowerBoundAnnotations() { - return Collections.singleton(atypeFactory.FENUM_UNQUALIFIED); + protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { + return new AnnotationMirrorSet(atypeFactory.FENUM_UNQUALIFIED); } // TODO: should we require a match between switch expression and cases? @@ -82,10 +90,9 @@ protected Set getExceptionParameterLowerBoundAnnotat @Override public boolean isValidUse( AnnotatedDeclaredType declarationType, AnnotatedDeclaredType useType, Tree tree) { - // The checker calls this method to compare the annotation used in a - // type to the modifier it adds to the class declaration. As our default - // modifier is FenumBottom, this results in an error when a non-subtype - // is used. Can we use FenumTop as default instead? + // The checker calls this method to compare the annotation used in a type to the modifier it + // adds to the class declaration. As our default modifier is FenumBottom, this results in an + // error when a non-subtype is used. Can we use FenumTop as default instead? return true; } } diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/FormatUtil.java b/checker/src/main/java/org/checkerframework/checker/formatter/FormatUtil.java deleted file mode 100644 index 0205c852ef67..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/formatter/FormatUtil.java +++ /dev/null @@ -1,216 +0,0 @@ -package org.checkerframework.checker.formatter; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.IllegalFormatConversionException; -import java.util.IllegalFormatException; -import java.util.Map; -import java.util.MissingFormatArgumentException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.checkerframework.checker.formatter.qual.ConversionCategory; -import org.checkerframework.checker.formatter.qual.ReturnsFormat; - -/** This class provides a collection of utilities to ease working with format strings. */ -public class FormatUtil { - private static class Conversion { - private final int index; - private final ConversionCategory cath; - - public Conversion(char c, int index) { - this.index = index; - this.cath = ConversionCategory.fromConversionChar(c); - } - - int index() { - return index; - } - - ConversionCategory category() { - return cath; - } - } - - /** - * 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 - */ - @ReturnsFormat - public static String asFormat(String format, ConversionCategory... cc) - throws IllegalFormatException { - ConversionCategory[] fcc = formatParameterCategories(format); - if (fcc.length != cc.length) { - throw new ExcessiveOrMissingFormatArgumentException(cc.length, fcc.length); - } - - for (int i = 0; i < cc.length; i++) { - if (cc[i] != fcc[i]) { - throw new IllegalFormatConversionCategoryException(cc[i], fcc[i]); - } - } - - return format; - } - - /** Throws an exception if the format is not syntactically valid. */ - public static void tryFormatSatisfiability(String format) throws IllegalFormatException { - @SuppressWarnings("unused") - String unused = String.format(format, (Object[]) null); - } - - /** - * Returns a {@link ConversionCategory} for every conversion found in the format string. - * - *

    Throws an exception if the format is not syntactically valid. - */ - public static ConversionCategory[] formatParameterCategories(String format) - throws IllegalFormatException { - tryFormatSatisfiability(format); - - int last = -1; // index of last argument referenced - int lasto = -1; // last ordinary index - int maxindex = -1; - - Conversion[] cs = parse(format); - Map conv = new HashMap<>(); - - for (Conversion c : cs) { - int index = c.index(); - switch (index) { - case -1: // relative index - break; - case 0: // ordinary index - lasto++; - last = lasto; - break; - default: // explicit index - last = index - 1; - break; - } - maxindex = Math.max(maxindex, last); - conv.put( - last, - ConversionCategory.intersect( - conv.containsKey(last) ? conv.get(last) : 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; - } - return res; - } - - // %[argument_index$][flags][width][.precision][t]conversion - private static final String formatSpecifier = - "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])"; - - private static Pattern fsPattern = Pattern.compile(formatSpecifier); - - 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('<'))) { - index = -1; // relative index - } else { - index = 0; // ordinary index - } - } - return index; - } - - private static char conversionCharFromFormat(Matcher m) { - String dt = m.group(5); - if (dt == null) { - return m.group(6).charAt(0); - } else { - return dt.charAt(0); - } - } - - private static Conversion[] parse(String format) { - ArrayList cs = new ArrayList<>(); - Matcher m = fsPattern.matcher(format); - while (m.find()) { - char c = conversionCharFromFormat(m); - switch (c) { - case '%': - case 'n': - break; - default: - cs.add(new Conversion(c, indexFromFormat(m))); - } - } - return cs.toArray(new Conversion[cs.size()]); - } - - public static class ExcessiveOrMissingFormatArgumentException - extends MissingFormatArgumentException { - private static final long serialVersionUID = 17000126L; - - private final int expected; - private final int found; - - /** - * Constructs an instance of this class with the actual argument length and the expected - * one. - */ - public ExcessiveOrMissingFormatArgumentException(int expected, int found) { - super("-"); - this.expected = expected; - this.found = found; - } - - public int getExpected() { - return expected; - } - - public int getFound() { - return found; - } - - @Override - public String getMessage() { - return String.format("Expected %d arguments but found %d.", expected, found); - } - } - - public static class IllegalFormatConversionCategoryException - extends IllegalFormatConversionException { - private static final long serialVersionUID = 17000126L; - - private final ConversionCategory expected; - private final ConversionCategory found; - - /** - * Constructs an instance of this class with the mismatched conversion and the expected one. - */ - public IllegalFormatConversionCategoryException( - ConversionCategory expected, ConversionCategory found) { - super( - expected.chars.length() == 0 ? '-' : expected.chars.charAt(0), - found.types == null ? Object.class : found.types[0]); - this.expected = expected; - this.found = found; - } - - public ConversionCategory getExpected() { - return expected; - } - - public ConversionCategory getFound() { - return found; - } - - @Override - public String getMessage() { - return String.format("Expected category %s but found %s.", expected, found); - } - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/FormatterAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/formatter/FormatterAnnotatedTypeFactory.java index 15a7169ea9ec..504be1a93bf7 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/FormatterAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/formatter/FormatterAnnotatedTypeFactory.java @@ -2,24 +2,32 @@ import com.sun.source.tree.LiteralTree; import com.sun.source.tree.Tree; -import java.util.IllegalFormatException; -import javax.lang.model.element.AnnotationMirror; + import org.checkerframework.checker.formatter.qual.ConversionCategory; import org.checkerframework.checker.formatter.qual.Format; import org.checkerframework.checker.formatter.qual.FormatBottom; +import org.checkerframework.checker.formatter.qual.FormatMethod; import org.checkerframework.checker.formatter.qual.InvalidFormat; import org.checkerframework.checker.formatter.qual.UnknownFormat; +import org.checkerframework.checker.formatter.util.FormatUtil; +import org.checkerframework.checker.signature.qual.CanonicalName; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.MostlyNoElementQualifierHierarchy; import org.checkerframework.framework.type.QualifierHierarchy; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.framework.util.GraphQualifierHierarchy; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory; +import org.checkerframework.framework.util.QualifierKind; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TypeSystemError; + +import java.lang.annotation.Annotation; +import java.util.IllegalFormatException; + +import javax.lang.model.element.AnnotationMirror; /** * Adds {@link Format} to the type of tree, if it is a {@code String} or {@code char} literal that @@ -33,15 +41,22 @@ public class FormatterAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The @{@link UnknownFormat} annotation. */ protected final AnnotationMirror UNKNOWNFORMAT = AnnotationBuilder.fromClass(elements, UnknownFormat.class); - /** The @{@link Format} annotation. */ - protected final AnnotationMirror FORMAT = AnnotationBuilder.fromClass(elements, Format.class); - /** The @{@link InvalidFormat} annotation. */ - protected final AnnotationMirror INVALIDFORMAT = - AnnotationBuilder.fromClass(elements, InvalidFormat.class); + /** The @{@link FormatBottom} annotation. */ protected final AnnotationMirror FORMATBOTTOM = AnnotationBuilder.fromClass(elements, FormatBottom.class); + /** The @{@link FormatMethod} annotation. */ + protected final AnnotationMirror FORMATMETHOD = + AnnotationBuilder.fromClass(elements, FormatMethod.class); + + /** The fully-qualified name of the {@link Format} qualifier. */ + protected static final @CanonicalName String FORMAT_NAME = Format.class.getCanonicalName(); + + /** The fully-qualified name of the {@link InvalidFormat} qualifier. */ + protected static final @CanonicalName String INVALIDFORMAT_NAME = + InvalidFormat.class.getCanonicalName(); + /** Syntax tree utilities. */ protected final FormatterTreeUtil treeUtil = new FormatterTreeUtil(checker); @@ -49,12 +64,30 @@ public class FormatterAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { public FormatterAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); + try { + // Use concatenation to avoid ShadowJar relocate + // "com.google.errorprone.annotations.FormatMethod" + @SuppressWarnings({ + "unchecked", // Class must be an annotation type + "signature:argument.type.incompatible" // Class name intentionally obfuscated + }) + Class cgFormatMethod = + (Class) + Class.forName( + "com.go".toString() + + "ogle.errorprone.annotations.FormatMethod"); + + addAliasedDeclAnnotation(cgFormatMethod, FormatMethod.class, FORMATMETHOD); + } catch (ClassNotFoundException cnfe) { + // Ignore if com.google.errorprone.annotations.FormatMethod cannot be found. + } + this.postInit(); } @Override - public QualifierHierarchy createQualifierHierarchy(MultiGraphFactory factory) { - return new FormatterQualifierHierarchy(factory); + protected QualifierHierarchy createQualifierHierarchy() { + return new FormatterQualifierHierarchy(); } @Override @@ -62,19 +95,116 @@ protected TreeAnnotator createTreeAnnotator() { return new ListTreeAnnotator(super.createTreeAnnotator(), new FormatterTreeAnnotator(this)); } + /* NO-AFU + * {@inheritDoc} + * + *

    If a method is annotated with {@code @FormatMethod}, remove any {@code @Format} annotation + * from its first argument. + */ + /* NO-AFU + @Override + public void wpiPrepareMethodForWriting(AMethod method) { + super.wpiPrepareMethodForWriting(method); + if (hasFormatMethodAnno(method)) { + AField param = method.parameters.get(0); + if (param != null) { + Set paramTypeAnnos = param.type.tlAnnotationsHere; + paramTypeAnnos.removeIf( + a -> a.def.name.equals("org.checkerframework.checker.formatter.qual.Format")); + } + } + */ + + /* NO-AFU + * {@inheritDoc} + * + *

    If a method is annotated with {@code @FormatMethod}, remove any {@code @Format} annotation + * from its first argument. + */ + /* NO-AFU + @Override + public void wpiPrepareMethodForWriting( + WholeProgramInferenceJavaParserStorage.CallableDeclarationAnnos methodAnnos, + Collection inSupertypes, + Collection inSubtypes) { + super.wpiPrepareMethodForWriting(methodAnnos, inSupertypes, inSubtypes); + if (hasFormatMethodAnno(methodAnnos)) { + AnnotatedTypeMirror atm = methodAnnos.getParameterType(0); + atm.removeAnnotationByClass(org.checkerframework.checker.formatter.qual.Format.class); + } + } + */ + + /* NO-AFU + * Returns true if the method has a {@code @FormatMethod} annotation. + * + * @param methodAnnos method annotations + * @return true if the method has a {@code @FormatMethod} annotation + */ + /* NO-AFU + private boolean hasFormatMethodAnno(AMethod methodAnnos) { + for (Annotation anno : methodAnnos.tlAnnotationsHere) { + String annoName = anno.def.name; + if (annoName.equals("org.checkerframework.checker.formatter.qual.FormatMethod") + || anno.def.name.equals("com.google.errorprone.annotations.FormatMethod")) { + return true; + } + } + */ + + /* NO-AFU + * Returns true if the method has a {@code @FormatMethod} annotation. + * + * @param methodAnnos method annotations + * @return true if the method has a {@code @FormatMethod} annotation + */ + /* NO-AFU + private boolean hasFormatMethodAnno( + WholeProgramInferenceJavaParserStorage.CallableDeclarationAnnos methodAnnos) { + AnnotationMirrorSet declarationAnnos = methodAnnos.getDeclarationAnnotations(); + return AnnotationUtils.containsSameByClass( + declarationAnnos, org.checkerframework.checker.formatter.qual.FormatMethod.class) + || AnnotationUtils.containsSameByName( + declarationAnnos, "com.google.errorprone.annotations.FormatMethod"); + } + */ + + /* NO-AFU + * Returns true if the method has a {@code @FormatMethod} annotation. + * + * @param methodAnnos method annotations + * @return true if the method has a {@code @FormatMethod} annotation + */ + /* NO-AFU + private boolean hasFormatMethodAnno( + WholeProgramInferenceJavaParserStorage.CallableDeclarationAnnos methodAnnos) { + AnnotationMirrorSet declarationAnnos = methodAnnos.getDeclarationAnnotations(); + return AnnotationUtils.containsSameByClass( + declarationAnnos, + org.checkerframework.checker.formatter.qual.FormatMethod.class) + || AnnotationUtils.containsSameByName( + // TODO: avoid com.google relocate + declarationAnnos, "com.google.errorprone.annotations.FormatMethod"); + } + */ + + /** The tree annotator for the Format String Checker. */ private class FormatterTreeAnnotator extends TreeAnnotator { + /** + * Create the tree annotator for the Format String Checker. + * + * @param atypeFactory the Format String Checker type factory + */ public FormatterTreeAnnotator(AnnotatedTypeFactory atypeFactory) { super(atypeFactory); } @Override public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) { - if (!type.isAnnotatedInHierarchy(FORMAT)) { + if (!type.hasAnnotationInHierarchy(UNKNOWNFORMAT)) { String format = null; if (tree.getKind() == Tree.Kind.STRING_LITERAL) { format = (String) tree.getValue(); - } else if (tree.getKind() == Tree.Kind.CHAR_LITERAL) { - format = Character.toString((Character) tree.getValue()); } if (format != null) { AnnotationMirror anno; @@ -95,16 +225,32 @@ public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) { } } - class FormatterQualifierHierarchy extends GraphQualifierHierarchy { + /** Qualifier hierarchy for the Formatter Checker. */ + class FormatterQualifierHierarchy extends MostlyNoElementQualifierHierarchy { - public FormatterQualifierHierarchy(MultiGraphFactory f) { - super(f, FORMATBOTTOM); + /** Qualifier kind for the @{@link Format} annotation. */ + private final QualifierKind FORMAT_KIND; + + /** Qualifier kind for the @{@link InvalidFormat} annotation. */ + private final QualifierKind INVALIDFORMAT_KIND; + + /** Creates a {@link FormatterQualifierHierarchy}. */ + public FormatterQualifierHierarchy() { + super( + FormatterAnnotatedTypeFactory.this.getSupportedTypeQualifiers(), + elements, + FormatterAnnotatedTypeFactory.this); + FORMAT_KIND = getQualifierKind(FORMAT_NAME); + INVALIDFORMAT_KIND = getQualifierKind(INVALIDFORMAT_NAME); } @Override - public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { - if (AnnotationUtils.areSameByName(subAnno, FORMAT) - && AnnotationUtils.areSameByName(superAnno, FORMAT)) { + protected boolean isSubtypeWithElements( + AnnotationMirror subAnno, + QualifierKind subKind, + AnnotationMirror superAnno, + QualifierKind superKind) { + if (subKind == FORMAT_KIND && superKind == FORMAT_KIND) { ConversionCategory[] rhsArgTypes = treeUtil.formatAnnotationToCategories(subAnno); ConversionCategory[] lhsArgTypes = treeUtil.formatAnnotationToCategories(superAnno); @@ -118,33 +264,24 @@ public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { } } return true; + } else if (subKind == INVALIDFORMAT_KIND && superKind == INVALIDFORMAT_KIND) { + return true; } - if (AnnotationUtils.areSameByName(superAnno, FORMAT)) { - superAnno = FORMAT; - } - if (AnnotationUtils.areSameByName(subAnno, FORMAT)) { - subAnno = FORMAT; - } - if (AnnotationUtils.areSameByName(superAnno, INVALIDFORMAT)) { - superAnno = INVALIDFORMAT; - } - if (AnnotationUtils.areSameByName(subAnno, INVALIDFORMAT)) { - subAnno = INVALIDFORMAT; - } - - return super.isSubtype(subAnno, superAnno); + throw new TypeSystemError("Unexpected kinds: %s %s", subKind, superKind); } @Override - public AnnotationMirror leastUpperBound(AnnotationMirror anno1, AnnotationMirror anno2) { - if (AnnotationUtils.areSameByName(anno1, FORMATBOTTOM)) { + protected AnnotationMirror leastUpperBoundWithElements( + AnnotationMirror anno1, + QualifierKind qualifierKind1, + AnnotationMirror anno2, + QualifierKind qualifierKind2, + QualifierKind lubKind) { + if (qualifierKind1.isBottom()) { return anno2; - } - if (AnnotationUtils.areSameByName(anno2, FORMATBOTTOM)) { + } else if (qualifierKind2.isBottom()) { return anno1; - } - if (AnnotationUtils.areSameByName(anno1, FORMAT) - && AnnotationUtils.areSameByName(anno2, FORMAT)) { + } else if (qualifierKind1 == FORMAT_KIND && qualifierKind2 == FORMAT_KIND) { ConversionCategory[] shorterArgTypesList = treeUtil.formatAnnotationToCategories(anno1); ConversionCategory[] longerArgTypesList = @@ -171,9 +308,9 @@ public AnnotationMirror leastUpperBound(AnnotationMirror anno1, AnnotationMirror resultArgTypes[i] = longerArgTypesList[i]; } return treeUtil.categoriesToFormatAnnotation(resultArgTypes); - } - if (AnnotationUtils.areSameByName(anno1, INVALIDFORMAT) - && AnnotationUtils.areSameByName(anno2, INVALIDFORMAT)) { + } else if (qualifierKind1 == INVALIDFORMAT_KIND + && qualifierKind2 == INVALIDFORMAT_KIND) { + assert !anno1.getElementValues().isEmpty(); assert !anno2.getElementValues().isEmpty(); @@ -193,15 +330,17 @@ public AnnotationMirror leastUpperBound(AnnotationMirror anno1, AnnotationMirror } @Override - public AnnotationMirror greatestLowerBound(AnnotationMirror anno1, AnnotationMirror anno2) { - if (AnnotationUtils.areSameByName(anno1, UNKNOWNFORMAT)) { + protected AnnotationMirror greatestLowerBoundWithElements( + AnnotationMirror anno1, + QualifierKind qualifierKind1, + AnnotationMirror anno2, + QualifierKind qualifierKind2, + QualifierKind glbKind) { + if (qualifierKind1.isTop()) { return anno2; - } - if (AnnotationUtils.areSameByName(anno2, UNKNOWNFORMAT)) { + } else if (qualifierKind2.isTop()) { return anno1; - } - if (AnnotationUtils.areSameByName(anno1, FORMAT) - && AnnotationUtils.areSameByName(anno2, FORMAT)) { + } else if (qualifierKind1 == FORMAT_KIND && qualifierKind2 == FORMAT_KIND) { ConversionCategory[] anno1ArgTypes = treeUtil.formatAnnotationToCategories(anno1); ConversionCategory[] anno2ArgTypes = treeUtil.formatAnnotationToCategories(anno2); @@ -219,9 +358,9 @@ public AnnotationMirror greatestLowerBound(AnnotationMirror anno1, AnnotationMir anno3ArgTypes[i] = ConversionCategory.union(anno1ArgTypes[i], anno2ArgTypes[i]); } return treeUtil.categoriesToFormatAnnotation(anno3ArgTypes); - } - if (AnnotationUtils.areSameByName(anno1, INVALIDFORMAT) - && AnnotationUtils.areSameByName(anno2, INVALIDFORMAT)) { + } else if (qualifierKind1 == INVALIDFORMAT_KIND + && qualifierKind2 == INVALIDFORMAT_KIND) { + assert !anno1.getElementValues().isEmpty(); assert !anno2.getElementValues().isEmpty(); diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/FormatterTransfer.java b/checker/src/main/java/org/checkerframework/checker/formatter/FormatterTransfer.java index 8c49feba27a4..e6b4c410c0a5 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/FormatterTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/formatter/FormatterTransfer.java @@ -1,8 +1,8 @@ package org.checkerframework.checker.formatter; -import javax.lang.model.element.AnnotationMirror; import org.checkerframework.checker.formatter.FormatterTreeUtil.Result; import org.checkerframework.checker.formatter.qual.ConversionCategory; +import org.checkerframework.checker.formatter.util.FormatUtil; import org.checkerframework.dataflow.analysis.RegularTransferResult; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; @@ -12,6 +12,8 @@ import org.checkerframework.framework.flow.CFTransfer; import org.checkerframework.framework.flow.CFValue; +import javax.lang.model.element.AnnotationMirror; + public class FormatterTransfer extends CFTransfer { public FormatterTransfer(CFAnalysis analysis) { 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 fe6b1da30845..4a7cc5cd977c 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/FormatterTreeUtil.java +++ b/checker/src/main/java/org/checkerframework/checker/formatter/FormatterTreeUtil.java @@ -5,28 +5,14 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.TypeCastTree; import com.sun.source.util.SimpleTreeVisitor; -import java.util.IllegalFormatException; -import java.util.List; -import java.util.Locale; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.type.ArrayType; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.NullType; -import javax.lang.model.type.PrimitiveType; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.SimpleElementVisitor7; -import javax.lang.model.util.SimpleTypeVisitor7; + import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.formatter.qual.ConversionCategory; import org.checkerframework.checker.formatter.qual.Format; import org.checkerframework.checker.formatter.qual.FormatMethod; import org.checkerframework.checker.formatter.qual.InvalidFormat; import org.checkerframework.checker.formatter.qual.ReturnsFormat; -import org.checkerframework.checker.signature.qual.BinaryName; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.cfg.node.ArrayCreationNode; import org.checkerframework.dataflow.cfg.node.FieldAccessNode; @@ -37,24 +23,49 @@ import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; + +import java.util.IllegalFormatException; +import java.util.List; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.NullType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.SimpleTypeVisitor8; /** * This class provides a collection of utilities to ease working with syntax trees that have * something to do with Formatters. */ public class FormatterTreeUtil { + /** The checker. */ public final BaseTypeChecker checker; + + /** The processing environment. */ public final ProcessingEnvironment processingEnv; + /** The value() element/field of an @Format annotation. */ + protected final ExecutableElement formatValueElement; + + /** The value() element/field of an @InvalidFormat annotation. */ + protected final ExecutableElement invalidFormatValueElement; + // private final ExecutableElement formatArgTypesElement; public FormatterTreeUtil(BaseTypeChecker checker) { this.checker = checker; this.processingEnv = checker.getProcessingEnvironment(); + formatValueElement = TreeUtils.getMethod(Format.class, "value", 0, processingEnv); + invalidFormatValueElement = + TreeUtils.getMethod(InvalidFormat.class, "value", 0, processingEnv); /* this.formatArgTypesElement = TreeUtils.getMethod( - org.checkerframework.checker.formatter.qual.Format.class.getCanonicalName(), + Format.class, "value", 0, processingEnv); @@ -129,27 +140,27 @@ public boolean isAsFormatCall(MethodInvocationNode node, AnnotatedTypeFactory at return anno != null; } - private ConversionCategory[] asFormatCallCategoriesLowLevel(MethodInvocationNode node) { + private ConversionCategory @Nullable [] asFormatCallCategoriesLowLevel( + MethodInvocationNode node) { Node vararg = node.getArgument(1); - if (vararg instanceof ArrayCreationNode) { - List convs = ((ArrayCreationNode) vararg).getInitializers(); - ConversionCategory[] res = new ConversionCategory[convs.size()]; - for (int i = 0; i < convs.size(); ++i) { - Node conv = convs.get(i); - if (conv instanceof FieldAccessNode) { - Class clazz = - typeMirrorToClass(((FieldAccessNode) conv).getType()); - if (clazz == ConversionCategory.class) { - res[i] = - ConversionCategory.valueOf(((FieldAccessNode) conv).getFieldName()); - continue; /* avoid returning null */ - } + if (!(vararg instanceof ArrayCreationNode)) { + return null; + } + List convs = ((ArrayCreationNode) vararg).getInitializers(); + ConversionCategory[] res = new ConversionCategory[convs.size()]; + for (int i = 0; i < convs.size(); ++i) { + Node conv = convs.get(i); + if (conv instanceof FieldAccessNode) { + Class clazz = + TypesUtils.getClassFromType(((FieldAccessNode) conv).getType()); + if (clazz == ConversionCategory.class) { + res[i] = ConversionCategory.valueOf(((FieldAccessNode) conv).getFieldName()); + continue; /* avoid returning null */ } - return null; } - return res; + return null; } - return null; + return res; } public Result asFormatCallCategories(MethodInvocationNode node) { @@ -157,58 +168,111 @@ public Result asFormatCallCategories(MethodInvocationNode return new Result<>(asFormatCallCategoriesLowLevel(node), node.getTree()); } - /** Returns true if {@code node} is a call to a method annotated with {@code @FormatMethod}. */ - public boolean isFormatCall(MethodInvocationTree node, AnnotatedTypeFactory atypeFactory) { - ExecutableElement method = TreeUtils.elementFromUse(node); + /** + * Returns true if {@code tree} is a call to a method annotated with {@code @FormatMethod}. + * + * @param tree a method call + * @param atypeFactory a type factory + * @return true if {@code tree} is a call to a method annotated with {@code @FormatMethod} + */ + public boolean isFormatMethodCall( + MethodInvocationTree tree, AnnotatedTypeFactory atypeFactory) { + ExecutableElement method = TreeUtils.elementFromUse(tree); AnnotationMirror anno = atypeFactory.getDeclAnnotation(method, FormatMethod.class); return anno != null; } - /** Returns true if the given ExpressionTree has type java.util.Locale. */ - public static boolean isLocale(ExpressionTree e, AnnotatedTypeFactory atypeFactory) { - return (typeMirrorToClass(atypeFactory.getAnnotatedType(e).getUnderlyingType()) - == Locale.class); + /** + * Creates a new FormatCall, or returns null. + * + * @param invocationTree a method invocation, where the method is annotated @FormatMethod + * @param atypeFactory the type factory + * @return a new FormatCall, or null if the invocation is of a method that is improperly + * annotated @FormatMethod + */ + public @Nullable FormatCall create( + MethodInvocationTree invocationTree, AnnotatedTypeFactory atypeFactory) { + FormatterTreeUtil ftu = ((FormatterAnnotatedTypeFactory) atypeFactory).treeUtil; + if (!ftu.isFormatMethodCall(invocationTree, atypeFactory)) { + return null; + } + + ExecutableElement methodElement = TreeUtils.elementFromUse(invocationTree); + int formatStringIndex = FormatterVisitor.formatStringIndex(methodElement); + if (formatStringIndex == -1) { + // Reporting the error is redundant if the method was declared in source code, because + // the visitor will have reported it; but it is necessary if the method was declared in + // byte code. + atypeFactory + .getChecker() + .reportError( + invocationTree, "format.method.invalid", methodElement.getSimpleName()); + return null; + } + ExpressionTree formatStringTree = invocationTree.getArguments().get(formatStringIndex); + AnnotatedTypeMirror formatStringType = atypeFactory.getAnnotatedType(formatStringTree); + List allArgs = invocationTree.getArguments(); + List args = + allArgs.subList(formatStringIndex + 1, allArgs.size()); + + return new FormatCall( + invocationTree, formatStringTree, formatStringType, args, atypeFactory); } /** Represents a format method invocation in the syntax tree. */ public class FormatCall { - private final AnnotatedTypeMirror formatAnno; + /** The call itself. */ + /*package-private*/ final MethodInvocationTree invocationTree; + + /** The format string argument. */ + private final ExpressionTree formatStringTree; + + /** The type of the format string argument. */ + private final AnnotatedTypeMirror formatStringType; + + /** The arguments that follow the format string argument. */ private final List args; - final MethodInvocationTree node; - private final ExpressionTree formatArg; + + /** The type factory. */ private final AnnotatedTypeFactory atypeFactory; - public FormatCall(MethodInvocationTree node, AnnotatedTypeFactory atypeFactory) { - this.node = node; - // TODO figure out how to make passing of environment - // objects such as atypeFactory, processingEnv, ... nicer + /** + * Create a new FormatCall object. + * + * @param invocationTree the call itself + * @param formatStringTree the format string argument + * @param formatStringType the type of the format string argument + * @param args the arguments that follow the format string argument + * @param atypeFactory the type factory + */ + private FormatCall( + MethodInvocationTree invocationTree, + ExpressionTree formatStringTree, + AnnotatedTypeMirror formatStringType, + List args, + AnnotatedTypeFactory atypeFactory) { + this.invocationTree = invocationTree; + this.formatStringTree = formatStringTree; + this.formatStringType = formatStringType; + this.args = args; this.atypeFactory = atypeFactory; - List theargs; - - theargs = node.getArguments(); - if (isLocale(theargs.get(0), atypeFactory)) { - // call with Locale as first argument - theargs = theargs.subList(1, theargs.size()); - } - - // TODO Check that the first parameter exists and is a string. - formatArg = theargs.get(0); - formatAnno = atypeFactory.getAnnotatedType(formatArg); - this.args = theargs.subList(1, theargs.size()); } /** - * Returns null if the format-string argument's type is annotated as {@code @Format}. - * Returns an error description if not annotated as {@code @Format}. + * Returns an error description if the format-string argument's type is not + * annotated as {@code @Format}. Returns null if it is annotated. + * + * @return an error description if the format string is not annotated as {@code @Format}, or + * null if it is */ - public final Result hasFormatAnnotation() { - if (!formatAnno.hasAnnotation(Format.class)) { + public final @Nullable Result errMissingFormatAnnotation() { + if (!formatStringType.hasAnnotation(Format.class)) { String msg = "(is a @Format annotation missing?)"; - AnnotationMirror inv = formatAnno.getAnnotation(InvalidFormat.class); + AnnotationMirror inv = formatStringType.getAnnotation(InvalidFormat.class); if (inv != null) { msg = invalidFormatAnnotationToErrorMessage(inv); } - return new Result<>(msg, formatArg); + return new Result<>(msg, formatStringTree); } return null; } @@ -222,12 +286,12 @@ public final Result getInvocationType() { InvocationType type = InvocationType.VARARG; if (args.size() == 1) { - final ExpressionTree first = args.get(0); + ExpressionTree first = args.get(0); TypeMirror argType = atypeFactory.getAnnotatedType(first).getUnderlyingType(); // figure out if argType is an array type = argType.accept( - new SimpleTypeVisitor7>() { + new SimpleTypeVisitor8>() { @Override protected InvocationType defaultAction( TypeMirror e, Class p) { @@ -244,18 +308,18 @@ public InvocationType visitArray(ArrayType t, Class p) { InvocationType, Class>() { @Override protected InvocationType defaultAction( - Tree node, Class p) { + Tree tree, Class p) { // just a normal array return InvocationType.ARRAY; } @Override public InvocationType visitTypeCast( - TypeCastTree node, Class p) { + TypeCastTree tree, Class p) { // it's a (Object[])null return atypeFactory .getAnnotatedType( - node + tree .getExpression()) .getUnderlyingType() .getKind() @@ -275,7 +339,7 @@ public InvocationType visitNull(NullType t, Class p) { Void.TYPE); } - ExpressionTree loc = node.getMethodSelect(); + ExpressionTree loc = invocationTree.getMethodSelect(); if (type != InvocationType.VARARG && !args.isEmpty()) { loc = args.get(0); } @@ -285,56 +349,65 @@ public InvocationType visitNull(NullType t, Class p) { /** * Returns the conversion category for every parameter. * + * @return the conversion categories of all the parameters * @see ConversionCategory */ public final ConversionCategory[] getFormatCategories() { - AnnotationMirror anno = formatAnno.getAnnotation(Format.class); + AnnotationMirror anno = formatStringType.getAnnotation(Format.class); return formatAnnotationToCategories(anno); } /** - * Returns the type of the function's parameters. Use {@link - * #isValidParameter(ConversionCategory, TypeMirror) isValidParameter} and {@link - * #isParameterNull(TypeMirror) isParameterNull} to work with the result. + * Returns the types of the arguments to the call. Use {@link #isValidArgument} and {@link + * #isArgumentNull} to work with the result. + * + * @return the types of the arguments to the call */ - public final Result[] getParamTypes() { + public final Result[] getArgTypes() { // One to suppress warning in javac, the other to suppress warning in Eclipse... @SuppressWarnings({"rawtypes", "unchecked"}) Result[] res = new Result[args.size()]; for (int i = 0; i < res.length; ++i) { ExpressionTree arg = args.get(i); - TypeMirror argType = atypeFactory.getAnnotatedType(arg).getUnderlyingType(); + TypeMirror argType; + if (TreeUtils.isNullExpression(arg)) { + argType = atypeFactory.getProcessingEnv().getTypeUtils().getNullType(); + } else { + argType = atypeFactory.getAnnotatedType(arg).getUnderlyingType(); + } res[i] = new Result<>(argType, arg); } return res; } /** - * Checks if the type of a parameter returned from {@link #getParamTypes()} is valid for the + * Checks if the type of an argument returned from {@link #getArgTypes()} is valid for the * passed ConversionCategory. + * + * @param formatCat a format specifier + * @param argType an argument type + * @return true if the argument can be passed to the format specifier */ - public final boolean isValidParameter(ConversionCategory formatCat, TypeMirror paramType) { - Class type = typeMirrorToClass(paramType); - if (type == null) { - // we did not recognize the parameter type - return false; - } - for (Class c : formatCat.types) { - if (c.isAssignableFrom(type)) { - return true; - } + public final boolean isValidArgument(ConversionCategory formatCat, TypeMirror argType) { + if (argType.getKind() == TypeKind.NULL || isArgumentNull(argType)) { + return true; } - return false; + Class type = TypesUtils.getClassFromType(argType); + return formatCat.isAssignableFrom(type); } /** - * Checks if the parameter returned from {@link #getParamTypes()} is a {@code null} - * expression. + * Checks if the argument returned from {@link #getArgTypes()} is a {@code null} expression. + * + * @param type a type + * @return true if the argument is a {@code null} expression */ - public final boolean isParameterNull(TypeMirror type) { + public final boolean isArgumentNull(TypeMirror type) { + // TODO: Just check whether it is the VOID TypeMirror. + // is it the null literal return type.accept( - new SimpleTypeVisitor7>() { + new SimpleTypeVisitor8>() { @Override protected Boolean defaultAction(TypeMirror e, Class p) { // it's not the null literal @@ -351,6 +424,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. * @@ -383,109 +458,60 @@ public AnnotationMirror exceptionToInvalidFormatAnnotation(IllegalFormatExceptio } /** - * Takes an invalid formatter string and, returns a syntax trees element that represents a - * {@link InvalidFormat} annotation with the invalid formatter string as value. + * Creates an {@link InvalidFormat} annotation with the given string as its value. + * + * @param invalidFormatString an invalid formatter string + * @return an {@link InvalidFormat} annotation with the given string as its value */ - // package-private - AnnotationMirror stringToInvalidFormatAnnotation(String invalidFormatString) { - AnnotationBuilder builder = - new AnnotationBuilder(processingEnv, InvalidFormat.class.getCanonicalName()); + /*package-private*/ AnnotationMirror stringToInvalidFormatAnnotation( + String invalidFormatString) { + AnnotationBuilder builder = new AnnotationBuilder(processingEnv, InvalidFormat.class); builder.setValue("value", invalidFormatString); return builder.build(); } + /** + * Gets the value() element/field out of an InvalidFormat annotation. + * + * @param anno an InvalidFormat annotation + * @return its value() element/field + */ + private String getInvalidFormatValue(AnnotationMirror anno) { + return (String) anno.getElementValues().get(invalidFormatValueElement).getValue(); + } + /** * Takes a syntax tree element that represents a {@link InvalidFormat} annotation, and returns * its value. + * + * @param anno an InvalidFormat annotation + * @return its value() element/field */ public String invalidFormatAnnotationToErrorMessage(AnnotationMirror anno) { - return "\"" + AnnotationUtils.getElementValue(anno, "value", String.class, true) + "\""; + return "\"" + getInvalidFormatValue(anno) + "\""; } /** - * Takes a list of ConversionCategory elements, and returns a syntax tree element that - * represents a {@link Format} annotation with the list as value. + * Creates a {@code @}{@link Format} annotation with the given list as its value. + * + * @param args conversion categories for the {@code @Format} annotation + * @return a {@code @}{@link Format} annotation with the given list as its value */ public AnnotationMirror categoriesToFormatAnnotation(ConversionCategory[] args) { - AnnotationBuilder builder = - new AnnotationBuilder(processingEnv, Format.class.getCanonicalName()); + AnnotationBuilder builder = new AnnotationBuilder(processingEnv, Format.class); builder.setValue("value", args); return builder.build(); } /** - * Takes a syntax tree element that represents a {@link Format} annotation, and returns its - * value. - */ - public ConversionCategory[] formatAnnotationToCategories(AnnotationMirror anno) { - List list = - AnnotationUtils.getElementValueEnumArray( - anno, "value", ConversionCategory.class, false); - return list.toArray(new ConversionCategory[] {}); - } - - /** Converts a TypeMirror to a Class. */ - private static class TypeMirrorToClassVisitor - extends SimpleTypeVisitor7, Class> { - @Override - public Class visitPrimitive(PrimitiveType t, Class v) { - switch (t.getKind()) { - case BOOLEAN: - return Boolean.class; - case BYTE: - return Byte.class; - case CHAR: - return Character.class; - case SHORT: - return Short.class; - case INT: - return Integer.class; - case LONG: - return Long.class; - case FLOAT: - return Float.class; - case DOUBLE: - return Double.class; - default: - return null; - } - } - - @Override - public Class visitDeclared(DeclaredType dt, Class v) { - return dt.asElement() - .accept( - new SimpleElementVisitor7, Class>() { - @Override - public Class visitType( - TypeElement e, Class v) { - try { - @SuppressWarnings( - "signature") // https://tinyurl.com/cfissue/658: - // Name.toString should be @PolySignature - @BinaryName String cname = e.getQualifiedName().toString(); - return Class.forName(cname); - } catch (ClassNotFoundException e1) { - return null; // the lookup should work for all - // the classes we care about - } - } - }, - Void.TYPE); - } - } - - /** The singleton instance of TypeMirrorToClassVisitor. */ - private static TypeMirrorToClassVisitor typeMirrorToClassVisitor = - new TypeMirrorToClassVisitor(); - - /** - * Converts a TypeMirror to a Class. + * Returns the value of a {@code @}{@link Format} annotation. * - * @param type a TypeMirror - * @return the class corresponding to the argument + * @param anno a {@code @}{@link Format} annotation + * @return the annotation's {@code value} element */ - private static final Class typeMirrorToClass(final TypeMirror type) { - return type.accept(typeMirrorToClassVisitor, Void.TYPE); + @SuppressWarnings("GetClassOnEnum") + public ConversionCategory[] formatAnnotationToCategories(AnnotationMirror anno) { + return AnnotationUtils.getElementValueEnumArray( + anno, formatValueElement, ConversionCategory.class); } } 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 47d31c067b5b..65e3c092432a 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/FormatterVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/formatter/FormatterVisitor.java @@ -6,23 +6,35 @@ import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; -import java.util.List; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.TypeMirror; + import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.formatter.FormatterTreeUtil.FormatCall; import org.checkerframework.checker.formatter.FormatterTreeUtil.InvocationType; import org.checkerframework.checker.formatter.FormatterTreeUtil.Result; import org.checkerframework.checker.formatter.qual.ConversionCategory; import org.checkerframework.checker.formatter.qual.FormatMethod; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; +import org.checkerframework.javacutil.UserError; + +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/* NO-AFU + import org.checkerframework.common.wholeprograminference.WholeProgramInference; +*/ /** * Whenever a format method invocation is found in the syntax tree, checks are performed as @@ -36,19 +48,34 @@ public FormatterVisitor(BaseTypeChecker checker) { } @Override - public Void visitMethodInvocation(MethodInvocationTree node, Void p) { - FormatterTreeUtil tu = atypeFactory.treeUtil; - if (tu.isFormatCall(node, atypeFactory)) { - FormatCall fc = atypeFactory.treeUtil.new FormatCall(node, atypeFactory); + public Void visitMethod(MethodTree tree, Void p) { + ExecutableElement methodElement = TreeUtils.elementFromDeclaration(tree); + if (atypeFactory.getDeclAnnotation(methodElement, FormatMethod.class) != null) { + int formatStringIndex = FormatterVisitor.formatStringIndex(methodElement); + if (formatStringIndex == -1) { + checker.reportError(tree, "format.method.invalid", methodElement.getSimpleName()); + } + } + return super.visitMethod(tree, p); + } - Result errMissingFormat = fc.hasFormatAnnotation(); + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { + FormatterTreeUtil ftu = atypeFactory.treeUtil; + FormatCall fc = ftu.create(tree, atypeFactory); + if (fc != null) { + MethodTree enclosingMethod = + TreePathUtil.enclosingMethod(atypeFactory.getPath(fc.invocationTree)); + + Result errMissingFormat = fc.errMissingFormatAnnotation(); if (errMissingFormat != null) { // The string's type has no @Format annotation. - if (isWrappedFormatCall(fc)) { + if (isWrappedFormatCall(fc, enclosingMethod)) { // Nothing to do, because call is legal. } else { // I.1 - tu.failure(errMissingFormat, "format.string.invalid", errMissingFormat.value()); + ftu.failure( + errMissingFormat, "format.string.invalid", errMissingFormat.value()); } } else { // The string has a @Format annotation. @@ -56,42 +83,54 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { ConversionCategory[] formatCats = fc.getFormatCategories(); switch (invc.value()) { case VARARG: - Result[] paramTypes = fc.getParamTypes(); - int paraml = paramTypes.length; + Result[] argTypes = fc.getArgTypes(); + int argl = argTypes.length; int formatl = formatCats.length; - if (paraml < formatl) { - // For assignments, format.missing.arguments is issued - // from commonAssignmentCheck. + if (argl < formatl) { + // For assignments, format.missing.arguments is issued from + // commonAssignmentCheck(). // II.1 - tu.failure(invc, "format.missing.arguments", formatl, paraml); + ftu.failure(invc, "format.missing.arguments", formatl, argl); } else { - if (paraml > formatl) { + if (argl > formatl) { // II.2 - tu.warning(invc, "format.excess.arguments", formatl, paraml); + ftu.warning(invc, "format.excess.arguments", formatl, argl); } for (int i = 0; i < formatl; ++i) { ConversionCategory formatCat = formatCats[i]; - Result param = paramTypes[i]; - TypeMirror paramType = param.value(); + Result arg = argTypes[i]; + TypeMirror argType = arg.value(); switch (formatCat) { case UNUSED: // I.2 - tu.warning(param, "format.argument.unused", " " + (1 + i)); + ftu.warning(arg, "format.argument.unused", " " + (1 + i)); break; case NULL: // I.3 - tu.failure(param, "format.specifier.null", " " + (1 + i)); + if (argType.getKind() == TypeKind.NULL) { + ftu.warning( + arg, "format.specifier.null", " " + (1 + i)); + } else { + ftu.failure( + arg, "format.specifier.null", " " + (1 + i)); + } break; case GENERAL: break; default: - if (!fc.isValidParameter(formatCat, paramType)) { + if (!fc.isValidArgument(formatCat, argType)) { // II.3 - tu.failure( - param, + ExecutableElement method = + TreeUtils.elementFromUse(tree); + CharSequence methodName = + ElementUtils.getSimpleDescription(method); + ftu.failure( + arg, "argument.type.incompatible", - paramType, + "in varargs position", + methodName, + argType, formatCat); } break; @@ -99,36 +138,58 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { } } break; - case NULLARRAY: - /* continue */ case ARRAY: + // III + if (!isWrappedFormatCall(fc, enclosingMethod)) { + ftu.warning(invc, "format.indirect.arguments"); + } + // TODO: If it is explict array construction, such as "new Object[] { + // ... }", then we could treat it like the VARARGS case, analyzing each + // argument. "new array" is probably rare, in the varargs position. + // fall through + case NULLARRAY: for (ConversionCategory cat : formatCats) { if (cat == ConversionCategory.NULL) { // I.3 - tu.failure(invc, "format.specifier.null", ""); + if (invc.value() == FormatterTreeUtil.InvocationType.NULLARRAY) { + ftu.warning(invc, "format.specifier.null", ""); + } else { + ftu.failure(invc, "format.specifier.null", ""); + } } if (cat == ConversionCategory.UNUSED) { // I.2 - tu.warning(invc, "format.argument.unused", ""); + ftu.warning(invc, "format.argument.unused", ""); } } - // III - tu.warning(invc, "format.indirect.arguments"); break; } } + + /* NO-AFU + // Support -Ainfer command-line argument. + WholeProgramInference wpi = atypeFactory.getWholeProgramInference(); + if (wpi != null && forwardsArguments(tree, enclosingMethod)) { + wpi.addMethodDeclarationAnnotation( + TreeUtils.elementFromDeclaration(enclosingMethod), + atypeFactory.FORMATMETHOD); + } + */ } - return super.visitMethodInvocation(node, p); + return super.visitMethodInvocation(tree, 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 + * @param enclosingMethod the method that contains the call + * @return true if {@code fc} is a call to a format method that forwards its containing method's + * arguments */ - private boolean isWrappedFormatCall(FormatCall fc) { - - MethodTree enclosingMethod = TreeUtils.enclosingMethod(atypeFactory.getPath(fc.node)); + private boolean isWrappedFormatCall(FormatCall fc, @Nullable MethodTree enclosingMethod) { if (enclosingMethod == null) { return false; } @@ -137,42 +198,95 @@ private boolean isWrappedFormatCall(FormatCall fc) { boolean withinFormatMethod = (atypeFactory.getDeclAnnotation(enclosingMethodElement, FormatMethod.class) != null); - if (!withinFormatMethod) { + return withinFormatMethod && forwardsArguments(fc.invocationTree, enclosingMethod); + } + + /** + * Returns true if {@code invocationTree}'s arguments are {@code enclosingMethod}'s formal + * parameters. In other words, {@code invocationTree} forwards {@code enclosingMethod}'s + * arguments. + * + *

    Only arguments from the first String formal parameter onward count. Returns false if there + * is no String formal parameter. + * + * @param invocationTree an invocation of a method + * @param enclosingMethod the method that contains the call + * @return true if {@code invocationTree} is a call to a method that forwards its containing + * method's arguments + */ + private boolean forwardsArguments( + MethodInvocationTree invocationTree, @Nullable MethodTree enclosingMethod) { + + if (enclosingMethod == null) { + return false; + } + + ExecutableElement enclosingMethodElement = + TreeUtils.elementFromDeclaration(enclosingMethod); + int paramIndex = formatStringIndex(enclosingMethodElement); + if (paramIndex == -1) { return false; } - List args = fc.node.getArguments(); + ExecutableElement calledMethodElement = TreeUtils.elementFromUse(invocationTree); + int callIndex = formatStringIndex(calledMethodElement); + if (callIndex == -1) { + throw new UserError( + "Method " + + calledMethodElement + + " is annotated @FormatMethod but has no String formal parameter"); + } + + List args = invocationTree.getArguments(); List params = enclosingMethod.getParameters(); - List paramElements = enclosingMethodElement.getParameters(); - // Strip off leading Locale arguments. - if (!args.isEmpty() && FormatterTreeUtil.isLocale(args.get(0), atypeFactory)) { - args = args.subList(1, args.size()); + if (params.size() - paramIndex != args.size() - callIndex) { + return false; } - if (!params.isEmpty() - && TypesUtils.isDeclaredOfName(paramElements.get(0).asType(), "java.util.Locale")) { - params = params.subList(1, params.size()); + while (paramIndex < params.size()) { + ExpressionTree argTree = args.get(callIndex); + if (argTree.getKind() != Tree.Kind.IDENTIFIER) { + return false; + } + VariableTree param = params.get(paramIndex); + if (param.getName() != ((IdentifierTree) argTree).getName()) { + return false; + } + paramIndex++; + callIndex++; } - if (args.size() == params.size()) { - for (int i = 0; i < args.size(); i++) { - ExpressionTree arg = args.get(i); - if (!(arg instanceof IdentifierTree - && ((IdentifierTree) arg).getName() == params.get(i).getName())) { - return false; - } + return true; + } + + // TODO: Should this be the last String argument? That would require that every method + // annotated with @FormatMethod uses varargs syntax. + /** + * Returns the index of the format string of a method: the first formal parameter with declared + * type String. + * + * @param m a method + * @return the index of the last String formal parameter, or -1 if none + */ + public static int formatStringIndex(ExecutableElement m) { + List params = m.getParameters(); + for (int i = 0; i < params.size(); i++) { + if (TypesUtils.isString(params.get(i).asType())) { + return i; } } - return true; + return -1; } @Override - protected void commonAssignmentCheck( + protected boolean commonAssignmentCheck( AnnotatedTypeMirror varType, AnnotatedTypeMirror valueType, Tree valueTree, - @CompilerMessageKey String errorKey) { - super.commonAssignmentCheck(varType, valueType, valueTree, errorKey); + @CompilerMessageKey String errorKey, + Object... extraArgs) { + boolean result = + super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs); AnnotationMirror rhs = valueType.getAnnotationInHierarchy(atypeFactory.UNKNOWNFORMAT); AnnotationMirror lhs = varType.getAnnotationInHierarchy(atypeFactory.UNKNOWNFORMAT); @@ -183,8 +297,8 @@ protected void commonAssignmentCheck( // For method calls, it is issued in visitMethodInvocation. if (rhs != null && lhs != null - && AnnotationUtils.areSameByName(rhs, atypeFactory.FORMAT) - && AnnotationUtils.areSameByName(lhs, atypeFactory.FORMAT)) { + && AnnotationUtils.areSameByName(rhs, FormatterAnnotatedTypeFactory.FORMAT_NAME) + && AnnotationUtils.areSameByName(lhs, FormatterAnnotatedTypeFactory.FORMAT_NAME)) { ConversionCategory[] rhsArgTypes = atypeFactory.treeUtil.formatAnnotationToCategories(rhs); ConversionCategory[] lhsArgTypes = @@ -196,7 +310,9 @@ protected void commonAssignmentCheck( "format.missing.arguments", varType.toString(), valueType.toString()); + result = false; } } + return result; } } diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/messages.properties b/checker/src/main/java/org/checkerframework/checker/formatter/messages.properties index 3ea3f5342026..318bfccf171c 100644 --- a/checker/src/main/java/org/checkerframework/checker/formatter/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/formatter/messages.properties @@ -1,3 +1,4 @@ +format.method.invalid=method %s is annotated @FormatMethod but has no String formal parameter format.string.invalid=invalid format string %s format.argument.unused=unused argument%s format.specifier.null=format specifier%s accepts only null as its argument 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 deleted file mode 100644 index 5c0871a6e025..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/formatter/qual/ConversionCategory.java +++ /dev/null @@ -1,285 +0,0 @@ -package org.checkerframework.checker.formatter.qual; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Date; -import java.util.HashSet; -import java.util.Set; -import org.checkerframework.dataflow.qual.Pure; - -/** - * Elements of this enumeration are used in a {@link Format Format} annotation to indicate the valid - * types that may be passed as a format parameter. For example: - * - *

    - * - *
    {@literal @}Format({ConversionCategory.GENERAL, ConversionCategory.INT})
    - * String f = "String '%s' has length %d";
    - * String.format(f, "Example", 7);
    - * - *
    - * - * The annotation indicates that the format string requires any Object as the first parameter - * ({@link ConversionCategory#GENERAL}) and an integer as the second parameter ({@link - * ConversionCategory#INT}). - * - * @see Format - * @checker_framework.manual #formatter-checker Format String Checker - */ -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"), - - /** - * Use if the parameter is of a basic types which represent Unicode characters: char, Character, - * byte, Byte, short, and Short. This conversion may also be applied to the types int and - * Integer when Character.isValidCodePoint(int) returns true. Applicable for conversions c, C. - */ - CHAR(new Class[] {Character.class, Byte.class, Short.class, Integer.class}, "cC"), - - /** - * Use if the parameter is an integral type: byte, Byte, short, Short, int and Integer, long, - * Long, and BigInteger. Applicable for conversions d, o, x, X. - */ - INT( - new Class[] {Byte.class, Short.class, Integer.class, Long.class, BigInteger.class}, - "doxX"), - - /** - * Use if the parameter is a floating-point type: float, Float, double, Double, and BigDecimal. - * Applicable for conversions e, E, f, g, G, a, A. - */ - FLOAT(new Class[] {Float.class, Double.class, BigDecimal.class}, "eEfgGaA"), - - /** - * Use if the parameter is a type which is capable of encoding a date or time: long, Long, - * Calendar, and Date. Applicable for conversions t, T. - */ - @SuppressWarnings("JdkObsolete") - TIME(new Class[] {Long.class, Calendar.class, Date.class}, "tT"), - - /** - * In a format string, multiple conversions may be applied to the same parameter. This is - * seldomly needed, but the following is an example of such use: - * - *
    -     *   format("Test %1$c %1$d", (int)42);
    -     * 
    - * - * In this example, the first parameter is interpreted as both a character and an int, therefore - * the parameter must be compatible with both conversion, and can therefore neither be char nor - * long. This intersection of conversions is called CHAR_AND_INT. - * - *

    One other conversion intersection is interesting, namely the intersection of INT and TIME, - * resulting in INT_AND_TIME. - * - *

    All other intersection either lead to an already existing type, or NULL, in which case it - * is illegal to pass object's of any type as parameter. - */ - CHAR_AND_INT(new Class[] {Byte.class, Short.class, Integer.class}, null), - - INT_AND_TIME(new Class[] {Long.class}, null), - - /** - * Use if no object of any type can be passed as parameter. In this case, the only legal value - * is null. This is seldomly needed, and indicates an error in most cases. For example: - * - *

    -     *   format("Test %1$f %1$d", null);
    -     * 
    - * - * Only null can be legally passed, passing a value such as 4 or 4.2 would lead to an exception. - */ - NULL(new Class[0], null), - - /** - * Use if a parameter is not used by the formatter. This is seldomly needed, and indicates an - * error in most cases. For example: - * - *
    -     *   format("Test %1$s %3$s", "a","unused","b");
    -     * 
    - * - * Only the first "a" and third "b" parameters are used, the second "unused" parameter is - * ignored. - */ - UNUSED(null /* everything */, null); - - /** Create a new conversion category. */ - ConversionCategory(Class[] types, String chars) { - this.types = types; - this.chars = chars; - } - - /** The format types. */ - @SuppressWarnings("ImmutableEnumChecker") // TODO: clean this up! - public final Class[] types; - - /** The format characters. */ - public final String chars; - - /** - * Use this function to get the category associated with a conversion character. For example: - * - *
    - * - *
    -     * ConversionCategory.fromConversionChar('d') == ConversionCategory.INT;
    -     * 
    - * - *
    - */ - public static ConversionCategory fromConversionChar(char c) { - for (ConversionCategory v : new ConversionCategory[] {GENERAL, CHAR, INT, FLOAT, TIME}) { - if (v.chars.contains(String.valueOf(c))) { - return v; - } - } - throw new IllegalArgumentException("Bad conversion character " + c); - } - - private static Set arrayToSet(E[] a) { - return new HashSet<>(Arrays.asList(a)); - } - - public static boolean isSubsetOf(ConversionCategory a, ConversionCategory b) { - return intersect(a, b) == a; - } - - /** - * Returns the intersection of two categories. This is seldomly needed. - * - *
    - * - *
    -     * ConversionCategory.intersect(INT, TIME) == INT_AND_TIME;
    -     * 
    - * - *
    - * - * @param a a category - * @param b a category - * @return the intersection of the two categories (their greatest lower bound) - */ - public static ConversionCategory intersect(ConversionCategory a, ConversionCategory b) { - if (a == UNUSED) { - return b; - } - if (b == UNUSED) { - return a; - } - if (a == GENERAL) { - return b; - } - if (b == GENERAL) { - return a; - } - - Set> as = arrayToSet(a.types); - 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); - if (vs.equals(as)) { - return v; - } - } - throw new RuntimeException(); - } - - /** - * Returns the union of two categories. This is seldomly needed. - * - *
    - * - *
    -     * ConversionCategory.union(INT, TIME) == GENERAL;
    -     * 
    - * - *
    - * - * @param a a category - * @param b a category - * @return the union of the two categories (their least upper bound) - */ - public static ConversionCategory union(ConversionCategory a, ConversionCategory b) { - if (a == UNUSED || b == UNUSED) { - return UNUSED; - } - if (a == GENERAL || b == GENERAL) { - return GENERAL; - } - if ((a == CHAR_AND_INT && b == INT_AND_TIME) || (a == INT_AND_TIME && b == CHAR_AND_INT)) { - // This is special-cased because the union of a.types and b.types - // does not include BigInteger.class, whereas the types for INT does. - // Returning INT here to prevent returning GENERAL below. - return INT; - } - - Set> as = arrayToSet(a.types); - 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); - if (vs.equals(as)) { - return v; - } - } - - return GENERAL; - } - - private String className(Class cls) { - if (cls == Boolean.class) { - return "boolean"; - } - if (cls == Character.class) { - return "char"; - } - if (cls == Byte.class) { - return "byte"; - } - if (cls == Short.class) { - return "short"; - } - if (cls == Integer.class) { - return "int"; - } - if (cls == Long.class) { - return "long"; - } - if (cls == Float.class) { - return "float"; - } - if (cls == Double.class) { - return "double"; - } - return cls.getSimpleName(); - } - - /** Returns a pretty printed {@link ConversionCategory}. */ - @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(", "); - } - sb.append(className(cls)); - first = false; - } - sb.append(")"); - return sb.toString(); - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/formatter/qual/FormatMethod.java b/checker/src/main/java/org/checkerframework/checker/formatter/qual/FormatMethod.java deleted file mode 100644 index 4d9254061b3a..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/formatter/qual/FormatMethod.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.checkerframework.checker.formatter.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; - -/** - * If this annotation is attached to a {@link java.util.Formatter#format(String, Object...) - * Formatter.format}-like method, then the first parameter, which must be of type String, is treated - * as a format string for the remaining arguments. The Format String Checker ensures that the - * arguments passed as varargs are compatible with the format string argument, and also permits them - * to be passed to {@link java.util.Formatter#format(String, Object...) Formatter.format}-like - * methods within the body. - * - *

    The initial String parameter may optionally be preceded by a Locale parameter. - * - * @checker_framework.manual #formatter-checker Format String Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) -public @interface FormatMethod {} diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/Effect.java b/checker/src/main/java/org/checkerframework/checker/guieffect/Effect.java index 629a6012e2d7..dd07607b67ae 100644 --- a/checker/src/main/java/org/checkerframework/checker/guieffect/Effect.java +++ b/checker/src/main/java/org/checkerframework/checker/guieffect/Effect.java @@ -1,6 +1,5 @@ package org.checkerframework.checker.guieffect; -import java.lang.annotation.Annotation; import org.checkerframework.checker.guieffect.qual.PolyUIEffect; import org.checkerframework.checker.guieffect.qual.SafeEffect; import org.checkerframework.checker.guieffect.qual.UIEffect; @@ -8,6 +7,8 @@ import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.dataflow.qual.SideEffectFree; +import java.lang.annotation.Annotation; + /** An effect -- either UIEffect, PolyUIEffect, or SafeEffect. */ public final class Effect { // Colin hates Java's comparable interface, so he's not using it @@ -47,9 +48,10 @@ public static Effect min(Effect l, Effect r) { } public static final class EffectRange { - public final Effect min, max; + public final Effect min; + public final Effect max; - public EffectRange(Effect min, Effect max) { + public EffectRange(@Nullable Effect min, @Nullable Effect max) { assert (min != null || max != null); // If one is null, fill in with the other this.min = (min != null ? min : max); @@ -80,6 +82,7 @@ public boolean isUI() { * * @return true if this is PolyUIEffect */ + @Pure public boolean isPoly() { return annotClass == PolyUIEffect.class; } @@ -106,11 +109,12 @@ public boolean equals(Effect e) { } @Override + @SuppressWarnings("interning:not.interned") // equality public boolean equals(@Nullable Object o) { if (o instanceof Effect) { return this.equals((Effect) o); } else { - return super.equals(o); + return this == o; } } diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectTypeFactory.java index d086350b3085..4e1ed86028fc 100644 --- a/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectTypeFactory.java @@ -8,16 +8,8 @@ import com.sun.source.tree.MethodTree; import com.sun.source.tree.ParenthesizedTree; import com.sun.source.tree.Tree; -import java.util.HashSet; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; + +import org.checkerframework.checker.guieffect.Effect.EffectRange; import org.checkerframework.checker.guieffect.qual.AlwaysSafe; import org.checkerframework.checker.guieffect.qual.PolyUI; import org.checkerframework.checker.guieffect.qual.PolyUIEffect; @@ -28,17 +20,32 @@ import org.checkerframework.checker.guieffect.qual.UIEffect; import org.checkerframework.checker.guieffect.qual.UIPackage; import org.checkerframework.checker.guieffect.qual.UIType; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; +import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.TypesUtils; +import java.util.HashSet; +import java.util.Map; +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.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + /** Annotated type factory for the GUI Effect Checker. */ public class GuiEffectTypeFactory extends BaseAnnotatedTypeFactory { @@ -67,6 +74,16 @@ public class GuiEffectTypeFactory extends BaseAnnotatedTypeFactory { */ protected final Set uiAnonClasses = new HashSet<>(); + /** The @{@link AlwaysSafe} annotation. */ + protected final AnnotationMirror ALWAYSSAFE = + AnnotationBuilder.fromClass(elements, AlwaysSafe.class); + + /** The @{@link PolyUI} annotation. */ + protected final AnnotationMirror POLYUI = AnnotationBuilder.fromClass(elements, PolyUI.class); + + /** The @{@link UI} annotation. */ + protected final AnnotationMirror UI = AnnotationBuilder.fromClass(elements, UI.class); + public GuiEffectTypeFactory(BaseTypeChecker checker, boolean spew) { // use true to enable flow inference, false to disable it super(checker, false); @@ -75,38 +92,12 @@ public GuiEffectTypeFactory(BaseTypeChecker checker, boolean spew) { this.postInit(); } - // Could move this to a public method on the checker class - public ExecutableElement findJavaOverride(ExecutableElement overrider, TypeMirror parentType) { - if (parentType.getKind() != TypeKind.NONE) { - if (debugSpew) { - System.err.println("Searching for overridden methods from " + parentType); - } - - TypeElement overriderClass = (TypeElement) overrider.getEnclosingElement(); - TypeElement elem = (TypeElement) ((DeclaredType) parentType).asElement(); - if (debugSpew) { - System.err.println("necessary TypeElements acquired: " + elem); - } - - for (Element e : elem.getEnclosedElements()) { - if (debugSpew) { - System.err.println("Considering element " + e); - } - if (e.getKind() == ElementKind.METHOD || e.getKind() == ElementKind.CONSTRUCTOR) { - ExecutableElement ex = (ExecutableElement) e; - boolean overrides = elements.overrides(overrider, ex, overriderClass); - if (overrides) { - return ex; - } - } - } - if (debugSpew) { - System.err.println("Done considering elements of " + parentType); - } - } - return null; - } - + /** + * Returns true if the given type is polymorphic. + * + * @param cls the type to test + * @return true if the given type is polymorphic + */ public boolean isPolymorphicType(TypeElement cls) { assert (cls != null); return getDeclAnnotation(cls, PolyUIType.class) != null @@ -132,8 +123,7 @@ public boolean isUIType(TypeElement cls) { } // Anon inner classes should not inherit the package annotation, since - // they're so often used for closures to run async on background - // threads. + // they're so often used for closures to run async on background threads. if (isAnonymousType(cls)) { // However, we need to look into Anonymous class effect inference if (uiAnonClasses.contains(cls)) { @@ -250,10 +240,9 @@ public Effect getDeclaredEffect(ExecutableElement methodElt) { return new Effect(UIEffect.class); } - // Anonymous inner types should just get the effect of the parent by - // default, rather than annotating every instance. Unless it's - // implementing a polymorphic supertype, in which case we still want the - // developer to be explicit. + // Anonymous inner types should just get the effect of the parent by default, rather than + // annotating every instance. Unless it's implementing a polymorphic supertype, in which + // case we still want the developer to be explicit. if (isAnonymousType(targetClassElt)) { boolean canInheritParentEffects = true; // Refine this for polymorphic parents DeclaredType directSuper = (DeclaredType) targetClassElt.getSuperclass(); @@ -273,7 +262,7 @@ public Effect getDeclaredEffect(ExecutableElement methodElt) { } if (canInheritParentEffects) { - Effect.EffectRange r = findInheritedEffectRange(targetClassElt, methodElt); + EffectRange r = findInheritedEffectRange(targetClassElt, methodElt); return (r != null ? Effect.min(r.min, r.max) : new Effect(SafeEffect.class)); } } @@ -285,23 +274,23 @@ public Effect getDeclaredEffect(ExecutableElement methodElt) { * Get the effect of a method call at its callsite, acknowledging polymorphic instantiation * using type use annotations. * - * @param node the method invocation as an AST node + * @param tree the method invocation as an AST node * @param callerReceiver the type of the receiver object if available. Used to resolve direct * calls like "super()" * @param methodElt the element of the callee method * @return the computed effect (SafeEffect or UIEffect) for the method call */ public Effect getComputedEffectAtCallsite( - MethodInvocationTree node, + MethodInvocationTree tree, AnnotatedTypeMirror.AnnotatedDeclaredType callerReceiver, ExecutableElement methodElt) { Effect targetEffect = getDeclaredEffect(methodElt); if (targetEffect.isPoly()) { AnnotatedTypeMirror srcType = null; - if (node.getMethodSelect().getKind() == Tree.Kind.MEMBER_SELECT) { - ExpressionTree src = ((MemberSelectTree) node.getMethodSelect()).getExpression(); + if (tree.getMethodSelect().getKind() == Tree.Kind.MEMBER_SELECT) { + ExpressionTree src = ((MemberSelectTree) tree.getMethodSelect()).getExpression(); srcType = getAnnotatedType(src); - } else if (node.getMethodSelect().getKind() == Tree.Kind.IDENTIFIER) { + } else if (tree.getMethodSelect().getKind() == Tree.Kind.IDENTIFIER) { // Tree.Kind.IDENTIFIER, e.g. a direct call like "super()" if (callerReceiver == null) { // Not enought information provided to instantiate this type-polymorphic effects @@ -309,7 +298,7 @@ public Effect getComputedEffectAtCallsite( } srcType = callerReceiver; } else { - throw new BugInCF("Unexpected getMethodSelect() kind at callsite " + node); + throw new TypeSystemError("Unexpected getMethodSelect() kind at callsite " + tree); } // Instantiate type-polymorphic effects @@ -340,8 +329,7 @@ public Effect getInferedEffectForLambdaExpression(LambdaExpressionTree lambdaTre return new Effect(UIEffect.class); } ExecutableElement functionalInterfaceMethodElt = - (ExecutableElement) - TreeUtils.findFunction(lambdaTree, checker.getProcessingEnvironment()); + TreeUtils.findFunction(lambdaTree, checker.getProcessingEnvironment()); if (debugSpew) { System.err.println("functionalInterfaceMethodElt found for lambda"); } @@ -404,174 +392,144 @@ public AnnotatedTypeMirror getAnnotatedType(Tree tree) { } // Only the visitMethod call should pass true for warnings - public Effect.EffectRange findInheritedEffectRange( + public EffectRange findInheritedEffectRange( TypeElement declaringType, ExecutableElement overridingMethod) { return findInheritedEffectRange(declaringType, overridingMethod, false, null); } - public Effect.EffectRange findInheritedEffectRange( + /** + * Find the greatest and least effects of methods the specified definition overrides. This + * method is used for two reasons: + * + *

    1. {@link GuiEffectVisitor#visitMethod(MethodTree,Void) GuiEffectVisitor.visitMethod} + * calls this to perform an effect override check (that a method's effect is less than or equal + * to the effect of any method it overrides). This use passes {@code true} for the {@code + * issueConflictWarning} in order to trigger warning messages. + * + *

    2. {@link #getDeclaredEffect(ExecutableElement) getDeclaredEffect} in this class uses this + * to infer the default effect of methods in anonymous inner classes. This use passes {@code + * false} for {@code issueConflictWarning}, because it only needs the return value. + * + * @param declaringType the type declaring the override + * @param overridingMethod the method override itself + * @param issueConflictWarning whether or not to issue warnings + * @param errorTree the method declaration AST node; used for reporting errors + * @return the min and max inherited effects, or null if none were discovered + */ + public @Nullable EffectRange findInheritedEffectRange( TypeElement declaringType, ExecutableElement overridingMethod, boolean issueConflictWarning, - Tree errorNode) { + Tree errorTree) { assert (declaringType != null); - ExecutableElement uiOverride = null; - ExecutableElement safeOverride = null; - ExecutableElement polyOverride = null; + ExecutableElement uiOverridden = null; + ExecutableElement safeOverridden = null; + ExecutableElement polyOverridden = null; // We must account for explicit annotation, type declaration annotations, and package - // annotations + // annotations. boolean isUI = (getDeclAnnotation(overridingMethod, UIEffect.class) != null || isUIType(declaringType)) && getDeclAnnotation(overridingMethod, SafeEffect.class) == null; boolean isPolyUI = getDeclAnnotation(overridingMethod, PolyUIEffect.class) != null; - // TODO: We must account for @UI and @AlwaysSafe annotations for extends - // and implements clauses, and do the proper substitution of @Poly effects and quals! - // List interfaces = declaringType.getInterfaces(); - TypeMirror superclass = declaringType.getSuperclass(); - while (superclass != null && superclass.getKind() != TypeKind.NONE) { - ExecutableElement overrides = findJavaOverride(overridingMethod, superclass); - if (overrides != null) { - Effect eff = getDeclaredEffect(overrides); - assert (eff != null); - if (eff.isSafe()) { - // found a safe override - safeOverride = overrides; - if (isUI && issueConflictWarning) { - checker.reportError( - errorNode, - "override.effect.invalid", - overridingMethod, - declaringType, - safeOverride, - superclass); - } - if (isPolyUI && issueConflictWarning) { - checker.reportError( - errorNode, - "override.effect.invalid.polymorphic", - overridingMethod, - declaringType, - safeOverride, - superclass); - } - } else if (eff.isUI()) { - // found a ui override - uiOverride = overrides; - } else { - assert (eff.isPoly()); - polyOverride = overrides; - // TODO: Is this right? is the supertype covered by the - // directSuperTypes() method all I need? Or should I be - // using that utility method that returns a set of - // annodecl-method pairs given a method that overrides stuff - // if (isUI && issueConflictWarning) { - // AnnotatedTypeMirror.AnnotatedDeclaredType supdecl = - // fromElement((TypeElement)(((DeclaredType)superclass).asElement()));//((DeclaredType)superclass).asElement()); - // // Need to special case an anonymous class with @UI on the decl, because - // // "new @UI Runnable {...}" parses as @UI on an anon class decl extending - // // Runnable. - // boolean isAnonInstantiation = - // TypesUtils.isAnonymousType(ElementUtils.getType(declaringType)) && - // getDeclAnnotation(declaringType, UI.class) != null; - // if (!isAnonInstantiation && !hasAnnotationByName(supdecl, UI.class)) { - // checker.reportError(errorNode, "override.effect.invalid", - // "non-UI instantiation of "+supdecl); - // If uncommenting this, change the above line to match other calls of - // checker.reportError(..., "override.effect.invalid", ...) - // } - // } - } + // Check for invalid overrides. + // AnnotatedTypes.overriddenMethods retrieves all transitive definitions overridden by this + // declaration. + Map overriddenMethods = + AnnotatedTypes.overriddenMethods(elements, this, overridingMethod); + + for (Map.Entry pair : + overriddenMethods.entrySet()) { + AnnotatedTypeMirror.AnnotatedDeclaredType overriddenType = pair.getKey(); + AnnotatedTypeMirror.AnnotatedExecutableType overriddenMethod = + AnnotatedTypes.asMemberOf(types, this, overriddenType, pair.getValue()); + ExecutableElement overriddenMethodElt = pair.getValue(); + if (debugSpew) { + System.err.println( + "Found " + + declaringType + + "::" + + overridingMethod + + " overrides " + + overriddenType + + "::" + + overriddenMethod); } - DeclaredType decl = (DeclaredType) superclass; - superclass = ((TypeElement) decl.asElement()).getSuperclass(); - } - - AnnotatedTypeMirror.AnnotatedDeclaredType annoDecl = fromElement(declaringType); - for (AnnotatedTypeMirror.AnnotatedDeclaredType ty : annoDecl.directSuperTypes()) { - ExecutableElement overrides = - findJavaOverride(overridingMethod, ty.getUnderlyingType()); - if (overrides != null) { - Effect eff = getDeclaredEffect(overrides); - if (eff.isSafe()) { - // found a safe override - safeOverride = overrides; - if (isUI && issueConflictWarning) { + Effect eff = getDeclaredEffect(overriddenMethodElt); + if (eff.isSafe()) { + safeOverridden = overriddenMethodElt; + if (isUI) { + checker.reportError( + errorTree, + "override.effect.invalid", + declaringType, + overridingMethod, + overriddenType, + safeOverridden); + } else if (isPolyUI) { + checker.reportError( + errorTree, + "override.effect.invalid.polymorphic", + declaringType, + overridingMethod, + overriddenType, + safeOverridden); + } + } else if (eff.isUI()) { + uiOverridden = overriddenMethodElt; + } else { + assert eff.isPoly(); + polyOverridden = overriddenMethodElt; + if (isUI) { + // Need to special case an anonymous class with @UI on the decl, because + // "new @UI Runnable {...}" + // parses as @UI on an anon class decl extending Runnable + boolean isAnonInstantiation = + isAnonymousType(declaringType) + && (fromElement(declaringType).hasAnnotation(UI.class) + || uiAnonClasses.contains(declaringType)); + if (!isAnonInstantiation && !overriddenType.hasAnnotation(UI.class)) { checker.reportError( - errorNode, - "override.effect.invalid", - overridingMethod, + errorTree, + "override.effect.invalid.nonui", declaringType, - safeOverride, - ty); - } - if (isPolyUI && issueConflictWarning) { - checker.reportError( - errorNode, - "override.effect.invalid.polymorphic", overridingMethod, - declaringType, - safeOverride, - ty); - } - } else if (eff.isUI()) { - // found a ui override - uiOverride = overrides; - } else { - assert (eff.isPoly()); - polyOverride = overrides; - if (isUI && issueConflictWarning) { - AnnotatedTypeMirror.AnnotatedDeclaredType supdecl = ty; - // Need to special case an anonymous class with @UI on - // the decl, because "new @UI Runnable {...}" parses as - // @UI on an anon class decl extending Runnable - boolean isAnonInstantiation = - isAnonymousType(declaringType) - && (fromElement(declaringType).hasAnnotation(UI.class) - || uiAnonClasses.contains(declaringType)); - if (!isAnonInstantiation && !supdecl.hasAnnotation(UI.class)) { - checker.reportError( - errorNode, - "override.effect.invalid.nonui", - overridingMethod, - declaringType, - polyOverride, - supdecl); - } + overriddenType, + polyOverridden); } } } } - // We don't need to issue warnings for inheriting from poly and a concrete effect. - if (uiOverride != null && safeOverride != null && issueConflictWarning) { + // We don't need to issue warnings for overriding both poly and a concrete effect. + if (uiOverridden != null && safeOverridden != null && issueConflictWarning) { // There may be more than two parent methods, but for now it's - // enough to know there are at least 2 in conflict + // enough to know there are at least 2 in conflict. checker.reportWarning( - errorNode, + errorTree, "override.effect.warning.inheritance", - overridingMethod, declaringType, - uiOverride.toString(), - uiOverride.getEnclosingElement().asType().toString(), - safeOverride.toString(), - safeOverride.getEnclosingElement().asType().toString()); + overridingMethod, + uiOverridden.getEnclosingElement().asType(), + uiOverridden, + safeOverridden.getEnclosingElement().asType(), + safeOverridden); } Effect min = - (safeOverride != null + (safeOverridden != null ? new Effect(SafeEffect.class) - : (polyOverride != null + : (polyOverridden != null ? new Effect(PolyUIEffect.class) - : (uiOverride != null ? new Effect(UIEffect.class) : null))); + : (uiOverridden != null ? new Effect(UIEffect.class) : null))); Effect max = - (uiOverride != null + (uiOverridden != null ? new Effect(UIEffect.class) - : (polyOverride != null + : (polyOverridden != null ? new Effect(PolyUIEffect.class) - : (safeOverride != null ? new Effect(SafeEffect.class) : null))); + : (safeOverridden != null ? new Effect(SafeEffect.class) : null))); if (debugSpew) { System.err.println( "Found " @@ -588,12 +546,12 @@ public Effect.EffectRange findInheritedEffectRange( if (min == null && max == null) { return null; } else { - return new Effect.EffectRange(min, max); + return new EffectRange(min, max); } } @Override - protected Set getDefaultTypeDeclarationBounds() { + protected AnnotationMirrorSet getDefaultTypeDeclarationBounds() { return qualHierarchy.getBottomAnnotations(); } @@ -657,7 +615,7 @@ public boolean hasExplicitEffect(ExecutableElement methElt) { */ @Override - public Void visitMethod(MethodTree node, AnnotatedTypeMirror type) { + public Void visitMethod(MethodTree tree, AnnotatedTypeMirror type) { AnnotatedTypeMirror.AnnotatedExecutableType methType = (AnnotatedTypeMirror.AnnotatedExecutableType) type; // Effect e = getDeclaredEffect(methType.getElement()); @@ -673,17 +631,13 @@ public Void visitMethod(MethodTree node, AnnotatedTypeMirror type) { // STEP 2: Fix up the method receiver annotation AnnotatedTypeMirror.AnnotatedDeclaredType receiverType = methType.getReceiverType(); - if (receiverType != null - && !receiverType.isAnnotatedInHierarchy( - AnnotationBuilder.fromClass(elements, UI.class))) { + if (receiverType != null && !receiverType.hasAnnotationInHierarchy(UI)) { receiverType.addAnnotation( isPolymorphicType(cls) - ? PolyUI.class - : fromElement(cls).hasAnnotation(UI.class) - ? UI.class - : AlwaysSafe.class); + ? POLYUI + : fromElement(cls).hasAnnotation(UI.class) ? UI : ALWAYSSAFE); } - return super.visitMethod(node, type); + return super.visitMethod(tree, type); } } } 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 0da2b7468520..36566bfd8966 100644 --- a/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/guieffect/GuiEffectVisitor.java @@ -11,15 +11,7 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; -import java.util.ArrayDeque; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.type.DeclaredType; + import org.checkerframework.checker.guieffect.qual.AlwaysSafe; import org.checkerframework.checker.guieffect.qual.PolyUI; import org.checkerframework.checker.guieffect.qual.PolyUIEffect; @@ -27,20 +19,40 @@ import org.checkerframework.checker.guieffect.qual.SafeEffect; import org.checkerframework.checker.guieffect.qual.UI; import org.checkerframework.checker.guieffect.qual.UIEffect; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeFactory.ParameterizedExecutableType; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.framework.util.AnnotatedTypes; import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.Pair; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; +import java.util.ArrayDeque; +import java.util.List; +import java.util.Map; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; + /** Require that only UI code invokes code with the UI effect. */ public class GuiEffectVisitor extends BaseTypeVisitor { + /** The type of the class currently being visited. */ + private @Nullable AnnotatedDeclaredType classType = null; + + /** The receiver type of the enclosing method tree. */ + private @Nullable AnnotatedDeclaredType receiverType = null; + /** Whether or not to display debugging information. */ protected final boolean debugSpew; // effStack and currentMethods should always be the same size. @@ -69,7 +81,7 @@ protected GuiEffectTypeFactory createTypeFactory() { // for any UI instantiations, safe otherwise @Override protected void checkMethodInvocability( - AnnotatedExecutableType method, MethodInvocationTree node) { + AnnotatedExecutableType method, MethodInvocationTree tree) { // The inherited version of this complains about invoking methods of @UI instantiations of // classes, which by default are annotated @AlwaysSafe, which for data type qualifiers is // reasonable, but it not what we want, since we want . @@ -126,24 +138,36 @@ protected boolean checkReceiverOverride() { checker.reportError( overriderTree, "override.receiver.invalid", - overriderMeth, - overriderTyp, - overriddenMeth, - overriddenTyp, overrider.getReceiverType(), - overridden.getReceiverType()); + overridden.getReceiverType(), + overriderType, + overrider, + overriddenType, + overridden); return false; } return true; } + /** + * Create a GuiEffectOverrideChecker. + * + * @param overriderTree the AST node of the overriding method or method reference + * @param overrider the type of the overriding method + * @param overridingType the type enclosing the overrider method, usually an + * AnnotatedDeclaredType; for Method References may be something else + * @param overridingReturnType the return type of the overriding method + * @param overridden the type of the overridden method + * @param overriddenType the declared type enclosing the overridden method + * @param overriddenReturnType the return type of the overridden method + */ public GuiEffectOverrideChecker( Tree overriderTree, - AnnotatedTypeMirror.AnnotatedExecutableType overrider, + AnnotatedExecutableType overrider, AnnotatedTypeMirror overridingType, AnnotatedTypeMirror overridingReturnType, AnnotatedExecutableType overridden, - AnnotatedTypeMirror.AnnotatedDeclaredType overriddenType, + AnnotatedDeclaredType overriddenType, AnnotatedTypeMirror overriddenReturnType) { super( overriderTree, @@ -176,8 +200,8 @@ protected OverrideChecker createOverrideChecker( } @Override - protected Set getExceptionParameterLowerBoundAnnotations() { - return Collections.singleton(AnnotationBuilder.fromClass(elements, AlwaysSafe.class)); + protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { + return new AnnotationMirrorSet(AnnotationBuilder.fromClass(elements, AlwaysSafe.class)); } @Override @@ -210,14 +234,15 @@ public boolean isValidUse( } @Override - public Void visitLambdaExpression(LambdaExpressionTree node, Void p) { - Void v = super.visitLambdaExpression(node, p); + @SuppressWarnings("interning:not.interned") // comparing AST nodes + public Void visitLambdaExpression(LambdaExpressionTree tree, Void p) { + Void v = super.visitLambdaExpression(tree, p); // If this is a lambda inferred to be @UI, scan up the path and re-check any assignments // involving it. - if (atypeFactory.isDirectlyMarkedUIThroughInference(node)) { + if (atypeFactory.isDirectlyMarkedUIThroughInference(tree)) { // Backtrack path to the lambda expression itself - TreePath path = visitorState.getPath(); - while (path.getLeaf() != node) { + TreePath path = getCurrentPath(); + while (path.getLeaf() != tree) { assert path.getLeaf().getKind() != Tree.Kind.COMPILATION_UNIT; path = path.getParentPath(); } @@ -227,7 +252,7 @@ public Void visitLambdaExpression(LambdaExpressionTree node, Void p) { } @Override - protected void checkExtendsImplements(ClassTree classTree) { + protected void checkExtendsAndImplements(ClassTree classTree) { // Skip this check } @@ -237,34 +262,38 @@ protected void checkConstructorResult( // Skip this check. } + @Override + protected void checkForPolymorphicQualifiers(ClassTree classTree) { + // Polymorphic qualifiers are legal on classes, so skip this check. + } + // Check that the invoked effect is <= permitted effect (effStack.peek()) @Override - public Void visitMethodInvocation(MethodInvocationTree node, Void p) { + public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { if (debugSpew) { - System.err.println("For invocation " + node + " in " + currentMethods.peek().getName()); + System.err.println("For invocation " + tree + " in " + currentMethods.peek().getName()); } // Target method annotations - ExecutableElement methodElt = TreeUtils.elementFromUse(node); + ExecutableElement methodElt = TreeUtils.elementFromUse(tree); if (debugSpew) { System.err.println("methodElt found"); } - Tree callerTree = TreeUtils.enclosingMethodOrLambda(getCurrentPath()); + Tree callerTree = TreePathUtil.enclosingMethodOrLambda(getCurrentPath()); if (callerTree == null) { // Static initializer; let's assume this is safe to have the UI effect if (debugSpew) { System.err.println("No enclosing method: likely static initializer"); } - return super.visitMethodInvocation(node, p); + return super.visitMethodInvocation(tree, p); } if (debugSpew) { System.err.println("callerTree found: " + callerTree.getKind()); } Effect targetEffect = - atypeFactory.getComputedEffectAtCallsite( - node, visitorState.getMethodReceiver(), methodElt); + atypeFactory.getComputedEffectAtCallsite(tree, receiverType, methodElt); Effect callerEffect = null; if (callerTree.getKind() == Tree.Kind.METHOD) { @@ -274,10 +303,9 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { } callerEffect = atypeFactory.getDeclaredEffect(callerElt); - final DeclaredType callerReceiverType = - this.visitorState.getClassType().getUnderlyingType(); + DeclaredType callerReceiverType = classType.getUnderlyingType(); assert callerReceiverType != null; - final TypeElement callerReceiverElt = (TypeElement) callerReceiverType.asElement(); + TypeElement callerReceiverElt = (TypeElement) callerReceiverType.asElement(); // Note: All these checks should be fast in the common case, but happen for every method // call inside the anonymous class. Consider a cache here if profiling surfaces this as // taking too long. @@ -339,20 +367,24 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { assert callerEffect != null; if (!Effect.lessThanOrEqualTo(targetEffect, callerEffect)) { - checker.reportError(node, "call.invalid.ui", targetEffect, callerEffect); + checker.reportError(tree, "call.invalid.ui", targetEffect, callerEffect); if (debugSpew) { - System.err.println("Issuing error for node: " + node); + System.err.println("Issuing error for tree: " + tree); } } if (debugSpew) { System.err.println( - "Successfully finished main non-recursive checkinv of invocation " + node); + "Successfully finished main non-recursive checkinv of invocation " + tree); } - return super.visitMethodInvocation(node, p); + return super.visitMethodInvocation(tree, p); } @Override - public Void visitMethod(MethodTree node, Void p) { + public Void visitMethod(MethodTree tree, Void p) { + AnnotatedExecutableType methodType = atypeFactory.getAnnotatedType(tree).deepCopy(); + AnnotatedDeclaredType previousReceiverType = receiverType; + receiverType = methodType.getReceiverType(); + // TODO: If the type we're in is a polymorphic (over effect qualifiers) type, the receiver // must be @PolyUI. Otherwise a "non-polymorphic" method of a polymorphic type could be // called on a UI instance, which then gets a Safe reference to itself (unsound!) that it @@ -363,18 +395,17 @@ public Void visitMethod(MethodTree node, Void p) { // subclasses a Safe instantiation, all is well. If it subclasses a UI instantiation, then // the receivers should probably be @UI in both new and override methods, so calls to // polymorphic methods of the parent class will work correctly. In which case for proving - // anything, the qualifier on sublasses of UI instantiations would always have to be - // @UI... Need to write down |- t for this system! And the judgments for method overrides - // and inheritance! Those are actually the hardest part of the system. + // anything, the qualifier on sublasses of UI instantiations would always have to be @UI... + // Need to write down |- t for this system! And the judgments for method overrides and + // inheritance! Those are actually the hardest part of the system. - ExecutableElement methElt = TreeUtils.elementFromDeclaration(node); + ExecutableElement methElt = TreeUtils.elementFromDeclaration(tree); if (debugSpew) { - System.err.println(); - System.err.println("Visiting method " + methElt); + System.err.println( + "Visiting method " + methElt + " of " + methElt.getEnclosingElement()); } // Check for conflicting (multiple) annotations - assert (methElt != null); // TypeMirror scratch = methElt.getReturnType(); AnnotationMirror targetUIP = atypeFactory.getDeclAnnotation(methElt, UIEffect.class); AnnotationMirror targetSafeP = atypeFactory.getDeclAnnotation(methElt, SafeEffect.class); @@ -383,13 +414,13 @@ public Void visitMethod(MethodTree node, Void p) { if ((targetUIP != null && (targetSafeP != null || targetPolyP != null)) || (targetSafeP != null && targetPolyP != null)) { - checker.reportError(node, "annotations.conflicts"); + checker.reportError(tree, "annotations.conflicts"); } if (targetPolyP != null && !atypeFactory.isPolymorphicType(targetClassElt)) { - checker.reportError(node, "polymorphism.invalid"); + checker.reportError(tree, "polymorphism.invalid"); } if (targetUIP != null && atypeFactory.isUIType(targetClassElt)) { - checker.reportWarning(node, "effects.redundant.uitype"); + checker.reportWarning(tree, "effects.redundant.uitype"); } // TODO: Report an error for polymorphic method bodies??? Until we fix the receiver @@ -397,17 +428,16 @@ public Void visitMethod(MethodTree node, Void p) { @SuppressWarnings("unused") // call has side effects Effect.EffectRange range = atypeFactory.findInheritedEffectRange( - ((TypeElement) methElt.getEnclosingElement()), methElt, true, node); + ((TypeElement) methElt.getEnclosingElement()), methElt, true, tree); // if (targetUIP == null && targetSafeP == null && targetPolyP == null) { - // implicitly annotate this method with the LUB of the effects of the methods it - // overrides + // implicitly annotate this method with the LUB of the effects of the methods it overrides // atypeFactory.fromElement(methElt).addAnnotation(range != null ? range.min.getAnnot() // : (isUIType(((TypeElement)methElt.getEnclosingElement())) ? UI.class : // AlwaysSafe.class)); // TODO: This line does nothing! AnnotatedTypeMirror.addAnnotation // silently ignores non-qualifier annotations! // System.err.println("ERROR: TREE ANNOTATOR SHOULD HAVE ADDED EXPLICIT ANNOTATION! (" - // +node.getName()+")"); + // +tree.getName()+")"); // atypeFactory // .fromElement(methElt) // .addAnnotation(atypeFactory.getDeclaredEffect(methElt).getAnnot()); @@ -415,7 +445,7 @@ public Void visitMethod(MethodTree node, Void p) { // We hang onto the current method here for ease. We back up the old // current method because this code is reentrant when we traverse methods of an inner class - currentMethods.addFirst(node); + currentMethods.addFirst(tree); // effStack.push(targetSafeP != null ? new Effect(AlwaysSafe.class) : // (targetPolyP != null ? new Effect(PolyUI.class) : // (targetUIP != null ? new Effect(UI.class) : @@ -428,36 +458,40 @@ public Void visitMethod(MethodTree node, Void p) { "Pushing " + effStack.peek() + " onto the stack when checking " + methElt); } - Void ret = super.visitMethod(node, p); + Void ret = super.visitMethod(tree, p); currentMethods.removeFirst(); effStack.removeFirst(); + receiverType = previousReceiverType; return ret; } @Override - public Void visitNewClass(NewClassTree node, Void p) { - Void v = super.visitNewClass(node, p); + @SuppressWarnings("interning:not.interned") // comparing AST nodes + public Void visitNewClass(NewClassTree tree, Void p) { + Void v = super.visitNewClass(tree, p); // If this is an anonymous inner class inferred to be @UI, scan up the path and re-check any // assignments involving it. - if (atypeFactory.isDirectlyMarkedUIThroughInference(node)) { + if (atypeFactory.isDirectlyMarkedUIThroughInference(tree)) { // Backtrack path to the new class expression itself - TreePath path = visitorState.getPath(); - while (path.getLeaf() != node) { + TreePath path = getCurrentPath(); + while (path.getLeaf() != tree) { assert path.getLeaf().getKind() != Tree.Kind.COMPILATION_UNIT; path = path.getParentPath(); } - scanUp(visitorState.getPath().getParentPath()); + scanUp(getCurrentPath().getParentPath()); } return v; } /** * 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,17 +517,20 @@ private void scanUp(TreePath path) { List args = invocationTree.getArguments(); ParameterizedExecutableType mType = atypeFactory.methodFromUse(invocationTree); AnnotatedExecutableType invokedMethod = mType.executableType; - List argsTypes = - AnnotatedTypes.expandVarArgs( - atypeFactory, invokedMethod, invocationTree.getArguments()); + ExecutableElement method = invokedMethod.getElement(); + CharSequence methodName = ElementUtils.getSimpleDescription(method); + List methodParams = method.getParameters(); + List paramTypes = invokedMethod.getParameterTypes(); for (int i = 0; i < args.size(); ++i) { if (args.get(i).getKind() == Tree.Kind.NEW_CLASS || args.get(i).getKind() == Tree.Kind.LAMBDA_EXPRESSION) { commonAssignmentCheck( - argsTypes.get(i), + paramTypes.get(i), atypeFactory.getAnnotatedType(args.get(i)), args.get(i), - "argument.type.incompatible"); + "argument.type.incompatible", + methodParams.get(i), + methodName); } } break; @@ -501,7 +538,7 @@ private void scanUp(TreePath path) { ReturnTree returnTree = (ReturnTree) tree; if (returnTree.getExpression().getKind() == Tree.Kind.NEW_CLASS || returnTree.getExpression().getKind() == Tree.Kind.LAMBDA_EXPRESSION) { - Tree enclosing = TreeUtils.enclosingMethodOrLambda(path); + Tree enclosing = TreePathUtil.enclosingMethodOrLambda(path); AnnotatedTypeMirror ret = null; if (enclosing.getKind() == Tree.Kind.METHOD) { MethodTree enclosingMethod = (MethodTree) enclosing; @@ -517,18 +554,11 @@ private void scanUp(TreePath path) { } if (ret != null) { - Pair preAssCtxt = - visitorState.getAssignmentContext(); - try { - visitorState.setAssignmentContext(Pair.of((Tree) returnTree, ret)); - commonAssignmentCheck( - ret, - atypeFactory.getAnnotatedType(returnTree.getExpression()), - returnTree.getExpression(), - "return.type.incompatible"); - } finally { - visitorState.setAssignmentContext(preAssCtxt); - } + commonAssignmentCheck( + ret, + atypeFactory.getAnnotatedType(returnTree.getExpression()), + returnTree.getExpression(), + "return.type.incompatible"); } } break; @@ -547,26 +577,40 @@ private void scanUp(TreePath path) { } // @Override - // public Void visitMemberSelect(MemberSelectTree node, Void p) { + // public Void visitMemberSelect(MemberSelectTree tree, Void p) { // TODO: Same effect checks as for methods - // return super.visitMemberSelect(node, p); + // return super.visitMemberSelect(tree, p); // } // @Override - // public void processClassTree(ClassTree node) { + // public void processClassTree(ClassTree tree) { // TODO: Check constraints on this class decl vs. parent class decl., and interfaces // TODO: This has to wait for now: maybe this will be easier with the isValidUse on the // TypeFactory. - // AnnotatedTypeMirror.AnnotatedDeclaredType atype = atypeFactory.fromClass(node); + // AnnotatedTypeMirror.AnnotatedDeclaredType atype = atypeFactory.fromClass(tree); // Push a null method and UI effect onto the stack for static field initialization // TODO: Figure out if this is safe! For static data, almost certainly, // but for statically initialized instance fields, I'm assuming those - // are implicitly moved into each constructor, which must then be @UI + // are implicitly moved into each constructor, which must then be @UI. // currentMethods.addFirst(null); // effStack.addFirst(new Effect(UIEffect.class)); - // super.processClassTree(node); + // super.processClassTree(tree); // currentMethods.removeFirst(); // effStack.removeFirst(); // } + + @Override + public void processClassTree(ClassTree classTree) { + AnnotatedDeclaredType previousClassType = classType; + AnnotatedDeclaredType previousReceiverType = receiverType; + receiverType = null; + classType = atypeFactory.getAnnotatedType(TreeUtils.elementFromDeclaration(classTree)); + try { + super.processClassTree(classTree); + } finally { + classType = previousClassType; + receiverType = previousReceiverType; + } + } } diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/messages.properties b/checker/src/main/java/org/checkerframework/checker/guieffect/messages.properties index a5ebeed25227..adf9817149d5 100644 --- a/checker/src/main/java/org/checkerframework/checker/guieffect/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/guieffect/messages.properties @@ -1,9 +1,9 @@ call.invalid.ui=Calling a method with %s effect from a context limited to %s effects. annotations.conflicts=A method may only have one effect annotation from @UI, @AlwaysSafe, and @PolyUI. -override.effect.invalid=A method override may only be @UI if it overrides an @UI method.%n %s in %s%n cannot override%n %s in %s -override.effect.invalid.polymorphic=A method override may only be @PolyUIEffect if it overrides a @PolyUIEffect method.%n %s in %s%n cannot override%n %s in %s -override.effect.invalid.nonui=A method override may only be @UI if it overrides an @UI method (overriding non-UI instantiation of supertype).%n %s in %s%n cannot override%n %s in %s -override.effect.warning.inheritance=%s in %s overrides a method with @UI effect (%s in %s) and another method with an @AlwaysSafe effect (%s in %s). This is discouraged. +override.effect.invalid=A method override may only be @UI if it overrides an @UI method.%nmethod in %s%n %s%n cannot override method in %s%n %s +override.effect.invalid.polymorphic=A method override may only be @PolyUIEffect if it overrides a @PolyUIEffect method.%nmethod in %s%n %s%n cannot override method in %s%n %s +override.effect.invalid.nonui=A method override may only be @UI if it overrides an @UI method (overriding non-UI instantiation of supertype).%nmethod in %s%n %s%n cannot override method in %s%n %s +override.effect.warning.inheritance=method in %s%n %s%noverrides a method with @UI effect in %s%n %s%nand another method with an @AlwaysSafe effect in %s%n %s%nThis is discouraged. polymorphism.invalid=Only @PolyUIType types may have @PolyUIEffect methods. inheritance.polymorphic.invalid=An effect-polymorphic type may only inherit from another effect-polymorphic type (%s extends/implements %s). effects.redundant.uitype=This method is annotated @UIEffect, which is redundant because the enclosing type is @UIType. diff --git a/checker/src/main/java/org/checkerframework/checker/guieffect/org-eclipse.astub b/checker/src/main/java/org/checkerframework/checker/guieffect/org-eclipse.astub index fa7e1824b1fe..df0a0a8fae36 100644 --- a/checker/src/main/java/org/checkerframework/checker/guieffect/org-eclipse.astub +++ b/checker/src/main/java/org/checkerframework/checker/guieffect/org-eclipse.astub @@ -1,3 +1,4 @@ +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.guieffect.qual.*; package org.eclipse.compare; @@ -310,7 +311,7 @@ public class BusyIndicator { @UIPackage package org.eclipse.swt.graphics; // XXX: Not 100%, but this seems safe. At least the .equals(), which is what my case study needs public final class RGB extends Object implements org.eclipse.swt.internal.SerializableCompatibility { - @SafeEffect public boolean equals(Object object); + @SafeEffect public boolean equals(@Nullable Object object); } public final class Font extends Resource { // Another SWT class that happens to work on other threads... (https://bugs.eclipse.org/bugs/show_bug.cgi?id=241062) @@ -321,7 +322,7 @@ public abstract class Resource { @SafeEffect public void dispose(); } public final class Image extends Resource implements Drawable { - @SafeEffect public boolean equals(Object object); + @SafeEffect public boolean equals(@Nullable Object object); @SafeEffect public int hashCode(); } diff --git a/checker/src/main/java/org/checkerframework/checker/i18n/I18nAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/i18n/I18nAnnotatedTypeFactory.java index 717667cc2a45..5ff26f76fa54 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18n/I18nAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/i18n/I18nAnnotatedTypeFactory.java @@ -4,11 +4,7 @@ import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.LiteralTree; import com.sun.source.tree.Tree; -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; + import org.checkerframework.checker.i18n.qual.Localized; import org.checkerframework.checker.i18n.qual.UnknownLocalized; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; @@ -19,6 +15,13 @@ import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; + public class I18nAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { public I18nAnnotatedTypeFactory(BaseTypeChecker checker) { @@ -53,14 +56,14 @@ public Void visitBinary(BinaryTree tree, AnnotatedTypeMirror type) { } @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMirror type) { + public Void visitCompoundAssignment(CompoundAssignmentTree tree, AnnotatedTypeMirror type) { type.removeAnnotation(LOCALIZED); return null; } @Override public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) { - if (!type.isAnnotatedInHierarchy(LOCALIZED)) { + if (!type.hasAnnotationInHierarchy(LOCALIZED)) { if (tree.getKind() == Tree.Kind.STRING_LITERAL && tree.getValue().equals("")) { type.addAnnotation(LOCALIZED); } diff --git a/checker/src/main/java/org/checkerframework/checker/i18n/I18nChecker.java b/checker/src/main/java/org/checkerframework/checker/i18n/I18nChecker.java index 0683079bc44b..47217ffc53ab 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18n/I18nChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/i18n/I18nChecker.java @@ -1,10 +1,11 @@ package org.checkerframework.checker.i18n; -import java.util.ArrayList; -import java.util.Collection; import org.checkerframework.framework.source.AggregateChecker; import org.checkerframework.framework.source.SourceChecker; +import java.util.ArrayList; +import java.util.Collection; + /** * A type-checker that enforces (and finds the violations of) two properties: * diff --git a/checker/src/main/java/org/checkerframework/checker/i18n/LocalizableKeyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/i18n/LocalizableKeyAnnotatedTypeFactory.java index 1abeecd6b9a7..1cf79a45706b 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18n/LocalizableKeyAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/i18n/LocalizableKeyAnnotatedTypeFactory.java @@ -1,9 +1,5 @@ package org.checkerframework.checker.i18n; -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; import org.checkerframework.checker.i18n.qual.LocalizableKey; import org.checkerframework.checker.i18n.qual.LocalizableKeyBottom; import org.checkerframework.checker.i18n.qual.UnknownLocalizableKey; @@ -12,6 +8,11 @@ import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + /** A PropertyKeyATF that uses LocalizableKey to annotate the keys. */ public class LocalizableKeyAnnotatedTypeFactory extends PropertyKeyAnnotatedTypeFactory { diff --git a/checker/src/main/java/org/checkerframework/checker/i18n/LocalizableKeyChecker.java b/checker/src/main/java/org/checkerframework/checker/i18n/LocalizableKeyChecker.java index 7e932e54cf5b..9099c7a124db 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18n/LocalizableKeyChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/i18n/LocalizableKeyChecker.java @@ -1,9 +1,11 @@ package org.checkerframework.checker.i18n; +import org.checkerframework.checker.propkey.PropertyKeyChecker; + import java.util.Locale; import java.util.ResourceBundle; + import javax.annotation.processing.SupportedOptions; -import org.checkerframework.checker.propkey.PropertyKeyChecker; /** * A type-checker that checks that only valid localizable keys are used when using localizing @@ -15,7 +17,8 @@ *

  • Properties files: A common method for localization using a properties file, * mapping the localization keys to localized messages. Programmers pass the property file * location via {@code propfiles} option (e.g. {@code - * -Apropfiles=/path/to/messages.properties}), separating multiple files by a colon ":". + * -Apropfiles=/path/to/messages.properties}), separating multiple files by {@link + * java.io.File#pathSeparator}. *
  • {@link ResourceBundle}: The proper recommended mechanism for localization. * Programmers pass the {@code baseName} name of the bundle via {@code bundlename} (e.g. * {@code -Abundlename=MyResource}. The checker uses the resource associated with the default diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterAnnotatedTypeFactory.java index d48a034b0441..0433f26211db 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterAnnotatedTypeFactory.java @@ -2,35 +2,43 @@ import com.sun.source.tree.LiteralTree; import com.sun.source.tree.Tree; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.InputStream; -import java.util.Collections; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; -import java.util.ResourceBundle; -import javax.lang.model.element.AnnotationMirror; + import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory; import org.checkerframework.checker.i18nformatter.qual.I18nFormat; import org.checkerframework.checker.i18nformatter.qual.I18nFormatBottom; import org.checkerframework.checker.i18nformatter.qual.I18nFormatFor; import org.checkerframework.checker.i18nformatter.qual.I18nInvalidFormat; import org.checkerframework.checker.i18nformatter.qual.I18nUnknownFormat; +import org.checkerframework.checker.i18nformatter.util.I18nFormatUtil; +import org.checkerframework.checker.signature.qual.CanonicalName; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.MostlyNoElementQualifierHierarchy; import org.checkerframework.framework.type.QualifierHierarchy; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.framework.util.GraphQualifierHierarchy; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory; +import org.checkerframework.framework.util.QualifierKind; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.plumelib.reflection.Signatures; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.ResourceBundle; + +import javax.lang.model.element.AnnotationMirror; + /** * Adds {@link I18nFormat} to the type of tree, if it is a {@code String} or {@code char} literal * that represents a satisfiable format. The annotation's value is set to be a list of appropriate @@ -47,18 +55,22 @@ public class I18nFormatterAnnotatedTypeFactory extends BaseAnnotatedTypeFactory /** The @{@link I18nUnknownFormat} annotation. */ protected final AnnotationMirror I18NUNKNOWNFORMAT = AnnotationBuilder.fromClass(elements, I18nUnknownFormat.class); - /** The @{@link I18nFormat} annotation. */ - protected final AnnotationMirror I18NFORMAT = - AnnotationBuilder.fromClass(elements, I18nFormat.class); - /** The @{@link I18nInvalidFormat} annotation. */ - protected final AnnotationMirror I18NINVALIDFORMAT = - AnnotationBuilder.fromClass(elements, I18nInvalidFormat.class); + /** The @{@link I18nFormatBottom} annotation. */ protected final AnnotationMirror I18NFORMATBOTTOM = AnnotationBuilder.fromClass(elements, I18nFormatBottom.class); - /** The @{@link I18nFormatFor} annotation. */ - protected final AnnotationMirror I18NFORMATFOR = - AnnotationBuilder.fromClass(elements, I18nFormatFor.class); + + /** The fully-qualified name of {@link I18nFormat}. */ + protected static final @CanonicalName String I18NFORMAT_NAME = + I18nFormat.class.getCanonicalName(); + + /** The fully-qualified name of {@link I18nInvalidFormat}. */ + protected static final @CanonicalName String I18NINVALIDFORMAT_NAME = + I18nInvalidFormat.class.getCanonicalName(); + + /** The fully-qualified name of {@link I18nFormatFor}. */ + protected static final @CanonicalName String I18NFORMATFOR_NAME = + I18nFormatFor.class.getCanonicalName(); /** Map from a translation file key to its value in the file. */ public final Map translations = Collections.unmodifiableMap(buildLookup()); @@ -74,7 +86,10 @@ public I18nFormatterAnnotatedTypeFactory(BaseTypeChecker checker) { } /** - * Builds a map from a translation file key to its value in the file. + * Builds a map from a translation file key to its value in the file. Builds the map for all + * files in the "-Apropfiles" command-line argument. + * + *

    Called only once, during initialization. * * @return a map from a translation file key to its value in the file */ @@ -82,85 +97,67 @@ private Map buildLookup() { Map result = new HashMap<>(); if (checker.hasOption("propfiles")) { - String names = checker.getOption("propfiles"); - String[] namesArr = names.split(":"); - - if (namesArr == null) { - System.err.println("Couldn't parse the properties files: <" + names + ">"); - } else { - for (String name : namesArr) { - try { - Properties prop = new Properties(); - - ClassLoader cl = this.getClass().getClassLoader(); - if (cl == null) { - // The class loader is null if the system class loader was used. - cl = ClassLoader.getSystemClassLoader(); - } - InputStream in = cl.getResourceAsStream(name); - - if (in == null) { - // If the classloader didn't manage to load the file, try whether a - // FileInputStream works. For absolute paths this might help. - try { - in = new FileInputStream(name); - } catch (FileNotFoundException e) { - // ignore - } - } - - if (in == null) { - System.err.println("Couldn't find the properties file: " + name); - // report(null, "propertykeychecker.filenotfound", name); + for (String propfile : checker.getStringsOption("propfiles", File.pathSeparator)) { + Properties prop = new Properties(); + ClassLoader cl = this.getClass().getClassLoader(); + if (cl == null) { + // The class loader is null if the system class loader was used. + cl = ClassLoader.getSystemClassLoader(); + } + try (InputStream in = cl.getResourceAsStream(propfile)) { + if (in != null) { + prop.load(in); + } else { + // If the classloader didn't manage to load the file, try whether a + // FileInputStream works. For absolute paths this might help. + try (InputStream fis = new FileInputStream(propfile)) { + prop.load(fis); + } catch (FileNotFoundException e) { + System.err.println("Couldn't find the properties file: " + propfile); + // report(null, "propertykeychecker.filenotfound", propfile); // return Collections.emptySet(); continue; } + } - prop.load(in); - - for (String key : prop.stringPropertyNames()) { - result.put(key, prop.getProperty(key)); - } - } catch (Exception e) { - // TODO: is there a nicer way to report messages, that are not connected to - // an AST node? One cannot use report, because it needs a node. - System.err.println( - "Exception in PropertyKeyChecker.keysOfPropertyFile: " + e); - e.printStackTrace(); + for (String key : prop.stringPropertyNames()) { + result.put(key, prop.getProperty(key)); } + } catch (Exception e) { + // TODO: is there a nicer way to report messages, that are not connected to + // an AST node? One cannot use `report`, because it needs a node. + System.err.println( + "Exception in PropertyKeyChecker.keysOfPropertyFile while processing " + + propfile + + ": " + + e); + e.printStackTrace(); } } } if (checker.hasOption("bundlenames")) { - String bundleNames = checker.getOption("bundlenames"); - String[] namesArr = bundleNames.split(":"); - - if (namesArr == null) { - System.err.println("Couldn't parse the resource bundles: <" + bundleNames + ">"); - } else { - for (String bundleName : namesArr) { - if (!Signatures.isBinaryName(bundleName)) { - System.err.println( - "Malformed resource bundle: <" - + bundleName - + "> should be a binary name."); - continue; - } - ResourceBundle bundle = ResourceBundle.getBundle(bundleName); - if (bundle == null) { - System.err.println( - "Couldn't find the resource bundle: <" - + bundleName - + "> for locale <" - + Locale.getDefault() - + ">."); - continue; - } + for (String bundleName : checker.getStringsOption("bundlenames", ':')) { + if (!Signatures.isBinaryName(bundleName)) { + System.err.println( + "Malformed resource bundle: <" + + bundleName + + "> should be a binary name."); + continue; + } + ResourceBundle bundle = ResourceBundle.getBundle(bundleName); + if (bundle == null) { + System.err.println( + "Couldn't find the resource bundle: <" + + bundleName + + "> for locale <" + + Locale.getDefault() + + ">."); + continue; + } - for (String key : bundle.keySet()) { - result.put(key, bundle.getString(key)); - } + for (String key : bundle.keySet()) { + result.put(key, bundle.getString(key)); } } } @@ -169,8 +166,8 @@ private Map buildLookup() { } @Override - public QualifierHierarchy createQualifierHierarchy(MultiGraphFactory factory) { - return new I18nFormatterQualifierHierarchy(factory); + protected QualifierHierarchy createQualifierHierarchy() { + return new I18nFormatterQualifierHierarchy(); } @Override @@ -186,12 +183,10 @@ public I18nFormatterTreeAnnotator(AnnotatedTypeFactory atypeFactory) { @Override public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) { - if (!type.isAnnotatedInHierarchy(I18NFORMAT)) { + if (!type.hasAnnotationInHierarchy(I18NUNKNOWNFORMAT)) { String format = null; if (tree.getKind() == Tree.Kind.STRING_LITERAL) { format = (String) tree.getValue(); - } else if (tree.getKind() == Tree.Kind.CHAR_LITERAL) { - format = Character.toString((Character) tree.getValue()); } if (format != null) { AnnotationMirror anno; @@ -214,16 +209,36 @@ public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) { } } - class I18nFormatterQualifierHierarchy extends GraphQualifierHierarchy { + /** I18nFormatterQualifierHierarchy. */ + class I18nFormatterQualifierHierarchy extends MostlyNoElementQualifierHierarchy { - public I18nFormatterQualifierHierarchy(MultiGraphFactory f) { - super(f, I18NFORMATBOTTOM); + /** Qualifier kind for the @{@link I18nFormat} annotation. */ + private final QualifierKind I18NFORMAT_KIND; + + /** Qualifier kind for the @{@link I18nFormatFor} annotation. */ + private final QualifierKind I18NFORMATFOR_KIND; + + /** Qualifier kind for the @{@link I18nInvalidFormat} annotation. */ + private final QualifierKind I18NINVALIDFORMAT_KIND; + + /** Creates I18nFormatterQualifierHierarchy. */ + public I18nFormatterQualifierHierarchy() { + super( + I18nFormatterAnnotatedTypeFactory.this.getSupportedTypeQualifiers(), + elements, + I18nFormatterAnnotatedTypeFactory.this); + this.I18NFORMAT_KIND = this.getQualifierKind(I18NFORMAT_NAME); + this.I18NFORMATFOR_KIND = this.getQualifierKind(I18NFORMATFOR_NAME); + this.I18NINVALIDFORMAT_KIND = this.getQualifierKind(I18NINVALIDFORMAT_NAME); } @Override - public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { - if (AnnotationUtils.areSameByName(subAnno, I18NFORMAT) - && AnnotationUtils.areSameByName(superAnno, I18NFORMAT)) { + protected boolean isSubtypeWithElements( + AnnotationMirror subAnno, + QualifierKind subKind, + AnnotationMirror superAnno, + QualifierKind superKind) { + if (subKind == I18NFORMAT_KIND && superKind == I18NFORMAT_KIND) { I18nConversionCategory[] rhsArgTypes = treeUtil.formatAnnotationToCategories(subAnno); @@ -240,48 +255,27 @@ public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { } } return true; + } else if ((subKind == I18NINVALIDFORMAT_KIND && superKind == I18NINVALIDFORMAT_KIND) + || (subKind == I18NFORMATFOR_KIND && superKind == I18NFORMATFOR_KIND)) { + return Objects.equals( + treeUtil.getI18nInvalidFormatValue(subAnno), + treeUtil.getI18nInvalidFormatValue(superAnno)); } - - if (AnnotationUtils.areSameByName(superAnno, I18NINVALIDFORMAT) - && AnnotationUtils.areSameByName(subAnno, I18NINVALIDFORMAT)) { - return AnnotationUtils.getElementValue(subAnno, "value", String.class, true) - .equals( - AnnotationUtils.getElementValue( - superAnno, "value", String.class, true)); - } - - if (AnnotationUtils.areSameByName(superAnno, I18NFORMAT)) { - superAnno = I18NFORMAT; - } - if (AnnotationUtils.areSameByName(subAnno, I18NFORMAT)) { - subAnno = I18NFORMAT; - } - if (AnnotationUtils.areSameByName(superAnno, I18NINVALIDFORMAT)) { - superAnno = I18NINVALIDFORMAT; - } - if (AnnotationUtils.areSameByName(subAnno, I18NINVALIDFORMAT)) { - subAnno = I18NINVALIDFORMAT; - } - if (AnnotationUtils.areSameByName(superAnno, I18NFORMATFOR)) { - superAnno = I18NFORMATFOR; - } - if (AnnotationUtils.areSameByName(subAnno, I18NFORMATFOR)) { - subAnno = I18NFORMATFOR; - } - - return super.isSubtype(subAnno, superAnno); + throw new TypeSystemError("Unexpected QualifierKinds: %s %s", subKind, superKind); } @Override - public AnnotationMirror leastUpperBound(AnnotationMirror anno1, AnnotationMirror anno2) { - if (AnnotationUtils.areSameByName(anno1, I18NFORMATBOTTOM)) { + protected AnnotationMirror leastUpperBoundWithElements( + AnnotationMirror anno1, + QualifierKind qualifierKind1, + AnnotationMirror anno2, + QualifierKind qualifierKind2, + QualifierKind lubKind) { + if (qualifierKind1.isBottom()) { return anno2; - } - if (AnnotationUtils.areSameByName(anno2, I18NFORMATBOTTOM)) { + } else if (qualifierKind2.isBottom()) { return anno1; - } - if (AnnotationUtils.areSameByName(anno1, I18NFORMAT) - && AnnotationUtils.areSameByName(anno2, I18NFORMAT)) { + } else if (qualifierKind1 == I18NFORMAT_KIND && qualifierKind2 == I18NFORMAT_KIND) { I18nConversionCategory[] shorterArgTypesList = treeUtil.formatAnnotationToCategories(anno1); I18nConversionCategory[] longerArgTypesList = @@ -308,11 +302,10 @@ public AnnotationMirror leastUpperBound(AnnotationMirror anno1, AnnotationMirror resultArgTypes[i] = longerArgTypesList[i]; } return treeUtil.categoriesToFormatAnnotation(resultArgTypes); - } - if (AnnotationUtils.areSameByName(anno1, I18NINVALIDFORMAT) - && AnnotationUtils.areSameByName(anno2, I18NINVALIDFORMAT)) { + } else if (qualifierKind1 == I18NINVALIDFORMAT_KIND + && qualifierKind2 == I18NINVALIDFORMAT_KIND) { + assert !anno1.getElementValues().isEmpty(); assert !anno1.getElementValues().isEmpty(); - assert !anno2.getElementValues().isEmpty(); if (AnnotationUtils.areSame(anno1, anno2)) { return anno1; @@ -324,11 +317,9 @@ public AnnotationMirror leastUpperBound(AnnotationMirror anno1, AnnotationMirror + " or " + treeUtil.invalidFormatAnnotationToErrorMessage(anno2) + ")"); - } - - // All @I18nFormatFor annotations are unrelated by subtyping. - if (AnnotationUtils.areSameByName(anno1, I18NFORMATFOR) + } else if (qualifierKind1 == I18NFORMATFOR_KIND && AnnotationUtils.areSame(anno1, anno2)) { + // @I18nFormatFor annotations are unrelated by subtyping, unless they are identical. return anno1; } @@ -336,15 +327,17 @@ public AnnotationMirror leastUpperBound(AnnotationMirror anno1, AnnotationMirror } @Override - public AnnotationMirror greatestLowerBound(AnnotationMirror anno1, AnnotationMirror anno2) { - if (AnnotationUtils.areSameByName(anno1, I18NUNKNOWNFORMAT)) { + protected AnnotationMirror greatestLowerBoundWithElements( + AnnotationMirror anno1, + QualifierKind qualifierKind1, + AnnotationMirror anno2, + QualifierKind qualifierKind2, + QualifierKind glbKind) { + if (qualifierKind1.isTop()) { return anno2; - } - if (AnnotationUtils.areSameByName(anno2, I18NUNKNOWNFORMAT)) { + } else if (qualifierKind2.isTop()) { return anno1; - } - if (AnnotationUtils.areSameByName(anno1, I18NFORMAT) - && AnnotationUtils.areSameByName(anno2, I18NFORMAT)) { + } else if (qualifierKind1 == I18NFORMAT_KIND && qualifierKind2 == I18NFORMAT_KIND) { I18nConversionCategory[] anno1ArgTypes = treeUtil.formatAnnotationToCategories(anno1); I18nConversionCategory[] anno2ArgTypes = @@ -365,10 +358,9 @@ public AnnotationMirror greatestLowerBound(AnnotationMirror anno1, AnnotationMir I18nConversionCategory.union(anno1ArgTypes[i], anno2ArgTypes[i]); } return treeUtil.categoriesToFormatAnnotation(anno3ArgTypes); - } - if (AnnotationUtils.areSameByName(anno1, I18NINVALIDFORMAT) - && AnnotationUtils.areSameByName(anno2, I18NINVALIDFORMAT)) { - assert !anno1.getElementValues().isEmpty(); + } else if (qualifierKind1 == I18NINVALIDFORMAT_KIND + && qualifierKind2 == I18NINVALIDFORMAT_KIND) { + assert !anno2.getElementValues().isEmpty(); if (AnnotationUtils.areSame(anno1, anno2)) { @@ -381,10 +373,9 @@ public AnnotationMirror greatestLowerBound(AnnotationMirror anno1, AnnotationMir + " and " + treeUtil.invalidFormatAnnotationToErrorMessage(anno2) + ")"); - } - // All @I18nFormatFor annotations are unrelated by subtyping. - if (AnnotationUtils.areSameByName(anno1, I18NFORMATFOR) + } else if (qualifierKind1 == I18NFORMATFOR_KIND && AnnotationUtils.areSame(anno1, anno2)) { + // @I18nFormatFor annotations are unrelated by subtyping, unless they are identical. return anno1; } diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterChecker.java b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterChecker.java index af8910612f36..01749e614ec1 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterChecker.java @@ -1,9 +1,10 @@ package org.checkerframework.checker.i18nformatter; -import javax.annotation.processing.SupportedOptions; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; +import javax.annotation.processing.SupportedOptions; + /** * A type-checker plug-in for the qualifier that finds syntactically invalid i18n-formatter calls * (MessageFormat.format()). diff --git a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTransfer.java b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTransfer.java index 5dd84f101920..5dd05f7058ad 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTransfer.java @@ -1,22 +1,22 @@ package org.checkerframework.checker.i18nformatter; -import javax.lang.model.element.AnnotationMirror; import org.checkerframework.checker.formatter.FormatterTreeUtil.Result; import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory; import org.checkerframework.checker.i18nformatter.qual.I18nInvalidFormat; import org.checkerframework.dataflow.analysis.ConditionalTransferResult; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; import org.checkerframework.dataflow.analysis.RegularTransferResult; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.flow.CFAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFTransfer; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.javacutil.AnnotationBuilder; +import javax.lang.model.element.AnnotationMirror; + /** * The transfer function for the Internationalization Format String Checker. * @@ -46,8 +46,7 @@ public TransferResult visitMethodInvocation( if (cats.value() == null) { tu.failure(cats, "i18nformat.indirect.arguments"); } else { - Receiver firstParam = - FlowExpressions.internalReprOf(atypeFactory, node.getArgument(0)); + JavaExpression firstParam = JavaExpression.fromNode(node.getArgument(0)); AnnotationMirror anno = atypeFactory.treeUtil.categoriesToFormatAnnotation(cats.value()); thenStore.insertValue(firstParam, anno); @@ -61,10 +60,9 @@ public TransferResult visitMethodInvocation( CFStore elseStore = thenStore.copy(); ConditionalTransferResult newResult = new ConditionalTransferResult<>(result.getResultValue(), thenStore, elseStore); - Receiver firstParam = FlowExpressions.internalReprOf(atypeFactory, node.getArgument(0)); + JavaExpression firstParam = JavaExpression.fromNode(node.getArgument(0)); AnnotationBuilder builder = - new AnnotationBuilder( - tu.processingEnv, I18nInvalidFormat.class.getCanonicalName()); + new AnnotationBuilder(tu.processingEnv, I18nInvalidFormat.class); // No need to set a value of @I18nInvalidFormat builder.setValue("value", ""); elseStore.insertValue(firstParam, builder.build()); @@ -74,7 +72,7 @@ public TransferResult visitMethodInvocation( // @I18nMakeFormat that will be used to annotate ResourceBundle.getString() so that when the // getString() method is called, this will check if the given key exist in the translation // file and annotate the result string with the correct format annotation according to the - // corresponding key's value + // corresponding key's value. if (tu.isMakeFormatCall(node, atypeFactory)) { Result cats = tu.makeFormatCallCategories(node, atypeFactory); if (cats.value() == null) { 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 38d156f455cc..e77920b1db9f 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTreeUtil.java +++ b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterTreeUtil.java @@ -5,21 +5,7 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.TypeCastTree; import com.sun.source.util.SimpleTreeVisitor; -import java.util.List; -import java.util.Map; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; -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.NullType; -import javax.lang.model.type.PrimitiveType; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.SimpleElementVisitor7; -import javax.lang.model.util.SimpleTypeVisitor7; + import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.formatter.FormatterTreeUtil.InvocationType; import org.checkerframework.checker.formatter.FormatterTreeUtil.Result; @@ -30,9 +16,10 @@ import org.checkerframework.checker.i18nformatter.qual.I18nInvalidFormat; import org.checkerframework.checker.i18nformatter.qual.I18nMakeFormat; import org.checkerframework.checker.i18nformatter.qual.I18nValidFormat; +import org.checkerframework.checker.i18nformatter.util.I18nFormatUtil; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.BinaryName; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; import org.checkerframework.dataflow.cfg.node.ArrayCreationNode; import org.checkerframework.dataflow.cfg.node.FieldAccessNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; @@ -41,13 +28,29 @@ import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.framework.util.FlowExpressionParseUtil; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionContext; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionParseException; +import org.checkerframework.framework.util.JavaExpressionParseUtil; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreeUtils; +import java.util.List; +import java.util.Map; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +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.NullType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.SimpleElementVisitor8; +import javax.lang.model.util.SimpleTypeVisitor8; + /** * This class provides a collection of utilities to ease working with syntax trees that have * something to do with I18nFormatters. @@ -55,12 +58,34 @@ * @checker_framework.manual #i18n-formatter-checker Internationalization Format String Checker */ public class I18nFormatterTreeUtil { + /** The checker. */ public final BaseTypeChecker checker; + + /** The processing environment. */ public final ProcessingEnvironment processingEnv; + /** The value() element/field of an @I18nFormat annotation. */ + protected final ExecutableElement i18nFormatValueElement; + + /** The value() element/field of an @I18nFormatFor annotation. */ + protected final ExecutableElement i18nFormatForValueElement; + + /** The value() element/field of an @I18nInvalidFormat annotation. */ + protected final ExecutableElement i18nInvalidFormatValueElement; + + /** + * Creates a new I18nFormatterTreeUtil. + * + * @param checker the checker + */ public I18nFormatterTreeUtil(BaseTypeChecker checker) { this.checker = checker; this.processingEnv = checker.getProcessingEnvironment(); + i18nFormatValueElement = TreeUtils.getMethod(I18nFormat.class, "value", 0, processingEnv); + i18nFormatForValueElement = + TreeUtils.getMethod(I18nFormatFor.class, "value", 0, processingEnv); + i18nInvalidFormatValueElement = + TreeUtils.getMethod(I18nInvalidFormat.class, "value", 0, processingEnv); } /** Describe the format annotation type. */ @@ -80,45 +105,71 @@ public AnnotationMirror exceptionToInvalidFormatAnnotation(IllegalArgumentExcept } /** - * Takes an invalid formatter string and returns a syntax trees element that represents a {@link - * I18nInvalidFormat} annotation with the invalid formatter string as value. + * Creates an {@link I18nInvalidFormat} annotation with the given string as its value. + * + * @param invalidFormatString an invalid formatter string + * @return an {@link I18nInvalidFormat} annotation with the given string as its value */ - // package-private - AnnotationMirror stringToInvalidFormatAnnotation(String invalidFormatString) { - AnnotationBuilder builder = - new AnnotationBuilder(processingEnv, I18nInvalidFormat.class.getCanonicalName()); + /*package-private*/ AnnotationMirror stringToInvalidFormatAnnotation( + String invalidFormatString) { + AnnotationBuilder builder = new AnnotationBuilder(processingEnv, I18nInvalidFormat.class); builder.setValue("value", invalidFormatString); return builder.build(); } + /** + * Gets the value() element/field out of an I18nInvalidFormat annotation. + * + * @param anno an I18nInvalidFormat annotation + * @return its value() element/field, or null if it does not have one + */ + /*package-private*/ @Nullable String getI18nInvalidFormatValue(AnnotationMirror anno) { + return AnnotationUtils.getElementValue( + anno, i18nInvalidFormatValueElement, String.class, null); + } + + /** + * Gets the value() element/field out of an I18NFormatFor annotation. + * + * @param anno an I18NFormatFor annotation + * @return its value() element/field + */ + /*package-private*/ String getI18nFormatForValue(AnnotationMirror anno) { + return AnnotationUtils.getElementValue(anno, i18nFormatForValueElement, String.class); + } + /** * Takes a syntax tree element that represents a {@link I18nInvalidFormat} annotation, and * returns its value. + * + * @param anno an I18nInvalidFormat annotation + * @return its value() element/field, within double-quotes */ public String invalidFormatAnnotationToErrorMessage(AnnotationMirror anno) { - return "\"" + AnnotationUtils.getElementValue(anno, "value", String.class, true) + "\""; + return "\"" + getI18nInvalidFormatValue(anno) + "\""; } /** - * Takes a list of ConversionCategory elements, and returns a syntax tree element that - * represents a {@link I18nFormat} annotation with the list as value. + * Creates a {@code @}{@link I18nFormat} annotation with the given list as its value. + * + * @param args conversion categories for the {@code @Format} annotation + * @return a {@code @}{@link I18nFormat} annotation with the given list as its value */ public AnnotationMirror categoriesToFormatAnnotation(I18nConversionCategory[] args) { - AnnotationBuilder builder = - new AnnotationBuilder(processingEnv, I18nFormat.class.getCanonicalName()); + AnnotationBuilder builder = new AnnotationBuilder(processingEnv, I18nFormat.class); builder.setValue("value", args); return builder.build(); } /** - * Takes a syntax tree element that represents a {@link I18nFormat} annotation, and returns its - * value. + * Takes an {@code @}{@link I18nFormat} annotation, and returns its {@code value} element + * + * @param anno an {@code @}{@link I18nFormat} annotation + * @return the {@code @}{@link I18nFormat} annotation's {@code value} element */ public I18nConversionCategory[] formatAnnotationToCategories(AnnotationMirror anno) { - List list = - AnnotationUtils.getElementValueEnumArray( - anno, "value", I18nConversionCategory.class, false); - return list.toArray(new I18nConversionCategory[] {}); + return AnnotationUtils.getElementValueEnumArray( + anno, i18nFormatValueElement, I18nConversionCategory.class); } /** @@ -173,7 +224,8 @@ public final void warning(Result res, @CompilerMessageKey String msgKey, Obje checker.reportWarning(res.location, msgKey, args); } - private I18nConversionCategory[] asFormatCallCategoriesLowLevel(MethodInvocationNode node) { + private I18nConversionCategory @Nullable [] asFormatCallCategoriesLowLevel( + MethodInvocationNode node) { Node vararg = node.getArgument(1); if (vararg instanceof ArrayCreationNode) { List convs = ((ArrayCreationNode) vararg).getInitializers(); @@ -217,17 +269,23 @@ public Result makeFormatCallCategories( return ret; } - /** Returns an I18nFormatCall instance, only if FormatFor is called. Otherwise, returns null. */ - public I18nFormatCall createFormatForCall( - MethodInvocationTree tree, - MethodInvocationNode node, - I18nFormatterAnnotatedTypeFactory atypeFactory) { + /** + * Returns an I18nFormatCall instance, only if there is an {@code @I18nFormatFor} annotation. + * Otherwise, returns null. + * + * @param tree method invocation tree + * @param atypeFactory type factory + * @return an I18nFormatCall instance, only if there is an {@code @I18nFormatFor} annotation. + * Otherwise, returns null. + */ + public @Nullable I18nFormatCall createFormatForCall( + MethodInvocationTree tree, I18nFormatterAnnotatedTypeFactory atypeFactory) { ExecutableElement method = TreeUtils.elementFromUse(tree); AnnotatedExecutableType methodAnno = atypeFactory.getAnnotatedType(method); for (AnnotatedTypeMirror paramType : methodAnno.getParameterTypes()) { // find @FormatFor if (paramType.getAnnotation(I18nFormatFor.class) != null) { - return atypeFactory.treeUtil.new I18nFormatCall(tree, node, atypeFactory); + return atypeFactory.treeUtil.new I18nFormatCall(tree, atypeFactory); } } return null; @@ -236,29 +294,52 @@ 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; + /** The type of the format string formal parameter. */ private AnnotatedTypeMirror formatAnno; - public I18nFormatCall( - MethodInvocationTree tree, - MethodInvocationNode node, - AnnotatedTypeFactory atypeFactory) { + /** + * Creates an {@code I18nFormatCall} for the given method invocation tree. + * + * @param tree method invocation tree + * @param atypeFactory type factory + */ + @SuppressWarnings("nullness:initialization.fields.uninitialized") + public I18nFormatCall(MethodInvocationTree tree, AnnotatedTypeFactory atypeFactory) { this.tree = tree; this.atypeFactory = atypeFactory; List theargs = tree.getArguments(); this.args = null; ExecutableElement method = TreeUtils.elementFromUse(tree); AnnotatedExecutableType methodAnno = atypeFactory.getAnnotatedType(method); - initialCheck(theargs, method, node, methodAnno); + initialCheck(theargs, method, methodAnno); + } + + /** + * Returns the AST node for the call. + * + * @return the AST node for the call + */ + public MethodInvocationTree getTree() { + return tree; } @Override @@ -269,14 +350,17 @@ public String toString() { /** * This method checks the validity of the FormatFor. If it is valid, this.args will be set * to the correct parameter arguments. Otherwise, it will be still null. + * + * @param theargs arguments to the format method call + * @param method the ExecutableElement of the format method + * @param methodAnno annotated type of {@code method} */ private void initialCheck( List theargs, ExecutableElement method, - MethodInvocationNode node, AnnotatedExecutableType methodAnno) { + // paramIndex is a 0-based index int paramIndex = -1; - Receiver paramArg = null; int i = 0; for (AnnotatedTypeMirror paramType : methodAnno.getParameterTypes()) { if (paramType.getAnnotation(I18nFormatFor.class) != null) { @@ -287,28 +371,16 @@ private void initialCheck( // Invalid FormatFor invocation return; } - FlowExpressionContext flowExprContext = - FlowExpressionContext.buildContextForMethodUse( - node, checker.getContext()); + String formatforArg = - AnnotationUtils.getElementValue( - paramType.getAnnotation(I18nFormatFor.class), - "value", - String.class, - false); - if (flowExprContext != null) { - try { - paramArg = - FlowExpressionParseUtil.parse( - formatforArg, - flowExprContext, - atypeFactory.getPath(tree), - true); - paramIndex = flowExprContext.arguments.indexOf(paramArg); - } catch (FlowExpressionParseException e) { - // report errors here - checker.reportError(tree, "i18nformat.invalid.formatfor"); - } + getI18nFormatForValue(paramType.getAnnotation(I18nFormatFor.class)); + + paramIndex = JavaExpressionParseUtil.parameterIndex(formatforArg); + if (paramIndex == -1) { + // report errors here + checker.reportError(tree, "i18nformat.invalid.formatfor"); + } else { + paramIndex--; } break; } @@ -337,8 +409,7 @@ public Result getFormatType() { invalidMessage = "(is a @I18nFormat annotation missing?)"; AnnotationMirror inv = formatAnno.getAnnotation(I18nInvalidFormat.class); if (inv != null) { - invalidMessage = - AnnotationUtils.getElementValue(inv, "value", String.class, true); + invalidMessage = getI18nInvalidFormatValue(inv); } } } else { @@ -366,12 +437,12 @@ public final Result getInvocationType() { InvocationType type = InvocationType.VARARG; if (args.size() == 1) { - final ExpressionTree first = args.get(0); + ExpressionTree first = args.get(0); TypeMirror argType = atypeFactory.getAnnotatedType(first).getUnderlyingType(); // figure out if argType is an array type = argType.accept( - new SimpleTypeVisitor7>() { + new SimpleTypeVisitor8>() { @Override protected InvocationType defaultAction( TypeMirror e, Class p) { @@ -388,18 +459,18 @@ public InvocationType visitArray(ArrayType t, Class p) { InvocationType, Class>() { @Override protected InvocationType defaultAction( - Tree node, Class p) { + Tree tree, Class p) { // just a normal array return InvocationType.ARRAY; } @Override public InvocationType visitTypeCast( - TypeCastTree node, Class p) { + TypeCastTree tree, Class p) { // it's a (Object[])null return atypeFactory .getAnnotatedType( - node + tree .getExpression()) .getUnderlyingType() .getKind() @@ -420,7 +491,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); } @@ -459,18 +530,13 @@ public boolean isValidParameter(I18nConversionCategory formatCat, TypeMirror par // we did not recognize the parameter type return false; } - for (Class c : formatCat.types) { - if (c.isAssignableFrom(type)) { - return true; - } - } - return false; + return formatCat.isAssignableFrom(type); } } /** Converts a TypeMirror to a Class. */ private static class TypeMirrorToClassVisitor - extends SimpleTypeVisitor7, Class> { + extends SimpleTypeVisitor8, Class> { @Override public Class visitPrimitive(PrimitiveType t, Class v) { switch (t.getKind()) { @@ -491,7 +557,7 @@ public Class visitPrimitive(PrimitiveType t, Class v) { case DOUBLE: return Double.class; default: - return null; + throw new BugInCF("unknown primitive type " + t); } } @@ -499,19 +565,19 @@ public Class visitPrimitive(PrimitiveType t, Class v) { public Class visitDeclared(DeclaredType dt, Class v) { return dt.asElement() .accept( - new SimpleElementVisitor7, Class>() { + new SimpleElementVisitor8, Class>() { @Override public Class visitType( - TypeElement e, Class v) { + TypeElement te, Class v) { try { @SuppressWarnings( "signature") // https://tinyurl.com/cfissue/658: // Name.toString should be @PolySignature - @BinaryName String cname = e.getQualifiedName().toString(); + @BinaryName String cname = te.getQualifiedName().toString(); return Class.forName(cname); - } catch (ClassNotFoundException e1) { - return null; // the lookup should work for all - // the classes we care about + } catch (ClassNotFoundException e) { + // The lookup should work for all the classes we care about. + throw new Error(e); } } }, @@ -529,7 +595,7 @@ public Class visitType( * @param type a TypeMirror * @return the class corresponding to the argument */ - private static final Class typeMirrorToClass(final TypeMirror type) { + private static Class typeMirrorToClass(TypeMirror type) { return type.accept(typeMirrorToClassVisitor, Void.TYPE); } } 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 125cfb266b82..5a0622769352 100644 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/i18nformatter/I18nFormatterVisitor.java @@ -2,8 +2,7 @@ import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.type.TypeMirror; + import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.formatter.FormatterTreeUtil.InvocationType; import org.checkerframework.checker.formatter.FormatterTreeUtil.Result; @@ -13,9 +12,14 @@ import org.checkerframework.checker.i18nformatter.qual.I18nFormatFor; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; -import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreeUtils; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeMirror; /** * Whenever a method with {@link I18nFormatFor} annotation is invoked, it will perform the format @@ -31,10 +35,8 @@ public I18nFormatterVisitor(BaseTypeChecker checker) { @Override public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { - MethodInvocationNode nodeNode = - atypeFactory.getFirstNodeOfKindForTree(tree, MethodInvocationNode.class); I18nFormatterTreeUtil tu = atypeFactory.treeUtil; - I18nFormatCall fc = tu.createFormatForCall(tree, nodeNode, atypeFactory); + I18nFormatCall fc = tu.createFormatForCall(tree, atypeFactory); if (fc != null) { checkInvocationFormatFor(fc); return p; @@ -47,8 +49,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,17 +60,17 @@ 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(); int paraml = paramTypes.length; int formatl = formatCats.length; - // For assignments, i18nformat.missing.arguments and - // i18nformat.excess.arguments are issued - // from commonAssignmentCheck. + // For assignments, "i18nformat.missing.arguments" and + // "i18nformat.excess.arguments" are + // issued from commonAssignmentCheck(). if (paraml < formatl) { tu.warning(invc, "i18nformat.missing.arguments", formatl, paraml); } @@ -89,9 +89,15 @@ private void checkInvocationFormatFor(I18nFormatCall fc) { break; default: if (!fc.isValidParameter(formatCat, paramType)) { + ExecutableElement method = + TreeUtils.elementFromUse(fc.getTree()); + CharSequence methodName = + ElementUtils.getSimpleDescription(method); tu.failure( param, "argument.type.incompatible", + "", // parameter name is not useful + methodName, paramType, formatCat); } @@ -118,21 +124,26 @@ private void checkInvocationFormatFor(I18nFormatCall fc) { } @Override - protected void commonAssignmentCheck( + protected boolean commonAssignmentCheck( AnnotatedTypeMirror varType, AnnotatedTypeMirror valueType, Tree valueTree, - @CompilerMessageKey String errorKey) { + @CompilerMessageKey String errorKey, + Object... extraArgs) { + boolean result = true; + AnnotationMirror rhs = valueType.getAnnotationInHierarchy(atypeFactory.I18NUNKNOWNFORMAT); AnnotationMirror lhs = varType.getAnnotationInHierarchy(atypeFactory.I18NUNKNOWNFORMAT); - // i18nformat.missing.arguments and i18nformat.excess.arguments are issued here for + // "i18nformat.missing.arguments" and "i18nformat.excess.arguments" are issued here for // assignments. - // For method calls, they are issued in checkInvocationFormatFor. + // For method calls, they are issued in checkInvocationFormatFor(). if (rhs != null && lhs != null - && AnnotationUtils.areSameByName(rhs, atypeFactory.I18NFORMAT) - && AnnotationUtils.areSameByName(lhs, atypeFactory.I18NFORMAT)) { + && AnnotationUtils.areSameByName( + rhs, I18nFormatterAnnotatedTypeFactory.I18NFORMAT_NAME) + && AnnotationUtils.areSameByName( + lhs, I18nFormatterAnnotatedTypeFactory.I18NFORMAT_NAME)) { I18nConversionCategory[] rhsArgTypes = atypeFactory.treeUtil.formatAnnotationToCategories(rhs); I18nConversionCategory[] lhsArgTypes = @@ -148,21 +159,27 @@ protected void commonAssignmentCheck( varType.toString(), valueType.toString()); } else if (rhsArgTypes.length > lhsArgTypes.length) { - // Since it is known that too many conversion categories were provided, - // issue a more specific error message to that effect than - // assignment.type.incompatible. + // Since it is known that too many conversion categories were provided, issue a more + // specific error message to that effect than "assignment.type.incompatible". checker.reportError( valueTree, "i18nformat.excess.arguments", varType.toString(), valueType.toString()); + result = false; } } - // By calling super.commonAssignmentCheck last, any i18nformat.excess.arguments message - // 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); + /// TODO: What does "take precedence over" mean? Both are issued, but the + /// "i18nformat.excess.arguments" appears first in the output. Is this meant to not call + /// super.commonAssignmentCheck() if `result` is already false? + // By calling super.commonAssignmentCheck() last, any "i18nformat.excess.arguments" + // message issued for a given line of code will take precedence over the + // "assignment.type.incompatible" + // issued by super.commonAssignmentCheck(). + result = + super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs) + && result; + return result; } } 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 deleted file mode 100644 index d1d816917c94..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/i18nformatter/qual/I18nConversionCategory.java +++ /dev/null @@ -1,182 +0,0 @@ -package org.checkerframework.checker.i18nformatter.qual; - -import java.util.Arrays; -import java.util.Date; -import java.util.HashSet; -import java.util.Set; - -/** - * 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}";
    - * 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 - */ -public enum I18nConversionCategory { - - /** - * Use if a parameter is not used by the formatter. - * - *
    -     * MessageFormat.format("{1}", a, b);
    -     * 
    - * - * Only the second argument ("b") is used. The first argument ("a") is ignored - */ - UNUSED(null /* everything */, null), - - /** Use if the parameter can be of any type. */ - GENERAL(null /* everything */, null), - - /** Use if the parameter can be of date, time, or number types. */ - DATE(new Class[] {Date.class, Number.class}, new String[] {"date", "time"}), - - /** - * Use if the parameter can be of number or choice types. An example of choice: - * - *
    {@code
    -     * format("{0, choice, 0#zero|1#one|1<{0, number} is more than 1}", 2)
    -     * }
    - * - * This will print "2 is more than 1". - */ - NUMBER(new Class[] {Number.class}, new String[] {"number", "choice"}); - - @SuppressWarnings("ImmutableEnumChecker") // TODO: clean this up! - public final Class[] types; - - @SuppressWarnings("ImmutableEnumChecker") // TODO: clean this up! - public final String[] strings; - - I18nConversionCategory(Class[] types, String[] strings) { - this.types = types; - this.strings = strings; - } - - /** Used by {@link #stringToI18nConversionCategory}. */ - static I18nConversionCategory[] namedCategories = new I18nConversionCategory[] {DATE, NUMBER}; - - /** - * Creates a conversion cagetogry from a string name. - * - *
    -     * I18nConversionCategory.stringToI18nConversionCategory("number") == I18nConversionCategory.NUMBER;
    -     * 
    - * - * @return the I18nConversionCategory associated with the given string - */ - public static I18nConversionCategory stringToI18nConversionCategory(String string) { - string = string.toLowerCase(); - for (I18nConversionCategory v : namedCategories) { - for (String s : v.strings) { - if (s.equals(string)) { - return v; - } - } - } - throw new IllegalArgumentException("Invalid format type " + string); - } - - private static Set arrayToSet(E[] a) { - return new HashSet<>(Arrays.asList(a)); - } - - /** - * Return true if a is a subset of b. - * - * @return true if a is a subset of b - */ - public static boolean isSubsetOf(I18nConversionCategory a, I18nConversionCategory b) { - return intersect(a, b) == a; - } - - /** - * Returns the intersection of the two given I18nConversionCategories. - * - *
    - * - *
    -     * I18nConversionCategory.intersect(DATE, NUMBER) == NUMBER;
    -     * 
    - * - *
    - */ - public static I18nConversionCategory intersect( - I18nConversionCategory a, I18nConversionCategory b) { - if (a == UNUSED) { - return b; - } - if (b == UNUSED) { - return a; - } - if (a == GENERAL) { - return b; - } - if (b == GENERAL) { - return a; - } - - Set> as = arrayToSet(a.types); - Set> bs = arrayToSet(b.types); - as.retainAll(bs); // intersection - for (I18nConversionCategory v : new I18nConversionCategory[] {DATE, NUMBER}) { - Set> vs = arrayToSet(v.types); - if (vs.equals(as)) { - return v; - } - } - throw new RuntimeException(); - } - - /** - * Returns the union of the two given I18nConversionCategories. - * - *
    -     * I18nConversionCategory.intersect(DATE, NUMBER) == DATE;
    -     * 
    - */ - public static I18nConversionCategory union(I18nConversionCategory a, I18nConversionCategory b) { - if (a == UNUSED || b == UNUSED) { - return UNUSED; - } - if (a == GENERAL || b == GENERAL) { - return GENERAL; - } - if (a == DATE || b == DATE) { - return DATE; - } - return NUMBER; - } - - /** Returns a pretty printed {@link I18nConversionCategory}. */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(this.name()); - if (this.types == null) { - sb.append(" conversion category (all types)"); - } else { - sb.append(" conversion category (one of: "); - boolean first = true; - for (Class cls : this.types) { - if (!first) { - sb.append(", "); - } - sb.append(cls.getCanonicalName()); - first = false; - } - sb.append(")"); - } - return sb.toString(); - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/index/BaseAnnotatedTypeFactoryForIndexChecker.java b/checker/src/main/java/org/checkerframework/checker/index/BaseAnnotatedTypeFactoryForIndexChecker.java new file mode 100644 index 000000000000..ab390781238a --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/index/BaseAnnotatedTypeFactoryForIndexChecker.java @@ -0,0 +1,77 @@ +package org.checkerframework.checker.index; + +import org.checkerframework.checker.index.qual.HasSubsequence; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; + +/** + * A class for functionality common to multiple type-checkers that are used by the Index Checker. + */ +public abstract class BaseAnnotatedTypeFactoryForIndexChecker extends BaseAnnotatedTypeFactory { + + /** The from() element/field of a @HasSubsequence annotation. */ + protected final ExecutableElement hasSubsequenceFromElement = + TreeUtils.getMethod(HasSubsequence.class, "from", 0, processingEnv); + + /** The to() element/field of a @HasSubsequence annotation. */ + protected final ExecutableElement hasSubsequenceToElement = + TreeUtils.getMethod(HasSubsequence.class, "to", 0, processingEnv); + + /** The subsequence() element/field of a @HasSubsequence annotation. */ + protected final ExecutableElement hasSubsequenceSubsequenceElement = + TreeUtils.getMethod(HasSubsequence.class, "subsequence", 0, processingEnv); + + /** + * Creates a new BaseAnnotatedTypeFactoryForIndexChecker. + * + * @param checker the checker + */ + public BaseAnnotatedTypeFactoryForIndexChecker(BaseTypeChecker checker) { + super(checker); + } + + /** + * Gets the from() element/field out of a HasSubsequence annotation. + * + * @param anno a HasSubsequence annotation + * @return its from() element/field + */ + public String hasSubsequenceFromValue(AnnotationMirror anno) { + String result = + AnnotationUtils.getElementValue(anno, hasSubsequenceFromElement, String.class); + assert result != null : "@AssumeAssertion(nullness)"; + return result; + } + + /** + * Gets the to() element/field out of a HasSubsequence annotation. + * + * @param anno a HasSubsequence annotation + * @return its to() element/field + */ + public String hasSubsequenceToValue(AnnotationMirror anno) { + String result = + AnnotationUtils.getElementValue(anno, hasSubsequenceToElement, String.class); + assert result != null : "@AssumeAssertion(nullness)"; + return result; + } + + /** + * Gets the subsequence() element/field out of a HasSubsequence annotation. + * + * @param anno a HasSubsequence annotation + * @return its subsequence() element/field + */ + public String hasSubsequenceSubsequenceValue(AnnotationMirror anno) { + String result = + AnnotationUtils.getElementValue( + anno, hasSubsequenceSubsequenceElement, String.class); + assert result != null : "@AssumeAssertion(nullness)"; + return result; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/index/IndexAbstractTransfer.java b/checker/src/main/java/org/checkerframework/checker/index/IndexAbstractTransfer.java index 50db72d695e3..2ce0f5a0403e 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/IndexAbstractTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/index/IndexAbstractTransfer.java @@ -1,6 +1,5 @@ package org.checkerframework.checker.index; -import javax.lang.model.element.AnnotationMirror; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.node.GreaterThanNode; @@ -13,6 +12,8 @@ import org.checkerframework.framework.flow.CFTransfer; import org.checkerframework.framework.flow.CFValue; +import javax.lang.model.element.AnnotationMirror; + /** * This class provides methods shared by the Index Checker's internal checkers in their transfer * functions. In particular, it provides a common framework for visiting comparison operators. diff --git a/checker/src/main/java/org/checkerframework/checker/index/IndexChecker.java b/checker/src/main/java/org/checkerframework/checker/index/IndexChecker.java index 7e031e381379..5dd34ae41b56 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/IndexChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/index/IndexChecker.java @@ -76,4 +76,9 @@ * * @checker_framework.manual #index-checker Index Checker */ -public class IndexChecker extends UpperBoundChecker {} +// @RelevantJavaTypes annotations appear on other checkers. +public class IndexChecker extends UpperBoundChecker { + + /** Creates the Index Checker. */ + public IndexChecker() {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/index/IndexMethodIdentifier.java b/checker/src/main/java/org/checkerframework/checker/index/IndexMethodIdentifier.java index 2adf239cd357..75d232b2db61 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/IndexMethodIdentifier.java +++ b/checker/src/main/java/org/checkerframework/checker/index/IndexMethodIdentifier.java @@ -2,11 +2,7 @@ import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; -import java.util.List; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; + import org.checkerframework.checker.index.qual.LengthOf; import org.checkerframework.dataflow.cfg.node.MethodAccessNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; @@ -15,30 +11,53 @@ import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.ExecutableElement; + /** - * This class stores information about interesting methods and allows its clients to query it to - * determine if a method belongs to a particular class. + * Given a Tree or other construct, this class has methods to query whether it is a particular + * method call. */ public class IndexMethodIdentifier { /** The {@code java.lang.Math#random()} method. */ private final ExecutableElement mathRandom; + /** The {@code java.util.Random#nextDouble()} method. */ private final ExecutableElement randomNextDouble; + /** The {@code java.util.Random#nextInt()} method. */ private final ExecutableElement randomNextInt; + /** The {@code java.lang.String#length()} method. */ private final ExecutableElement stringLength; + /** The {@code java.lang.Math#min()} methods. */ private final List mathMinMethods; + /** The {@code java.lang.Math#max()} methods. */ private final List mathMaxMethods; - private final AnnotatedTypeFactory factory; + /** The LengthOf.value argument/element. */ + private final ExecutableElement lengthOfValueElement; + + /** + * The {@code java.lang.String#indexOf} and {@code #lastIndexOf} methods that take a string as a + * non-receiver parameter. + */ + private final List indexOfStringMethods; - public IndexMethodIdentifier(AnnotatedTypeFactory factory) { - this.factory = factory; - ProcessingEnvironment processingEnv = factory.getProcessingEnv(); + /** The type factory. */ + private final AnnotatedTypeFactory atypeFactory; + + public IndexMethodIdentifier(AnnotatedTypeFactory atypeFactory) { + this.atypeFactory = atypeFactory; + ProcessingEnvironment processingEnv = atypeFactory.getProcessingEnv(); mathRandom = TreeUtils.getMethod("java.lang.Math", "random", 0, processingEnv); randomNextDouble = TreeUtils.getMethod("java.util.Random", "nextDouble", 0, processingEnv); randomNextInt = TreeUtils.getMethod("java.util.Random", "nextInt", 1, processingEnv); @@ -47,17 +66,55 @@ public IndexMethodIdentifier(AnnotatedTypeFactory factory) { mathMinMethods = TreeUtils.getMethods("java.lang.Math", "min", 2, processingEnv); mathMaxMethods = TreeUtils.getMethods("java.lang.Math", "max", 2, processingEnv); + + indexOfStringMethods = new ArrayList<>(4); + indexOfStringMethods.add( + TreeUtils.getMethod( + "java.lang.String", "indexOf", processingEnv, "java.lang.String")); + indexOfStringMethods.add( + TreeUtils.getMethod( + "java.lang.String", "indexOf", processingEnv, "java.lang.String", "int")); + indexOfStringMethods.add( + TreeUtils.getMethod( + "java.lang.String", "lastIndexOf", processingEnv, "java.lang.String")); + indexOfStringMethods.add( + TreeUtils.getMethod( + "java.lang.String", + "lastIndexOf", + processingEnv, + "java.lang.String", + "int")); + + lengthOfValueElement = TreeUtils.getMethod(LengthOf.class, "value", 0, processingEnv); + } + + /** + * Returns true iff the argument is an invocation of String#indexOf or String#lastIndexOf that + * takes a string parameter. + * + * @param methodTree the method invocation tree to be tested + * @return true iff the argument is an invocation of one of String's indexOf or lastIndexOf + * methods that takes another string as a parameter + */ + public boolean isIndexOfString(Tree methodTree) { + ProcessingEnvironment processingEnv = atypeFactory.getProcessingEnv(); + return TreeUtils.isMethodInvocation(methodTree, indexOfStringMethods, processingEnv); } - /** Returns true iff the argument is an invocation of Math.min. */ + /** + * Returns true iff the argument is an invocation of Math.min. + * + * @param methodTree the method invocation tree to be tested + * @return true iff the argument is an invocation of Math.min() + */ public boolean isMathMin(Tree methodTree) { - ProcessingEnvironment processingEnv = factory.getProcessingEnv(); + ProcessingEnvironment processingEnv = atypeFactory.getProcessingEnv(); return TreeUtils.isMethodInvocation(methodTree, mathMinMethods, processingEnv); } /** Returns true iff the argument is an invocation of Math.max. */ public boolean isMathMax(Tree methodTree) { - ProcessingEnvironment processingEnv = factory.getProcessingEnv(); + ProcessingEnvironment processingEnv = atypeFactory.getProcessingEnv(); return TreeUtils.isMethodInvocation(methodTree, mathMaxMethods, processingEnv); } @@ -84,7 +141,7 @@ public boolean isRandomNextInt(Tree tree, ProcessingEnvironment processingEnv) { * this} */ public boolean isLengthOfMethodInvocation(Tree tree) { - if (tree.getKind() != Kind.METHOD_INVOCATION) { + if (tree.getKind() != Tree.Kind.METHOD_INVOCATION) { return false; } return isLengthOfMethodInvocation(TreeUtils.elementFromUse((MethodInvocationTree) tree)); @@ -103,13 +160,12 @@ public boolean isLengthOfMethodInvocation(ExecutableElement ele) { return true; } - AnnotationMirror len = factory.getDeclAnnotation(ele, LengthOf.class); - if (len == null) { + AnnotationMirror lengthOfAnno = atypeFactory.getDeclAnnotation(ele, LengthOf.class); + if (lengthOfAnno == null) { return false; } - List values = - AnnotationUtils.getElementValueArray(len, "value", String.class, false); - return values.contains("this"); + AnnotationValue lengthOfValue = lengthOfAnno.getElementValues().get(lengthOfValueElement); + return AnnotationUtils.annotationValueContains(lengthOfValue, "this"); } /** diff --git a/checker/src/main/java/org/checkerframework/checker/index/IndexRefinementInfo.java b/checker/src/main/java/org/checkerframework/checker/index/IndexRefinementInfo.java index 7e4ac3fa569c..31e4c5d28542 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/IndexRefinementInfo.java +++ b/checker/src/main/java/org/checkerframework/checker/index/IndexRefinementInfo.java @@ -1,7 +1,6 @@ package org.checkerframework.checker.index; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.ConditionalTransferResult; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.node.BinaryOperationNode; @@ -10,7 +9,10 @@ import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.TypeSystemError; + +import javax.lang.model.element.AnnotationMirror; /** * This struct contains all of the information that the refinement functions need. It's called by @@ -20,24 +22,51 @@ */ public class IndexRefinementInfo { - public Node left, right; + /** The left operand. */ + public final Node left; + + /** The right operand. */ + public final Node right; /** - * Annotation for left and right expressions. Might be null if dataflow doesn't have a value for - * the expression. + * Annotation for left expressions. Might be null if dataflow doesn't have a value for the + * expression. */ - public AnnotationMirror leftAnno, rightAnno; + public final @Nullable AnnotationMirror leftAnno; + + /** + * Annotation for right expressions. Might be null if dataflow doesn't have a value for the + * expression. + */ + public final @Nullable AnnotationMirror rightAnno; + + /** The then store. */ + public final CFStore thenStore; - public CFStore thenStore, elseStore; - public ConditionalTransferResult newResult; + /** The else store. */ + public final CFStore elseStore; + /** The new result, after refinement. */ + public final ConditionalTransferResult newResult; + + /** + * Creates a new IndexRefinementInfo. + * + * @param left the left operand + * @param right the right operand + * @param result the new result, after refinement + * @param analysis the CFAbstractAnalysis + */ public IndexRefinementInfo( TransferResult result, CFAbstractAnalysis analysis, - Node r, - Node l) { - right = r; - left = l; + Node right, + Node left) { + this.right = right; + this.left = left; + + thenStore = result.getThenStore(); + elseStore = result.getElseStore(); if (analysis.getValue(right) == null || analysis.getValue(left) == null) { leftAnno = null; @@ -48,10 +77,6 @@ public IndexRefinementInfo( QualifierHierarchy hierarchy = analysis.getTypeFactory().getQualifierHierarchy(); rightAnno = getAnno(analysis.getValue(right).getAnnotations(), hierarchy); leftAnno = getAnno(analysis.getValue(left).getAnnotations(), hierarchy); - - thenStore = result.getThenStore(); - elseStore = result.getElseStore(); - newResult = new ConditionalTransferResult<>(result.getResultValue(), thenStore, elseStore); } @@ -64,11 +89,17 @@ public IndexRefinementInfo( this(result, analysis, node.getRightOperand(), node.getLeftOperand()); } - private static AnnotationMirror getAnno( - Set set, QualifierHierarchy hierarchy) { - Set tops = hierarchy.getTopAnnotations(); + /** + * Returns the annotation (from the given set) in the given hierarchy. + * + * @param set a set of annotations + * @param hierarchy a qualifier hierarchy + * @return the annotation (from {@code set}) in the given hierarchy + */ + private static AnnotationMirror getAnno(AnnotationMirrorSet set, QualifierHierarchy hierarchy) { + AnnotationMirrorSet tops = hierarchy.getTopAnnotations(); if (tops.size() != 1) { - throw new BugInCF( + throw new TypeSystemError( "%s: Found %d tops, but expected one.%nFound: %s", IndexRefinementInfo.class, tops.size(), tops); } diff --git a/checker/src/main/java/org/checkerframework/checker/index/IndexUtil.java b/checker/src/main/java/org/checkerframework/checker/index/IndexUtil.java index 62c63ddcad8f..9f86692a4e40 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/IndexUtil.java +++ b/checker/src/main/java/org/checkerframework/checker/index/IndexUtil.java @@ -4,21 +4,35 @@ import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; + import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypesUtils; /** A collection of utility functions used by several Index Checker subcheckers. */ public class IndexUtil { - /** Determines whether the type is a sequence supported by this checker. */ + + /** Do not instantiate IndexUtil. */ + private IndexUtil() { + throw new Error("Do not instantiate IndexUtil."); + } + + /** + * Returns true if the type is a sequence supported by this checker. + * + * @param type a type + * @return true if the type is a sequence supported by this checker + */ public static boolean isSequenceType(TypeMirror type) { return type.getKind() == TypeKind.ARRAY || TypesUtils.isString(type); } /** Gets a sequence tree for a length access tree, or null if it is not a length access. */ - public static ExpressionTree getLengthSequenceTree( + public static @Nullable ExpressionTree getLengthSequenceTree( Tree lengthTree, IndexMethodIdentifier imf, ProcessingEnvironment processingEnv) { if (TreeUtils.isArrayLengthAccess(lengthTree)) { return ((MemberSelectTree) lengthTree).getExpression(); diff --git a/checker/src/main/java/org/checkerframework/checker/index/OffsetDependentTypesHelper.java b/checker/src/main/java/org/checkerframework/checker/index/OffsetDependentTypesHelper.java index 2fa6bd1e3ba1..316ac4f43f2a 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/OffsetDependentTypesHelper.java +++ b/checker/src/main/java/org/checkerframework/checker/index/OffsetDependentTypesHelper.java @@ -1,15 +1,13 @@ package org.checkerframework.checker.index; import com.sun.source.tree.MemberSelectTree; -import com.sun.source.util.TreePath; -import org.checkerframework.checker.index.upperbound.OffsetEquation; -import org.checkerframework.dataflow.analysis.FlowExpressions; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.value.ValueCheckerUtils; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.framework.util.FlowExpressionParseUtil; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionContext; -import org.checkerframework.framework.util.dependenttypes.DependentTypesError; import org.checkerframework.framework.util.dependenttypes.DependentTypesHelper; import org.checkerframework.framework.util.dependenttypes.DependentTypesTreeAnnotator; import org.checkerframework.javacutil.TreeUtils; @@ -19,63 +17,18 @@ * addition or subtraction of several Java expressions. For example, {@code array.length - 1}. */ public class OffsetDependentTypesHelper extends DependentTypesHelper { - public OffsetDependentTypesHelper(AnnotatedTypeFactory factory) { - super(factory); + public OffsetDependentTypesHelper(AnnotatedTypeFactory atypeFactory) { + super(atypeFactory); } @Override - protected String standardizeString( - final String expression, - FlowExpressionContext context, - TreePath localScope, - boolean useLocalScope) { - if (DependentTypesError.isExpressionError(expression)) { - return expression; - } - if (expression.indexOf('-') == -1 && expression.indexOf('+') == -1) { - // The expression contains no "-" or "+", so it can be standardized directly. - FlowExpressions.Receiver result; - try { - result = - FlowExpressionParseUtil.parse( - expression, context, localScope, useLocalScope); - } catch (FlowExpressionParseUtil.FlowExpressionParseException e) { - return new DependentTypesError(expression, e).toString(); - } - if (result == null) { - return new DependentTypesError(expression, " ").toString(); - } - if (result instanceof FlowExpressions.FieldAccess - && ((FlowExpressions.FieldAccess) result).isFinal()) { - Object constant = - ((FlowExpressions.FieldAccess) result).getField().getConstantValue(); - if (constant != null && !(constant instanceof String)) { - return constant.toString(); - } - } - return result.toString(); - } - - // The expression is a sum of several terms. This expression is standardized by splitting it - // into individual terms in an OffsetEquation and standardizing each term. - OffsetEquation equation = OffsetEquation.createOffsetFromJavaExpression(expression); - if (equation.hasError()) { - return equation.getError(); - } - try { - // Standardize individual terms of the expression. - equation.standardizeAndViewpointAdaptExpressions( - context, localScope, useLocalScope, factory); - } catch (FlowExpressionParseUtil.FlowExpressionParseException e) { - return new DependentTypesError(expression, e).toString(); - } - - return equation.toString(); + protected @Nullable JavaExpression transform(JavaExpression javaExpr) { + return ValueCheckerUtils.optimize(javaExpr, atypeFactory); } @Override - public TreeAnnotator createDependentTypesTreeAnnotator(AnnotatedTypeFactory factory) { - return new DependentTypesTreeAnnotator(factory, this) { + public TreeAnnotator createDependentTypesTreeAnnotator() { + return new DependentTypesTreeAnnotator(atypeFactory, this) { @Override public Void visitMemberSelect(MemberSelectTree tree, AnnotatedTypeMirror type) { // UpperBoundTreeAnnotator changes the type of array.length to @LTEL("array"). diff --git a/checker/src/main/java/org/checkerframework/checker/index/Subsequence.java b/checker/src/main/java/org/checkerframework/checker/index/Subsequence.java index 632550c761a1..fca1e8aa2ceb 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/Subsequence.java +++ b/checker/src/main/java/org/checkerframework/checker/index/Subsequence.java @@ -1,28 +1,30 @@ package org.checkerframework.checker.index; import com.sun.source.tree.Tree; -import com.sun.source.util.TreePath; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; + import org.checkerframework.checker.index.qual.HasSubsequence; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.FieldAccess; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; -import org.checkerframework.framework.type.AnnotatedTypeFactory; -import org.checkerframework.framework.util.BaseContext; -import org.checkerframework.framework.util.FlowExpressionParseUtil; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionContext; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionParseException; -import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.framework.source.SourceChecker; +import org.checkerframework.framework.util.JavaExpressionParseUtil; +import org.checkerframework.framework.util.JavaExpressionParseUtil.JavaExpressionParseException; +import org.checkerframework.framework.util.StringToJavaExpression; import org.checkerframework.javacutil.TreeUtils; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.VariableElement; + /** Holds information from {@link HasSubsequence} annotations. */ public class Subsequence { /** Name of the Subsequence. */ public final String array; + /** First index of the subsequence in the backing sequence. */ public final String from; + /** Last index of the subsequence in the backing sequence. */ public final String to; @@ -43,7 +45,8 @@ private Subsequence(String array, String from, String to) { * @param factory an AnnotatedTypeFactory * @return null or a new Subsequence from the declaration of {@code varTree} */ - public static Subsequence getSubsequenceFromTree(Tree varTree, AnnotatedTypeFactory factory) { + public static @Nullable Subsequence getSubsequenceFromTree( + Tree varTree, BaseAnnotatedTypeFactoryForIndexChecker factory) { if (!(varTree.getKind() == Tree.Kind.IDENTIFIER || varTree.getKind() == Tree.Kind.MEMBER_SELECT @@ -53,27 +56,24 @@ public static Subsequence getSubsequenceFromTree(Tree varTree, AnnotatedTypeFact Element element = TreeUtils.elementFromTree(varTree); AnnotationMirror hasSub = factory.getDeclAnnotation(element, HasSubsequence.class); - return createSubsequence(hasSub, null, null); + return createSubsequence(hasSub, factory); } /** + * Factory method to create a representation of a subsequence. + * * @param hasSub {@link HasSubsequence} annotation or null + * @param factory the type factory * @return a new Subsequence object representing {@code hasSub} or null */ - private static Subsequence createSubsequence( - AnnotationMirror hasSub, TreePath currentPath, FlowExpressionContext context) { + private static @Nullable Subsequence createSubsequence( + AnnotationMirror hasSub, BaseAnnotatedTypeFactoryForIndexChecker factory) { if (hasSub == null) { return null; } - String from = AnnotationUtils.getElementValue(hasSub, "from", String.class, false); - String to = AnnotationUtils.getElementValue(hasSub, "to", String.class, false); - String array = AnnotationUtils.getElementValue(hasSub, "subsequence", String.class, false); - - if (context != null && currentPath != null) { - from = standardizeAndViewpointAdapt(from, currentPath, context); - to = standardizeAndViewpointAdapt(to, currentPath, context); - array = standardizeAndViewpointAdapt(array, currentPath, context); - } + String from = factory.hasSubsequenceFromValue(hasSub); + String to = factory.hasSubsequenceToValue(hasSub); + String array = factory.hasSubsequenceSubsequenceValue(hasSub); return new Subsequence(array, from, to); } @@ -82,77 +82,69 @@ private static Subsequence createSubsequence( * Returns a Subsequence representing the {@link HasSubsequence} annotation on the declaration * of {@code rec} or null if there is not such annotation. * - * @param rec some tree + * @param expr some tree * @param factory an AnnotatedTypeFactory - * @param currentPath the path at which to viewpoint adapt the subsequence - * @param context the context in which to viewpoint adapt the subsequence * @return null or a new Subsequence from the declaration of {@code varTree} */ - public static Subsequence getSubsequenceFromReceiver( - Receiver rec, - AnnotatedTypeFactory factory, - TreePath currentPath, - FlowExpressionContext context) { - if (rec == null) { + public static @Nullable Subsequence getSubsequenceFromReceiver( + JavaExpression expr, BaseAnnotatedTypeFactoryForIndexChecker factory) { + if (!(expr instanceof FieldAccess)) { return null; } - Element element; - if (rec instanceof FieldAccess) { - element = ((FieldAccess) rec).getField(); - } else { + FieldAccess fa = (FieldAccess) expr; + VariableElement element = fa.getField(); + AnnotationMirror hasSub = factory.getDeclAnnotation(element, HasSubsequence.class); + if (hasSub == null) { return null; } - return createSubsequence( - factory.getDeclAnnotation(element, HasSubsequence.class), currentPath, context); - } - /* - * Helper function to standardize and viewpoint adapt a String given a path and a context. - * Wraps FlowExpressionParseUtil#parse. If a parse exception is encountered, this returns - * its argument. - */ - private static String standardizeAndViewpointAdapt( - String s, TreePath currentPath, FlowExpressionContext context) { - try { - return FlowExpressionParseUtil.parse(s, context, currentPath, false).toString(); - } catch (FlowExpressionParseException e) { - return s; - } + String array = + standardizeAndViewpointAdapt( + factory.hasSubsequenceSubsequenceValue(hasSub), fa, factory.getChecker()); + String from = + standardizeAndViewpointAdapt( + factory.hasSubsequenceFromValue(hasSub), fa, factory.getChecker()); + String to = + standardizeAndViewpointAdapt( + factory.hasSubsequenceToValue(hasSub), fa, factory.getChecker()); + + return new Subsequence(array, from, to); } /** - * If the passed receiver is a FieldAccess, returns the context associated with it. Otherwise - * returns null. + * Helper function to standardize and viewpoint-adapt a string at a given field access. Wraps + * {@link JavaExpressionParseUtil#parse}. If a parse exception is encountered, this returns its + * argument. * - *

    Used to standardize and viewpoint adapt arguments to HasSubsequence annotations. + * @param s a Java expression string + * @param fieldAccess the field access + * @param checker used to parse the expression + * @return the argument, standardized and viewpoint-adapted */ - public static FlowExpressionContext getContextFromReceiver(Receiver rec, BaseContext checker) { - if (rec == null) { - return null; + private static String standardizeAndViewpointAdapt( + String s, FieldAccess fieldAccess, SourceChecker checker) { + JavaExpression parseResult; + try { + parseResult = StringToJavaExpression.atFieldDecl(s, fieldAccess.getField(), checker); + } catch (JavaExpressionParseException e) { + return s; } - if (rec instanceof FlowExpressions.FieldAccess) { - FieldAccess fa = (FlowExpressions.FieldAccess) rec; - return new FlowExpressionParseUtil.FlowExpressionContext( - fa.getReceiver(), null, checker); - } else { - return null; - } + return parseResult.atFieldAccess(fieldAccess.getReceiver()).toString(); } /** * Returns the additive inverse of the given String. That is, if the result of this method is * some String s', then s + s' == 0 will evaluate to true. Note that this relies on the fact - * that the Flow Expression Parser cannot parse multiplication, so it naively just changes '-' - * to '+' and vice-versa. + * that the JavaExpression parser cannot parse multiplication, so it naively just changes '-' to + * '+' and vice-versa. * - *

    The passed String is standardized and viewpoint adapted before this transformation is - * applied. + * @param s a Java expression string + * @return the negated string */ - public static String negateString( - String s, TreePath currentPath, FlowExpressionContext context) { - String original = standardizeAndViewpointAdapt(s, currentPath, context); + public static String negateString(String s) { + String original = s; String result = ""; if (!original.startsWith("-")) { result += '-'; diff --git a/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanAnnotatedTypeFactory.java index 9c41b21608b1..641527d52316 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanAnnotatedTypeFactory.java @@ -3,21 +3,14 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.type.TypeKind; + +import org.checkerframework.checker.index.BaseAnnotatedTypeFactoryForIndexChecker; import org.checkerframework.checker.index.OffsetDependentTypesHelper; import org.checkerframework.checker.index.qual.LessThan; import org.checkerframework.checker.index.qual.LessThanBottom; import org.checkerframework.checker.index.qual.LessThanUnknown; import org.checkerframework.checker.index.upperbound.OffsetEquation; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.value.ValueAnnotatedTypeFactory; import org.checkerframework.common.value.ValueChecker; @@ -26,23 +19,49 @@ import org.checkerframework.common.value.qual.ArrayLenRange; import org.checkerframework.common.value.qual.IntRange; import org.checkerframework.common.value.qual.IntVal; -import org.checkerframework.dataflow.analysis.FlowExpressions.FieldAccess; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.ElementQualifierHierarchy; import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionParseException; -import org.checkerframework.framework.util.GraphQualifierHierarchy; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory; +import org.checkerframework.framework.util.JavaExpressionParseUtil.JavaExpressionParseException; import org.checkerframework.framework.util.dependenttypes.DependentTypesHelper; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.plumelib.util.CollectionsPlume; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; -public class LessThanAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { - private final AnnotationMirror BOTTOM = +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.util.Elements; + +/** The type factory for the Less Than Checker. */ +public class LessThanAnnotatedTypeFactory extends BaseAnnotatedTypeFactoryForIndexChecker { + /** The @LessThanBottom annotation. */ + private final AnnotationMirror LESS_THAN_BOTTOM = AnnotationBuilder.fromClass(elements, LessThanBottom.class); - public final AnnotationMirror UNKNOWN = + + /** The @LessThanUnknown annotation. */ + public final AnnotationMirror LESS_THAN_UNKNOWN = AnnotationBuilder.fromClass(elements, LessThanUnknown.class); + /** The LessThan.value argument/element. */ + private final ExecutableElement lessThanValueElement = + TreeUtils.getMethod(LessThan.class, "value", 0, processingEnv); + + /** + * Creates a new LessThanAnnotatedTypeFactory. + * + * @param checker the type-checker associated with this type factory + */ public LessThanAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); postInit(); @@ -70,18 +89,26 @@ protected DependentTypesHelper createDependentTypesHelper() { } @Override - public QualifierHierarchy createQualifierHierarchy(MultiGraphFactory factory) { - return new LessThanQualifierHierarchy(factory); + protected QualifierHierarchy createQualifierHierarchy() { + return new LessThanQualifierHierarchy(this.getSupportedTypeQualifiers(), elements); } - class LessThanQualifierHierarchy extends GraphQualifierHierarchy { - - public LessThanQualifierHierarchy(MultiGraphFactory f) { - super(f, BOTTOM); + /** LessThanQualifierHierarchy. */ + class LessThanQualifierHierarchy extends ElementQualifierHierarchy { + + /** + * Creates a LessThanQualifierHierarchy from the given classes. + * + * @param qualifierClasses classes of annotations that are the qualifiers + * @param elements element utils + */ + public LessThanQualifierHierarchy( + Set> qualifierClasses, Elements elements) { + super(qualifierClasses, elements, LessThanAnnotatedTypeFactory.this); } @Override - public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { + public boolean isSubtypeQualifiers(AnnotationMirror subAnno, AnnotationMirror superAnno) { List subList = getLessThanExpressions(subAnno); if (subList == null) { return true; @@ -95,35 +122,33 @@ public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { } @Override - public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2) { - if (isSubtype(a1, a2)) { + public AnnotationMirror leastUpperBoundQualifiers( + AnnotationMirror a1, AnnotationMirror a2) { + if (isSubtypeQualifiers(a1, a2)) { return a2; - } else if (isSubtype(a2, a1)) { + } else if (isSubtypeQualifiers(a2, a1)) { return a1; } List a1List = getLessThanExpressions(a1); List a2List = getLessThanExpressions(a2); - List lub = new ArrayList<>(a1List); - lub.retainAll(a2List); - - return createLessThanQualifier(lub); + a1List.retainAll(a2List); // intersection + return createLessThanQualifier(a1List); } @Override - public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror a2) { - if (isSubtype(a1, a2)) { + public AnnotationMirror greatestLowerBoundQualifiers( + AnnotationMirror a1, AnnotationMirror a2) { + if (isSubtypeQualifiers(a1, a2)) { return a1; - } else if (isSubtype(a2, a1)) { + } else if (isSubtypeQualifiers(a2, a1)) { return a2; } List a1List = getLessThanExpressions(a1); List a2List = getLessThanExpressions(a2); - List glb = new ArrayList<>(a1List); - glb.addAll(a2List); - - return createLessThanQualifier(glb); + CollectionsPlume.adjoinAll(a1List, a2List); // union + return createLessThanQualifier(a1List); } } @@ -136,7 +161,7 @@ public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror */ public boolean isLessThan(Tree left, String right) { AnnotatedTypeMirror leftATM = getAnnotatedType(left); - return isLessThan(leftATM.getAnnotationInHierarchy(UNKNOWN), right); + return isLessThan(leftATM.getAnnotationInHierarchy(LESS_THAN_UNKNOWN), right); } /** @@ -146,9 +171,10 @@ public boolean isLessThan(Tree left, String right) { * @param right the second value to compare (an expression) * @return is left less than right? */ - public static boolean isLessThan(AnnotationMirror left, String right) { + public boolean isLessThan(AnnotationMirror left, String right) { List expressions = getLessThanExpressions(left); if (expressions == null) { + // `left` is @LessThanBottom return true; } return expressions.contains(right); @@ -159,10 +185,11 @@ public static boolean isLessThan(AnnotationMirror left, String right) { * * @param smaller the first value to compare * @param bigger the second value to compare + * @param path used to parse expressions strings * @return {@code smaller < bigger}, using information from the Value Checker */ public boolean isLessThanByValue(Tree smaller, String bigger, TreePath path) { - Long smallerValue = ValueCheckerUtils.getMinValue(smaller, getValueAnnotatedTypeFactory()); + Long smallerValue = ValueCheckerUtils.getMaxValue(smaller, getValueAnnotatedTypeFactory()); if (smallerValue == null) { return false; } @@ -189,48 +216,45 @@ public boolean isLessThanByValue(Tree smaller, String bigger, TreePath path) { * @param expression the expression whose minimum value to retrieve * @param tree where to determine the value * @param path the path to {@code tree} + * @return the minimum value of {@code expression} at {@code tree} */ private long getMinValueFromString(String expression, Tree tree, TreePath path) { - Receiver expressionRec; + ValueAnnotatedTypeFactory valueAtypeFactory = getValueAnnotatedTypeFactory(); + JavaExpression expressionJe; try { - expressionRec = - getValueAnnotatedTypeFactory() - .getReceiverFromJavaExpressionString(expression, path); - } catch (FlowExpressionParseException e) { + expressionJe = valueAtypeFactory.parseJavaExpressionString(expression, path); + } catch (JavaExpressionParseException e) { return Long.MIN_VALUE; } AnnotationMirror intRange = - getValueAnnotatedTypeFactory() - .getAnnotationFromReceiver(expressionRec, tree, IntRange.class); + valueAtypeFactory.getAnnotationFromJavaExpression( + expressionJe, tree, IntRange.class); if (intRange != null) { - return ValueAnnotatedTypeFactory.getRange(intRange).from; + return valueAtypeFactory.getRange(intRange).from; } AnnotationMirror intValue = - getValueAnnotatedTypeFactory() - .getAnnotationFromReceiver(expressionRec, tree, IntVal.class); + valueAtypeFactory.getAnnotationFromJavaExpression(expressionJe, tree, IntVal.class); if (intValue != null) { - List possibleValues = ValueAnnotatedTypeFactory.getIntValues(intValue); + List possibleValues = valueAtypeFactory.getIntValues(intValue); return Collections.min(possibleValues); } - if (expressionRec instanceof FieldAccess) { - FieldAccess fieldAccess = ((FieldAccess) expressionRec); + if (expressionJe instanceof FieldAccess) { + FieldAccess fieldAccess = ((FieldAccess) expressionJe); if (fieldAccess.getReceiver().getType().getKind() == TypeKind.ARRAY) { // array.length might not be in the store, so check for the length of the array. AnnotationMirror arrayRange = - getValueAnnotatedTypeFactory() - .getAnnotationFromReceiver( - fieldAccess.getReceiver(), tree, ArrayLenRange.class); + valueAtypeFactory.getAnnotationFromJavaExpression( + fieldAccess.getReceiver(), tree, ArrayLenRange.class); if (arrayRange != null) { - return ValueAnnotatedTypeFactory.getRange(arrayRange).from; + return valueAtypeFactory.getRange(arrayRange).from; } AnnotationMirror arrayLen = - getValueAnnotatedTypeFactory() - .getAnnotationFromReceiver(expressionRec, tree, ArrayLen.class); + valueAtypeFactory.getAnnotationFromJavaExpression( + expressionJe, tree, ArrayLen.class); if (arrayLen != null) { - List possibleValues = - ValueAnnotatedTypeFactory.getArrayLength(arrayLen); + List possibleValues = valueAtypeFactory.getArrayLength(arrayLen); return Collections.min(possibleValues); } // Even arrays that we know nothing about must have at least zero length. @@ -250,7 +274,7 @@ private long getMinValueFromString(String expression, Tree tree, TreePath path) */ public boolean isLessThanOrEqual(Tree left, String right) { AnnotatedTypeMirror leftATM = getAnnotatedType(left); - return isLessThanOrEqual(leftATM.getAnnotationInHierarchy(UNKNOWN), right); + return isLessThanOrEqual(leftATM.getAnnotationInHierarchy(LESS_THAN_UNKNOWN), right); } /** @@ -260,7 +284,7 @@ public boolean isLessThanOrEqual(Tree left, String right) { * @param right the second value to compare * @return is left less than or equal to right? */ - public static boolean isLessThanOrEqual(AnnotationMirror left, String right) { + public boolean isLessThanOrEqual(AnnotationMirror left, String right) { List expressions = getLessThanExpressions(left); if (expressions == null) { // left is bottom so it is always less than right. @@ -269,12 +293,10 @@ public static boolean isLessThanOrEqual(AnnotationMirror left, String right) { if (expressions.contains(right)) { return true; } - // {@code @LessThan("end + 1")} is equivalent to {@code @LessThanOrEqual("end")}. for (String expression : expressions) { - if (expression.endsWith(" + 1")) { - if (expression.substring(0, expression.length() - 4).equals(right)) { - return true; - } + if (expression.endsWith(" + 1") + && expression.substring(0, expression.length() - 4).equals(right)) { + return true; } } return false; @@ -283,23 +305,30 @@ public static boolean isLessThanOrEqual(AnnotationMirror left, String right) { /** * Returns a sorted, modifiable list of expressions that {@code expression} is less than. If the * {@code expression} is annotated with {@link LessThanBottom}, null is returned. + * + * @param expression an expression + * @return expressions that {@code expression} is less than */ - public List getLessThanExpressions(ExpressionTree expression) { + public @Nullable List getLessThanExpressions(ExpressionTree expression) { AnnotatedTypeMirror annotatedTypeMirror = getAnnotatedType(expression); - return getLessThanExpressions(annotatedTypeMirror.getAnnotationInHierarchy(UNKNOWN)); + return getLessThanExpressions( + annotatedTypeMirror.getAnnotationInHierarchy(LESS_THAN_UNKNOWN)); } /** * Creates a less than qualifier given the expressions. * *

    If expressions is null, {@link LessThanBottom} is returned. If expressions is empty, - * {@link LessThanUnknown} is returned. Otherwise, {@code LessThan(expressions)} is returned. + * {@link LessThanUnknown} is returned. Otherwise, {@code @LessThan(expressions)} is returned. + * + * @param expressions a list of expressions + * @return a @LessThan qualifier with the given arguments */ - public AnnotationMirror createLessThanQualifier(List expressions) { + public AnnotationMirror createLessThanQualifier(@Nullable List expressions) { if (expressions == null) { - return BOTTOM; + return LESS_THAN_BOTTOM; } else if (expressions.isEmpty()) { - return UNKNOWN; + return LESS_THAN_UNKNOWN; } else { AnnotationBuilder builder = new AnnotationBuilder(processingEnv, LessThan.class); builder.setValue("value", expressions); @@ -313,19 +342,24 @@ public AnnotationMirror createLessThanQualifier(String expression) { } /** - * Returns a modifiable list of expressions in the annotation sorted. If the annotation is - * {@link LessThanBottom}, return null. If the annotation is {@link LessThanUnknown} return the - * empty list. + * If the annotation is LessThan, returns a list of expressions in the annotation. If the + * annotation is {@link LessThanBottom}, returns null. If the annotation is {@link + * LessThanUnknown}, returns the empty list. + * + * @param annotation an annotation from the same hierarchy as LessThan + * @return the list of expressions in the annotation */ - public static List getLessThanExpressions(AnnotationMirror annotation) { - if (AnnotationUtils.areSameByClass(annotation, LessThanBottom.class)) { + public @Nullable List getLessThanExpressions(AnnotationMirror annotation) { + if (AnnotationUtils.areSameByName( + annotation, "org.checkerframework.checker.index.qual.LessThanBottom")) { return null; - } else if (AnnotationUtils.areSameByClass(annotation, LessThanUnknown.class)) { - return new ArrayList<>(); + } else if (AnnotationUtils.areSameByName( + annotation, "org.checkerframework.checker.index.qual.LessThanUnknown")) { + return Collections.emptyList(); } else { - List list = - AnnotationUtils.getElementValueArray(annotation, "value", String.class, true); - return list; + // The annotation is @LessThan. + return AnnotationUtils.getElementValueArray( + annotation, lessThanValueElement, String.class); } } } diff --git a/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanChecker.java b/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanChecker.java index 26a5abf83d0a..43cafb61bf31 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanChecker.java @@ -1,10 +1,12 @@ package org.checkerframework.checker.index.inequality; -import java.util.LinkedHashSet; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.value.ValueChecker; +import org.checkerframework.framework.qual.RelevantJavaTypes; import org.checkerframework.framework.source.SuppressWarningsPrefix; +import java.util.Set; + /** * An internal checker that estimates which expression's values are less than other expressions' * values. @@ -12,11 +14,22 @@ * @checker_framework.manual #index-checker Index Checker */ @SuppressWarningsPrefix({"index", "lessthan"}) +@RelevantJavaTypes({ + Byte.class, + Short.class, + Integer.class, + Long.class, + Character.class, + byte.class, + short.class, + int.class, + long.class, + char.class, +}) public class LessThanChecker extends BaseTypeChecker { @Override - protected LinkedHashSet> getImmediateSubcheckerClasses() { - LinkedHashSet> checkers = - super.getImmediateSubcheckerClasses(); + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); checkers.add(ValueChecker.class); return checkers; } diff --git a/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanTransfer.java b/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanTransfer.java index b4e6e7b8b532..b3552e7b31c2 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/index/inequality/LessThanTransfer.java @@ -1,26 +1,27 @@ package org.checkerframework.checker.index.inequality; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.type.TypeKind; import org.checkerframework.checker.index.IndexAbstractTransfer; import org.checkerframework.common.value.ValueAnnotatedTypeFactory; import org.checkerframework.common.value.ValueCheckerUtils; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; -import org.checkerframework.dataflow.analysis.FlowExpressions.ValueLiteral; import org.checkerframework.dataflow.analysis.RegularTransferResult; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.cfg.node.NumericalSubtractionNode; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.ValueLiteral; import org.checkerframework.framework.flow.CFAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFValue; -import org.checkerframework.framework.util.FlowExpressionParseUtil; +import org.checkerframework.framework.util.JavaExpressionParseUtil; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.plumelib.util.CollectionsPlume; + +import java.util.Collections; +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.type.TypeKind; /** * Implements 3 refinement rules: @@ -48,23 +49,26 @@ protected void refineGT( AnnotationMirror rightAnno, CFStore store, TransferInput in) { - LessThanAnnotatedTypeFactory factory = - (LessThanAnnotatedTypeFactory) analysis.getTypeFactory(); // left > right so right < left // Refine right to @LessThan("left") - Receiver leftRec = FlowExpressions.internalReprOf(factory, left); - if (leftRec != null && leftRec.isUnassignableByOtherCode()) { - List lessThanExpressions = - LessThanAnnotatedTypeFactory.getLessThanExpressions(rightAnno); + JavaExpression leftJe = JavaExpression.fromNode(left); + if (leftJe != null && leftJe.isUnassignableByOtherCode()) { + if (isDoubleOrFloatLiteral(leftJe)) { + return; + } + LessThanAnnotatedTypeFactory factory = + (LessThanAnnotatedTypeFactory) analysis.getTypeFactory(); + List lessThanExpressions = factory.getLessThanExpressions(rightAnno); if (lessThanExpressions == null) { // right is already bottom, nothing to refine. return; } - if (!isDoubleOrFloatLiteral(leftRec)) { - lessThanExpressions.add(leftRec.toString()); + String leftString = leftJe.toString(); + if (!lessThanExpressions.contains(leftString)) { + lessThanExpressions = CollectionsPlume.append(lessThanExpressions, leftString); + JavaExpression rightJe = JavaExpression.fromNode(right); + store.insertValue(rightJe, factory.createLessThanQualifier(lessThanExpressions)); } - Receiver rightRec = FlowExpressions.internalReprOf(analysis.getTypeFactory(), right); - store.insertValue(rightRec, factory.createLessThanQualifier(lessThanExpressions)); } } @@ -80,23 +84,26 @@ protected void refineGTE( // left >= right so right is less than left // Refine right to @LessThan("left + 1") - LessThanAnnotatedTypeFactory factory = - (LessThanAnnotatedTypeFactory) analysis.getTypeFactory(); // left > right so right is less than left // Refine right to @LessThan("left") - Receiver leftRec = FlowExpressions.internalReprOf(factory, left); - if (leftRec != null && leftRec.isUnassignableByOtherCode()) { - List lessThanExpressions = - LessThanAnnotatedTypeFactory.getLessThanExpressions(rightAnno); + JavaExpression leftJe = JavaExpression.fromNode(left); + if (leftJe != null && leftJe.isUnassignableByOtherCode()) { + if (isDoubleOrFloatLiteral(leftJe)) { + return; + } + LessThanAnnotatedTypeFactory factory = + (LessThanAnnotatedTypeFactory) analysis.getTypeFactory(); + List lessThanExpressions = factory.getLessThanExpressions(rightAnno); if (lessThanExpressions == null) { // right is already bottom, nothing to refine. return; } - if (!isDoubleOrFloatLiteral(leftRec)) { - lessThanExpressions.add(leftRec.toString() + " + 1"); + String leftIncremented = incrementedExpression(leftJe); + if (!lessThanExpressions.contains(leftIncremented)) { + lessThanExpressions = CollectionsPlume.append(lessThanExpressions, leftIncremented); + JavaExpression rightJe = JavaExpression.fromNode(right); + store.insertValue(rightJe, factory.createLessThanQualifier(lessThanExpressions)); } - Receiver rightRec = FlowExpressions.internalReprOf(analysis.getTypeFactory(), right); - store.insertValue(rightRec, factory.createLessThanQualifier(lessThanExpressions)); } } @@ -106,18 +113,19 @@ public TransferResult visitNumericalSubtraction( NumericalSubtractionNode n, TransferInput in) { LessThanAnnotatedTypeFactory factory = (LessThanAnnotatedTypeFactory) analysis.getTypeFactory(); - Receiver leftRec = FlowExpressions.internalReprOf(factory, n.getLeftOperand()); - if (leftRec != null && leftRec.isUnassignableByOtherCode()) { + JavaExpression leftJe = JavaExpression.fromNode(n.getLeftOperand()); + if (leftJe != null && leftJe.isUnassignableByOtherCode()) { ValueAnnotatedTypeFactory valueFactory = factory.getValueAnnotatedTypeFactory(); Long right = ValueCheckerUtils.getMinValue(n.getRightOperand().getTree(), valueFactory); if (right != null && 0 < right) { // left - right < left iff 0 < right List expressions = getLessThanExpressions(n.getLeftOperand()); - if (expressions == null) { - expressions = new ArrayList<>(); - } - if (!isDoubleOrFloatLiteral(leftRec)) { - expressions.add(leftRec.toString()); + if (!isDoubleOrFloatLiteral(leftJe)) { + if (expressions == null) { + expressions = Collections.singletonList(leftJe.toString()); + } else { + expressions = CollectionsPlume.append(expressions, leftJe.toString()); + } } AnnotationMirror refine = factory.createLessThanQualifier(expressions); CFValue value = analysis.createSingleAnnotationValue(refine, n.getType()); @@ -128,29 +136,58 @@ public TransferResult visitNumericalSubtraction( return super.visitNumericalSubtraction(n, in); } - /** Return the expressions that {@code node} are less than. */ + /** + * Return the expressions that {@code node} is less than. + * + * @param node a CFG node + * @return the expressions that {@code node} is less than + */ private List getLessThanExpressions(Node node) { - Set s = analysis.getValue(node).getAnnotations(); - LessThanAnnotatedTypeFactory factory = - (LessThanAnnotatedTypeFactory) analysis.getTypeFactory(); + AnnotationMirrorSet s = analysis.getValue(node).getAnnotations(); if (s != null && !s.isEmpty()) { - return LessThanAnnotatedTypeFactory.getLessThanExpressions( - factory.getQualifierHierarchy().findAnnotationInHierarchy(s, factory.UNKNOWN)); + LessThanAnnotatedTypeFactory factory = + (LessThanAnnotatedTypeFactory) analysis.getTypeFactory(); + return factory.getLessThanExpressions( + factory.getQualifierHierarchy() + .findAnnotationInHierarchy(s, factory.LESS_THAN_UNKNOWN)); } else { return Collections.emptyList(); } } /** - * Return true if {@code receiver} is a double or float literal, which can't be parsed by {@link - * FlowExpressionParseUtil}. + * Return true if {@code expr} is a double or float literal, which can't be parsed by {@link + * JavaExpressionParseUtil}. */ - private boolean isDoubleOrFloatLiteral(Receiver receiver) { - if (receiver instanceof ValueLiteral) { - return receiver.getType().getKind() == TypeKind.DOUBLE - || receiver.getType().getKind() == TypeKind.FLOAT; + private boolean isDoubleOrFloatLiteral(JavaExpression expr) { + if (expr instanceof ValueLiteral) { + return expr.getType().getKind() == TypeKind.DOUBLE + || expr.getType().getKind() == TypeKind.FLOAT; } else { return false; } } + + /** + * Return the string representation of {@code expr + 1}. + * + * @param expr a JavaExpression + * @return the string representation of {@code expr + 1} + */ + private String incrementedExpression(JavaExpression expr) { + expr = ValueCheckerUtils.optimize(expr, analysis.getTypeFactory()); + if (expr instanceof ValueLiteral) { + ValueLiteral literal = (ValueLiteral) expr; + if (literal.getValue() instanceof Number) { + long longLiteral = ((Number) literal.getValue()).longValue(); + if (longLiteral != Long.MAX_VALUE) { + return (longLiteral + 1) + "L"; + } + } + } + + // Could do more optimization to merge with a literal at end of `exprString`. Is that + // needed? + return expr + " + 1"; + } } 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 0d608e568953..85a5f6fafed5 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 @@ -3,17 +3,21 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.Tree; -import java.util.ArrayList; -import java.util.List; -import javax.lang.model.element.AnnotationMirror; + import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.index.Subsequence; import org.checkerframework.checker.index.upperbound.OffsetEquation; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.util.FlowExpressionParseUtil; +import org.checkerframework.framework.util.JavaExpressionParseUtil; +import org.plumelib.util.CollectionsPlume; +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; + +/** The visitor for the Less Than Checker. */ public class LessThanVisitor extends BaseTypeVisitor { private static final @CompilerMessageKey String FROM_GT_TO = "from.gt.to"; @@ -23,8 +27,13 @@ public LessThanVisitor(BaseTypeChecker checker) { } @Override - protected void commonAssignmentCheck( - Tree varTree, ExpressionTree valueTree, @CompilerMessageKey String errorKey) { + protected boolean commonAssignmentCheck( + Tree varTree, + ExpressionTree valueTree, + @CompilerMessageKey String errorKey, + Object... extraArgs) { + + boolean result = true; // check that when an assignment to a variable declared as @HasSubsequence(a, from, to) // occurs, from <= to. @@ -36,11 +45,13 @@ protected void commonAssignmentCheck( anm = atypeFactory.getAnnotationMirrorFromJavaExpressionString( subSeq.from, varTree, getCurrentPath()); - } catch (FlowExpressionParseUtil.FlowExpressionParseException e) { + } catch (JavaExpressionParseUtil.JavaExpressionParseException e) { anm = null; } - if (anm == null || !LessThanAnnotatedTypeFactory.isLessThanOrEqual(anm, subSeq.to)) { + LessThanAnnotatedTypeFactory factory = getTypeFactory(); + + if (anm == null || !factory.isLessThanOrEqual(anm, subSeq.to)) { // issue an error checker.reportError( valueTree, @@ -50,25 +61,29 @@ protected void commonAssignmentCheck( anm == null ? "@LessThanUnknown" : anm, subSeq.to, subSeq.to); + result = false; } } - super.commonAssignmentCheck(varTree, valueTree, errorKey); + result = super.commonAssignmentCheck(varTree, valueTree, errorKey, extraArgs) && result; + return result; } @Override - protected void commonAssignmentCheck( + protected boolean 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 - // is "a". + // Also skip the check if the only expression is "a + 1" and the valueTree is "a". List expressions = - LessThanAnnotatedTypeFactory.getLessThanExpressions( - varType.getEffectiveAnnotationInHierarchy(atypeFactory.UNKNOWN)); + getTypeFactory() + .getLessThanExpressions( + varType.getEffectiveAnnotationInHierarchy( + atypeFactory.LESS_THAN_UNKNOWN)); if (expressions != null) { boolean isLessThan = true; for (String expression : expressions) { @@ -95,30 +110,29 @@ protected void commonAssignmentCheck( commonAssignmentCheckEndDiagnostic( true, "isLessThan", varType, valueType, valueTree); // skip call to super, everything is OK. - return; + return true; } } - super.commonAssignmentCheck(varType, valueType, valueTree, errorKey); + return super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs); } @Override protected boolean isTypeCastSafe(AnnotatedTypeMirror castType, AnnotatedTypeMirror exprType) { AnnotationMirror exprLTAnno = - exprType.getEffectiveAnnotationInHierarchy(atypeFactory.UNKNOWN); + exprType.getEffectiveAnnotationInHierarchy(atypeFactory.LESS_THAN_UNKNOWN); if (exprLTAnno != null) { - List initialAnnotations = - LessThanAnnotatedTypeFactory.getLessThanExpressions(exprLTAnno); + LessThanAnnotatedTypeFactory factory = getTypeFactory(); + List initialAnnotations = factory.getLessThanExpressions(exprLTAnno); if (initialAnnotations != null) { - List updatedAnnotations = new ArrayList<>(); - - for (String annotation : initialAnnotations) { - OffsetEquation updatedAnnotation = - OffsetEquation.createOffsetFromJavaExpression(annotation); - updatedAnnotations.add(updatedAnnotation.toString()); - } + List updatedAnnotations = + CollectionsPlume.mapList( + annotation -> + OffsetEquation.createOffsetFromJavaExpression(annotation) + .toString(), + initialAnnotations); exprType.replaceAnnotation( atypeFactory.createLessThanQualifier(updatedAnnotations)); diff --git a/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundAnnotatedTypeFactory.java index dbcb799e3123..add0f7f22507 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundAnnotatedTypeFactory.java @@ -2,16 +2,13 @@ import com.sun.source.tree.BinaryTree; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LiteralTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; import com.sun.source.tree.UnaryTree; -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; + +import org.checkerframework.checker.index.BaseAnnotatedTypeFactoryForIndexChecker; import org.checkerframework.checker.index.IndexMethodIdentifier; import org.checkerframework.checker.index.inequality.LessThanAnnotatedTypeFactory; import org.checkerframework.checker.index.inequality.LessThanChecker; @@ -30,7 +27,9 @@ import org.checkerframework.checker.index.qual.SubstringIndexFor; import org.checkerframework.checker.index.searchindex.SearchIndexAnnotatedTypeFactory; import org.checkerframework.checker.index.searchindex.SearchIndexChecker; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.signedness.qual.SignedPositive; +import org.checkerframework.checker.signedness.qual.SignednessGlb; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.value.ValueAnnotatedTypeFactory; import org.checkerframework.common.value.ValueChecker; @@ -45,6 +44,14 @@ import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TreeUtils; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; + /** * Implements the introduction rules for the Lower Bound Checker. * @@ -84,39 +91,52 @@ * is positive. * */ -public class LowerBoundAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { +public class LowerBoundAnnotatedTypeFactory extends BaseAnnotatedTypeFactoryForIndexChecker { /** The canonical @{@link GTENegativeOne} annotation. */ public final AnnotationMirror GTEN1 = AnnotationBuilder.fromClass(elements, GTENegativeOne.class); + /** The canonical @{@link NonNegative} annotation. */ public final AnnotationMirror NN = AnnotationBuilder.fromClass(elements, NonNegative.class); + /** The canonical @{@link Positive} annotation. */ public final AnnotationMirror POS = AnnotationBuilder.fromClass(elements, Positive.class); + /** The bottom annotation. */ public final AnnotationMirror BOTTOM = AnnotationBuilder.fromClass(elements, LowerBoundBottom.class); + /** The canonical @{@link LowerBoundUnknown} annotation. */ public final AnnotationMirror UNKNOWN = AnnotationBuilder.fromClass(elements, LowerBoundUnknown.class); + /** The canonical @{@link PolyLowerBound} annotation. */ public final AnnotationMirror POLY = AnnotationBuilder.fromClass(elements, PolyLowerBound.class); + /** Predicates about method calls. */ private final IndexMethodIdentifier imf; + /** + * Create a new LowerBoundAnnotatedTypeFactory. + * + * @param checker the type-checker + */ public LowerBoundAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - // Any annotations that are aliased to @NonNegative, @Positive, - // or @GTENegativeOne must also be aliased in the constructor of - // ValueAnnotatedTypeFactory to the appropriate @IntRangeFrom* - // annotation. - addAliasedAnnotation(IndexFor.class, NN); - addAliasedAnnotation(IndexOrLow.class, GTEN1); - addAliasedAnnotation(IndexOrHigh.class, NN); - addAliasedAnnotation(LengthOf.class, NN); - addAliasedAnnotation(PolyIndex.class, POLY); - addAliasedAnnotation(SubstringIndexFor.class, GTEN1); + // Any annotations that are aliased to @NonNegative, @Positive, or @GTENegativeOne must also + // be aliased in the constructor of ValueAnnotatedTypeFactory to the appropriate + // @IntRangeFrom* annotation. + addAliasedTypeAnnotation(IndexFor.class, NN); + addAliasedTypeAnnotation(IndexOrLow.class, GTEN1); + addAliasedTypeAnnotation(IndexOrHigh.class, NN); + addAliasedTypeAnnotation(LengthOf.class, NN); + addAliasedTypeAnnotation(PolyIndex.class, POLY); + addAliasedTypeAnnotation(SubstringIndexFor.class, GTEN1); + + addAliasedTypeAnnotation(SignedPositive.class, NN); + addAliasedTypeAnnotation(SignednessGlb.class, NN); imf = new IndexMethodIdentifier(this); @@ -146,13 +166,13 @@ protected Set> createSupportedTypeQualifiers() { private void addLowerBoundTypeFromValueType( AnnotatedTypeMirror valueType, AnnotatedTypeMirror type) { AnnotationMirror anm = getLowerBoundAnnotationFromValueType(valueType); - if (!type.isAnnotatedInHierarchy(UNKNOWN)) { + if (!type.hasAnnotationInHierarchy(UNKNOWN)) { if (!areSameByClass(anm, LowerBoundUnknown.class)) { type.addAnnotation(anm); } return; } - if (qualHierarchy.isSubtype(anm, type.getAnnotationInHierarchy(UNKNOWN))) { + if (typeHierarchy.isSubtypeShallowEffective(anm, type)) { type.replaceAnnotation(anm); } } @@ -175,7 +195,15 @@ public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, bool // If dataflow shouldn't be used to compute this type, then do not use the result from // the Value Checker, because dataflow is used to compute that type. (Without this, // "int i = 1; --i;" fails.) - if (iUseFlow && tree != null && TreeUtils.isExpressionTree(tree)) { + if (tree != null + // Necessary to check that an ajava file isn't being parsed, because the call + // to the Value Checker's getAnnotatedType() method can fail during parsing: + // the check in GenericAnnotatedTypeFactory#addComputedTypeAnnotations only + // checks if the **current** type factory is parsing, not whether the parent + // checker's type factory is parsing. + && !ajavaTypes.isParsing() + && TreeUtils.isExpressionTree(tree) + && (iUseFlow || tree instanceof LiteralTree)) { AnnotatedTypeMirror valueType = getValueAnnotatedTypeFactory().getAnnotatedType(tree); addLowerBoundTypeFromValueType(valueType, type); } @@ -217,7 +245,7 @@ private AnnotationMirror getLowerBoundAnnotationFromValueType(AnnotatedTypeMirro } /** Determine the annotation that should be associated with a literal. */ - AnnotationMirror anmFromVal(long val) { + /*package-private*/ AnnotationMirror anmFromVal(long val) { if (val >= 1) { return POS; } else if (val >= 0) { @@ -333,10 +361,14 @@ public Void visitMethodInvocation(MethodInvocationTree tree, AnnotatedTypeMirror if (imf.isMathMax(tree)) { ExpressionTree left = tree.getArguments().get(0); ExpressionTree right = tree.getArguments().get(1); + AnnotatedTypeMirror leftType = getAnnotatedType(left); + AnnotatedTypeMirror rightType = getAnnotatedType(right); type.replaceAnnotation( - qualHierarchy.greatestLowerBound( - getAnnotatedType(left).getAnnotationInHierarchy(POS), - getAnnotatedType(right).getAnnotationInHierarchy(POS))); + qualHierarchy.greatestLowerBoundShallow( + leftType.getAnnotationInHierarchy(POS), + leftType.getUnderlyingType(), + rightType.getAnnotationInHierarchy(POS), + rightType.getUnderlyingType())); } return super.visitMethodInvocation(tree, type); } @@ -370,7 +402,7 @@ public Void visitBinary(BinaryTree tree, AnnotatedTypeMirror type) { * Looks up the minlen of a member select tree. Returns null if the tree doesn't represent an * array's length field. */ - Integer getMinLenFromMemberSelectTree(MemberSelectTree tree) { + /*package-private*/ @Nullable Integer getMinLenFromMemberSelectTree(MemberSelectTree tree) { if (TreeUtils.isArrayLengthAccess(tree)) { return ValueCheckerUtils.getMinLenFromTree(tree, getValueAnnotatedTypeFactory()); } @@ -381,7 +413,8 @@ Integer getMinLenFromMemberSelectTree(MemberSelectTree tree) { * Looks up the minlen of a method invocation tree. Returns null if the tree doesn't represent * an string length method. */ - Integer getMinLenFromMethodInvocationTree(MethodInvocationTree tree) { + /*package-private*/ @Nullable Integer getMinLenFromMethodInvocationTree( + MethodInvocationTree tree) { if (imf.isLengthOfMethodInvocation(tree)) { return ValueCheckerUtils.getMinLenFromTree(tree, getValueAnnotatedTypeFactory()); } @@ -398,7 +431,8 @@ Integer getMinLenFromMethodInvocationTree(MethodInvocationTree tree) { * @return an AnnotationMirror representing the result if the special case is valid, or null if * not */ - AnnotationMirror checkForMathRandomSpecialCase(NumericalMultiplicationNode node) { + /*package-private*/ @Nullable AnnotationMirror checkForMathRandomSpecialCase( + NumericalMultiplicationNode node) { AnnotationMirror forwardRes = checkForMathRandomSpecialCase( node.getLeftOperand().getTree(), node.getRightOperand().getTree()); @@ -415,10 +449,11 @@ AnnotationMirror checkForMathRandomSpecialCase(NumericalMultiplicationNode node) } /** - * Return true if randTree is a call to Math.random() or Random.nextDouble(), and arrLenTree is - * someArray.length. + * Return a non-null value if randTree is a call to Math.random() or Random.nextDouble(), and + * arrLenTree is someArray.length. */ - private AnnotationMirror checkForMathRandomSpecialCase(Tree randTree, Tree arrLenTree) { + private @Nullable AnnotationMirror checkForMathRandomSpecialCase( + Tree randTree, Tree arrLenTree) { if (randTree.getKind() == Tree.Kind.METHOD_INVOCATION && TreeUtils.isArrayLengthAccess(arrLenTree)) { MethodInvocationTree miTree = (MethodInvocationTree) randTree; diff --git a/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundChecker.java b/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundChecker.java index ea51e54a2fc7..adaa12abd55f 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundChecker.java @@ -1,13 +1,16 @@ package org.checkerframework.checker.index.lowerbound; -import java.util.HashSet; -import java.util.LinkedHashSet; import org.checkerframework.checker.index.inequality.LessThanChecker; import org.checkerframework.checker.index.searchindex.SearchIndexChecker; +import org.checkerframework.checker.signature.qual.FullyQualifiedName; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.value.ValueChecker; +import org.checkerframework.framework.qual.RelevantJavaTypes; import org.checkerframework.framework.source.SuppressWarningsPrefix; +import java.util.HashSet; +import java.util.Set; + /** * A type-checker for preventing fixed-length sequences such as arrays or strings from being * accessed with values that are too low. Normally bundled as part of the Index Checker. @@ -15,13 +18,25 @@ * @checker_framework.manual #index-checker Index Checker */ @SuppressWarningsPrefix({"index", "lowerbound"}) +@RelevantJavaTypes({ + Byte.class, + Short.class, + Integer.class, + Long.class, + Character.class, + byte.class, + short.class, + int.class, + long.class, + char.class, +}) public class LowerBoundChecker extends BaseTypeChecker { /** * These collection classes have some subtypes whose length can change and some subtypes whose * length cannot change. Lower bound checker warnings are skipped at uses of them. */ - private HashSet collectionBaseTypeNames; + private final HashSet collectionBaseTypeNames; /** * A type-checker for preventing fixed-length sequences such as arrays or strings from being @@ -36,7 +51,7 @@ public LowerBoundChecker() { } @Override - public boolean shouldSkipUses(String typeName) { + public boolean shouldSkipUses(@FullyQualifiedName String typeName) { if (collectionBaseTypeNames.contains(typeName)) { return true; } @@ -44,9 +59,8 @@ public boolean shouldSkipUses(String typeName) { } @Override - protected LinkedHashSet> getImmediateSubcheckerClasses() { - LinkedHashSet> checkers = - super.getImmediateSubcheckerClasses(); + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); checkers.add(ValueChecker.class); checkers.add(LessThanChecker.class); checkers.add(SearchIndexChecker.class); diff --git a/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundTransfer.java b/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundTransfer.java index 07557c1599bb..fdd566a4816d 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/index/lowerbound/LowerBoundTransfer.java @@ -5,10 +5,7 @@ import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; -import java.util.List; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.type.TypeKind; + import org.checkerframework.checker.index.IndexAbstractTransfer; import org.checkerframework.checker.index.IndexRefinementInfo; import org.checkerframework.checker.index.qual.GTENegativeOne; @@ -17,8 +14,6 @@ import org.checkerframework.checker.index.qual.Positive; import org.checkerframework.checker.index.upperbound.OffsetEquation; import org.checkerframework.common.value.ValueCheckerUtils; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; import org.checkerframework.dataflow.analysis.RegularTransferResult; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; @@ -33,14 +28,21 @@ import org.checkerframework.dataflow.cfg.node.NumericalSubtractionNode; import org.checkerframework.dataflow.cfg.node.SignedRightShiftNode; import org.checkerframework.dataflow.cfg.node.UnsignedRightShiftNode; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.flow.CFAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeFactory; -import org.checkerframework.framework.util.FlowExpressionParseUtil; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreeUtils; +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; + /** * Implements dataflow refinement rules based on tests: <, >, ==, and their derivatives. * @@ -163,24 +165,32 @@ public class LowerBoundTransfer extends IndexAbstractTransfer { /** The canonical {@link GTENegativeOne} annotation. */ public final AnnotationMirror GTEN1; + /** The canonical {@link NonNegative} annotation. */ public final AnnotationMirror NN; + /** The canonical {@link Positive} annotation. */ public final AnnotationMirror POS; + /** The canonical {@link LowerBoundUnknown} annotation. */ public final AnnotationMirror UNKNOWN; - // The ATF (Annotated Type Factory). - private LowerBoundAnnotatedTypeFactory aTypeFactory; + /** The annotated type factory. */ + private final LowerBoundAnnotatedTypeFactory atypeFactory; + /** + * Create a new LowerBoundTransfer. + * + * @param analysis the CFAnalysis + */ public LowerBoundTransfer(CFAnalysis analysis) { super(analysis); - aTypeFactory = (LowerBoundAnnotatedTypeFactory) analysis.getTypeFactory(); + atypeFactory = (LowerBoundAnnotatedTypeFactory) analysis.getTypeFactory(); // Initialize qualifiers. - GTEN1 = aTypeFactory.GTEN1; - NN = aTypeFactory.NN; - POS = aTypeFactory.POS; - UNKNOWN = aTypeFactory.UNKNOWN; + GTEN1 = atypeFactory.GTEN1; + NN = atypeFactory.NN; + POS = atypeFactory.POS; + UNKNOWN = atypeFactory.UNKNOWN; } /** @@ -196,7 +206,7 @@ private void notEqualToValue( Long integerLiteral = ValueCheckerUtils.getExactValue( - mLiteral.getTree(), aTypeFactory.getValueAnnotatedTypeFactory()); + mLiteral.getTree(), atypeFactory.getValueAnnotatedTypeFactory()); if (integerLiteral == null) { return; @@ -204,19 +214,19 @@ private void notEqualToValue( long intLiteral = integerLiteral.longValue(); if (intLiteral == 0) { - if (aTypeFactory.areSameByClass(otherAnno, NonNegative.class)) { + if (atypeFactory.areSameByClass(otherAnno, NonNegative.class)) { List internals = splitAssignments(otherNode); for (Node internal : internals) { - Receiver rec = FlowExpressions.internalReprOf(aTypeFactory, internal); - store.insertValue(rec, POS); + JavaExpression je = JavaExpression.fromNode(internal); + store.insertValue(je, POS); } } } else if (intLiteral == -1) { - if (aTypeFactory.areSameByClass(otherAnno, GTENegativeOne.class)) { + if (atypeFactory.areSameByClass(otherAnno, GTENegativeOne.class)) { List internals = splitAssignments(otherNode); for (Node internal : internals) { - Receiver rec = FlowExpressions.internalReprOf(aTypeFactory, internal); - store.insertValue(rec, NN); + JavaExpression je = JavaExpression.fromNode(internal); + store.insertValue(je, NN); } } } @@ -243,10 +253,9 @@ protected TransferResult strengthenAnnotationOfEqualTo( return result; } - // There is also special processing to look - // for literals on one side of the equals and a GTEN1 or NN on the other, so that - // those types can be promoted in the branch where their values are not equal to certain - // literals. + // There is also special processing to look for literals on one side of the equals and a + // GTEN1 or NN on the other, so that those types can be promoted in the branch where their + // values are not equal to certain literals. CFStore notEqualsStore = notEqualTo ? rfi.thenStore : rfi.elseStore; notEqualToValue(rfi.left, rfi.right, rfi.rightAnno, notEqualsStore); notEqualToValue(rfi.right, rfi.left, rfi.leftAnno, notEqualsStore); @@ -267,11 +276,11 @@ private void notEqualsLessThan( if (!isNonNegative(leftAnno) || !isNonNegative(otherAnno)) { return; } - Receiver otherRec = FlowExpressions.internalReprOf(aTypeFactory, otherNode); - if (aTypeFactory + JavaExpression otherJe = JavaExpression.fromNode(otherNode); + if (atypeFactory .getLessThanAnnotatedTypeFactory() - .isLessThanOrEqual(leftNode.getTree(), otherRec.toString())) { - store.insertValue(otherRec, POS); + .isLessThanOrEqual(leftNode.getTree(), otherJe.toString())) { + store.insertValue(otherJe, POS); } } @@ -297,18 +306,18 @@ protected void refineGT( return; } - Receiver leftRec = FlowExpressions.internalReprOf(aTypeFactory, left); + JavaExpression leftJe = JavaExpression.fromNode(left); if (AnnotationUtils.areSame(rightAnno, GTEN1)) { - store.insertValue(leftRec, NN); + store.insertValue(leftJe, NN); return; } if (AnnotationUtils.areSame(rightAnno, NN)) { - store.insertValue(leftRec, POS); + store.insertValue(leftJe, POS); return; } if (AnnotationUtils.areSame(rightAnno, POS)) { - store.insertValue(leftRec, POS); + store.insertValue(leftJe, POS); return; } } @@ -334,12 +343,15 @@ protected void refineGTE( return; } - Receiver leftRec = FlowExpressions.internalReprOf(aTypeFactory, left); + JavaExpression leftJe = JavaExpression.fromNode(left); AnnotationMirror newLBType = - aTypeFactory.getQualifierHierarchy().greatestLowerBound(rightAnno, leftAnno); + atypeFactory + .getQualifierHierarchy() + .greatestLowerBoundShallow( + rightAnno, right.getType(), leftAnno, left.getType()); - store.insertValue(leftRec, newLBType); + store.insertValue(leftJe, newLBType); } /** @@ -419,7 +431,7 @@ private AnnotationMirror getAnnotationForPlus( // Check if the right side's value is known at compile time. Long valRight = ValueCheckerUtils.getExactValue( - rightExprNode.getTree(), aTypeFactory.getValueAnnotatedTypeFactory()); + rightExprNode.getTree(), atypeFactory.getValueAnnotatedTypeFactory()); if (valRight != null) { return getAnnotationForLiteralPlus(valRight.intValue(), leftAnno); } @@ -429,7 +441,7 @@ private AnnotationMirror getAnnotationForPlus( // Check if the left side's value is known at compile time. Long valLeft = ValueCheckerUtils.getExactValue( - leftExprNode.getTree(), aTypeFactory.getValueAnnotatedTypeFactory()); + leftExprNode.getTree(), atypeFactory.getValueAnnotatedTypeFactory()); if (valLeft != null) { return getAnnotationForLiteralPlus(valLeft.intValue(), rightAnno); } @@ -439,16 +451,16 @@ private AnnotationMirror getAnnotationForPlus( * nn + * -> * * pos + gte-1 -> nn */ - if (aTypeFactory.areSameByClass(leftAnno, Positive.class) - && aTypeFactory.areSameByClass(rightAnno, Positive.class)) { + if (atypeFactory.areSameByClass(leftAnno, Positive.class) + && atypeFactory.areSameByClass(rightAnno, Positive.class)) { return POS; } - if (aTypeFactory.areSameByClass(leftAnno, NonNegative.class)) { + if (atypeFactory.areSameByClass(leftAnno, NonNegative.class)) { return rightAnno; } - if (aTypeFactory.areSameByClass(rightAnno, NonNegative.class)) { + if (atypeFactory.areSameByClass(rightAnno, NonNegative.class)) { return leftAnno; } @@ -480,7 +492,7 @@ private AnnotationMirror getAnnotationForMinus( Long valRight = ValueCheckerUtils.getExactValue( minusNode.getRightOperand().getTree(), - aTypeFactory.getValueAnnotatedTypeFactory()); + atypeFactory.getValueAnnotatedTypeFactory()); if (valRight != null) { AnnotationMirror leftAnno = getLowerBoundAnnotation(minusNode.getLeftOperand(), p); // Instead of a separate method for subtraction, add the negative of a constant. @@ -489,34 +501,33 @@ private AnnotationMirror getAnnotationForMinus( Tree leftExpr = minusNode.getLeftOperand().getTree(); Integer minLen = null; - // Check if the left side is a field access of an array's length, - // or invocation of String.length. If so, - // try to look up the MinLen of the array, and potentially keep + // Check if the left side is a field access of an array's length, or invocation of + // String.length. If so, try to look up the MinLen of the array, and potentially keep // this either NN or POS instead of GTEN1 or LBU. if (leftExpr.getKind() == Tree.Kind.MEMBER_SELECT) { MemberSelectTree mstree = (MemberSelectTree) leftExpr; - minLen = aTypeFactory.getMinLenFromMemberSelectTree(mstree); + minLen = atypeFactory.getMinLenFromMemberSelectTree(mstree); } else if (leftExpr.getKind() == Tree.Kind.METHOD_INVOCATION) { MethodInvocationTree mitree = (MethodInvocationTree) leftExpr; - minLen = aTypeFactory.getMinLenFromMethodInvocationTree(mitree); + minLen = atypeFactory.getMinLenFromMethodInvocationTree(mitree); } if (minLen != null) { - result = aTypeFactory.anmFromVal(minLen - valRight); + result = atypeFactory.anmFromVal(minLen - valRight); } return result; } OffsetEquation leftExpression = - OffsetEquation.createOffsetFromNode(minusNode.getLeftOperand(), aTypeFactory, '+'); + OffsetEquation.createOffsetFromNode(minusNode.getLeftOperand(), atypeFactory, '+'); if (leftExpression != null) { - if (aTypeFactory + if (atypeFactory .getLessThanAnnotatedTypeFactory() .isLessThan(minusNode.getRightOperand().getTree(), leftExpression.toString())) { return POS; } - if (aTypeFactory + if (atypeFactory .getLessThanAnnotatedTypeFactory() .isLessThanOrEqual( minusNode.getRightOperand().getTree(), leftExpression.toString())) { @@ -567,7 +578,7 @@ private AnnotationMirror getAnnotationForMultiply( NumericalMultiplicationNode node, TransferInput p) { // Special handling for multiplying an array length by a Math.random(). - AnnotationMirror randomSpecialCaseResult = aTypeFactory.checkForMathRandomSpecialCase(node); + AnnotationMirror randomSpecialCaseResult = atypeFactory.checkForMathRandomSpecialCase(node); if (randomSpecialCaseResult != null) { return randomSpecialCaseResult; } @@ -578,7 +589,7 @@ private AnnotationMirror getAnnotationForMultiply( Long valRight = ValueCheckerUtils.getExactValue( node.getRightOperand().getTree(), - aTypeFactory.getValueAnnotatedTypeFactory()); + atypeFactory.getValueAnnotatedTypeFactory()); if (valRight != null) { return getAnnotationForLiteralMultiply(valRight.intValue(), leftAnno); } @@ -588,7 +599,7 @@ private AnnotationMirror getAnnotationForMultiply( Long valLeft = ValueCheckerUtils.getExactValue( node.getLeftOperand().getTree(), - aTypeFactory.getValueAnnotatedTypeFactory()); + atypeFactory.getValueAnnotatedTypeFactory()); if (valLeft != null) { return getAnnotationForLiteralMultiply(valLeft.intValue(), rightAnno); } @@ -631,7 +642,7 @@ private AnnotationMirror addAnnotationForLiteralDivideRight( // division by zero. Division by zero is treated as bottom so that users aren't warned // about dead code that's dividing by zero. This code assumes that non-dead code won't // include literal divide by zeros... - return aTypeFactory.BOTTOM; + return atypeFactory.BOTTOM; } else if (val == 1) { return leftAnno; } else if (val >= 2) { @@ -666,7 +677,7 @@ private AnnotationMirror getAnnotationForDivide( Long valRight = ValueCheckerUtils.getExactValue( node.getRightOperand().getTree(), - aTypeFactory.getValueAnnotatedTypeFactory()); + atypeFactory.getValueAnnotatedTypeFactory()); if (valRight != null) { return addAnnotationForLiteralDivideRight(valRight.intValue(), leftAnno); } @@ -677,7 +688,7 @@ private AnnotationMirror getAnnotationForDivide( Long valLeft = ValueCheckerUtils.getExactValue( node.getLeftOperand().getTree(), - aTypeFactory.getValueAnnotatedTypeFactory()); + atypeFactory.getValueAnnotatedTypeFactory()); if (valLeft != null) { return addAnnotationForLiteralDivideLeft(valLeft.intValue(), leftAnno); } @@ -718,19 +729,8 @@ protected void addInformationFromPreconditions( for (VariableTree variableTree : paramTrees) { if (TreeUtils.typeOf(variableTree).getKind() == TypeKind.CHAR) { - - Receiver rec = null; - try { - rec = - FlowExpressionParseUtil.internalReprOfVariable( - aTypeFactory, variableTree); - } catch (FlowExpressionParseUtil.FlowExpressionParseException e) { - // do nothing - } - - if (rec != null) { - info.insertValue(rec, aTypeFactory.NN); - } + JavaExpression je = JavaExpression.fromVariableTree(variableTree); + info.insertValuePermitNondeterministic(je, atypeFactory.NN); } } } @@ -754,7 +754,7 @@ public AnnotationMirror getAnnotationForRemainder( Long valRight = ValueCheckerUtils.getExactValue( node.getRightOperand().getTree(), - aTypeFactory.getValueAnnotatedTypeFactory()); + atypeFactory.getValueAnnotatedTypeFactory()); if (valRight != null) { return addAnnotationForLiteralRemainder(valRight.intValue()); } @@ -811,42 +811,51 @@ private AnnotationMirror getAnnotationForAnd( * Returns true if the argument is the @Positive type annotation. * * @param anm the annotation to test - * @return true if the the argument is the @Positive type annotation + * @return true if the argument is the @Positive type annotation */ private boolean isPositive(AnnotationMirror anm) { - return aTypeFactory.areSameByClass(anm, Positive.class); + return atypeFactory.areSameByClass(anm, Positive.class); } /** * Returns true if the argument is the @NonNegative type annotation (or a stronger one). * * @param anm the annotation to test - * @return true if the the argument is the @NonNegative type annotation + * @return true if the argument is the @NonNegative type annotation */ private boolean isNonNegative(AnnotationMirror anm) { - return aTypeFactory.areSameByClass(anm, NonNegative.class) || isPositive(anm); + return atypeFactory.areSameByClass(anm, NonNegative.class) || isPositive(anm); } /** * Returns true if the argument is the @GTENegativeOne type annotation (or a stronger one). * * @param anm the annotation to test - * @return true if the the argument is the @GTENegativeOne type annotation + * @return true if the argument is the @GTENegativeOne type annotation */ private boolean isGTEN1(AnnotationMirror anm) { - return aTypeFactory.areSameByClass(anm, GTENegativeOne.class) || isNonNegative(anm); + return atypeFactory.areSameByClass(anm, GTENegativeOne.class) || isNonNegative(anm); } private AnnotationMirror getLowerBoundAnnotation( Node subNode, TransferInput p) { CFValue value = p.getValueOfSubNode(subNode); + if (value == null) { + throw new BugInCF("value==null for getLowerBoundAnnotation(%s, %s)%n", subNode, p); + } return getLowerBoundAnnotation(value); } + /** + * Returns the lower bound annotation for the given value. + * + * @param cfValue a value + * @return the lower bound annotation for the given value + */ private AnnotationMirror getLowerBoundAnnotation(CFValue cfValue) { - return aTypeFactory + return atypeFactory .getQualifierHierarchy() - .findAnnotationInHierarchy(cfValue.getAnnotations(), aTypeFactory.UNKNOWN); + .findAnnotationInHierarchy(cfValue.getAnnotations(), atypeFactory.UNKNOWN); } @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 df3e84591d03..dfeef36f9b01 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 @@ -4,7 +4,7 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.NewArrayTree; import com.sun.source.tree.Tree; -import javax.lang.model.element.AnnotationMirror; + import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.index.Subsequence; import org.checkerframework.checker.index.qual.NonNegative; @@ -12,7 +12,9 @@ import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.util.FlowExpressionParseUtil; +import org.checkerframework.framework.util.JavaExpressionParseUtil; + +import javax.lang.model.element.AnnotationMirror; /** * Implements the actual checks to make sure that array accesses aren't too low. Will issue a @@ -61,12 +63,17 @@ public Void visitNewArray(NewArrayTree tree, Void type) { } @Override - protected void commonAssignmentCheck( - Tree varTree, ExpressionTree valueTree, @CompilerMessageKey String errorKey) { + protected boolean commonAssignmentCheck( + 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. + boolean result = true; + Subsequence subSeq = Subsequence.getSubsequenceFromTree(varTree, atypeFactory); if (subSeq != null) { AnnotationMirror anm; @@ -74,7 +81,7 @@ protected void commonAssignmentCheck( anm = atypeFactory.getAnnotationMirrorFromJavaExpressionString( subSeq.from, varTree, getCurrentPath()); - } catch (FlowExpressionParseUtil.FlowExpressionParseException e) { + } catch (JavaExpressionParseUtil.JavaExpressionParseException e) { anm = null; } if (anm == null @@ -85,9 +92,11 @@ protected void commonAssignmentCheck( FROM_NOT_NN, subSeq.from, anm == null ? "@LowerBoundUnknown" : anm); + result = false; } } - super.commonAssignmentCheck(varTree, valueTree, errorKey); + result = super.commonAssignmentCheck(varTree, valueTree, errorKey, extraArgs) && result; + return result; } } diff --git a/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenAnnotatedTypeFactory.java index b824ed069758..d713d95fad32 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenAnnotatedTypeFactory.java @@ -5,16 +5,7 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; -import javax.lang.model.element.AnnotationMirror; + import org.checkerframework.checker.index.IndexMethodIdentifier; import org.checkerframework.checker.index.IndexUtil; import org.checkerframework.checker.index.qual.PolyLength; @@ -22,21 +13,35 @@ import org.checkerframework.checker.index.qual.SameLen; import org.checkerframework.checker.index.qual.SameLenBottom; import org.checkerframework.checker.index.qual.SameLenUnknown; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.common.value.ValueCheckerUtils; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; +import org.checkerframework.dataflow.expression.ArrayCreation; +import org.checkerframework.dataflow.expression.ClassName; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.ValueLiteral; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.ElementQualifierHierarchy; import org.checkerframework.framework.type.QualifierHierarchy; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.framework.util.FlowExpressionParseUtil; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionParseException; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory; +import org.checkerframework.framework.util.JavaExpressionParseUtil; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.util.Elements; /** * The SameLen Checker is used to determine whether there are multiple fixed-length sequences (such @@ -71,19 +76,26 @@ public class SameLenAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The @{@link SameLenUnknown} annotation. */ public final AnnotationMirror UNKNOWN = AnnotationBuilder.fromClass(elements, SameLenUnknown.class); + /** The @{@link SameLenBottom} annotation. */ private final AnnotationMirror BOTTOM = AnnotationBuilder.fromClass(elements, SameLenBottom.class); + /** The @{@link PolySameLen} annotation. */ private final AnnotationMirror POLY = AnnotationBuilder.fromClass(elements, PolySameLen.class); + /** The SameLen.value field/element. */ + final ExecutableElement sameLenValueElement = + TreeUtils.getMethod(SameLen.class, "value", 0, processingEnv); + + /** Predicates about method calls. */ private final IndexMethodIdentifier imf = new IndexMethodIdentifier(this); /** Create a new SameLenAnnotatedTypeFactory. */ public SameLenAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - addAliasedAnnotation(PolyLength.class, POLY); + addAliasedTypeAnnotation(PolyLength.class, POLY); this.postInit(); } @@ -105,8 +117,8 @@ protected Set> createSupportedTypeQualifiers() { } @Override - public QualifierHierarchy createQualifierHierarchy(MultiGraphFactory factory) { - return new SameLenQualifierHierarchy(factory); + protected QualifierHierarchy createQualifierHierarchy() { + return new SameLenQualifierHierarchy(this.getSupportedTypeQualifiers(), elements); } // Handles case "user-written SameLen" @@ -115,29 +127,19 @@ public AnnotatedTypeMirror getAnnotatedTypeLhs(Tree tree) { AnnotatedTypeMirror atm = super.getAnnotatedTypeLhs(tree); if (tree.getKind() == Tree.Kind.VARIABLE) { - AnnotationMirror anm = atm.getAnnotation(SameLen.class); - if (anm != null) { - - Receiver r; - try { - r = FlowExpressionParseUtil.internalReprOfVariable(this, (VariableTree) tree); - } catch (FlowExpressionParseException ex) { - r = null; - } - - if (r != null) { - String varName = r.toString(); - - List exprs = - ValueCheckerUtils.getValueOfAnnotationWithStringArgument(anm); - if (exprs.contains(varName)) { - exprs.remove(varName); - } - if (exprs.isEmpty()) { - atm.replaceAnnotation(UNKNOWN); - } else { - atm.replaceAnnotation(createSameLen(exprs)); - } + AnnotationMirror sameLenAnno = atm.getAnnotation(SameLen.class); + if (sameLenAnno != null) { + JavaExpression je = JavaExpression.fromVariableTree((VariableTree) tree); + String varName = je.toString(); + + List exprs = + AnnotationUtils.getElementValueArray( + sameLenAnno, sameLenValueElement, String.class); + exprs.remove(varName); + if (exprs.isEmpty()) { + atm.replaceAnnotation(UNKNOWN); + } else { + atm.replaceAnnotation(createSameLen(exprs)); } } } @@ -146,13 +148,15 @@ public AnnotatedTypeMirror getAnnotatedTypeLhs(Tree tree) { } /** Returns true if the given expression may appear in a @SameLen annotation. */ - public static boolean mayAppearInSameLen(Receiver receiver) { - return !receiver.containsUnknown() - && !(receiver instanceof FlowExpressions.ArrayCreation) - && !(receiver instanceof FlowExpressions.ClassName) - // Big expressions cause a stack overflow in FlowExpressionParseUtil. + public static boolean mayAppearInSameLen(JavaExpression expr) { + return !expr.containsUnknown() + && !(expr instanceof ArrayCreation) + && !(expr instanceof ClassName) + // avoid SameLen expressions with e.g. literal String constants + && !(expr instanceof ValueLiteral) + // Big expressions cause a stack overflow in JavaExpressionParseUtil. // So limit them to an arbitrary length of 999. - && receiver.toString().length() < 1000; + && expr.toString().length() < 1000; } /** @@ -162,15 +166,17 @@ public static boolean mayAppearInSameLen(Receiver receiver) { * so @SameLen({"a","b","c"} and @SameLen({"c","f","g"} are actually the same type -- both * should usually be replaced by a SameLen with the union of the lists of names. */ - private final class SameLenQualifierHierarchy extends MultiGraphQualifierHierarchy { + private final class SameLenQualifierHierarchy extends ElementQualifierHierarchy { /** - * Create a SameLenQualifierHierarchy. + * Creates a SameLenQualifierHierarchy from the given classes. * - * @param factory the MultiGraphFactory to use to construct this + * @param qualifierClasses classes of annotations that are the qualifiers + * @param elements element utils */ - public SameLenQualifierHierarchy(MultiGraphQualifierHierarchy.MultiGraphFactory factory) { - super(factory); + public SameLenQualifierHierarchy( + Set> qualifierClasses, Elements elements) { + super(qualifierClasses, elements, SameLenAnnotatedTypeFactory.this); } @Override @@ -179,36 +185,57 @@ public AnnotationMirror getTopAnnotation(AnnotationMirror start) { } /** - * If the collections are disjoint, returns null. Otherwise, returns their union. The - * collections must not contain duplicates. + * If the collections are both non-empty and disjoint, returns null. Otherwise, returns + * their union. The collections must not contain duplicates. + * + * @param c1 a collection of Strings (intended to be the value argument of a SameLen + * annotation) + * @param c2 another collection of Strings + * @return if the two inputs are disjoint (i.e., have no elements in common) and both are + * non-empty, returns null. Otherwise, returns the union of the two collections (which, + * if one collection is empty, is just the other collection). The result is sorted. */ - private Set unionIfNotDisjoint(Collection c1, Collection c2) { + private @Nullable Collection unionIfNotDisjoint( + Collection c1, Collection c2) { + if (c1.isEmpty()) { + return c2; + } else if (c2.isEmpty()) { + return c1; + } Set result = new TreeSet<>(c1); + boolean disjoint = true; for (String s : c2) { if (!result.add(s)) { - return null; + disjoint = false; } } - return result; + if (!disjoint) { + return result; + } else { + return null; + } } // The GLB of two SameLen annotations is the union of the two sets of arrays, or is bottom // if the sets do not intersect. @Override - public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror a2) { - if (AnnotationUtils.hasElementValue(a1, "value") - && AnnotationUtils.hasElementValue(a2, "value")) { - List a1Val = ValueCheckerUtils.getValueOfAnnotationWithStringArgument(a1); - List a2Val = ValueCheckerUtils.getValueOfAnnotationWithStringArgument(a2); - - Set exprs = unionIfNotDisjoint(a1Val, a2Val); + public AnnotationMirror greatestLowerBoundQualifiers( + AnnotationMirror a1, AnnotationMirror a2) { + if (areSameByClass(a1, SameLen.class) && areSameByClass(a2, SameLen.class)) { + List a1Val = + AnnotationUtils.getElementValueArray(a1, sameLenValueElement, String.class); + List a2Val = + AnnotationUtils.getElementValueArray(a2, sameLenValueElement, String.class); + + Collection exprs = unionIfNotDisjoint(a1Val, a2Val); if (exprs == null) { return BOTTOM; } else { return createSameLen(exprs); } } else { - // the glb is either one of the annotations (if the other is top), or bottom. + // If one of the annotations is top, the glb is the other annotation; otherwise + // bottom. if (areSameByClass(a1, SameLenUnknown.class)) { return a2; } else if (areSameByClass(a2, SameLenUnknown.class)) { @@ -222,11 +249,13 @@ public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror // The LUB of two SameLen annotations is the intersection of the two sets of arrays, or is // top if they do not intersect. @Override - public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2) { - if (AnnotationUtils.hasElementValue(a1, "value") - && AnnotationUtils.hasElementValue(a2, "value")) { - List a1Val = ValueCheckerUtils.getValueOfAnnotationWithStringArgument(a1); - List a2Val = ValueCheckerUtils.getValueOfAnnotationWithStringArgument(a2); + public AnnotationMirror leastUpperBoundQualifiers( + AnnotationMirror a1, AnnotationMirror a2) { + if (areSameByClass(a1, SameLen.class) && areSameByClass(a2, SameLen.class)) { + List a1Val = + AnnotationUtils.getElementValueArray(a1, sameLenValueElement, String.class); + List a2Val = + AnnotationUtils.getElementValueArray(a2, sameLenValueElement, String.class); if (!Collections.disjoint(a1Val, a2Val)) { a1Val.retainAll(a2Val); @@ -251,19 +280,21 @@ && areSameByClass(a2, PolySameLen.class)) { } @Override - public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { + public boolean isSubtypeQualifiers(AnnotationMirror subAnno, AnnotationMirror superAnno) { if (areSameByClass(subAnno, SameLenBottom.class)) { return true; } else if (areSameByClass(superAnno, SameLenUnknown.class)) { return true; } else if (areSameByClass(subAnno, PolySameLen.class)) { return areSameByClass(superAnno, PolySameLen.class); - } else if (AnnotationUtils.hasElementValue(subAnno, "value") - && AnnotationUtils.hasElementValue(superAnno, "value")) { + } else if (areSameByClass(subAnno, SameLen.class) + && areSameByClass(superAnno, SameLen.class)) { List subArrays = - ValueCheckerUtils.getValueOfAnnotationWithStringArgument(subAnno); + AnnotationUtils.getElementValueArray( + subAnno, sameLenValueElement, String.class); List superArrays = - ValueCheckerUtils.getValueOfAnnotationWithStringArgument(superAnno); + AnnotationUtils.getElementValueArray( + superAnno, sameLenValueElement, String.class); if (subArrays.containsAll(superArrays)) { return true; @@ -290,25 +321,25 @@ public SameLenTreeAnnotator(SameLenAnnotatedTypeFactory factory) { // Case "new array" for "new T[a.length]" @Override - public Void visitNewArray(NewArrayTree node, AnnotatedTypeMirror type) { - if (node.getDimensions().size() == 1) { - Tree dimensionTree = node.getDimensions().get(0); + public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { + if (tree.getDimensions().size() == 1) { + Tree dimensionTree = tree.getDimensions().get(0); ExpressionTree sequenceTree = IndexUtil.getLengthSequenceTree(dimensionTree, imf, processingEnv); if (sequenceTree != null) { AnnotationMirror sequenceAnno = getAnnotatedType(sequenceTree).getAnnotationInHierarchy(UNKNOWN); - Receiver rec = FlowExpressions.internalReprOf(this.atypeFactory, sequenceTree); - if (mayAppearInSameLen(rec)) { - String recString = rec.toString(); + JavaExpression sequenceExpr = JavaExpression.fromTree(sequenceTree); + if (mayAppearInSameLen(sequenceExpr)) { + String recString = sequenceExpr.toString(); if (areSameByClass(sequenceAnno, SameLenUnknown.class)) { sequenceAnno = createSameLen(Collections.singletonList(recString)); } else if (areSameByClass(sequenceAnno, SameLen.class)) { // Add the sequence whose length is being used to the annotation. List exprs = - ValueCheckerUtils.getValueOfAnnotationWithStringArgument( - sequenceAnno); + AnnotationUtils.getElementValueArray( + sequenceAnno, sameLenValueElement, String.class); int index = Collections.binarySearch(exprs, recString); if (index < 0) { exprs.add(-index - 1, recString); @@ -334,14 +365,14 @@ public List getSameLensFromString( sameLenAnno = getAnnotationFromJavaExpressionString( sequenceExpression, tree, currentPath, SameLen.class); - } catch (FlowExpressionParseUtil.FlowExpressionParseException e) { + } catch (JavaExpressionParseUtil.JavaExpressionParseException e) { // ignore parse errors - sameLenAnno = null; + return Collections.emptyList(); } if (sameLenAnno == null) { - return new ArrayList<>(); + return Collections.emptyList(); } - return ValueCheckerUtils.getValueOfAnnotationWithStringArgument(sameLenAnno); + return AnnotationUtils.getElementValueArray(sameLenAnno, sameLenValueElement, String.class); } /// @@ -349,56 +380,44 @@ public List getSameLensFromString( /// /** - * Creates a @SameLen annotation whose values are the given strings, from an ordered - * collection such as a list or TreeSet in which the strings are in alphabetical order. + * Creates a @SameLen annotation whose values are the given strings. + * + * @param exprs the values for the @SameLen annotation. This must be an ordered + * collection such as a list or TreeSet in which the strings are in alphabetical order. + * @return a @SameLen annotation whose values are the given strings */ public AnnotationMirror createSameLen(Collection exprs) { AnnotationBuilder builder = new AnnotationBuilder(processingEnv, SameLen.class); - String[] exprArray = exprs.toArray(new String[0]); + String[] exprArray = exprs.toArray(new String[exprs.size()]); builder.setValue("value", exprArray); return builder.build(); } - // In Java 9, this method can be eliminated: it is simple enough for clients to inline, using - // List.of. - /** - * Combines the given arrays and annotations into a single SameLen annotation. See {@link - * #createCombinedSameLen(List, List)}. - */ - public AnnotationMirror createCombinedSameLen( - Receiver rec1, Receiver rec2, AnnotationMirror a1, AnnotationMirror a2) { - List receivers = new ArrayList<>(); - receivers.add(rec1); - receivers.add(rec2); - List annos = new ArrayList<>(); - annos.add(a1); - annos.add(a2); - return createCombinedSameLen(receivers, annos); - } - /** - * Generates a SameLen that includes each receiver, as well as everything in the annotations2, + * Generates a SameLen that includes each expression, as well as everything in the annotations2, * if they are SameLen annotations. * - * @param receivers a list of receivers representing arrays to be included in the combined + * @param exprs a list of expressions representing arrays to be included in the combined * annotation * @param annos a list of annotations * @return a combined SameLen annotation */ public AnnotationMirror createCombinedSameLen( - List receivers, List annos) { + List exprs, List annos) { - Set exprs = new TreeSet<>(); - for (Receiver rec : receivers) { - if (mayAppearInSameLen(rec)) { - exprs.add(rec.toString()); + Set strings = new TreeSet<>(); + for (JavaExpression expr : exprs) { + if (mayAppearInSameLen(expr)) { + strings.add(expr.toString()); } } for (AnnotationMirror anno : annos) { if (areSameByClass(anno, SameLen.class)) { - exprs.addAll(ValueCheckerUtils.getValueOfAnnotationWithStringArgument(anno)); + strings.addAll( + AnnotationUtils.getElementValueArray( + anno, sameLenValueElement, String.class)); } } - return createSameLen(exprs); + return createSameLen(strings); } } diff --git a/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenChecker.java b/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenChecker.java index c731f6ecb987..1b33b608da35 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenChecker.java @@ -9,5 +9,11 @@ * * @checker_framework.manual #index-checker Index Checker */ +// This @RelevantJavaTypes annotation is incorrect, because @SameLen can apply to an arbitrary +// user-defined datatype: https://checkerframework.org/manual/#index-annotating-fixed-size . +// @RelevantJavaTypes({CharSequence.class, Object[].class, Object.class}) @SuppressWarningsPrefix({"index", "samelen"}) -public class SameLenChecker extends BaseTypeChecker {} +public class SameLenChecker extends BaseTypeChecker { + /** Create a new SameLenChecker. */ + public SameLenChecker() {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenTransfer.java b/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenTransfer.java index cadeb9ca9683..69db0830c8b3 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenTransfer.java @@ -3,18 +3,11 @@ import com.sun.source.tree.MethodTree; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.type.TypeKind; + import org.checkerframework.checker.index.IndexUtil; import org.checkerframework.checker.index.qual.SameLen; -import org.checkerframework.common.value.ValueCheckerUtils; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.ConditionalTransferResult; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.UnderlyingAST; @@ -23,13 +16,24 @@ import org.checkerframework.dataflow.cfg.node.FieldAccessNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.flow.CFAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFTransfer; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.util.FlowExpressionParseUtil; +import org.checkerframework.framework.util.JavaExpressionParseUtil; +import org.checkerframework.javacutil.AnnotationUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; /** * The transfer function for the SameLen checker. Contains three cases: @@ -43,27 +47,33 @@ */ public class SameLenTransfer extends CFTransfer { - private SameLenAnnotatedTypeFactory aTypeFactory; + /** The annotated type factory. */ + private final SameLenAnnotatedTypeFactory atypeFactory; - /** Shorthand for aTypeFactory.UNKNOWN. */ - private AnnotationMirror UNKNOWN; + /** Shorthand for atypeFactory.UNKNOWN. */ + private final AnnotationMirror UNKNOWN; + /** + * Create a new SameLenTransfer. + * + * @param analysis the CFAnalysis + */ public SameLenTransfer(CFAnalysis analysis) { super(analysis); - this.aTypeFactory = (SameLenAnnotatedTypeFactory) analysis.getTypeFactory(); - this.UNKNOWN = aTypeFactory.UNKNOWN; + this.atypeFactory = (SameLenAnnotatedTypeFactory) analysis.getTypeFactory(); + this.UNKNOWN = atypeFactory.UNKNOWN; } /** * Gets the receiver sequence of a length access node, or null if {@code lengthNode} is not a * length access. */ - private Node getLengthReceiver(Node lengthNode) { + private @Nullable Node getLengthReceiver(Node lengthNode) { if (isArrayLengthAccess(lengthNode)) { // lengthNode is a.length FieldAccessNode lengthFieldAccessNode = (FieldAccessNode) lengthNode; return lengthFieldAccessNode.getReceiver(); - } else if (aTypeFactory.getMethodIdentifier().isLengthOfMethodInvocation(lengthNode)) { + } else if (atypeFactory.getMethodIdentifier().isLengthOfMethodInvocation(lengthNode)) { // lengthNode is s.length() MethodInvocationNode lengthMethodInvocationNode = (MethodInvocationNode) lengthNode; return lengthMethodInvocationNode.getTarget().getReceiver(); @@ -87,28 +97,23 @@ public TransferResult visitAssignment( if (lengthNodeReceiver != null) { // "new T[a.length]" or "new T[s.length()]" is the right hand side of the - // assignment. lengthNode is known to be "lengthNodeReceiver.length" or + // assignment. + // lengthNode is known to be "lengthNodeReceiver.length" or // "lengthNodeReceiver.length()" // targetRec is the receiver for the left hand side of the assignment. - Receiver targetRec = - FlowExpressions.internalReprOf( - analysis.getTypeFactory(), node.getTarget()); - Receiver otherRec = - FlowExpressions.internalReprOf( - analysis.getTypeFactory(), lengthNodeReceiver); + JavaExpression targetRec = JavaExpression.fromNode(node.getTarget()); + JavaExpression otherRec = JavaExpression.fromNode(lengthNodeReceiver); AnnotationMirror lengthNodeAnnotation = - aTypeFactory + atypeFactory .getAnnotatedType(lengthNodeReceiver.getTree()) .getAnnotationInHierarchy(UNKNOWN); AnnotationMirror combinedSameLen = - // In Java 9, this can be: - // aTypeFactory.createCombinedSameLen( - // List.of(targetRec, otherRec), List.of(lengthNodeAnnotation)); - aTypeFactory.createCombinedSameLen( - targetRec, otherRec, UNKNOWN, lengthNodeAnnotation); + atypeFactory.createCombinedSameLen( + Arrays.asList(targetRec, otherRec), + Arrays.asList(UNKNOWN, lengthNodeAnnotation)); propagateCombinedSameLen(combinedSameLen, node, result.getRegularStore()); return result; @@ -117,30 +122,26 @@ public TransferResult visitAssignment( } AnnotationMirror rightAnno = - aTypeFactory + atypeFactory .getAnnotatedType(node.getExpression().getTree()) .getAnnotationInHierarchy(UNKNOWN); // If the left side of the assignment is an array or a string, then have both the right and // left side be SameLen of each other. - Receiver targetRec = - FlowExpressions.internalReprOf(analysis.getTypeFactory(), node.getTarget()); + JavaExpression targetRec = JavaExpression.fromNode(node.getTarget()); - Receiver exprRec = - FlowExpressions.internalReprOf(analysis.getTypeFactory(), node.getExpression()); + JavaExpression exprRec = JavaExpression.fromNode(node.getExpression()); if (IndexUtil.isSequenceType(node.getTarget().getType()) - || (rightAnno != null && aTypeFactory.areSameByClass(rightAnno, SameLen.class))) { + || (rightAnno != null && atypeFactory.areSameByClass(rightAnno, SameLen.class))) { AnnotationMirror rightAnnoOrUnknown = rightAnno == null ? UNKNOWN : rightAnno; AnnotationMirror combinedSameLen = - // In Java 9, this can be: - // aTypeFactory.createCombinedSameLen( - // List.of(targetRec, exprRec), List.of(rightAnnoOrUnknown)); - aTypeFactory.createCombinedSameLen( - targetRec, exprRec, UNKNOWN, rightAnnoOrUnknown); + atypeFactory.createCombinedSameLen( + Arrays.asList(targetRec, exprRec), + Arrays.asList(UNKNOWN, rightAnnoOrUnknown)); propagateCombinedSameLen(combinedSameLen, node, result.getRegularStore()); } @@ -157,19 +158,21 @@ public TransferResult visitAssignment( * @param store the store to modify */ private void propagateCombinedSameLen(AnnotationMirror sameLenAnno, Node node, CFStore store) { - TreePath currentPath = aTypeFactory.getPath(node.getTree()); + TreePath currentPath = atypeFactory.getPath(node.getTree()); if (currentPath == null) { return; } - for (String expr : ValueCheckerUtils.getValueOfAnnotationWithStringArgument(sameLenAnno)) { - Receiver recS; + for (String exprString : + AnnotationUtils.getElementValueArray( + sameLenAnno, atypeFactory.sameLenValueElement, String.class)) { + JavaExpression je; try { - recS = aTypeFactory.getReceiverFromJavaExpressionString(expr, currentPath); - } catch (FlowExpressionParseUtil.FlowExpressionParseException e) { + je = atypeFactory.parseJavaExpressionString(exprString, currentPath); + } catch (JavaExpressionParseUtil.JavaExpressionParseException e) { continue; } - store.clearValue(recS); - store.insertValue(recS, sameLenAnno); + store.clearValue(je); + store.insertValue(je, sameLenAnno); } } @@ -183,26 +186,30 @@ private boolean isArrayLengthAccess(Node node) { /** * Handles refinement of equality comparisons. Assumes "a == b" or "a.length == b.length" * evaluates to true. The method gives a and b SameLen of each other in the store. + * + * @param left the first argument to the equality operator + * @param right the second argument to the equality operator + * @param store the store in which to perform refinement */ private void refineEq(Node left, Node right, CFStore store) { - List receivers = new ArrayList<>(); - List annos = new ArrayList<>(); + List exprs = new ArrayList<>(2); + List annos = new ArrayList<>(2); for (Node internal : splitAssignments(left)) { - receivers.add(FlowExpressions.internalReprOf(analysis.getTypeFactory(), internal)); + exprs.add(JavaExpression.fromNode(internal)); annos.add(getAnno(internal)); } for (Node internal : splitAssignments(right)) { - receivers.add(FlowExpressions.internalReprOf(analysis.getTypeFactory(), internal)); + exprs.add(JavaExpression.fromNode(internal)); annos.add(getAnno(internal)); } - AnnotationMirror combinedSameLen = aTypeFactory.createCombinedSameLen(receivers, annos); + AnnotationMirror combinedSameLen = atypeFactory.createCombinedSameLen(exprs, annos); propagateCombinedSameLen(combinedSameLen, left, store); } /** - * Return n's annotation from the SameLen hierarchy. + * Returns {@code n}'s annotation from the SameLen hierarchy. * *

    analysis.getValue fails if called on an lvalue. However, this method needs to always * succeed, even when n is an lvalue. Consider this code: @@ -212,16 +219,19 @@ private void refineEq(Node left, Node right, CFStore store) { * where a, b, and c are all arrays, and a has type {@code @SameLen("d")}. Afterwards, all three * should have the type {@code @SameLen({"a", "b", "c", "d"})}, but in order to accomplish this, * this method must return the type of a, which is an lvalue. + * + * @param n a node whose SameLen annotation to return + * @return {@code n}'s annotation from the SameLen hierarchy */ AnnotationMirror getAnno(Node n) { if (n.isLValue()) { - return aTypeFactory.getAnnotatedType(n.getTree()).getAnnotationInHierarchy(UNKNOWN); + return atypeFactory.getAnnotatedType(n.getTree()).getAnnotationInHierarchy(UNKNOWN); } CFValue cfValue = analysis.getValue(n); if (cfValue == null) { return UNKNOWN; } - return aTypeFactory + return atypeFactory .getQualifierHierarchy() .findAnnotationInHierarchy(cfValue.getAnnotations(), UNKNOWN); } @@ -267,26 +277,28 @@ protected void addInformationFromPreconditions( super.addInformationFromPreconditions(info, factory, method, methodTree, methodElement); List paramTrees = methodTree.getParameters(); - List paramNames = new ArrayList<>(); - List params = new ArrayList<>(); + int numParams = paramTrees.size(); + List paramNames = new ArrayList<>(numParams); + List params = new ArrayList<>(numParams); for (VariableTree tree : paramTrees) { paramNames.add(tree.getName().toString()); - params.add(aTypeFactory.getAnnotatedType(tree)); + params.add(atypeFactory.getAnnotatedType(tree)); } - for (int index = 0; index < params.size(); index++) { + for (int index = 0; index < numParams; index++) { - // if the parameter has a samelen annotation, then look - // for other parameters in that annotation and propagate - // default the other annotation so that it is symmetric + // If the parameter has a samelen annotation, then look for other parameters in that + // annotation and propagate default the other annotation so that it is symmetric. AnnotatedTypeMirror atm = params.get(index); - AnnotationMirror anm = atm.getAnnotation(SameLen.class); - if (anm == null) { + AnnotationMirror sameLenAnno = atm.getAnnotation(SameLen.class); + if (sameLenAnno == null) { continue; } - List values = ValueCheckerUtils.getValueOfAnnotationWithStringArgument(anm); + List values = + AnnotationUtils.getElementValueArray( + sameLenAnno, atypeFactory.sameLenValueElement, String.class); for (String value : values) { int otherParamIndex = paramNames.indexOf(value); if (otherParamIndex == -1) { @@ -296,19 +308,11 @@ protected void addInformationFromPreconditions( // the SameLen value is in the list of params, so modify the type of // that param in the store AnnotationMirror newSameLen = - aTypeFactory.createSameLen( + atypeFactory.createSameLen( Collections.singletonList(paramNames.get(index))); - Receiver otherParamRec = null; - try { - otherParamRec = - FlowExpressionParseUtil.internalReprOfVariable( - aTypeFactory, paramTrees.get(otherParamIndex)); - } catch (FlowExpressionParseUtil.FlowExpressionParseException e) { - // do nothing - } - if (otherParamRec != null) { - info.insertValue(otherParamRec, newSameLen); - } + JavaExpression otherParamRec = + JavaExpression.fromVariableTree(paramTrees.get(otherParamIndex)); + info.insertValuePermitNondeterministic(otherParamRec, newSameLen); } } } 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 613c17fd584c..4244f77ad0c2 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 @@ -2,22 +2,24 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.Tree; -import java.util.Collection; -import java.util.Collections; -import java.util.TreeSet; -import javax.lang.model.element.AnnotationMirror; + import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.index.IndexUtil; import org.checkerframework.checker.index.qual.PolySameLen; import org.checkerframework.checker.index.qual.SameLen; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; -import org.checkerframework.common.value.ValueCheckerUtils; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; +import java.util.Collection; +import java.util.Collections; +import java.util.TreeSet; + +import javax.lang.model.element.AnnotationMirror; + public class SameLenVisitor extends BaseTypeVisitor { public SameLenVisitor(BaseTypeChecker checker) { super(checker); @@ -29,34 +31,38 @@ public SameLenVisitor(BaseTypeChecker checker) { *

    {@inheritDoc} */ @Override - protected void commonAssignmentCheck( + protected boolean 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 && !(valueType.hasAnnotation(PolySameLen.class) && varType.hasAnnotation(PolySameLen.class))) { - Receiver rhs = FlowExpressions.internalReprOf(atypeFactory, (ExpressionTree) valueTree); + JavaExpression rhs = JavaExpression.fromTree((ExpressionTree) valueTree); if (rhs != null && SameLenAnnotatedTypeFactory.mayAppearInSameLen(rhs)) { String rhsExpr = rhs.toString(); - AnnotationMirror am = valueType.getAnnotation(SameLen.class); + AnnotationMirror sameLenAnno = valueType.getAnnotation(SameLen.class); Collection exprs; - if (am == null) { + if (sameLenAnno == null) { exprs = Collections.singletonList(rhsExpr); } else { exprs = new TreeSet<>( - ValueCheckerUtils.getValueOfAnnotationWithStringArgument(am)); + AnnotationUtils.getElementValueArray( + sameLenAnno, + atypeFactory.sameLenValueElement, + String.class)); exprs.add(rhsExpr); } AnnotationMirror newSameLen = atypeFactory.createSameLen(exprs); valueType.replaceAnnotation(newSameLen); } } - super.commonAssignmentCheck(varType, valueType, valueTree, errorKey); + return super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs); } } diff --git a/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexAnnotatedTypeFactory.java index 6ed76e751007..2613f403ead0 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexAnnotatedTypeFactory.java @@ -1,14 +1,5 @@ package org.checkerframework.checker.index.searchindex; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; import org.checkerframework.checker.index.qual.NegativeIndexFor; import org.checkerframework.checker.index.qual.SearchIndexBottom; import org.checkerframework.checker.index.qual.SearchIndexFor; @@ -17,11 +8,25 @@ import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.value.ValueAnnotatedTypeFactory; import org.checkerframework.common.value.ValueChecker; -import org.checkerframework.common.value.ValueCheckerUtils; +import org.checkerframework.framework.type.ElementQualifierHierarchy; import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.util.Elements; /** * The Search Index Checker is used to help type the results of calls to the JDK's binary search @@ -32,11 +37,24 @@ public class SearchIndexAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The @{@link SearchIndexUnknown} annotation. */ public final AnnotationMirror UNKNOWN = AnnotationBuilder.fromClass(elements, SearchIndexUnknown.class); + /** The @{@link SearchIndexBottom} annotation. */ public final AnnotationMirror BOTTOM = AnnotationBuilder.fromClass(elements, SearchIndexBottom.class); - /** Create a new SearchIndexAnnotatedTypeFactory. */ + /** The NegativeIndexFor.value field/element. */ + protected final ExecutableElement negativeIndexForValueElement = + TreeUtils.getMethod(NegativeIndexFor.class, "value", 0, processingEnv); + + /** The SearchIndexFor.value field/element. */ + protected final ExecutableElement searchIndexForValueElement = + TreeUtils.getMethod(SearchIndexFor.class, "value", 0, processingEnv); + + /** + * Create a new SearchIndexAnnotatedTypeFactory. + * + * @param checker the type-checker associated with this + */ public SearchIndexAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); @@ -62,20 +80,45 @@ protected Set> createSupportedTypeQualifiers() { } @Override - public QualifierHierarchy createQualifierHierarchy( - MultiGraphQualifierHierarchy.MultiGraphFactory factory) { - return new SearchIndexQualifierHierarchy(factory); + protected QualifierHierarchy createQualifierHierarchy() { + return new SearchIndexQualifierHierarchy(this.getSupportedTypeQualifiers(), elements); + } + + /** + * Returns the {@code value} field/element of the given annotation. + * + * @param am a @NegativeIndexFor or @SearchIndexFor annotation + * @return the {@code value} field/element of the given annotation + */ + private List getValueElement(AnnotationMirror am) { + if (areSameByClass(am, NegativeIndexFor.class)) { + return AnnotationUtils.getElementValueArray( + am, negativeIndexForValueElement, String.class); + } else if (areSameByClass(am, SearchIndexFor.class)) { + return AnnotationUtils.getElementValueArray( + am, searchIndexForValueElement, String.class); + } else { + throw new TypeSystemError("indexForValue(%s)", am); + } } - private final class SearchIndexQualifierHierarchy extends MultiGraphQualifierHierarchy { + /** SearchIndexQualifierHierarchy. */ + private final class SearchIndexQualifierHierarchy extends ElementQualifierHierarchy { + /** + * Creates a SearchIndexQualifierHierarchy from the given classes. + * + * @param qualifierClasses classes of annotations that are the qualifiers + * @param elements element utils + */ public SearchIndexQualifierHierarchy( - MultiGraphQualifierHierarchy.MultiGraphFactory factory) { - super(factory); + Set> qualifierClasses, Elements elements) { + super(qualifierClasses, elements, SearchIndexAnnotatedTypeFactory.this); } @Override - public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror a2) { + public AnnotationMirror greatestLowerBoundQualifiers( + AnnotationMirror a1, AnnotationMirror a2) { if (AnnotationUtils.areSame(a1, UNKNOWN)) { return a2; } @@ -88,30 +131,34 @@ public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror if (AnnotationUtils.areSame(a2, BOTTOM)) { return a2; } - if (isSubtype(a1, a2)) { + if (isSubtypeQualifiers(a1, a2)) { return a1; } - if (isSubtype(a2, a1)) { + if (isSubtypeQualifiers(a2, a1)) { return a2; } // If neither is a subtype of the other, then create an // annotation that combines their values. // Each annotation is either NegativeIndexFor or SearchIndexFor. - Set combinedArrays = - new HashSet<>(ValueCheckerUtils.getValueOfAnnotationWithStringArgument(a1)); - combinedArrays.addAll(ValueCheckerUtils.getValueOfAnnotationWithStringArgument(a2)); + Set combinedSet = new HashSet<>(getValueElement(a1)); + combinedSet.addAll(getValueElement(a2)); + // The list is backed by the given array. + List combinedList = + Arrays.asList(combinedSet.toArray(new String[combinedSet.size()])); + // NegativeIndexFor <: SearchIndexFor. if (areSameByClass(a1, NegativeIndexFor.class) || areSameByClass(a2, NegativeIndexFor.class)) { - return createNegativeIndexFor(Arrays.asList(combinedArrays.toArray(new String[0]))); + return createNegativeIndexFor(combinedList); } else { - return createSearchIndexFor(Arrays.asList(combinedArrays.toArray(new String[0]))); + return createSearchIndexFor(combinedList); } } @Override - public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2) { + public AnnotationMirror leastUpperBoundQualifiers( + AnnotationMirror a1, AnnotationMirror a2) { if (AnnotationUtils.areSame(a1, UNKNOWN)) { return a1; } @@ -124,20 +171,18 @@ public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2 if (AnnotationUtils.areSame(a2, BOTTOM)) { return a1; } - if (isSubtype(a1, a2)) { + if (isSubtypeQualifiers(a1, a2)) { return a2; } - if (isSubtype(a2, a1)) { + if (isSubtypeQualifiers(a2, a1)) { return a1; } // If neither is a subtype of the other, then create an // annotation that includes only their overlapping values. // Each annotation is either NegativeIndexFor or SearchIndexFor. - List arrayIntersection = - ValueCheckerUtils.getValueOfAnnotationWithStringArgument(a1); - arrayIntersection.retainAll( - ValueCheckerUtils.getValueOfAnnotationWithStringArgument(a2)); + List arrayIntersection = getValueElement(a1); + arrayIntersection.retainAll(getValueElement(a2)); // intersection if (arrayIntersection.isEmpty()) { return UNKNOWN; @@ -145,16 +190,14 @@ public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2 if (areSameByClass(a1, SearchIndexFor.class) || areSameByClass(a2, SearchIndexFor.class)) { - return createSearchIndexFor( - Arrays.asList(arrayIntersection.toArray(new String[0]))); + return createSearchIndexFor(arrayIntersection); } else { - return createNegativeIndexFor( - Arrays.asList(arrayIntersection.toArray(new String[0]))); + return createNegativeIndexFor(arrayIntersection); } } @Override - public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { + public boolean isSubtypeQualifiers(AnnotationMirror subAnno, AnnotationMirror superAnno) { if (areSameByClass(superAnno, SearchIndexUnknown.class)) { return true; } @@ -169,10 +212,8 @@ public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { } // Each annotation is either NegativeIndexFor or SearchIndexFor. - List superArrays = - ValueCheckerUtils.getValueOfAnnotationWithStringArgument(superAnno); - List subArrays = - ValueCheckerUtils.getValueOfAnnotationWithStringArgument(subAnno); + List superArrays = getValueElement(superAnno); + List subArrays = getValueElement(subAnno); // Subtyping requires: // * subtype is NegativeIndexFor or supertype is SearchIndexFor @@ -189,8 +230,7 @@ AnnotationMirror createNegativeIndexFor(List arrays) { return UNKNOWN; } - arrays = new ArrayList<>(new HashSet<>(arrays)); // remove duplicates - Collections.sort(arrays); + arrays = new ArrayList<>(new TreeSet<>(arrays)); // remove duplicates and sort AnnotationBuilder builder = new AnnotationBuilder(processingEnv, NegativeIndexFor.class); builder.setValue("value", arrays); @@ -203,8 +243,7 @@ AnnotationMirror createSearchIndexFor(List arrays) { return UNKNOWN; } - arrays = new ArrayList<>(new HashSet<>(arrays)); // remove duplicates - Collections.sort(arrays); + arrays = new ArrayList<>(new TreeSet<>(arrays)); // remove duplicates and sort AnnotationBuilder builder = new AnnotationBuilder(processingEnv, SearchIndexFor.class); builder.setValue("value", arrays); diff --git a/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexChecker.java b/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexChecker.java index da017829cac4..fad7e9e235c3 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexChecker.java @@ -1,10 +1,12 @@ package org.checkerframework.checker.index.searchindex; -import java.util.LinkedHashSet; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.value.ValueChecker; +import org.checkerframework.framework.qual.RelevantJavaTypes; import org.checkerframework.framework.source.SuppressWarningsPrefix; +import java.util.Set; + /** * An internal checker that assists the Index Checker in typing the results of calls to the JDK's * {@link java.util.Arrays#binarySearch(Object[],Object) Arrays.binarySearch} routine. @@ -12,12 +14,23 @@ * @checker_framework.manual #index-checker Index Checker */ @SuppressWarningsPrefix({"index", "searchindex"}) +@RelevantJavaTypes({ + Byte.class, + Short.class, + Integer.class, + Long.class, + Character.class, + byte.class, + short.class, + int.class, + long.class, + char.class, +}) public class SearchIndexChecker extends BaseTypeChecker { @Override - protected LinkedHashSet> getImmediateSubcheckerClasses() { - LinkedHashSet> checkers = - super.getImmediateSubcheckerClasses(); + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); checkers.add(ValueChecker.class); return checkers; } diff --git a/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexTransfer.java b/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexTransfer.java index 7d73c9685845..060428fa680b 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/index/searchindex/SearchIndexTransfer.java @@ -1,17 +1,20 @@ package org.checkerframework.checker.index.searchindex; -import java.util.List; -import javax.lang.model.element.AnnotationMirror; import org.checkerframework.checker.index.IndexAbstractTransfer; import org.checkerframework.checker.index.qual.NegativeIndexFor; import org.checkerframework.checker.index.qual.SearchIndexFor; import org.checkerframework.common.value.ValueCheckerUtils; -import org.checkerframework.dataflow.analysis.FlowExpressions; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.flow.CFAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.javacutil.AnnotationUtils; + +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; /** * The transfer function for the SearchIndexFor checker. Allows {@link SearchIndexFor} to be refined @@ -21,12 +24,17 @@ */ public class SearchIndexTransfer extends IndexAbstractTransfer { - // The ATF (Annotated Type Factory). - private SearchIndexAnnotatedTypeFactory aTypeFactory; + /** The annotated type factory. */ + private final SearchIndexAnnotatedTypeFactory atypeFactory; + /** + * Create a new SearchIndexTransfer. + * + * @param analysis the CFAnalysis + */ public SearchIndexTransfer(CFAnalysis analysis) { super(analysis); - aTypeFactory = (SearchIndexAnnotatedTypeFactory) analysis.getTypeFactory(); + atypeFactory = (SearchIndexAnnotatedTypeFactory) analysis.getTypeFactory(); } /** @@ -57,16 +65,16 @@ private void refineSearchIndexToNegativeIndexFor( assert valueToCompareTo == 0 || valueToCompareTo == -1; Long leftValue = ValueCheckerUtils.getExactValue( - left.getTree(), aTypeFactory.getValueAnnotatedTypeFactory()); + left.getTree(), atypeFactory.getValueAnnotatedTypeFactory()); if (leftValue != null && leftValue == valueToCompareTo) { - AnnotationMirror rightSI = - aTypeFactory.getAnnotationMirror(right.getTree(), SearchIndexFor.class); - if (rightSI != null) { + AnnotationMirror rightSIF = + atypeFactory.getAnnotationMirror(right.getTree(), SearchIndexFor.class); + if (rightSIF != null) { List arrays = - ValueCheckerUtils.getValueOfAnnotationWithStringArgument(rightSI); - AnnotationMirror nif = aTypeFactory.createNegativeIndexFor(arrays); - store.insertValue( - FlowExpressions.internalReprOf(analysis.getTypeFactory(), right), nif); + AnnotationUtils.getElementValueArray( + rightSIF, atypeFactory.searchIndexForValueElement, String.class); + AnnotationMirror nif = atypeFactory.createNegativeIndexFor(arrays); + store.insertValue(JavaExpression.fromNode(right), nif); } } } diff --git a/checker/src/main/java/org/checkerframework/checker/index/substringindex/SubstringIndexAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/index/substringindex/SubstringIndexAnnotatedTypeFactory.java index d97a029678a1..e5f5f2224a77 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/substringindex/SubstringIndexAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/index/substringindex/SubstringIndexAnnotatedTypeFactory.java @@ -1,10 +1,6 @@ package org.checkerframework.checker.index.substringindex; -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; +import org.checkerframework.checker.index.IndexChecker; import org.checkerframework.checker.index.OffsetDependentTypesHelper; import org.checkerframework.checker.index.qual.SubstringIndexBottom; import org.checkerframework.checker.index.qual.SubstringIndexFor; @@ -13,12 +9,20 @@ import org.checkerframework.checker.index.upperbound.UBQualifier.LessThanLengthOf; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.ElementQualifierHierarchy; import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy; import org.checkerframework.framework.util.dependenttypes.DependentTypesHelper; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.util.Elements; + /** * Builds types with annotations from the Substring Index checker hierarchy, which contains * the @{@link SubstringIndexFor} annotation. @@ -28,11 +32,16 @@ public class SubstringIndexAnnotatedTypeFactory extends BaseAnnotatedTypeFactory /** The top qualifier of the Substring Index hierarchy. */ public final AnnotationMirror UNKNOWN = AnnotationBuilder.fromClass(elements, SubstringIndexUnknown.class); + /** The bottom qualifier of the Substring Index hierarchy. */ public final AnnotationMirror BOTTOM = AnnotationBuilder.fromClass(elements, SubstringIndexBottom.class); - /** Create a new SubstringIndexAnnotatedTypeFactory. */ + /** + * Create a new SubstringIndexAnnotatedTypeFactory. + * + * @param checker the associated checker + */ public SubstringIndexAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); @@ -55,11 +64,9 @@ protected Set> createSupportedTypeQualifiers() { SubstringIndexBottom.class)); } - /** Creates the Substring Index qualifier hierarchy. */ @Override - public QualifierHierarchy createQualifierHierarchy( - MultiGraphQualifierHierarchy.MultiGraphFactory factory) { - return new SubstringIndexQualifierHierarchy(factory); + protected QualifierHierarchy createQualifierHierarchy() { + return new SubstringIndexQualifierHierarchy(this.getSupportedTypeQualifiers(), elements); } /** @@ -77,15 +84,22 @@ protected DependentTypesHelper createDependentTypesHelper() { * SubstringIndexBottom}, and elements of type {@link SubstringIndexFor} that follow the * subtyping relation of {@link UBQualifier}. */ - private final class SubstringIndexQualifierHierarchy extends MultiGraphQualifierHierarchy { - + private final class SubstringIndexQualifierHierarchy extends ElementQualifierHierarchy { + + /** + * Creates a SubstringIndexQualifierHierarchy from the given classes. + * + * @param qualifierClasses classes of annotations that are the qualifiers + * @param elements element utils + */ public SubstringIndexQualifierHierarchy( - MultiGraphQualifierHierarchy.MultiGraphFactory factory) { - super(factory); + Set> qualifierClasses, Elements elements) { + super(qualifierClasses, elements, SubstringIndexAnnotatedTypeFactory.this); } @Override - public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror a2) { + public AnnotationMirror greatestLowerBoundQualifiers( + AnnotationMirror a1, AnnotationMirror a2) { if (AnnotationUtils.areSame(a1, UNKNOWN)) { return a2; } @@ -98,14 +112,19 @@ public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror if (AnnotationUtils.areSame(a2, BOTTOM)) { return a2; } - UBQualifier ubq1 = UBQualifier.createUBQualifier(a1); - UBQualifier ubq2 = UBQualifier.createUBQualifier(a2); + UBQualifier ubq1 = + UBQualifier.createUBQualifier( + a1, (IndexChecker) checker.getUltimateParentChecker()); + UBQualifier ubq2 = + UBQualifier.createUBQualifier( + a2, (IndexChecker) checker.getUltimateParentChecker()); UBQualifier glb = ubq1.glb(ubq2); return convertUBQualifierToAnnotation(glb); } @Override - public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2) { + public AnnotationMirror leastUpperBoundQualifiers( + AnnotationMirror a1, AnnotationMirror a2) { if (AnnotationUtils.areSame(a1, UNKNOWN)) { return a1; } @@ -118,14 +137,18 @@ public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2 if (AnnotationUtils.areSame(a2, BOTTOM)) { return a1; } - UBQualifier ubq1 = UBQualifier.createUBQualifier(a1); - UBQualifier ubq2 = UBQualifier.createUBQualifier(a2); + UBQualifier ubq1 = + UBQualifier.createUBQualifier( + a1, (IndexChecker) checker.getUltimateParentChecker()); + UBQualifier ubq2 = + UBQualifier.createUBQualifier( + a2, (IndexChecker) checker.getUltimateParentChecker()); UBQualifier lub = ubq1.lub(ubq2); return convertUBQualifierToAnnotation(lub); } @Override - public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { + public boolean isSubtypeQualifiers(AnnotationMirror subAnno, AnnotationMirror superAnno) { if (areSameByClass(superAnno, SubstringIndexUnknown.class)) { return true; } @@ -139,8 +162,12 @@ public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { return false; } - UBQualifier subtype = UBQualifier.createUBQualifier(subAnno); - UBQualifier supertype = UBQualifier.createUBQualifier(superAnno); + UBQualifier subtype = + UBQualifier.createUBQualifier( + subAnno, (IndexChecker) checker.getUltimateParentChecker()); + UBQualifier supertype = + UBQualifier.createUBQualifier( + superAnno, (IndexChecker) checker.getUltimateParentChecker()); return subtype.isSubtype(supertype); } } diff --git a/checker/src/main/java/org/checkerframework/checker/index/substringindex/SubstringIndexChecker.java b/checker/src/main/java/org/checkerframework/checker/index/substringindex/SubstringIndexChecker.java index 7a0fcadf629a..20e44ef50e46 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/substringindex/SubstringIndexChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/index/substringindex/SubstringIndexChecker.java @@ -1,6 +1,7 @@ package org.checkerframework.checker.index.substringindex; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.qual.RelevantJavaTypes; import org.checkerframework.framework.source.SuppressWarningsPrefix; /** @@ -11,4 +12,9 @@ * @checker_framework.manual #index-substringindex Index Checker */ @SuppressWarningsPrefix({"index", "substringindex"}) -public class SubstringIndexChecker extends BaseTypeChecker {} +// int.class is for @SubstringIndexFor +@RelevantJavaTypes({CharSequence.class, Object[].class, int.class}) +public class SubstringIndexChecker extends BaseTypeChecker { + /** Creates a SubstringIndexChecker. */ + public SubstringIndexChecker() {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/index/upperbound/OffsetEquation.java b/checker/src/main/java/org/checkerframework/checker/index/upperbound/OffsetEquation.java index 652e10552454..3a323570523c 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/upperbound/OffsetEquation.java +++ b/checker/src/main/java/org/checkerframework/checker/index/upperbound/OffsetEquation.java @@ -1,63 +1,78 @@ package org.checkerframework.checker.index.upperbound; -import com.sun.source.util.TreePath; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import javax.lang.model.element.Element; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.value.ValueAnnotatedTypeFactory; -import org.checkerframework.common.value.ValueChecker; import org.checkerframework.common.value.ValueCheckerUtils; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; -import org.checkerframework.dataflow.analysis.FlowExpressions.Unknown; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.cfg.node.NumericalAdditionNode; import org.checkerframework.dataflow.cfg.node.NumericalSubtractionNode; -import org.checkerframework.framework.type.AnnotatedTypeFactory; -import org.checkerframework.framework.util.FlowExpressionParseUtil; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionContext; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionParseException; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.Unknown; import org.checkerframework.framework.util.dependenttypes.DependentTypesError; import org.checkerframework.javacutil.AnnotationProvider; import org.checkerframework.javacutil.TreeUtils; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; + /** * An offset equation is 2 sets of Java expression strings, one set of added terms and one set of * subtracted terms, and a single integer constant. The Java expression strings have been * standardized and viewpoint-adapted. + * + *

    An OffsetEquation is mutable. */ public class OffsetEquation { + /** The equation for 0 (zero). */ public static final OffsetEquation ZERO = createOffsetForInt(0); + + /** The equation for -1. */ public static final OffsetEquation NEG_1 = createOffsetForInt(-1); + + /** The equation for 1. */ public static final OffsetEquation ONE = createOffsetForInt(1); + /** Mutable list of terms that have been added to this. */ private final List addedTerms; + + /** Mutable list of terms that have been subtracted from this. */ private final List subtractedTerms; - private int intValue = 0; - private String error = null; + /** The integer offset. */ + private int intValue; + + /** Non-null if an error has occurred. */ + private @Nullable String error; + + /** Create a new OffsetEquation. */ private OffsetEquation() { - addedTerms = new ArrayList<>(); - subtractedTerms = new ArrayList<>(); + addedTerms = new ArrayList<>(1); + subtractedTerms = new ArrayList<>(1); + this.intValue = 0; + this.error = null; } - private OffsetEquation(OffsetEquation other) { + /** + * Create a new OffsetEquation that is a copy of the given one. + * + * @param other the OffsetEquation to copy + */ + protected OffsetEquation(OffsetEquation other) { this.addedTerms = new ArrayList<>(other.addedTerms); this.subtractedTerms = new ArrayList<>(other.subtractedTerms); - this.error = other.error; this.intValue = other.intValue; + this.error = other.error; } public boolean hasError() { return error != null; } - public String getError() { + public @Nullable String getError() { return error; } @@ -132,7 +147,7 @@ public String toString() { * @return a copy of this equation with array.length and string.length() removed or null if no * array.lengths or string.length() could be removed */ - public OffsetEquation removeSequenceLengths(List sequences) { + public @Nullable OffsetEquation removeSequenceLengths(List sequences) { OffsetEquation copy = new OffsetEquation(this); boolean simplified = false; for (String sequence : sequences) { @@ -149,6 +164,7 @@ public OffsetEquation removeSequenceLengths(List sequences) { } return simplified ? copy : null; } + /** * Adds or subtracts the other equation to a copy of this one. * @@ -247,119 +263,6 @@ public boolean isNegativeOrZero() { return isInt() && getInt() <= 0; } - /** - * Evaluates an offset term. If the term is an integer constant, returns its value. Otherwise, - * returns null. - * - * @param factory the AnnotatedTypeFactory used to access elements annotations. It can be null. - */ - private Integer evalConstantTerm(Receiver termReceiver, BaseAnnotatedTypeFactory factory) { - if (termReceiver instanceof FlowExpressions.ValueLiteral) { - // Integer literal - Object value = ((FlowExpressions.ValueLiteral) termReceiver).getValue(); - if (value instanceof Integer) { - return (Integer) value; - } - } else if (termReceiver instanceof FlowExpressions.MethodCall) { - // TODO: generalize - // Length of string literal - FlowExpressions.MethodCall call = (FlowExpressions.MethodCall) termReceiver; - if (call.getElement().getSimpleName().toString().equals("length")) { - Receiver callReceiver = call.getReceiver(); - if (callReceiver instanceof FlowExpressions.ValueLiteral) { - Object value = ((FlowExpressions.ValueLiteral) callReceiver).getValue(); - if (value instanceof String) { - return ((String) value).length(); - } - } - } - } else if (factory != null && termReceiver instanceof FlowExpressions.LocalVariable) { - Element element = ((FlowExpressions.LocalVariable) termReceiver).getElement(); - Long exactValue = - ValueCheckerUtils.getExactValue( - element, factory.getTypeFactoryOfSubchecker(ValueChecker.class)); - - if (exactValue != null) { - return exactValue.intValue(); - } - } - - return null; - } - - /** - * Standardizes and viewpoint-adapts string terms in the list based on the supplied context. - * Terms that evaluate to a integer constant are removed from the list, and the constants are - * added to or subtracted from the intValue field. - * - * @param factory the AnnotatedTypeFactory used for annotation accessing. It can be null. - */ - private void standardizeAndViewpointAdaptExpressions( - List terms, - boolean subtract, - FlowExpressionContext context, - TreePath scope, - boolean useLocalScope, - AnnotatedTypeFactory factory) - throws FlowExpressionParseException { - // Standardize all terms and remove constants - int length = terms.size(), j = 0; - for (int i = 0; i < length; ++i) { - String term = terms.get(i); - Receiver receiver = FlowExpressionParseUtil.parse(term, context, scope, useLocalScope); - Integer termConstant = evalConstantTerm(receiver, (BaseAnnotatedTypeFactory) factory); - if (termConstant == null) { - terms.set(j, receiver.toString()); - ++j; - } else if (subtract) { - intValue -= termConstant; - } else { - intValue += termConstant; - } - } - // Remove remaining elements from the end of the list - terms.subList(j, length).clear(); - } - - /** - * Standardizes and viewpoint-adapts the string terms based us the supplied context. - * - * @param context a FlowExpressionContext - * @param scope local scope - * @param useLocalScope whether or not local scope is used - * @param factory an AnnotatedTypeFactory used for annotation accessing. It can be null. - * @throws FlowExpressionParseException if any term isn't able to be parsed this exception is - * thrown. If this happens, no string terms are changed. - */ - public void standardizeAndViewpointAdaptExpressions( - FlowExpressionContext context, - TreePath scope, - boolean useLocalScope, - AnnotatedTypeFactory factory) - throws FlowExpressionParseException { - - standardizeAndViewpointAdaptExpressions( - addedTerms, false, context, scope, useLocalScope, factory); - standardizeAndViewpointAdaptExpressions( - subtractedTerms, true, context, scope, useLocalScope, factory); - } - - /** - * Standardizes and viewpoint-adapts the string terms based us the supplied context. - * - * @param context a FlowExpressionContext - * @param scope local scope - * @param useLocalScope whether or not local scope is used - * @throws FlowExpressionParseException if any term isn't able to be parsed this exception is - * thrown. If this happens, no string terms are changed. - */ - public void standardizeAndViewpointAdaptExpressions( - FlowExpressionContext context, TreePath scope, boolean useLocalScope) - throws FlowExpressionParseException { - - standardizeAndViewpointAdaptExpressions(context, scope, useLocalScope, null); - } - /** * Adds the term to this equation. If string is an integer, then it is added or subtracted, * depending on operator, from the int value of this equation. Otherwise, the term is placed in @@ -370,6 +273,10 @@ public void standardizeAndViewpointAdaptExpressions( */ private void addTerm(char operator, String term) { term = term.trim(); + if (operator == '-' && term.equals("2147483648")) { + addInt(-2147483648); + return; + } if (isInt(term)) { int literal = parseInt(term); addInt(operator == '-' ? -1 * literal : literal); @@ -402,7 +309,7 @@ private void addInt(int value) { * @param equationSet a set of offset equations * @return the offset equation that is an int value or null if there isn't one */ - public static OffsetEquation getIntOffsetEquation(Set equationSet) { + public static @Nullable OffsetEquation getIntOffsetEquation(Set equationSet) { for (OffsetEquation eq : equationSet) { if (eq.isInt()) { return eq; @@ -410,6 +317,7 @@ public static OffsetEquation getIntOffsetEquation(Set equationSe } return null; } + /** * Creates an offset equation that is only the int value specified. * @@ -472,8 +380,17 @@ public static OffsetEquation createOffsetFromJavaExpression(String expressionEqu return equation; } + /** A regular expression that matches an integer literal. */ + private static Pattern intPattern = Pattern.compile("[-+]?[0-9]+"); + + /** + * Returns true if the given string is an integer literal + * + * @param string a string + * @return true if the given string is an integer literal + */ private static boolean isInt(String string) { - return string.isEmpty() || string.matches("[-+]?[0-9]+"); + return intPattern.matcher(string).matches(); } private static int parseInt(String intLiteral) { @@ -507,7 +424,7 @@ private static int indexOf(String string, char a, char b, int index) { * @param op '+' or '-' * @return an offset equation from value of known or null if the value isn't known */ - public static OffsetEquation createOffsetFromNodesValue( + public static @Nullable OffsetEquation createOffsetFromNodesValue( Node node, ValueAnnotatedTypeFactory factory, char op) { assert op == '+' || op == '-'; if (node.getTree() != null && TreeUtils.isExpressionTree(node.getTree())) { @@ -532,8 +449,8 @@ public static OffsetEquation createOffsetFromNodesValue( * on the value of op. * *

    Otherwise the return equation is created by converting the node to a {@link - * org.checkerframework.dataflow.analysis.FlowExpressions.Receiver} and then added as a term to - * the returned equation. If op is '-' then it is a subtracted term. + * org.checkerframework.dataflow.expression.JavaExpression} and then added as a term to the + * returned equation. If op is '-' then it is a subtracted term. * * @param node the Node from which to create an offset equation * @param factory an AnnotationTypeFactory @@ -548,10 +465,18 @@ public static OffsetEquation createOffsetFromNode( return eq; } + /** + * Updates an offset equation from a Node. + * + * @param node the Node from which to create an offset equation + * @param factory an AnnotationTypeFactory + * @param eq an OffsetEquation to update + * @param op '+' or '-' + */ private static void createOffsetFromNode( Node node, AnnotationProvider factory, OffsetEquation eq, char op) { - Receiver r = FlowExpressions.internalReprOf(factory, node); - if (r instanceof Unknown || r == null) { + JavaExpression je = JavaExpression.fromNode(node); + if (je instanceof Unknown || je == null) { if (node instanceof NumericalAdditionNode) { createOffsetFromNode( ((NumericalAdditionNode) node).getLeftOperand(), factory, eq, op); @@ -567,7 +492,7 @@ private static void createOffsetFromNode( eq.error = node.toString(); } } else { - eq.addTerm(op, r.toString()); + eq.addTerm(op, je.toString()); } } } diff --git a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UBQualifier.java b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UBQualifier.java index 37a101f20a4f..2e99a826fc2d 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UBQualifier.java +++ b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UBQualifier.java @@ -1,33 +1,36 @@ package org.checkerframework.checker.index.upperbound; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.AnnotationMirror; import org.checkerframework.checker.index.qual.LTEqLengthOf; import org.checkerframework.checker.index.qual.LTLengthOf; import org.checkerframework.checker.index.qual.LTOMLengthOf; -import org.checkerframework.checker.index.qual.PolyUpperBound; import org.checkerframework.checker.index.qual.SubstringIndexFor; -import org.checkerframework.checker.index.qual.UpperBoundBottom; -import org.checkerframework.checker.index.qual.UpperBoundUnknown; +import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.Pair; +import org.checkerframework.javacutil.TypeSystemError; +import org.plumelib.util.CollectionsPlume; +import org.plumelib.util.IPair; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; /** - * Abstraction for Upper Bound annotations. - * - *

    {@link UpperBoundUnknown} is modeled as {@link UpperBoundUnknownQualifier} and {@link - * UpperBoundBottom} is modeled as {@link UBQualifier.UpperBoundBottomQualifier}. + * Abstraction for Upper Bound annotations. This abstract class has 4 subclasses, each of which is a + * nested class: {@link LessThanLengthOf}, {@link UpperBoundUnknownQualifier}, {@code + * UpperBoundBottomQualifier}, and {@code PolyQualifier}. * *

    {@link LTLengthOf} is modeled by {@link LessThanLengthOf}. {@link LTEqLengthOf} is equivalent * to @{@link LessThanLengthOf} with an offset of -1. {@link LTOMLengthOf} is equivalent to @{@link @@ -39,10 +42,11 @@ public abstract class UBQualifier { * Create a UBQualifier from the given annotation. * * @param am the annotation to turn into a UBQualifier + * @param ubChecker used to obtain the fields of {@code am} * @return a UBQualifier that represents the same information as the given annotation */ - public static UBQualifier createUBQualifier(AnnotationMirror am) { - return createUBQualifier(am, null); + public static UBQualifier createUBQualifier(AnnotationMirror am, UpperBoundChecker ubChecker) { + return createUBQualifier(am, null, ubChecker); } /** @@ -50,50 +54,168 @@ public static UBQualifier createUBQualifier(AnnotationMirror am) { * * @param am the annotation to turn into a UBQualifier * @param offset the extra offset; may be null + * @param ubChecker used to obtain the fields of {@code am} * @return a UBQualifier that represents the same information as the given annotation (plus an * optional offset) */ - public static UBQualifier createUBQualifier(AnnotationMirror am, String offset) { - if (AnnotationUtils.areSameByClass(am, UpperBoundUnknown.class)) { + public static UBQualifier createUBQualifier( + AnnotationMirror am, @Nullable String offset, UpperBoundChecker ubChecker) { + switch (AnnotationUtils.annotationName(am)) { + case "org.checkerframework.checker.index.qual.UpperBoundUnknown": + return UpperBoundUnknownQualifier.UNKNOWN; + case "org.checkerframework.checker.index.qual.UpperBoundBottom": + return UpperBoundBottomQualifier.BOTTOM; + case "org.checkerframework.checker.index.qual.UpperBoundLiteral": + int intValue = + AnnotationUtils.getElementValueInt( + am, ubChecker.upperBoundLiteralValueElement); + return UpperBoundLiteralQualifier.create(intValue); + case "org.checkerframework.checker.index.qual.LTLengthOf": + return parseLTLengthOf(am, offset, ubChecker); + case "org.checkerframework.checker.index.qual.SubstringIndexFor": + return parseSubstringIndexFor(am, offset, ubChecker); + case "org.checkerframework.checker.index.qual.LTEqLengthOf": + return parseLTEqLengthOf(am, offset, ubChecker); + case "org.checkerframework.checker.index.qual.LTOMLengthOf": + return parseLTOMLengthOf(am, offset, ubChecker); + case "org.checkerframework.checker.index.qual.PolyUpperBound": + // TODO: Ignores offset. Should we check that offset is not set? + return PolyQualifier.POLY; + default: + throw new TypeSystemError("createUBQualifier(%s, %s, ...)", am, offset); + } + } + + /** A cache for the {@link #nCopiesEmptyStringCache} method. */ + private static final List> nCopiesEmptyStringCache = new ArrayList<>(10); + + static { + nCopiesEmptyStringCache.add(Collections.emptyList()); + nCopiesEmptyStringCache.add(Collections.singletonList("")); + nCopiesEmptyStringCache.add(Collections.nCopies(2, "")); + nCopiesEmptyStringCache.add(Collections.nCopies(3, "")); + nCopiesEmptyStringCache.add(Collections.nCopies(4, "")); + nCopiesEmptyStringCache.add(Collections.nCopies(5, "")); + nCopiesEmptyStringCache.add(Collections.nCopies(6, "")); + nCopiesEmptyStringCache.add(Collections.nCopies(7, "")); + nCopiesEmptyStringCache.add(Collections.nCopies(8, "")); + nCopiesEmptyStringCache.add(Collections.nCopies(9, "")); + } + + /** + * Equivalent to {@code Collections.nCopies(n, "")}. + * + * @param n the length of the list + * @return an immutable list of {@code n} copies of {@code ""} + */ + private static List nCopiesEmptyString(int n) { + if (n < 10) { + return nCopiesEmptyStringCache.get(n); + } else { + return Collections.nCopies(n, ""); + } + } + + /** + * Create a UBQualifier from a @LTLengthOf annotation. + * + * @param ltLengthOfAnno a @LTLengthOf annotation + * @param extraOffset the extra offset + * @param ubChecker used to obtain the fields of {@code am} + * @return a UBQualifier created from the @LTLengthOf annotation + */ + private static UBQualifier parseLTLengthOf( + AnnotationMirror ltLengthOfAnno, String extraOffset, UpperBoundChecker ubChecker) { + List sequences = + AnnotationUtils.getElementValueArray( + ltLengthOfAnno, ubChecker.ltLengthOfValueElement, String.class); + if (sequences.isEmpty()) { + // These annotations can be created by delocalization of an LTLengthOf annotation + // that only contains local variables at a call site. return UpperBoundUnknownQualifier.UNKNOWN; - } else if (AnnotationUtils.areSameByClass(am, UpperBoundBottom.class)) { - return UpperBoundBottomQualifier.BOTTOM; - } else if (AnnotationUtils.areSameByClass(am, LTLengthOf.class) - || AnnotationUtils.areSameByClass(am, SubstringIndexFor.class)) { - return parseLTLengthOf(am, offset); - } else if (AnnotationUtils.areSameByClass(am, LTEqLengthOf.class)) { - return parseLTEqLengthOf(am, offset); - } else if (AnnotationUtils.areSameByClass(am, LTOMLengthOf.class)) { - return parseLTOMLengthOf(am, offset); - } else if (AnnotationUtils.areSameByClass(am, PolyUpperBound.class)) { - // TODO: Ignores offset. Should we check that offset is not set? - return PolyQualifier.POLY; - } - assert false; - return UpperBoundUnknownQualifier.UNKNOWN; + } + List offsets = + AnnotationUtils.getElementValueArray( + ltLengthOfAnno, + ubChecker.ltLengthOfOffsetElement, + String.class, + nCopiesEmptyString(sequences.size())); + return createUBQualifier(sequences, offsets, extraOffset); } - private static UBQualifier parseLTLengthOf(AnnotationMirror am, String extraOffset) { + /** + * Create a UBQualifier from a @SubstringIndexFor annotation. + * + * @param substringIndexForAnno a @SubstringIndexFor annotation + * @param extraOffset the extra offset + * @param ubChecker used for obtaining arguments/elements from {@code substringIndexForAnno} + * @return a UBQualifier created from the @SubstringIndexFor annotation + */ + private static UBQualifier parseSubstringIndexFor( + AnnotationMirror substringIndexForAnno, + String extraOffset, + UpperBoundChecker ubChecker) { List sequences = - AnnotationUtils.getElementValueArray(am, "value", String.class, true); - List offset = - AnnotationUtils.getElementValueArray(am, "offset", String.class, true); - if (offset.isEmpty()) { - offset = Collections.nCopies(sequences.size(), ""); + AnnotationUtils.getElementValueArray( + substringIndexForAnno, + ubChecker.substringIndexForValueElement, + String.class); + if (sequences.isEmpty()) { + // These annotations can be created by delocalization of a SubstringIndexFor annotation + // that only contains local variables at a call site. + return UpperBoundUnknownQualifier.UNKNOWN; } - return createUBQualifier(sequences, offset, extraOffset); + List offsets = + AnnotationUtils.getElementValueArray( + substringIndexForAnno, + ubChecker.substringIndexForOffsetElement, + String.class); + if (offsets.isEmpty()) { + offsets = nCopiesEmptyString(sequences.size()); + } + return createUBQualifier(sequences, offsets, extraOffset); } - private static UBQualifier parseLTEqLengthOf(AnnotationMirror am, String extraOffset) { + /** + * Create a UBQualifier from a @LTEqLengthOf annotation. + * + * @param am a @LTEqLengthOf annotation + * @param extraOffset the extra offset + * @param ubChecker used for obtaining fields from {@code am} + * @return a UBQualifier created from the @LTEqLengthOf annotation + */ + private static UBQualifier parseLTEqLengthOf( + AnnotationMirror am, String extraOffset, UpperBoundChecker ubChecker) { List sequences = - AnnotationUtils.getElementValueArray(am, "value", String.class, true); + AnnotationUtils.getElementValueArray( + am, ubChecker.ltEqLengthOfValueElement, String.class); + if (sequences.isEmpty()) { + // These annotations can be created by delocalization of an LTEqLengthOf annotation + // that only contains local variables at a call site. + return UpperBoundUnknownQualifier.UNKNOWN; + } List offset = Collections.nCopies(sequences.size(), "-1"); return createUBQualifier(sequences, offset, extraOffset); } - private static UBQualifier parseLTOMLengthOf(AnnotationMirror am, String extraOffset) { + /** + * Create a UBQualifier from a @LTOMLengthOf annotation. + * + * @param am a @LTOMLengthOf annotation + * @param extraOffset offset to add to each element of offsets; may be null + * @param ubChecker used for obtaining fields from {@code am} + * @return a UBQualifier created from the @LTOMLengthOf annotation + */ + private static UBQualifier parseLTOMLengthOf( + AnnotationMirror am, @Nullable String extraOffset, UpperBoundChecker ubChecker) { List sequences = - AnnotationUtils.getElementValueArray(am, "value", String.class, true); + AnnotationUtils.getElementValueArray( + am, ubChecker.ltOMLengthOfValueElement, String.class); + if (sequences.isEmpty()) { + // These annotations can be created by delocalization of an LTOMLengthOf annotation + // that only contains local variables at a call site. + return UpperBoundUnknownQualifier.UNKNOWN; + } List offset = Collections.nCopies(sequences.size(), "1"); return createUBQualifier(sequences, offset, extraOffset); } @@ -103,8 +225,17 @@ public static UBQualifier createUBQualifier(String sequence, String offset) { Collections.singletonList(sequence), Collections.singletonList(offset)); } - public static UBQualifier createUBQualifier(AnnotatedTypeMirror type, AnnotationMirror top) { - return createUBQualifier(type.getEffectiveAnnotationInHierarchy(top)); + /** + * Create an upper bound qualifier. + * + * @param type the type from which to obtain an annotation + * @param top the top annotation in a hierarchy; the annotation in this hierarchy will be used + * @param ubChecker used to obtain the fields of {@code am} + * @return a new upper bound qualifier + */ + public static UBQualifier createUBQualifier( + AnnotatedTypeMirror type, AnnotationMirror top, UpperBoundChecker ubChecker) { + return createUBQualifier(type.getEffectiveAnnotationInHierarchy(top), ubChecker); } /** @@ -132,7 +263,7 @@ public static UBQualifier createUBQualifier(List sequences, List * @return an {@link UBQualifier} for the sequences with the given offsets */ public static UBQualifier createUBQualifier( - List sequences, List offsets, String extraOffset) { + List sequences, List offsets, @Nullable String extraOffset) { assert !sequences.isEmpty(); OffsetEquation extraEq; @@ -145,30 +276,7 @@ public static UBQualifier createUBQualifier( } } - Map> map = new HashMap<>(); - if (offsets.isEmpty()) { - for (String sequence : sequences) { - map.put(sequence, Collections.singleton(extraEq)); - } - } else { - assert sequences.size() == offsets.size(); - for (int i = 0; i < sequences.size(); i++) { - String sequence = sequences.get(i); - String offset = offsets.get(i); - Set set = map.get(sequence); - if (set == null) { - set = new HashSet<>(); - map.put(sequence, set); - } - OffsetEquation eq = OffsetEquation.createOffsetFromJavaExpression(offset); - if (eq.hasError()) { - return UpperBoundUnknownQualifier.UNKNOWN; - } - eq = eq.copyAdd('+', extraEq); - set.add(eq); - } - } - return new LessThanLengthOf(map); + return new LessThanLengthOf(sequences, offsets, extraEq); } /** @@ -200,14 +308,39 @@ public boolean isLessThanLengthQualifier() { return false; } + /** + * Returns true if this UBQualifier represents a literal integer. + * + * @return true if this UBQualifier represents a literal integer + */ + public boolean isLiteral() { + return false; + } + + /** + * Returns true if this UBQualifier is the top type. + * + * @return true if this UBQualifier is the top type + */ public boolean isUnknown() { return false; } + /** + * Returns true if this UBQualifier is the bottom type. + * + * @return true if this UBQualifier is the bottom type + */ public boolean isBottom() { return false; } + /** + * Return true if this is UBQualifier.PolyQualifier. + * + * @return true if this is UBQualifier.PolyQualifier + */ + @Pure public boolean isPoly() { return false; } @@ -278,14 +411,192 @@ public boolean isLessThanOrEqualTo(String sequence) { /** The less-than-length-of qualifier (@LTLengthOf). */ public static class LessThanLengthOf extends UBQualifier { + + // There are two representations for sequences and offsets. + // In source code, they are represented by two parallel arrays, as in + // @LTLengthOf(value = {"a", "b", "a", "c"}, offset = {"-1", "x", "y", "0"}). + // In this implementation, they are represented by a single map; the above would be + // { "a" : {"-1", "y"}, "b" : {"x"}, "c" : {"0"} } + // Code in this class transforms from one representation to the other. + /** Maps from sequence name to offset. */ private final Map> map; + /** + * Returns a copy of the map. + * + * @return a copy of the map + */ + private Map> copyMap() { + Map> result = + new HashMap<>(CollectionsPlume.mapCapacity(map)); + for (String sequenceName : map.keySet()) { + Set oldEquations = map.get(sequenceName); + Set newEquations = + new HashSet<>(CollectionsPlume.mapCapacity(oldEquations)); + for (OffsetEquation offsetEquation : oldEquations) { + newEquations.add(new OffsetEquation(offsetEquation)); + } + result.put(sequenceName, newEquations); + } + return result; + } + + /** + * Returns true if the given integer literal is a subtype of this. The literal is a subtype + * of this if, for every offset expression, {@code literal + offset <= -1}. + * + * @param i an integer + * @return true if the given integer literal is a subtype of this + */ + /*package-private*/ boolean literalIsSubtype(int i) { + for (Map.Entry> entry : map.entrySet()) { + for (OffsetEquation equation : entry.getValue()) { + if (!equation.isInt()) { + return false; + } + int offset = equation.getInt(); + if (i + offset > -1) { + return false; + } + } + } + return true; + } + + /** + * Convert the parallel array representation to the map representation. + * + * @param sequences non-empty list of sequences + * @param offsets list of offset, if empty, an offset of 0 is used + * @param extraEq offset to add to each element of offsets; may be null + * @return the map representation of a {@link UBQualifier}, or null if there is an error + */ + private static @Nullable Map> sequencesAndOffsetsToMap( + List sequences, List offsets, @Nullable OffsetEquation extraEq) { + + Map> map = + new HashMap<>(CollectionsPlume.mapCapacity(sequences)); + if (offsets.isEmpty()) { + for (String sequence : sequences) { + // Not `Collections.singleton(extraEq)` because the values get modified + Set thisSet = new HashSet<>(1); + thisSet.add(extraEq); + map.put(sequence, thisSet); + } + } else { + assert sequences.size() == offsets.size(); + for (int i = 0; i < sequences.size(); i++) { + String sequence = sequences.get(i); + String offset = offsets.get(i); + Set set = map.computeIfAbsent(sequence, __ -> new HashSet<>()); + OffsetEquation eq = OffsetEquation.createOffsetFromJavaExpression(offset); + if (eq.hasError()) { + return null; + } + eq = eq.copyAdd('+', extraEq); + set.add(eq); + } + } + return map; + } + + /** A triple that is the return type of {@link #mapToSequencesAndOffsets}. */ + private static class SequencesOffsetsAndClass { + /** List of sequences. */ + public final List sequences; + + /** List of offsets. */ + public final List offsets; + + /** The class of the annotation to be built. */ + public final Class annoClass; + + /** + * Creates a new SequencesOffsetsAndClass. + * + * @param sequences list of sequences + * @param offsets list of offsets + * @param annoClass the class of the annotation to be built + */ + public SequencesOffsetsAndClass( + List sequences, + List offsets, + Class annoClass) { + + this.sequences = sequences; + this.offsets = offsets; + this.annoClass = annoClass; + } + } + + /** + * Given the map representation, returns parallel-arrays representation. + * + * @param map the internal representation of LessThanLengthOf + * @param buildSubstringIndexAnnotation if true, the annoClass in the result is + * ubstringIndexFor.class + * @return the external representation + */ + private static SequencesOffsetsAndClass mapToSequencesAndOffsets( + Map> map, boolean buildSubstringIndexAnnotation) { + List<@KeyFor("map") String> sortedSequences = new ArrayList<>(map.keySet()); + Collections.sort(sortedSequences); + List sequences = new ArrayList<>(); + List offsets = new ArrayList<>(); + boolean isLTEq = true; + boolean isLTOM = true; + for (String sequence : sortedSequences) { + // The offsets for this sequence. + List thisOffsets = new ArrayList<>(); + for (OffsetEquation eq : map.get(sequence)) { + isLTEq = isLTEq && eq.equals(OffsetEquation.NEG_1); + isLTOM = isLTOM && eq.equals(OffsetEquation.ONE); + thisOffsets.add(eq.toString()); + } + Collections.sort(thisOffsets); + for (String offset : thisOffsets) { + sequences.add(sequence); + offsets.add(offset); + } + } + Class annoClass; + if (buildSubstringIndexAnnotation) { + annoClass = SubstringIndexFor.class; + } else if (isLTEq) { + annoClass = LTEqLengthOf.class; + } else if (isLTOM) { + annoClass = LTOMLengthOf.class; + } else { + annoClass = LTLengthOf.class; + } + return new SequencesOffsetsAndClass(sequences, offsets, annoClass); + } + + // End of code for manipulating the representation + + /** + * Create a new LessThanLengthOf, from the internal representation. + * + * @param map a map from sequence name to offse + */ private LessThanLengthOf(Map> map) { assert !map.isEmpty(); this.map = map; } + /** + * Create a new LessThanLengthOf from the parallel array representation. + * + * @param sequences non-empty list of sequences + * @param offsets list of offset, if empty, an offset of 0 is used + * @param extraEq offset to add to each element of offsets; may be null + */ + private LessThanLengthOf( + List sequences, List offsets, @Nullable OffsetEquation extraEq) { + this(sequencesAndOffsetsToMap(sequences, offsets, extraEq)); + } + @Override public boolean hasSequenceWithOffset(String sequence, int offset) { Set offsets = map.get(sequence); @@ -331,6 +642,7 @@ public boolean isLessThanLengthOfAny(List sequences) { } return false; } + /** * Is a value with this type less than the length of the sequence? * @@ -396,40 +708,25 @@ public AnnotationMirror convertToSubstringIndexAnnotation(ProcessingEnvironment */ private AnnotationMirror convertToAnnotation( ProcessingEnvironment env, boolean buildSubstringIndexAnnotation) { - List sortedSequences = new ArrayList<>(map.keySet()); - Collections.sort(sortedSequences); - List sequences = new ArrayList<>(); - List offsets = new ArrayList<>(); - boolean isLTEq = true; - boolean isLTOM = true; - for (String sequence : sortedSequences) { - List sortOffsets = new ArrayList<>(); - for (OffsetEquation eq : map.get(sequence)) { - isLTEq = isLTEq && eq.equals(OffsetEquation.NEG_1); - isLTOM = isLTOM && eq.equals(OffsetEquation.ONE); - sortOffsets.add(eq.toString()); - } - Collections.sort(sortOffsets); - for (String offset : sortOffsets) { - sequences.add(sequence); - offsets.add(offset); - } - } - AnnotationBuilder builder; - if (buildSubstringIndexAnnotation) { - builder = new AnnotationBuilder(env, SubstringIndexFor.class); + SequencesOffsetsAndClass soc = + mapToSequencesAndOffsets(map, buildSubstringIndexAnnotation); + List sequences = soc.sequences; + List offsets = soc.offsets; + Class annoClass = soc.annoClass; + + AnnotationBuilder builder = new AnnotationBuilder(env, annoClass); + if (annoClass == SubstringIndexFor.class) { builder.setValue("value", sequences); builder.setValue("offset", offsets); - } else if (isLTEq) { - builder = new AnnotationBuilder(env, LTEqLengthOf.class); + } else if (annoClass == LTEqLengthOf.class) { builder.setValue("value", sequences); - } else if (isLTOM) { - builder = new AnnotationBuilder(env, LTOMLengthOf.class); + } else if (annoClass == LTOMLengthOf.class) { builder.setValue("value", sequences); - } else { - builder = new AnnotationBuilder(env, LTLengthOf.class); + } else if (annoClass == LTLengthOf.class) { builder.setValue("value", sequences); builder.setValue("offset", offsets); + } else { + throw new TypeSystemError("What annoClass? " + annoClass); } return builder.build(); } @@ -474,9 +771,9 @@ public boolean isLessThanLengthQualifier() { /** * If superType is Unknown, return true. If superType is Bottom, return false. * - *

    Otherwise, this qualifier must contain all the sequences in superType. For each the - * offsets for each sequence in superType, there must be an offset in this qualifier for the - * sequence that is greater than or equal to the super offset. + *

    Otherwise, return true if this qualifier contains all the sequences in superType, AND + * for each of the offsets for each sequence in superType, there is an offset in this + * qualifier for the sequence that is greater than or equal to the super offset. * * @param superType other qualifier * @return whether this qualifier is a subtype of superType @@ -487,6 +784,8 @@ public boolean isSubtype(UBQualifier superType) { return true; } else if (superType.isBottom()) { return false; + } else if (superType.isLiteral()) { + return false; } LessThanLengthOf superTypeLTL = (LessThanLengthOf) superType; @@ -549,17 +848,20 @@ public UBQualifier lub(UBQualifier other) { return other; } else if (other.isBottom()) { return this; + } else if (other.isLiteral()) { + return other.lub(this); } LessThanLengthOf otherLtl = (LessThanLengthOf) other; Set sequences = new HashSet<>(map.keySet()); sequences.retainAll(otherLtl.map.keySet()); - Map> lubMap = new HashMap<>(); + Map> lubMap = + new HashMap<>(CollectionsPlume.mapCapacity(sequences)); for (String sequence : sequences) { - Set lub = new HashSet<>(); Set offsets1 = map.get(sequence); Set offsets2 = otherLtl.map.get(sequence); + Set lub = new HashSet<>(offsets1.size() + offsets2.size()); for (OffsetEquation offset1 : offsets1) { for (OffsetEquation offset2 : offsets2) { if (offset2.lessThanOrEqual(offset1)) { @@ -626,7 +928,7 @@ private void widenLub(LessThanLengthOf other, Map> l || !containsSame(other.map.keySet(), lubMap.keySet())) { return; } - List> remove = new ArrayList<>(); + List> remove = new ArrayList<>(); for (Map.Entry> entry : lubMap.entrySet()) { String sequence = entry.getKey(); Set lubOffsets = entry.getValue(); @@ -641,7 +943,7 @@ private void widenLub(LessThanLengthOf other, Map> l int thisInt = OffsetEquation.getIntOffsetEquation(thisOffsets).getInt(); int otherInt = OffsetEquation.getIntOffsetEquation(otherOffsets).getInt(); if (thisInt != otherInt) { - remove.add(Pair.of(sequence, lubEq)); + remove.add(IPair.of(sequence, lubEq)); } } else if (thisOffsets.contains(lubEq) && otherOffsets.contains(lubEq)) { // continue; @@ -650,11 +952,12 @@ private void widenLub(LessThanLengthOf other, Map> l } } } - for (Pair pair : remove) { - Set offsets = lubMap.get(pair.first); + for (IPair pair : remove) { + String sequence = pair.first; + Set offsets = lubMap.get(sequence); offsets.remove(pair.second); if (offsets.isEmpty()) { - lubMap.remove(pair.first); + lubMap.remove(sequence); } } } @@ -665,13 +968,15 @@ public UBQualifier glb(UBQualifier other) { return this; } else if (other.isBottom()) { return other; + } else if (other.isLiteral()) { + return other.glb(this); } LessThanLengthOf otherLtl = (LessThanLengthOf) other; Set sequences = new HashSet<>(map.keySet()); sequences.addAll(otherLtl.map.keySet()); - Map> glbMap = new HashMap<>(); + Map> glbMap = new HashMap<>(sequences.size()); for (String sequence : sequences) { Set glb = map.get(sequence); Set otherglb = otherLtl.map.get(sequence); @@ -680,14 +985,22 @@ public UBQualifier glb(UBQualifier other) { } else if (otherglb != null) { glb.addAll(otherglb); } - glbMap.put(sequence, simplifyOffsets(glb)); + glbMap.put(sequence, removeSmallerInts(glb)); } return new LessThanLengthOf(glbMap); } - /** Keeps only the largest offset equation that is only an int value. */ - private Set simplifyOffsets(Set offsets) { - Set newOff = new HashSet<>(); + /** + * Returns a copy of the argument, but it contains just one offset equation that is an int + * value -- the largest one in the argument. Any non-int offset equations appear in the + * result. Does not side effect its argument. + * + * @param offsets a set of offset equations + * @return a copy of the argument with just one int value (the largest in the input) and + * arbitrarily many non-ints + */ + private Set removeSmallerInts(Set offsets) { + Set newOff = new HashSet<>(offsets.size()); OffsetEquation literal = null; for (OffsetEquation eq : offsets) { if (eq.isInt()) { @@ -717,7 +1030,7 @@ private Set simplifyOffsets(Set offsets) { */ @Override public UBQualifier plusOffset(Node node, UpperBoundAnnotatedTypeFactory factory) { - return pluseOrMinusOffset(node, factory, '+'); + return plusOrMinusOffset(node, factory, '+'); } /** @@ -731,17 +1044,33 @@ public UBQualifier plusOffset(Node node, UpperBoundAnnotatedTypeFactory factory) */ @Override public UBQualifier minusOffset(Node node, UpperBoundAnnotatedTypeFactory factory) { - return pluseOrMinusOffset(node, factory, '-'); + return plusOrMinusOffset(node, factory, '-'); } - private UBQualifier pluseOrMinusOffset( + /** + * Adds node as a positive or negative offset to a copy of this qualifier. This is done by + * creating an offset equation for node and then adding or subtracting that equation to + * every offset equation in a copy of this object. + * + * @param node a Node + * @param factory an AnnotatedTypeFactory + * @param op either '-' or '+' + * @return a copy of this qualifier with node add as an offset + */ + private UBQualifier plusOrMinusOffset( Node node, UpperBoundAnnotatedTypeFactory factory, char op) { assert op == '-' || op == '+'; + // Try treating the offset as both an OffsetEquation and as a value. + // Use whichever is not null, or glb the two. + OffsetEquation newOffset = OffsetEquation.createOffsetFromNode(node, factory, op); LessThanLengthOf nodeOffsetQualifier = null; if (!newOffset.hasError()) { - nodeOffsetQualifier = (LessThanLengthOf) addOffset(newOffset); + UBQualifier nodeOffsetQualifierMaybe = addOffset(newOffset); + if (!(nodeOffsetQualifierMaybe instanceof UpperBoundUnknownQualifier)) { + nodeOffsetQualifier = (LessThanLengthOf) nodeOffsetQualifierMaybe; + } } OffsetEquation valueOffset = @@ -749,7 +1078,10 @@ private UBQualifier pluseOrMinusOffset( node, factory.getValueAnnotatedTypeFactory(), op); LessThanLengthOf valueOffsetQualifier = null; if (valueOffset != null && !valueOffset.hasError()) { - valueOffsetQualifier = (LessThanLengthOf) addOffset(valueOffset); + UBQualifier valueOffsetQualifierMaybe = addOffset(valueOffset); + if (!(valueOffsetQualifierMaybe instanceof UpperBoundUnknownQualifier)) { + valueOffsetQualifier = (LessThanLengthOf) valueOffsetQualifierMaybe; + } } if (valueOffsetQualifier == null) { @@ -802,19 +1134,15 @@ public UBQualifier minusOffset(int value) { * @param sequences access of the length of these sequences are removed * @return a copy of this qualifier with some offsets removed */ - public UBQualifier removeSequenceLengthAccess(final List sequences) { + public UBQualifier removeSequenceLengthAccess(List sequences) { if (sequences.isEmpty()) { return UpperBoundUnknownQualifier.UNKNOWN; } OffsetEquationFunction removeSequenceLengthsFunc = - new OffsetEquationFunction() { - @Override - public OffsetEquation compute(OffsetEquation eq) { - return eq.removeSequenceLengths(sequences); - } - }; + eq -> eq.removeSequenceLengths(sequences); return computeNewOffsets(removeSequenceLengthsFunc); } + /** * Returns a copy of this qualifier with sequence-offset pairs where in the original the * offset contains an access of an sequence length in {@code sequences}. The sequence length @@ -824,35 +1152,32 @@ public OffsetEquation compute(OffsetEquation eq) { * @param sequences access of the length of these sequences are removed * @return a copy of this qualifier with some offsets removed */ - public UBQualifier removeSequenceLengthAccessAndNeg1(final List sequences) { + public UBQualifier removeSequenceLengthAccessAndNeg1(List sequences) { if (sequences.isEmpty()) { return UpperBoundUnknownQualifier.UNKNOWN; } OffsetEquationFunction removeSequenceLenFunc = - new OffsetEquationFunction() { - @Override - public OffsetEquation compute(OffsetEquation eq) { - OffsetEquation newEq = eq.removeSequenceLengths(sequences); - if (newEq == null) { - return null; - } - if (newEq.getInt() == -1) { - return newEq.copyAdd('+', OffsetEquation.ONE); - } - return newEq; + eq -> { + OffsetEquation newEq = eq.removeSequenceLengths(sequences); + if (newEq == null) { + return null; } + if (newEq.getInt() == -1) { + return newEq.copyAdd('+', OffsetEquation.ONE); + } + return newEq; }; return computeNewOffsets(removeSequenceLenFunc); } - private UBQualifier addOffset(final OffsetEquation newOffset) { - OffsetEquationFunction addOffsetFunc = - new OffsetEquationFunction() { - @Override - public OffsetEquation compute(OffsetEquation eq) { - return eq.copyAdd('+', newOffset); - } - }; + /** + * Returns a new qualifier, which is this qualifier plus the given offset. + * + * @param newOffset the offset to add to this + * @return a new qualifier, which is this qualifier plus the given offset + */ + private UBQualifier addOffset(OffsetEquation newOffset) { + OffsetEquationFunction addOffsetFunc = eq -> eq.copyAdd('+', newOffset); return computeNewOffsets(addOffsetFunc); } @@ -871,16 +1196,7 @@ public UBQualifier divide(int divisor) { if (divisor == 1) { return this; } else if (divisor > 1) { - OffsetEquationFunction divideFunc = - new OffsetEquationFunction() { - @Override - public OffsetEquation compute(OffsetEquation eq) { - if (eq.isNegativeOrZero()) { - return eq; - } - return null; - } - }; + OffsetEquationFunction divideFunc = eq -> (eq.isNegativeOrZero() ? eq : null); return computeNewOffsets(divideFunc); } return UpperBoundUnknownQualifier.UNKNOWN; @@ -926,29 +1242,34 @@ public Iterable getSequences() { return map.keySet(); } - /** Generates a new UBQualifer without the given sequence and offset. */ + /** + * Generates a new UBQualifer without the given (sequence, offset) pair. Other occurrences + * of the sequence and the offset may remain in the result, but not together. + * + * @param sequence a Java expression representing a string + * @param offset an integral offset + * @return a new UBQualifer without the given sequence and offset + */ public UBQualifier removeOffset(String sequence, int offset) { OffsetEquation offsetEq = OffsetEquation.createOffsetForInt(offset); - List sequences = new ArrayList<>(); - List offsets = new ArrayList<>(); - for (String seq : this.map.keySet()) { - Set offsetSet = this.map.get(seq); - for (OffsetEquation off : offsetSet) { - if (!sequence.equals(seq) && !off.equals(offsetEq)) { - sequences.add(seq); - offsets.add(off.toString()); - } + Map> newMap = copyMap(); + Set equations = newMap.get(sequence); + if (equations != null) { + equations.remove(offsetEq); + if (equations.isEmpty()) { + newMap.remove(sequence); } } - if (sequences.isEmpty()) { + + if (newMap.isEmpty()) { return UpperBoundUnknownQualifier.UNKNOWN; } else { - return UBQualifier.createUBQualifier(sequences, offsets); + return new LessThanLengthOf(newMap); } } /** Functional interface that operates on {@link OffsetEquation}s. */ - interface OffsetEquationFunction { + private interface OffsetEquationFunction { /** * Returns the result of the computation or null if the passed equation should be * removed. @@ -957,7 +1278,7 @@ interface OffsetEquationFunction { * @return the result of the computation or null if the passed equation should be * removed */ - OffsetEquation compute(OffsetEquation eq); + @Nullable OffsetEquation compute(OffsetEquation eq); } /** @@ -994,9 +1315,111 @@ private UBQualifier computeNewOffsets(OffsetEquationFunction f) { } } + /** Represents an integer value that is known at compile time. */ + public static class UpperBoundLiteralQualifier extends UBQualifier { + + /** Represents the value -1. */ + public static final UpperBoundLiteralQualifier NEGATIVEONE = + new UpperBoundLiteralQualifier(-1); + + /** Represents the value 0. */ + public static final UpperBoundLiteralQualifier ZERO = new UpperBoundLiteralQualifier(0); + + /** Represents the value 1. */ + public static final UpperBoundLiteralQualifier ONE = new UpperBoundLiteralQualifier(1); + + /** + * Creates a new UpperBoundLiteralQualifier, without using cached values. + * + * @param value the integer value + */ + private UpperBoundLiteralQualifier(int value) { + this.value = value; + } + + /** + * Creates an UpperBoundLiteralQualifier. + * + * @param value the integer value + * @return an UpperBoundLiteralQualifier + */ + public static UpperBoundLiteralQualifier create(int value) { + switch (value) { + case -1: + return NEGATIVEONE; + case 0: + return ZERO; + case 1: + return ONE; + default: + return new UpperBoundLiteralQualifier(value); + } + } + + /** The integer value. */ + private final int value; + + /** + * Returns the integer value. + * + * @return the integer value + */ + public int getValue() { + return value; + } + + @Override + public boolean isLiteral() { + return true; + } + + @Override + public boolean isSubtype(UBQualifier superType) { + if (superType.isUnknown()) { + return true; + } else if (superType.isBottom()) { + return false; + } else if (superType.isPoly()) { + return false; + } else if (superType.isLiteral()) { + int otherValue = ((UpperBoundLiteralQualifier) superType).value; + return value == otherValue; + } + + LessThanLengthOf superTypeLTL = (LessThanLengthOf) superType; + return superTypeLTL.literalIsSubtype(value); + } + + @Override + public UBQualifier lub(UBQualifier other) { + if (isSubtype(other)) { + return other; + } else { + return UpperBoundUnknownQualifier.UNKNOWN; + } + } + + @Override + public UBQualifier glb(UBQualifier other) { + if (isSubtype(other)) { + return this; + } else { + return UpperBoundBottomQualifier.BOTTOM; + } + } + + @Override + public String toString() { + return "Literal(" + value + ")"; + } + } + + /** The top type qualifier. */ public static class UpperBoundUnknownQualifier extends UBQualifier { - static final UBQualifier UNKNOWN = new UpperBoundUnknownQualifier(); + /** The canonical representative. */ + public static final UBQualifier UNKNOWN = new UpperBoundUnknownQualifier(); + /** This class is a singleton. */ private UpperBoundUnknownQualifier() {} @Override @@ -1025,8 +1448,13 @@ public String toString() { } } + /** The bottom qualifier for the upperbound type system. */ private static class UpperBoundBottomQualifier extends UBQualifier { - static final UBQualifier BOTTOM = new UpperBoundBottomQualifier(); + /** The canonical bottom qualifier for the upperbound type system. */ + public static final UBQualifier BOTTOM = new UpperBoundBottomQualifier(); + + /** This class is a singleton. */ + private UpperBoundBottomQualifier() {} @Override public boolean isBottom() { @@ -1054,10 +1482,16 @@ public String toString() { } } + /** The polymorphic qualifier. */ private static class PolyQualifier extends UBQualifier { - static final UBQualifier POLY = new PolyQualifier(); + /** The canonical representative. */ + public static final UBQualifier POLY = new PolyQualifier(); + + /** This class is a singleton. */ + private PolyQualifier() {} @Override + @Pure public boolean isPoly() { return true; } diff --git a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundAnnotatedTypeFactory.java index 7e7eb39ec0dd..ed90a94a4d40 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundAnnotatedTypeFactory.java @@ -3,20 +3,14 @@ import com.sun.source.tree.BinaryTree; import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LiteralTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.UnaryTree; import com.sun.source.util.TreePath; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; + +import org.checkerframework.checker.index.BaseAnnotatedTypeFactoryForIndexChecker; +import org.checkerframework.checker.index.IndexChecker; import org.checkerframework.checker.index.IndexMethodIdentifier; import org.checkerframework.checker.index.IndexUtil; import org.checkerframework.checker.index.OffsetDependentTypesHelper; @@ -37,6 +31,7 @@ import org.checkerframework.checker.index.qual.SameLen; import org.checkerframework.checker.index.qual.SearchIndexFor; import org.checkerframework.checker.index.qual.UpperBoundBottom; +import org.checkerframework.checker.index.qual.UpperBoundLiteral; import org.checkerframework.checker.index.qual.UpperBoundUnknown; import org.checkerframework.checker.index.samelen.SameLenAnnotatedTypeFactory; import org.checkerframework.checker.index.samelen.SameLenChecker; @@ -45,34 +40,50 @@ import org.checkerframework.checker.index.substringindex.SubstringIndexAnnotatedTypeFactory; import org.checkerframework.checker.index.substringindex.SubstringIndexChecker; import org.checkerframework.checker.index.upperbound.UBQualifier.LessThanLengthOf; +import org.checkerframework.checker.index.upperbound.UBQualifier.UpperBoundLiteralQualifier; import org.checkerframework.checker.index.upperbound.UBQualifier.UpperBoundUnknownQualifier; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.value.ValueAnnotatedTypeFactory; import org.checkerframework.common.value.ValueChecker; import org.checkerframework.common.value.ValueCheckerUtils; import org.checkerframework.common.value.qual.BottomVal; -import org.checkerframework.dataflow.analysis.FlowExpressions; +import org.checkerframework.common.value.util.Range; import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.flow.CFAbstractStore; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.ElementQualifierHierarchy; import org.checkerframework.framework.type.QualifierHierarchy; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionParseException; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory; +import org.checkerframework.framework.util.JavaExpressionParseUtil.JavaExpressionParseException; import org.checkerframework.framework.util.dependenttypes.DependentTypesHelper; import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; -import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; +import org.plumelib.util.IPair; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.util.Elements; /** * Implements the introduction rules for the Upper Bound Checker. @@ -82,7 +93,7 @@ *

      *
    • 1. Math.min has unusual semantics that combines annotations for the UBC. *
    • 2. The return type of Random.nextInt depends on the argument, but is not equal to it, so a - * polymorhpic qualifier is insufficient. + * polymorphic qualifier is insufficient. *
    • 3. Unary negation on a NegativeIndexFor (from the SearchIndex Checker) results in a * LTLengthOf for the same arrays. *
    • 4. Right shifting by a constant between 0 and 30 preserves the type of the left side @@ -99,31 +110,68 @@ *
    • 10. Special handling for Math.random: Math.random() * array.length is LTL array. *
    */ -public class UpperBoundAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { +public class UpperBoundAnnotatedTypeFactory extends BaseAnnotatedTypeFactoryForIndexChecker { /** The @{@link UpperBoundUnknown} annotation. */ public final AnnotationMirror UNKNOWN = AnnotationBuilder.fromClass(elements, UpperBoundUnknown.class); + /** The @{@link UpperBoundBottom} annotation. */ public final AnnotationMirror BOTTOM = AnnotationBuilder.fromClass(elements, UpperBoundBottom.class); + /** The @{@link PolyUpperBound} annotation. */ public final AnnotationMirror POLY = AnnotationBuilder.fromClass(elements, PolyUpperBound.class); + /** The @{@link UpperBoundLiteral}(-1) annotation. */ + public final AnnotationMirror NEGATIVEONE = + new AnnotationBuilder(getProcessingEnv(), UpperBoundLiteral.class) + .setValue("value", -1) + .build(); + + /** The @{@link UpperBoundLiteral}(0) annotation. */ + public final AnnotationMirror ZERO = + new AnnotationBuilder(getProcessingEnv(), UpperBoundLiteral.class) + .setValue("value", 0) + .build(); + + /** The @{@link UpperBoundLiteral}(1) annotation. */ + public final AnnotationMirror ONE = + new AnnotationBuilder(getProcessingEnv(), UpperBoundLiteral.class) + .setValue("value", 1) + .build(); + + /** The NegativeIndexFor.value element/field. */ + public final ExecutableElement negativeIndexForValueElement = + TreeUtils.getMethod(NegativeIndexFor.class, "value", 0, processingEnv); + + /** The SameLen.value element/field. */ + public final ExecutableElement sameLenValueElement = + TreeUtils.getMethod(SameLen.class, "value", 0, processingEnv); + + /** The LTLengthOf.value element/field. */ + public final ExecutableElement ltLengthOfValueElement = + TreeUtils.getMethod(LTLengthOf.class, "value", 0, processingEnv); + + /** The LTLengthOf.offset element/field. */ + public final ExecutableElement ltLengthOfOffsetElement = + TreeUtils.getMethod(LTLengthOf.class, "offset", 0, processingEnv); + + /** Predicates about what method an invocation is calling. */ private final IndexMethodIdentifier imf; /** Create a new UpperBoundAnnotatedTypeFactory. */ public UpperBoundAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - addAliasedAnnotation(IndexFor.class, LTLengthOf.class, true); - addAliasedAnnotation(IndexOrLow.class, LTLengthOf.class, true); - addAliasedAnnotation(IndexOrHigh.class, LTEqLengthOf.class, true); - addAliasedAnnotation(SearchIndexFor.class, LTLengthOf.class, true); - addAliasedAnnotation(NegativeIndexFor.class, LTLengthOf.class, true); - addAliasedAnnotation(LengthOf.class, LTEqLengthOf.class, true); - addAliasedAnnotation(PolyIndex.class, POLY); + addAliasedTypeAnnotation(IndexFor.class, LTLengthOf.class, true); + addAliasedTypeAnnotation(IndexOrLow.class, LTLengthOf.class, true); + addAliasedTypeAnnotation(IndexOrHigh.class, LTEqLengthOf.class, true); + addAliasedTypeAnnotation(SearchIndexFor.class, LTLengthOf.class, true); + addAliasedTypeAnnotation(NegativeIndexFor.class, LTLengthOf.class, true); + addAliasedTypeAnnotation(LengthOf.class, LTEqLengthOf.class, true); + addAliasedTypeAnnotation(PolyIndex.class, POLY); imf = new IndexMethodIdentifier(this); @@ -144,6 +192,7 @@ protected Set> createSupportedTypeQualifiers() { LTEqLengthOf.class, LTLengthOf.class, LTOMLengthOf.class, + UpperBoundLiteral.class, UpperBoundBottom.class, PolyUpperBound.class)); } @@ -198,7 +247,7 @@ public LessThanAnnotatedTypeFactory getLessThanAnnotatedTypeFactory() { @Override public void addComputedTypeAnnotations(Element element, AnnotatedTypeMirror type) { super.addComputedTypeAnnotations(element, type); - if (element != null) { + if (element != null && !ajavaTypes.isParsing()) { AnnotatedTypeMirror valueType = getValueAnnotatedTypeFactory().getAnnotatedType(element); addUpperBoundTypeFromValueType(valueType, type); @@ -211,7 +260,10 @@ public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, bool // If dataflow shouldn't be used to compute this type, then do not use the result from // the Value Checker, because dataflow is used to compute that type. (Without this, // "int i = 1; --i;" fails.) - if (iUseFlow && tree != null && TreeUtils.isExpressionTree(tree)) { + if (iUseFlow + && tree != null + && !ajavaTypes.isParsing() + && TreeUtils.isExpressionTree(tree)) { AnnotatedTypeMirror valueType = getValueAnnotatedTypeFactory().getAnnotatedType(tree); addUpperBoundTypeFromValueType(valueType, type); } @@ -254,9 +306,14 @@ protected Void scan(AnnotatedTypeMirror type, Void aVoid) { AnnotationMirror anm = type.getAnnotation(LTLengthOf.class); if (anm != null) { List sequences = - AnnotationUtils.getElementValueArray(anm, "value", String.class, true); + AnnotationUtils.getElementValueArray( + anm, ltLengthOfValueElement, String.class); List offsets = - AnnotationUtils.getElementValueArray(anm, "offset", String.class, true); + AnnotationUtils.getElementValueArray( + anm, + ltLengthOfOffsetElement, + String.class, + Collections.emptyList()); if (sequences != null && offsets != null && sequences.size() != offsets.size() @@ -280,7 +337,7 @@ protected DependentTypesHelper createDependentTypesHelper() { * Queries the SameLen Checker to return the type that the SameLen Checker associates with the * given tree. */ - public AnnotationMirror sameLenAnnotationFromTree(Tree tree) { + public @Nullable AnnotationMirror sameLenAnnotationFromTree(Tree tree) { AnnotatedTypeMirror sameLenType = getSameLenAnnotatedTypeFactory().getAnnotatedType(tree); return sameLenType.getAnnotation(SameLen.class); } @@ -301,9 +358,15 @@ public boolean isRandomNextInt(Tree methodTree) { return imf.isRandomNextInt(methodTree, processingEnv); } + /** + * Creates a new @LTLengthOf annotation. + * + * @param names the arguments to @LTLengthOf + * @return a new @LTLengthOf annotation with the given arguments + */ AnnotationMirror createLTLengthOfAnnotation(String... names) { if (names == null || names.length == 0) { - throw new BugInCF( + throw new TypeSystemError( "createLTLengthOfAnnotation: bad argument %s", Arrays.toString(names)); } AnnotationBuilder builder = new AnnotationBuilder(getProcessingEnv(), LTLengthOf.class); @@ -311,9 +374,15 @@ AnnotationMirror createLTLengthOfAnnotation(String... names) { return builder.build(); } + /** + * Creates a new @LTEqLengthOf annotation. + * + * @param names the arguments to @LTEqLengthOf + * @return a new @LTEqLengthOf annotation with the given arguments + */ AnnotationMirror createLTEqLengthOfAnnotation(String... names) { if (names == null || names.length == 0) { - throw new BugInCF( + throw new TypeSystemError( "createLTEqLengthOfAnnotation: bad argument %s", Arrays.toString(names)); } AnnotationBuilder builder = new AnnotationBuilder(getProcessingEnv(), LTEqLengthOf.class); @@ -338,29 +407,28 @@ public boolean hasLowerBoundTypeByClass(Node node, Class c } @Override - public QualifierHierarchy createQualifierHierarchy(MultiGraphFactory factory) { - return new UpperBoundQualifierHierarchy(factory); + protected QualifierHierarchy createQualifierHierarchy() { + return new UpperBoundQualifierHierarchy(this.getSupportedTypeQualifiers(), elements); } - /** - * The qualifier hierarchy for the upperbound type system. The qh is responsible for determining - * the relationships within the qualifiers - especially subtyping relations. - */ - protected final class UpperBoundQualifierHierarchy extends MultiGraphQualifierHierarchy { + /** The qualifier hierarchy for the upperbound type system. */ + protected final class UpperBoundQualifierHierarchy extends ElementQualifierHierarchy { /** - * Create an UpperBoundQualifierHierarchy. + * Creates an UpperBoundQualifierHierarchy from the given classes. * - * @param factory the MultiGraphFactory to use to construct this + * @param qualifierClasses classes of annotations that are the qualifiers + * @param elements element utils */ - public UpperBoundQualifierHierarchy( - MultiGraphQualifierHierarchy.MultiGraphFactory factory) { - super(factory); + UpperBoundQualifierHierarchy( + Collection> qualifierClasses, Elements elements) { + super(qualifierClasses, elements, UpperBoundAnnotatedTypeFactory.this); } @Override - public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror a2) { - UBQualifier a1Obj = UBQualifier.createUBQualifier(a1); - UBQualifier a2Obj = UBQualifier.createUBQualifier(a2); + public AnnotationMirror greatestLowerBoundQualifiers( + AnnotationMirror a1, AnnotationMirror a2) { + UBQualifier a1Obj = UBQualifier.createUBQualifier(a1, (IndexChecker) checker); + UBQualifier a2Obj = UBQualifier.createUBQualifier(a2, (IndexChecker) checker); UBQualifier glb = a1Obj.glb(a2Obj); return convertUBQualifierToAnnotation(glb); } @@ -373,9 +441,10 @@ public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror * @return the least upper bound of a1 and a2 */ @Override - public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2) { - UBQualifier a1Obj = UBQualifier.createUBQualifier(a1); - UBQualifier a2Obj = UBQualifier.createUBQualifier(a2); + public AnnotationMirror leastUpperBoundQualifiers( + AnnotationMirror a1, AnnotationMirror a2) { + UBQualifier a1Obj = UBQualifier.createUBQualifier(a1, (IndexChecker) checker); + UBQualifier a2Obj = UBQualifier.createUBQualifier(a2, (IndexChecker) checker); UBQualifier lub = a1Obj.lub(a2Obj); return convertUBQualifierToAnnotation(lub); } @@ -383,8 +452,9 @@ public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2 @Override public AnnotationMirror widenedUpperBound( AnnotationMirror newQualifier, AnnotationMirror previousQualifier) { - UBQualifier a1Obj = UBQualifier.createUBQualifier(newQualifier); - UBQualifier a2Obj = UBQualifier.createUBQualifier(previousQualifier); + UBQualifier a1Obj = UBQualifier.createUBQualifier(newQualifier, (IndexChecker) checker); + UBQualifier a2Obj = + UBQualifier.createUBQualifier(previousQualifier, (IndexChecker) checker); UBQualifier lub = a1Obj.widenUpperBound(a2Obj); return convertUBQualifierToAnnotation(lub); } @@ -395,17 +465,19 @@ public int numberOfIterationsBeforeWidening() { } /** - * Computes subtyping as per the subtyping in the qualifier hierarchy structure unless both - * annotations are the same. In this case, rhs is a subtype of lhs iff rhs contains at least - * every element of lhs. + * {@inheritDoc} * - * @return true if rhs is a subtype of lhs, false otherwise + *

    Computes subtyping as per the subtyping in the qualifier hierarchy structure unless + * both annotations have the same class. In this case, rhs is a subtype of lhs iff rhs + * contains every element of lhs. */ @Override - public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { - UBQualifier subtype = UBQualifier.createUBQualifier(subAnno); - UBQualifier supertype = UBQualifier.createUBQualifier(superAnno); - return subtype.isSubtype(supertype); + public boolean isSubtypeQualifiers(AnnotationMirror subAnno, AnnotationMirror superAnno) { + UBQualifier subtypeQual = + UBQualifier.createUBQualifier(subAnno, (IndexChecker) checker); + UBQualifier supertypeQual = + UBQualifier.createUBQualifier(superAnno, (IndexChecker) checker); + return subtypeQual.isSubtype(supertypeQual); } } @@ -427,7 +499,7 @@ public UpperBoundTreeAnnotator(UpperBoundAnnotatedTypeFactory factory) { *

      *
    • Math.min has unusual semantics that combines annotations for the UBC. *
    • The return type of Random.nextInt depends on the argument, but is not equal to it, - * so a polymorhpic qualifier is insufficient. + * so a polymorphic qualifier is insufficient. *
    * * Other methods should not be special-cased here unless there is a compelling reason to do @@ -440,32 +512,111 @@ public Void visitMethodInvocation(MethodInvocationTree tree, AnnotatedTypeMirror AnnotatedTypeMirror rightType = getAnnotatedType(tree.getArguments().get(1)); type.replaceAnnotation( - qualHierarchy.greatestLowerBound( + qualHierarchy.greatestLowerBoundShallow( leftType.getAnnotationInHierarchy(UNKNOWN), - rightType.getAnnotationInHierarchy(UNKNOWN))); + leftType.getUnderlyingType(), + rightType.getAnnotationInHierarchy(UNKNOWN), + rightType.getUnderlyingType())); } if (isRandomNextInt(tree)) { AnnotatedTypeMirror argType = getAnnotatedType(tree.getArguments().get(0)); AnnotationMirror anno = argType.getAnnotationInHierarchy(UNKNOWN); - UBQualifier qualifier = UBQualifier.createUBQualifier(anno); + UBQualifier qualifier = UBQualifier.createUBQualifier(anno, (IndexChecker) checker); qualifier = qualifier.plusOffset(1); type.replaceAnnotation(convertUBQualifierToAnnotation(qualifier)); } + if (imf.isIndexOfString(tree)) { + // String#indexOf(String) and its variants that also take a String technically + // return (and are annotated as) @LTEqLengthOf the receiver. However, the result is + // always @LTLengthOf the receiver unless both the receiver and the target string + // are the empty string: "".indexOf("") returns 0, which isn't an index into "". So, + // this special case modifies the return type of these methods if either the + // parameter or the receiver is known (by the Value Checker) to not be the empty + // string. There are three ways the Value Checker might have that information: + // either string could have a @StringVal annotation whose value doesn't include "", + // either could have an @ArrayLen annotation whose value doesn't contain zero, or + // either could have an @ArrayLenRange annotation whose from value is any positive + // integer. + ValueAnnotatedTypeFactory vatf = + ((UpperBoundAnnotatedTypeFactory) this.atypeFactory) + .getValueAnnotatedTypeFactory(); + AnnotatedTypeMirror argType = vatf.getAnnotatedType(tree.getArguments().get(0)); + AnnotatedTypeMirror receiverType = vatf.getReceiverType(tree); + if (definitelyIsNotTheEmptyString(argType, vatf) + || definitelyIsNotTheEmptyString(receiverType, vatf)) { + String receiverName = JavaExpression.getReceiver(tree).toString(); + UBQualifier ltLengthOfReceiver = + UBQualifier.createUBQualifier(receiverName, "0"); + AnnotationMirror currentReturnAnno = type.getAnnotationInHierarchy(UNKNOWN); + UBQualifier currentUBQualifier = + UBQualifier.createUBQualifier( + currentReturnAnno, (IndexChecker) checker); + UBQualifier result = currentUBQualifier.glb(ltLengthOfReceiver); + type.replaceAnnotation(convertUBQualifierToAnnotation(result)); + } + } return super.visitMethodInvocation(tree, type); } + /** + * Returns true if the given Value Checker annotations guarantee that the annotated element + * is not the empty string. + * + * @param atm an annotated type from the Value Checker + * @param vatf the Value Annotated Type Factory + * @return true iff atm contains a {@code StringVal} annotation whose value doesn't contain + * "", an {@code ArrayLen} annotation whose value doesn't contain 0, or an {@code + * ArrayLenRange} annotation whose from value is greater than 0 + */ + private boolean definitelyIsNotTheEmptyString( + AnnotatedTypeMirror atm, ValueAnnotatedTypeFactory vatf) { + AnnotationMirrorSet annos = atm.getAnnotations(); + for (AnnotationMirror anno : annos) { + switch (AnnotationUtils.annotationName(anno)) { + case ValueAnnotatedTypeFactory.STRINGVAL_NAME: + List strings = vatf.getStringValues(anno); + if (strings != null && !strings.contains("")) { + return true; + } + break; + case ValueAnnotatedTypeFactory.ARRAYLEN_NAME: + List lengths = vatf.getArrayLength(anno); + if (lengths != null && !lengths.contains(0)) { + return true; + } + break; + default: + Range range = vatf.getRange(anno); + if (range != null && range.from > 0) { + return true; + } + break; + } + } + return false; + } + + @Override + public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) { + // Could also handle long literals, but array indexes are always ints. + if (tree.getKind() == Tree.Kind.INT_LITERAL) { + type.addAnnotation(createLiteral(((Integer) tree.getValue()).intValue())); + } + return super.visitLiteral(tree, type); + } + /* Handles case 3. */ @Override - public Void visitUnary(UnaryTree node, AnnotatedTypeMirror type) { + public Void visitUnary(UnaryTree tree, AnnotatedTypeMirror type) { // Dataflow refines this type if possible - if (node.getKind() == Kind.BITWISE_COMPLEMENT) { + if (tree.getKind() == Tree.Kind.BITWISE_COMPLEMENT) { addAnnotationForBitwiseComplement( - getSearchIndexAnnotatedTypeFactory().getAnnotatedType(node.getExpression()), + getSearchIndexAnnotatedTypeFactory().getAnnotatedType(tree.getExpression()), type); } else { type.addAnnotation(UNKNOWN); } - return super.visitUnary(node, type); + return super.visitUnary(tree, type); } /** @@ -493,9 +644,11 @@ public Void visitUnary(UnaryTree node, AnnotatedTypeMirror type) { */ private void addAnnotationForBitwiseComplement( AnnotatedTypeMirror searchIndexType, AnnotatedTypeMirror typeDst) { - if (containsSameByClass(searchIndexType.getAnnotations(), NegativeIndexFor.class)) { - AnnotationMirror nif = searchIndexType.getAnnotation(NegativeIndexFor.class); - List arrays = ValueCheckerUtils.getValueOfAnnotationWithStringArgument(nif); + AnnotationMirror nif = searchIndexType.getAnnotation(NegativeIndexFor.class); + if (nif != null) { + List arrays = + AnnotationUtils.getElementValueArray( + nif, negativeIndexForValueElement, String.class); List negativeOnes = Collections.nCopies(arrays.size(), "-1"); UBQualifier qual = UBQualifier.createUBQualifier(arrays, negativeOnes); typeDst.addAnnotation(convertUBQualifierToAnnotation(qual)); @@ -505,10 +658,10 @@ private void addAnnotationForBitwiseComplement( } @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMirror type) { + public Void visitCompoundAssignment(CompoundAssignmentTree tree, AnnotatedTypeMirror type) { // Dataflow refines this type if possible type.addAnnotation(UNKNOWN); - return super.visitCompoundAssignment(node, type); + return super.visitCompoundAssignment(tree, type); } @Override @@ -559,9 +712,10 @@ private void addAnnotationForRightShift( if (lowerBoundATF.isNonNegative(left)) { AnnotationMirror annotation = getAnnotatedType(left).getAnnotationInHierarchy(UNKNOWN); - // For non-negative numbers, right shift is equivalent to division by a power of two + // For non-negative numbers, right shift is equivalent to division by a power of + // two. // The range of the shift amount is limited to 0..30 to avoid overflows and int/long - // differences + // differences. Long shiftAmount = ValueCheckerUtils.getExactValue(right, getValueAnnotatedTypeFactory()); if (shiftAmount != null && shiftAmount >= 0 && shiftAmount < Integer.SIZE - 1) { @@ -569,7 +723,8 @@ private void addAnnotationForRightShift( // Support average by shift just like for division UBQualifier plusDivQualifier = plusTreeDivideByVal(divisor, left); if (!plusDivQualifier.isUnknown()) { - UBQualifier qualifier = UBQualifier.createUBQualifier(annotation); + UBQualifier qualifier = + UBQualifier.createUBQualifier(annotation, (IndexChecker) checker); qualifier = qualifier.glb(plusDivQualifier); annotation = convertUBQualifierToAnnotation(qualifier); } @@ -587,22 +742,27 @@ private void addAnnotationForAnd( ExpressionTree left, ExpressionTree right, AnnotatedTypeMirror type) { LowerBoundAnnotatedTypeFactory lowerBoundATF = getLowerBoundAnnotatedTypeFactory(); AnnotatedTypeMirror leftType = getAnnotatedType(left); - AnnotationMirror leftResultType = UNKNOWN; + AnnotationMirror leftResultAnno = UNKNOWN; if (lowerBoundATF.isNonNegative(left)) { - leftResultType = leftType.getAnnotationInHierarchy(UNKNOWN); + leftResultAnno = leftType.getAnnotationInHierarchy(UNKNOWN); } AnnotatedTypeMirror rightType = getAnnotatedType(right); - AnnotationMirror rightResultType = UNKNOWN; + AnnotationMirror rightResultAnno = UNKNOWN; if (lowerBoundATF.isNonNegative(right)) { - rightResultType = rightType.getAnnotationInHierarchy(UNKNOWN); + rightResultAnno = rightType.getAnnotationInHierarchy(UNKNOWN); } - type.addAnnotation(qualHierarchy.greatestLowerBound(leftResultType, rightResultType)); + type.addAnnotation( + qualHierarchy.greatestLowerBoundShallow( + leftResultAnno, + leftType.getUnderlyingType(), + rightResultAnno, + rightType.getUnderlyingType())); } /** Gets a sequence tree for a length access tree, or null if it is not a length access. */ - private ExpressionTree getLengthSequenceTree(ExpressionTree lengthTree) { + private @Nullable ExpressionTree getLengthSequenceTree(ExpressionTree lengthTree) { return IndexUtil.getLengthSequenceTree(lengthTree, imf, processingEnv); } @@ -623,12 +783,15 @@ private void addAnnotationForRemainder( UBQualifier result = UpperBoundUnknownQualifier.UNKNOWN; // if numerator >= 0, then numerator%divisor <= numerator if (lowerBoundATF.isNonNegative(numeratorTree)) { - result = UBQualifier.createUBQualifier(getAnnotatedType(numeratorTree), UNKNOWN); + result = + UBQualifier.createUBQualifier( + getAnnotatedType(numeratorTree), UNKNOWN, (IndexChecker) checker); } // if divisor >= 0, then numerator%divisor < divisor if (lowerBoundATF.isNonNegative(divisorTree)) { UBQualifier divisor = - UBQualifier.createUBQualifier(getAnnotatedType(divisorTree), UNKNOWN); + UBQualifier.createUBQualifier( + getAnnotatedType(divisorTree), UNKNOWN, (IndexChecker) checker); result = result.glb(divisor.plusOffset(1)); } resultType.addAnnotation(convertUBQualifierToAnnotation(result)); @@ -659,7 +822,8 @@ private void addAnnotationForDivide( UBQualifier result = UpperBoundUnknownQualifier.UNKNOWN; UBQualifier numerator = - UBQualifier.createUBQualifier(getAnnotatedType(numeratorTree), UNKNOWN); + UBQualifier.createUBQualifier( + getAnnotatedType(numeratorTree), UNKNOWN, (IndexChecker) checker); if (numerator.isLessThanLengthQualifier()) { result = ((LessThanLengthOf) numerator).divide(divisor.intValue()); } @@ -683,21 +847,30 @@ private void addAnnotationForDivide( } /** - * if numeratorTree is a + b and divisor greater than 1, and a and b are less than the - * length of some sequence, then (a + b) / divisor is less than the length of that sequence. + * If {@code numeratorTree} is "a + b" and {@code divisor} is greater than 1, and a and b + * are less than the length of some sequence, then "(a + b) / divisor" is less than the + * length of that sequence. + * + * @param divisor the divisor + * @param numeratorTree an addition tree that is divided by {@code divisor} + * @return a qualifier for the division */ private UBQualifier plusTreeDivideByVal(int divisor, ExpressionTree numeratorTree) { numeratorTree = TreeUtils.withoutParens(numeratorTree); - if (divisor < 2 || numeratorTree.getKind() != Kind.PLUS) { + if (divisor < 2 || numeratorTree.getKind() != Tree.Kind.PLUS) { return UpperBoundUnknownQualifier.UNKNOWN; } BinaryTree plusTree = (BinaryTree) numeratorTree; UBQualifier left = UBQualifier.createUBQualifier( - getAnnotatedType(plusTree.getLeftOperand()), UNKNOWN); + getAnnotatedType(plusTree.getLeftOperand()), + UNKNOWN, + (IndexChecker) checker); UBQualifier right = UBQualifier.createUBQualifier( - getAnnotatedType(plusTree.getRightOperand()), UNKNOWN); + getAnnotatedType(plusTree.getRightOperand()), + UNKNOWN, + (IndexChecker) checker); if (left.isLessThanLengthQualifier() && right.isLessThanLengthQualifier()) { LessThanLengthOf leftLTL = (LessThanLengthOf) left; LessThanLengthOf rightLTL = (LessThanLengthOf) right; @@ -757,6 +930,33 @@ private void addAnnotationForMultiply( } } + /** + * Creates a @{@link UpperBoundLiteral} annotation. + * + * @param i the integer + * @return a @{@link UpperBoundLiteral} annotation + */ + public AnnotationMirror createLiteral(int i) { + switch (i) { + case -1: + return NEGATIVEONE; + case 0: + return ZERO; + case 1: + return ONE; + default: + return new AnnotationBuilder(getProcessingEnv(), UpperBoundLiteral.class) + .setValue("value", i) + .build(); + } + } + + /** + * Convert the internal representation to an annotation. + * + * @param qualifier a UBQualifier + * @return an annotation corresponding to the given qualifier + */ public AnnotationMirror convertUBQualifierToAnnotation(UBQualifier qualifier) { if (qualifier.isUnknown()) { return UNKNOWN; @@ -764,13 +964,15 @@ public AnnotationMirror convertUBQualifierToAnnotation(UBQualifier qualifier) { return BOTTOM; } else if (qualifier.isPoly()) { return POLY; + } else if (qualifier.isLiteral()) { + return createLiteral(((UpperBoundLiteralQualifier) qualifier).getValue()); } LessThanLengthOf ltlQualifier = (LessThanLengthOf) qualifier; return ltlQualifier.convertToAnnotation(processingEnv); } - UBQualifier fromLessThan(ExpressionTree tree, TreePath treePath) { + @Nullable UBQualifier fromLessThan(ExpressionTree tree, TreePath treePath) { List lessThanExpressions = getLessThanAnnotatedTypeFactory().getLessThanExpressions(tree); if (lessThanExpressions == null) { @@ -783,7 +985,7 @@ UBQualifier fromLessThan(ExpressionTree tree, TreePath treePath) { return null; } - UBQualifier fromLessThanOrEqual(ExpressionTree tree, TreePath treePath) { + @Nullable UBQualifier fromLessThanOrEqual(ExpressionTree tree, TreePath treePath) { List lessThanExpressions = getLessThanAnnotatedTypeFactory().getLessThanExpressions(tree); if (lessThanExpressions == null) { @@ -793,34 +995,35 @@ UBQualifier fromLessThanOrEqual(ExpressionTree tree, TreePath treePath) { return ubQualifier; } - private UBQualifier fromLessThanOrEqual( + private @Nullable UBQualifier fromLessThanOrEqual( Tree tree, TreePath treePath, List lessThanExpressions) { UBQualifier ubQualifier = null; for (String expression : lessThanExpressions) { - Pair receiverAndOffset; + IPair exprAndOffset; try { - receiverAndOffset = - getReceiverAndOffsetFromJavaExpressionString(expression, treePath); - } catch (FlowExpressionParseException e) { - receiverAndOffset = null; + exprAndOffset = + getExpressionAndOffsetFromJavaExpressionString(expression, treePath); + } catch (JavaExpressionParseException e) { + exprAndOffset = null; } - if (receiverAndOffset == null) { + if (exprAndOffset == null) { continue; } - FlowExpressions.Receiver receiver = receiverAndOffset.first; - String offset = receiverAndOffset.second; + JavaExpression je = exprAndOffset.first; + String offset = exprAndOffset.second; - if (!CFAbstractStore.canInsertReceiver(receiver)) { + if (!CFAbstractStore.canInsertJavaExpression(je)) { continue; } CFStore store = getStoreBefore(tree); - CFValue value = store.getValue(receiver); + CFValue value = store.getValue(je); if (value != null && value.getAnnotations().size() == 1) { UBQualifier newUBQ = UBQualifier.createUBQualifier( qualHierarchy.findAnnotationInHierarchy( value.getAnnotations(), UNKNOWN), - AnnotatedTypeFactory.negateConstant(offset)); + AnnotatedTypeFactory.negateConstant(offset), + (IndexChecker) checker); if (ubQualifier == null) { ubQualifier = newUBQ; } else { diff --git a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundChecker.java b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundChecker.java index be40a56445eb..2e4fbf840d3c 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundChecker.java @@ -1,26 +1,75 @@ package org.checkerframework.checker.index.upperbound; -import java.util.HashSet; -import java.util.LinkedHashSet; import org.checkerframework.checker.index.inequality.LessThanChecker; import org.checkerframework.checker.index.lowerbound.LowerBoundChecker; +import org.checkerframework.checker.index.qual.LTEqLengthOf; +import org.checkerframework.checker.index.qual.LTLengthOf; +import org.checkerframework.checker.index.qual.LTOMLengthOf; +import org.checkerframework.checker.index.qual.SubstringIndexFor; +import org.checkerframework.checker.index.qual.UpperBoundLiteral; import org.checkerframework.checker.index.samelen.SameLenChecker; import org.checkerframework.checker.index.searchindex.SearchIndexChecker; import org.checkerframework.checker.index.substringindex.SubstringIndexChecker; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.signature.qual.FullyQualifiedName; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.value.ValueChecker; +import org.checkerframework.framework.qual.RelevantJavaTypes; import org.checkerframework.framework.source.SuppressWarningsPrefix; +import org.checkerframework.javacutil.TreeUtils; + +import java.util.HashSet; +import java.util.Set; + +import javax.lang.model.element.ExecutableElement; /** * A type-checker for preventing arrays from being accessed with values that are too high. * * @checker_framework.manual #index-checker Index Checker */ +@RelevantJavaTypes({ + Byte.class, + Short.class, + Integer.class, + Long.class, + Character.class, + byte.class, + short.class, + int.class, + long.class, + char.class, +}) @SuppressWarningsPrefix({"index", "upperbound"}) public class UpperBoundChecker extends BaseTypeChecker { + /** The SubstringIndexFor.value argument/element. */ + public @MonotonicNonNull ExecutableElement substringIndexForValueElement; + + /** The SubstringIndexFor.offset argument/element. */ + public @MonotonicNonNull ExecutableElement substringIndexForOffsetElement; + + /** The LTLengthOf.value argument/element. */ + public @MonotonicNonNull ExecutableElement ltLengthOfValueElement; + + /** The LTLengthOf.offset argument/element. */ + public @MonotonicNonNull ExecutableElement ltLengthOfOffsetElement; - private HashSet collectionBaseTypeNames; + /** The LTEqLengthOf.value argument/element. */ + public @MonotonicNonNull ExecutableElement ltEqLengthOfValueElement; + /** The LTOMLengthOf.value argument/element. */ + public @MonotonicNonNull ExecutableElement ltOMLengthOfValueElement; + + /** The UpperBoundLiteral.value element/field. */ + public @MonotonicNonNull ExecutableElement upperBoundLiteralValueElement; + + /** + * These collection classes have some subtypes whose length can change and some subtypes whose + * length cannot change. Warnings are skipped at uses of them. + */ + private final HashSet collectionBaseTypeNames; + + /** Create a new UpperBoundChecker. */ public UpperBoundChecker() { // These classes are bases for both mutable and immutable sequence collections, which // contain methods that change the length. @@ -33,7 +82,24 @@ public UpperBoundChecker() { } @Override - public boolean shouldSkipUses(String typeName) { + public void initChecker() { + super.initChecker(); + substringIndexForValueElement = + TreeUtils.getMethod(SubstringIndexFor.class, "value", 0, processingEnv); + substringIndexForOffsetElement = + TreeUtils.getMethod(SubstringIndexFor.class, "offset", 0, processingEnv); + ltLengthOfValueElement = TreeUtils.getMethod(LTLengthOf.class, "value", 0, processingEnv); + ltLengthOfOffsetElement = TreeUtils.getMethod(LTLengthOf.class, "offset", 0, processingEnv); + ltEqLengthOfValueElement = + TreeUtils.getMethod(LTEqLengthOf.class, "value", 0, processingEnv); + ltOMLengthOfValueElement = + TreeUtils.getMethod(LTOMLengthOf.class, "value", 0, processingEnv); + upperBoundLiteralValueElement = + TreeUtils.getMethod(UpperBoundLiteral.class, "value", 0, processingEnv); + } + + @Override + public boolean shouldSkipUses(@FullyQualifiedName String typeName) { if (collectionBaseTypeNames.contains(typeName)) { return true; } @@ -41,9 +107,8 @@ public boolean shouldSkipUses(String typeName) { } @Override - protected LinkedHashSet> getImmediateSubcheckerClasses() { - LinkedHashSet> checkers = - super.getImmediateSubcheckerClasses(); + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); checkers.add(SubstringIndexChecker.class); checkers.add(SearchIndexChecker.class); checkers.add(SameLenChecker.class); diff --git a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundTransfer.java b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundTransfer.java index a9c0ebfe8e72..b0ccca0ff3a8 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/index/upperbound/UpperBoundTransfer.java @@ -2,10 +2,7 @@ import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; + import org.checkerframework.checker.index.IndexAbstractTransfer; import org.checkerframework.checker.index.IndexRefinementInfo; import org.checkerframework.checker.index.Subsequence; @@ -16,12 +13,8 @@ import org.checkerframework.checker.index.qual.SubstringIndexFor; import org.checkerframework.checker.index.upperbound.UBQualifier.LessThanLengthOf; import org.checkerframework.checker.index.upperbound.UBQualifier.UpperBoundUnknownQualifier; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.value.ValueCheckerUtils; -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.MethodCall; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; import org.checkerframework.dataflow.analysis.RegularTransferResult; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; @@ -29,12 +22,16 @@ import org.checkerframework.dataflow.cfg.node.AssignmentNode; import org.checkerframework.dataflow.cfg.node.CaseNode; import org.checkerframework.dataflow.cfg.node.FieldAccessNode; +import org.checkerframework.dataflow.cfg.node.IntegerLiteralNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.cfg.node.NumericalAdditionNode; import org.checkerframework.dataflow.cfg.node.NumericalMultiplicationNode; import org.checkerframework.dataflow.cfg.node.NumericalSubtractionNode; import org.checkerframework.dataflow.cfg.node.TypeCastNode; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.MethodCall; import org.checkerframework.dataflow.util.NodeUtils; import org.checkerframework.framework.flow.CFAbstractStore; import org.checkerframework.framework.flow.CFAnalysis; @@ -42,7 +39,16 @@ import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionContext; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; /** * Contains the transfer functions for the upper bound type system, a part of the Index Checker. @@ -104,11 +110,21 @@ */ public class UpperBoundTransfer extends IndexAbstractTransfer { - private UpperBoundAnnotatedTypeFactory atypeFactory; + /** The type factory associated with this transfer function. */ + private final UpperBoundAnnotatedTypeFactory atypeFactory; + /** The int TypeMirror. */ + private final TypeMirror intTM; + + /** + * Creates a new UpperBoundTransfer. + * + * @param analysis the analysis for this transfer function + */ public UpperBoundTransfer(CFAnalysis analysis) { super(analysis); atypeFactory = (UpperBoundAnnotatedTypeFactory) analysis.getTypeFactory(); + intTM = atypeFactory.types.getPrimitiveType(TypeKind.INT); } /** @@ -141,16 +157,15 @@ public TransferResult visitAssignment( Node dim = acNode.getDimension(0); UBQualifier previousQualifier = getUBQualifier(dim, in); - Receiver arrayRec = - FlowExpressions.internalReprOf(analysis.getTypeFactory(), node.getTarget()); - String arrayString = arrayRec.toString(); + JavaExpression arrayExpr = JavaExpression.fromNode(node.getTarget()); + String arrayString = arrayExpr.toString(); LessThanLengthOf newInfo = (LessThanLengthOf) UBQualifier.createUBQualifier(arrayString, "-1"); UBQualifier combined = previousQualifier.glb(newInfo); AnnotationMirror newAnno = atypeFactory.convertUBQualifierToAnnotation(combined); - Receiver dimRec = FlowExpressions.internalReprOf(analysis.getTypeFactory(), dim); - result.getRegularStore().insertValue(dimRec, newAnno); + JavaExpression dimExpr = JavaExpression.fromNode(dim); + result.getRegularStore().insertValue(dimExpr, newAnno); propagateToOperands(newInfo, dim, in, result.getRegularStore()); } return result; @@ -209,8 +224,8 @@ private void propagateToMultiplicationOperand( } UBQualifier qual = getUBQualifier(node, in); UBQualifier newQual = qual.glb(typeOfMultiplication); - Receiver rec = FlowExpressions.internalReprOf(atypeFactory, node); - store.insertValue(rec, atypeFactory.convertUBQualifierToAnnotation(newQual)); + JavaExpression je = JavaExpression.fromNode(node); + store.insertValue(je, atypeFactory.convertUBQualifierToAnnotation(newQual)); } } @@ -238,8 +253,8 @@ private void propagateToSubtractionOperands( UBQualifier newInfo = typeOfSubtraction.minusOffset(node.getRightOperand(), atypeFactory); UBQualifier newLeft = left.glb(newInfo); - Receiver leftRec = FlowExpressions.internalReprOf(atypeFactory, node.getLeftOperand()); - store.insertValue(leftRec, atypeFactory.convertUBQualifierToAnnotation(newLeft)); + JavaExpression leftJe = JavaExpression.fromNode(node.getLeftOperand()); + store.insertValue(leftJe, atypeFactory.convertUBQualifierToAnnotation(newLeft)); } /** @@ -263,14 +278,14 @@ private void propagateToAdditionOperand( UBQualifier operandQual = getUBQualifier(operand, in); UBQualifier newQual = operandQual.glb(typeOfAddition.plusOffset(other, atypeFactory)); - /** If the node is NN, add an LTEL to the qual. If POS, add an LTL. */ + // If the node is NonNegative, add an LTEL to the qual. If Positive, add an LTL. if (atypeFactory.hasLowerBoundTypeByClass(other, Positive.class)) { newQual = newQual.glb(typeOfAddition.plusOffset(1)); } else if (atypeFactory.hasLowerBoundTypeByClass(other, NonNegative.class)) { newQual = newQual.glb(typeOfAddition); } - Receiver operandRec = FlowExpressions.internalReprOf(atypeFactory, operand); - store.insertValue(operandRec, atypeFactory.convertUBQualifierToAnnotation(newQual)); + JavaExpression operandJe = JavaExpression.fromNode(operand); + store.insertValue(operandJe, atypeFactory.convertUBQualifierToAnnotation(newQual)); } /** @@ -286,10 +301,14 @@ protected void refineGT( CFStore store, TransferInput in) { // larger > smaller - UBQualifier largerQual = UBQualifier.createUBQualifier(largerAnno); + UBQualifier largerQual = + UBQualifier.createUBQualifier( + largerAnno, (UpperBoundChecker) atypeFactory.getChecker()); // larger + 1 >= smaller UBQualifier largerQualPlus1 = largerQual.plusOffset(1); - UBQualifier rightQualifier = UBQualifier.createUBQualifier(smallerAnno); + UBQualifier rightQualifier = + UBQualifier.createUBQualifier( + smallerAnno, (UpperBoundChecker) atypeFactory.getChecker()); UBQualifier refinedRight = rightQualifier.glb(largerQualPlus1); if (largerQualPlus1.isLessThanLengthQualifier()) { @@ -298,8 +317,8 @@ protected void refineGT( refineSubtrahendWithOffset(larger, smaller, true, in, store); - Receiver rightRec = FlowExpressions.internalReprOf(analysis.getTypeFactory(), smaller); - store.insertValue(rightRec, atypeFactory.convertUBQualifierToAnnotation(refinedRight)); + JavaExpression rightJe = JavaExpression.fromNode(smaller); + store.insertValue(rightJe, atypeFactory.convertUBQualifierToAnnotation(refinedRight)); } /** @@ -314,8 +333,12 @@ protected void refineGTE( AnnotationMirror rightAnno, CFStore store, TransferInput in) { - UBQualifier leftQualifier = UBQualifier.createUBQualifier(leftAnno); - UBQualifier rightQualifier = UBQualifier.createUBQualifier(rightAnno); + UBQualifier leftQualifier = + UBQualifier.createUBQualifier( + leftAnno, (UpperBoundChecker) atypeFactory.getChecker()); + UBQualifier rightQualifier = + UBQualifier.createUBQualifier( + rightAnno, (UpperBoundChecker) atypeFactory.getChecker()); UBQualifier refinedRight = rightQualifier.glb(leftQualifier); if (leftQualifier.isLessThanLengthQualifier()) { @@ -324,8 +347,8 @@ protected void refineGTE( refineSubtrahendWithOffset(left, right, false, in, store); - Receiver rightRec = FlowExpressions.internalReprOf(analysis.getTypeFactory(), right); - store.insertValue(rightRec, atypeFactory.convertUBQualifierToAnnotation(refinedRight)); + JavaExpression rightJe = JavaExpression.fromNode(right); + store.insertValue(rightJe, atypeFactory.convertUBQualifierToAnnotation(refinedRight)); } /** @@ -363,8 +386,8 @@ private void refineSubtrahendWithOffset( minuendQual .plusOffset(offsetNode, atypeFactory) .plusOffset(offsetAddOne ? 1 : 0)); - Receiver subtrahendRec = FlowExpressions.internalReprOf(atypeFactory, subtrahend); - store.insertValue(subtrahendRec, atypeFactory.convertUBQualifierToAnnotation(newQual)); + JavaExpression subtrahendJe = JavaExpression.fromNode(subtrahend); + store.insertValue(subtrahendJe, atypeFactory.convertUBQualifierToAnnotation(newQual)); } } @@ -402,21 +425,25 @@ private void refineEq( Node right, AnnotationMirror rightAnno, CFStore store) { - UBQualifier leftQualifier = UBQualifier.createUBQualifier(leftAnno); - UBQualifier rightQualifier = UBQualifier.createUBQualifier(rightAnno); + UBQualifier leftQualifier = + UBQualifier.createUBQualifier( + leftAnno, (UpperBoundChecker) atypeFactory.getChecker()); + UBQualifier rightQualifier = + UBQualifier.createUBQualifier( + rightAnno, (UpperBoundChecker) atypeFactory.getChecker()); UBQualifier glb = rightQualifier.glb(leftQualifier); AnnotationMirror glbAnno = atypeFactory.convertUBQualifierToAnnotation(glb); List internalsRight = splitAssignments(right); for (Node internal : internalsRight) { - Receiver rightRec = FlowExpressions.internalReprOf(analysis.getTypeFactory(), internal); - store.insertValue(rightRec, glbAnno); + JavaExpression rightJe = JavaExpression.fromNode(internal); + store.insertValue(rightJe, glbAnno); } List internalsLeft = splitAssignments(left); for (Node internal : internalsLeft) { - Receiver leftRec = FlowExpressions.internalReprOf(analysis.getTypeFactory(), internal); - store.insertValue(leftRec, glbAnno); + JavaExpression leftJe = JavaExpression.fromNode(internal); + store.insertValue(leftJe, glbAnno); } } @@ -429,9 +456,8 @@ private void refineEq( private void refineNeqSequenceLength( Node lengthAccess, Node otherNode, AnnotationMirror otherNodeAnno, CFStore store) { - Receiver receiver = null; - // If lengthAccess is "receiver.length - c" where c is an integer constant, stores c - // into lengthOffset + // If lengthAccess is "receiver.length - c" where c is an integer constant, + // then lengthOffset is "c". int lengthOffset = 0; if (lengthAccess instanceof NumericalSubtractionNode) { NumericalSubtractionNode subtraction = (NumericalSubtractionNode) lengthAccess; @@ -450,21 +476,24 @@ private void refineNeqSequenceLength( } } + JavaExpression receiver = null; if (NodeUtils.isArrayLengthFieldAccess(lengthAccess)) { FieldAccess fa = - FlowExpressions.internalReprOfFieldAccess( - atypeFactory, (FieldAccessNode) lengthAccess); + (FieldAccess) + JavaExpression.fromNodeFieldAccess((FieldAccessNode) lengthAccess); receiver = fa.getReceiver(); } else if (atypeFactory.getMethodIdentifier().isLengthOfMethodInvocation(lengthAccess)) { - Receiver ma = FlowExpressions.internalReprOf(atypeFactory, lengthAccess); + JavaExpression ma = JavaExpression.fromNode(lengthAccess); if (ma instanceof MethodCall) { receiver = ((MethodCall) ma).getReceiver(); } } if (receiver != null && !receiver.containsUnknown()) { - UBQualifier otherQualifier = UBQualifier.createUBQualifier(otherNodeAnno); + UBQualifier otherQualifier = + UBQualifier.createUBQualifier( + otherNodeAnno, (UpperBoundChecker) atypeFactory.getChecker()); String sequence = receiver.toString(); // Check if otherNode + c - 1 < receiver.length if (otherQualifier.hasSequenceWithOffset(sequence, lengthOffset - 1)) { @@ -473,10 +502,9 @@ private void refineNeqSequenceLength( UBQualifier.createUBQualifier(sequence, Integer.toString(lengthOffset)); otherQualifier = otherQualifier.glb(newQualifier); for (Node internal : splitAssignments(otherNode)) { - Receiver leftRec = - FlowExpressions.internalReprOf(analysis.getTypeFactory(), internal); + JavaExpression leftJe = JavaExpression.fromNode(internal); store.insertValue( - leftRec, atypeFactory.convertUBQualifierToAnnotation(otherQualifier)); + leftJe, atypeFactory.convertUBQualifierToAnnotation(otherQualifier)); } } } @@ -501,7 +529,7 @@ private void refineNeqSequenceLength( @Override public TransferResult visitNumericalAddition( NumericalAdditionNode n, TransferInput in) { - // type of leftNode + rightNode is glb(t, s) where + // type of leftNode + rightNode is glb(t, s) where // t = minusOffset(type(leftNode), rightNode) and // s = minusOffset(type(rightNode), leftNode) @@ -528,13 +556,15 @@ public TransferResult visitNumericalAddition( } /** - * Return the result of adding i to j, when expression i has type @LTLengthOf(value = "f2", - * offset = "f1.length") int and expression j is less than or equal to the length of f1, then - * the type of i + j is @LTLengthOf("f2"). + * Return the result of adding i to j. + * + *

    When expression i has type {@code @LTLengthOf(value = "f2", offset = "f1.length") int} and + * expression j is less than or equal to the length of f1, then the type of i + j + * is @LTLengthOf("f2"). * - *

    Similarly, return the result of adding i to j, when expression i has type @LTLengthOf - * (value = "f2", offset = "f1.length - 1") int and expression j is less than the length of f1, - * then the type of i + j is @LTLengthOf("f2"). + *

    When expression i has type {@code @LTLengthOf (value = "f2", offset = "f1.length - 1") + * int} and expression j is less than the length of f1, then the type of i + j + * is @LTLengthOf("f2"). * * @param i the type of the expression added to j * @param j the type of the expression added to i @@ -542,18 +572,18 @@ public TransferResult visitNumericalAddition( */ private UBQualifier removeSequenceLengths(LessThanLengthOf i, LessThanLengthOf j) { List lessThan = new ArrayList<>(); - List lessThanOrEqaul = new ArrayList<>(); + List lessThanOrEqual = new ArrayList<>(); for (String sequence : i.getSequences()) { if (i.isLessThanLengthOf(sequence)) { lessThan.add(sequence); } else if (i.hasSequenceWithOffset(sequence, -1)) { - lessThanOrEqaul.add(sequence); + lessThanOrEqual.add(sequence); } } // Creates a qualifier that is the same a j with the array.length offsets removed. If // an offset doesn't have an array.length, then the offset/array pair is removed. If // there are no such pairs, Unknown is returned. - UBQualifier lessThanEqQ = j.removeSequenceLengthAccess(lessThanOrEqaul); + UBQualifier lessThanEqQ = j.removeSequenceLengthAccess(lessThanOrEqual); // Creates a qualifier that is the same a j with the array.length - 1 offsets removed. If // an offset doesn't have an array.length, then the offset/array pair is removed. If // there are no such pairs, Unknown is returned. @@ -595,53 +625,47 @@ public TransferResult visitNumericalSubtraction( || subtractionResult.hasSequenceWithOffset(b, 0)) { TreePath currentPath = this.atypeFactory.getPath(n.getTree()); - FlowExpressions.Receiver rec; + JavaExpression je; try { - rec = - UpperBoundVisitor.getReceiverFromJavaExpressionString( + je = + UpperBoundVisitor.parseJavaExpressionString( b, atypeFactory, currentPath); } catch (NullPointerException npe) { - // I have no idea why this seems to happen only on a few JDK classes. - // It appears to only happen during the preprocessing step - the NPE - // is thrown while trying to find the enclosing class of a class tree, - // which is null. I can't find a reproducible - // test case that's smaller than the size of DualPivotQuicksort. - // Since this refinement is optional, but useful elsewhere, catching this - // NPE here and returning is always safe. + // I have no idea why this seems to happen only on a few JDK classes. It + // appears to only happen during the preprocessing step - the NPE is thrown + // while trying to find the enclosing class of a class tree, which is null. + // I can't find a reproducible test case that's smaller than the size of + // DualPivotQuicksort. Since this refinement is optional, but useful + // elsewhere, catching this NPE here and returning is always safe. return createTransferResult(n, in, leftWithOffset); } - FlowExpressionContext context = - Subsequence.getContextFromReceiver(rec, atypeFactory.getContext()); - Subsequence subsequence = - Subsequence.getSubsequenceFromReceiver( - rec, atypeFactory, currentPath, context); + Subsequence.getSubsequenceFromReceiver(je, atypeFactory); if (subsequence != null) { String from = subsequence.from; String to = subsequence.to; String a = subsequence.array; - Receiver leftOp = - FlowExpressions.internalReprOf(atypeFactory, n.getLeftOperand()); - Receiver rightOp = - FlowExpressions.internalReprOf(atypeFactory, n.getRightOperand()); + JavaExpression leftOp = JavaExpression.fromNode(n.getLeftOperand()); + JavaExpression rightOp = JavaExpression.fromNode(n.getRightOperand()); if (rightOp.toString().equals(from)) { + LessThanAnnotatedTypeFactory lessThanAtypeFactory = + atypeFactory.getLessThanAnnotatedTypeFactory(); AnnotationMirror lessThanType = - atypeFactory - .getLessThanAnnotatedTypeFactory() + lessThanAtypeFactory .getAnnotatedType(n.getLeftOperand().getTree()) .getAnnotation(LessThan.class); if (lessThanType != null - && LessThanAnnotatedTypeFactory.isLessThan(lessThanType, to)) { + && lessThanAtypeFactory.isLessThan(lessThanType, to)) { UBQualifier ltlA = UBQualifier.createUBQualifier(a, "0"); leftWithOffset = leftWithOffset.glb(ltlA); } else if (leftOp.toString().equals(to) || (lessThanType != null - && LessThanAnnotatedTypeFactory.isLessThanOrEqual( + && lessThanAtypeFactory.isLessThanOrEqual( lessThanType, to))) { // It's necessary to check if leftOp == to because LessThan doesn't // infer that things are less than or equal to themselves. @@ -661,28 +685,32 @@ public TransferResult visitNumericalSubtraction( * * @param n sequence length access node */ - private TransferResult visitLengthAccess( - Node n, TransferInput in, Receiver sequenceRec, Tree sequenceTree) { + private @Nullable TransferResult visitLengthAccess( + Node n, + TransferInput in, + JavaExpression sequenceJe, + Tree sequenceTree) { if (sequenceTree == null) { return null; } // Look up the SameLen type of the sequence. AnnotationMirror sameLenAnno = atypeFactory.sameLenAnnotationFromTree(sequenceTree); - List sameLenSequences = - sameLenAnno == null - ? new ArrayList<>() - : ValueCheckerUtils.getValueOfAnnotationWithStringArgument(sameLenAnno); - - if (!sameLenSequences.contains(sequenceRec.toString())) { - sameLenSequences.add(sequenceRec.toString()); + List sameLenSequences; + if (sameLenAnno == null) { + sameLenSequences = Collections.singletonList(sequenceJe.toString()); + } else { + sameLenSequences = + AnnotationUtils.getElementValueArray( + sameLenAnno, atypeFactory.sameLenValueElement, String.class); + String sequenceString = sequenceJe.toString(); + if (!sameLenSequences.contains(sequenceString)) { + sameLenSequences.add(sequenceString); + } } - ArrayList offsets = new ArrayList<>(sameLenSequences.size()); - for (@SuppressWarnings("unused") String s : sameLenSequences) { - offsets.add("-1"); - } + List offsets = Collections.nCopies(sameLenSequences.size(), "-1"); - if (CFAbstractStore.canInsertReceiver(sequenceRec)) { + if (CFAbstractStore.canInsertJavaExpression(sequenceJe)) { UBQualifier qualifier = UBQualifier.createUBQualifier(sameLenSequences, offsets); UBQualifier previous = getUBQualifier(n, in); return createTransferResult(n, in, qualifier.glb(previous)); @@ -699,10 +727,10 @@ private TransferResult visitLengthAccess( public TransferResult visitFieldAccess( FieldAccessNode n, TransferInput in) { if (NodeUtils.isArrayLengthFieldAccess(n)) { - FieldAccess arrayLength = FlowExpressions.internalReprOfFieldAccess(atypeFactory, n); - Receiver arrayRec = arrayLength.getReceiver(); + FieldAccess arrayLength = (FieldAccess) JavaExpression.fromNodeFieldAccess(n); + JavaExpression arrayJe = arrayLength.getReceiver(); Tree arrayTree = n.getReceiver().getTree(); - TransferResult result = visitLengthAccess(n, in, arrayRec, arrayTree); + TransferResult result = visitLengthAccess(n, in, arrayJe, arrayTree); if (result != null) { return result; } @@ -719,14 +747,14 @@ public TransferResult visitMethodInvocation( MethodInvocationNode n, TransferInput in) { if (atypeFactory.getMethodIdentifier().isLengthOfMethodInvocation(n)) { - Receiver stringLength = FlowExpressions.internalReprOf(atypeFactory, n); + JavaExpression stringLength = JavaExpression.fromNode(n); if (stringLength instanceof MethodCall) { - Receiver receiverRec = ((MethodCall) stringLength).getReceiver(); + JavaExpression receiverJe = ((MethodCall) stringLength).getReceiver(); Tree receiverTree = n.getTarget().getReceiver().getTree(); // receiverTree is null when the receiver is implicit "this". if (receiverTree != null) { TransferResult result = - visitLengthAccess(n, in, receiverRec, receiverTree); + visitLengthAccess(n, in, receiverJe, receiverTree); if (result != null) { return result; } @@ -765,7 +793,9 @@ private UBQualifier getUBQualifierForAddition(Node n, TransferInput in) { QualifierHierarchy hierarchy = analysis.getTypeFactory().getQualifierHierarchy(); - Receiver rec = FlowExpressions.internalReprOf(atypeFactory, n); + JavaExpression je = JavaExpression.fromNode(n); CFValue value = null; - if (CFAbstractStore.canInsertReceiver(rec)) { - value = in.getRegularStore().getValue(rec); + if (CFAbstractStore.canInsertJavaExpression(je)) { + value = in.getRegularStore().getValue(je); } if (value == null) { value = analysis.getValue(n); @@ -794,8 +824,8 @@ private UBQualifier getUBQualifier(Node n, TransferInput in) { UBQualifier qualifier = getUBQualifier(hierarchy, value); if (qualifier.isUnknown()) { // The qualifier from the store or analysis might be UNKNOWN if there was some error. - // For example, - // @LTLength("a") int i = 4; // error + // For example, + // @LTLength("a") int i = 4; // error // The type of i in the store is @UpperBoundUnknown, but the type of i as computed by // the type factory is @LTLength("a"), so use that type. CFValue valueFromFactory = getValueFromFactory(n.getTree(), n); @@ -808,27 +838,19 @@ private UBQualifier getUBQualifier(QualifierHierarchy hierarchy, CFValue value) if (value == null) { return UpperBoundUnknownQualifier.UNKNOWN; } - Set set = value.getAnnotations(); + AnnotationMirrorSet set = value.getAnnotations(); AnnotationMirror anno = hierarchy.findAnnotationInHierarchy(set, atypeFactory.UNKNOWN); if (anno == null) { return UpperBoundUnknownQualifier.UNKNOWN; } - return UBQualifier.createUBQualifier(anno); + return UBQualifier.createUBQualifier(anno, (UpperBoundChecker) atypeFactory.getChecker()); } private TransferResult createTransferResult( Node n, TransferInput in, UBQualifier qualifier) { AnnotationMirror newAnno = atypeFactory.convertUBQualifierToAnnotation(qualifier); CFValue value = analysis.createSingleAnnotationValue(newAnno, n.getType()); - if (in.containsTwoStores()) { - CFStore thenStore = in.getThenStore(); - CFStore elseStore = in.getElseStore(); - return new ConditionalTransferResult<>( - finishValue(value, thenStore, elseStore), thenStore, elseStore); - } else { - CFStore info = in.getRegularStore(); - return new RegularTransferResult<>(finishValue(value, info), info); - } + return createTransferResult(value, in); } @Override @@ -836,12 +858,35 @@ public TransferResult visitCase( CaseNode n, TransferInput in) { TransferResult result = super.visitCase(n, in); // Refines subtrahend in the switch expression - // TODO: this cannot be done in strengthenAnnotationOfEqualTo, because that does not provide - // transfer input - Node caseNode = n.getCaseOperand(); - AssignmentNode assign = (AssignmentNode) n.getSwitchOperand(); + // TODO: This cannot be done in strengthenAnnotationOfEqualTo, because that does not provide + // transfer input. + List caseNodes = n.getCaseOperands(); + AssignmentNode assign = n.getSwitchOperand(); Node switchNode = assign.getExpression(); - refineSubtrahendWithOffset(switchNode, caseNode, false, in, result.getThenStore()); + for (Node caseNode : caseNodes) { + refineSubtrahendWithOffset(switchNode, caseNode, false, in, result.getThenStore()); + } return result; } + + @Override + public TransferResult visitIntegerLiteral( + IntegerLiteralNode n, TransferInput pi) { + TransferResult result = super.visitIntegerLiteral(n, pi); + + int intValue = n.getValue(); + AnnotationMirror newAnno; + switch (intValue) { + case 0: + newAnno = atypeFactory.ZERO; + break; + case -1: + newAnno = atypeFactory.NEGATIVEONE; + break; + default: + return result; + } + CFValue c = new CFValue(analysis, AnnotationMirrorSet.singleton(newAnno), intTM); + return new RegularTransferResult<>(c, result.getRegularStore()); + } } 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 35b0d8e81263..c04714bbc46b 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 @@ -6,38 +6,41 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.NewArrayTree; import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; import com.sun.source.util.TreePath; -import java.util.List; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.type.TypeKind; + import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.index.Subsequence; import org.checkerframework.checker.index.qual.HasSubsequence; import org.checkerframework.checker.index.qual.LTLengthOf; import org.checkerframework.checker.index.samelen.SameLenAnnotatedTypeFactory; import org.checkerframework.checker.index.upperbound.UBQualifier.LessThanLengthOf; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.common.value.ValueAnnotatedTypeFactory; import org.checkerframework.common.value.ValueCheckerUtils; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.FieldAccess; -import org.checkerframework.dataflow.analysis.FlowExpressions.LocalVariable; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; -import org.checkerframework.dataflow.analysis.FlowExpressions.ThisReference; -import org.checkerframework.dataflow.analysis.FlowExpressions.ValueLiteral; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.LocalVariable; +import org.checkerframework.dataflow.expression.ThisReference; +import org.checkerframework.dataflow.expression.ValueLiteral; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType; -import org.checkerframework.framework.util.FlowExpressionParseUtil; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionContext; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionParseException; +import org.checkerframework.framework.util.JavaExpressionParseUtil.JavaExpressionParseException; +import org.checkerframework.framework.util.StringToJavaExpression; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; -import org.checkerframework.javacutil.Pair; +import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; +import org.plumelib.util.IPair; + +import java.util.Collections; +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.type.TypeKind; /** Warns about array accesses that could be too high. */ public class UpperBoundVisitor extends BaseTypeVisitor { @@ -74,20 +77,25 @@ public Void visitArrayAccess(ArrayAccessTree tree, Void type) { /** Warns about LTLengthOf annotations with arguments whose lengths do not match. */ @Override - public Void visitAnnotation(AnnotationTree node, Void p) { - AnnotationMirror anno = TreeUtils.annotationFromAnnotationTree(node); + public Void visitAnnotation(AnnotationTree tree, Void p) { + AnnotationMirror anno = TreeUtils.annotationFromAnnotationTree(tree); if (atypeFactory.areSameByClass(anno, LTLengthOf.class)) { - List args = node.getArguments(); + List args = tree.getArguments(); if (args.size() == 2) { // If offsets are provided, there must be the same number of them as there are // arrays. List sequences = - AnnotationUtils.getElementValueArray(anno, "value", String.class, true); + AnnotationUtils.getElementValueArray( + anno, atypeFactory.ltLengthOfValueElement, String.class); List offsets = - AnnotationUtils.getElementValueArray(anno, "offset", String.class, true); + AnnotationUtils.getElementValueArray( + anno, + atypeFactory.ltLengthOfOffsetElement, + String.class, + Collections.emptyList()); if (sequences.size() != offsets.size() && !offsets.isEmpty()) { checker.reportError( - node, + tree, "different.length.sequences.offsets", sequences.size(), offsets.size()); @@ -95,47 +103,51 @@ public Void visitAnnotation(AnnotationTree node, Void p) { } } } else if (atypeFactory.areSameByClass(anno, HasSubsequence.class)) { - // Check that the arguments to a HasSubsequence annotation are valid flow expressions, + // Check that the arguments to a HasSubsequence annotation are valid JavaExpressions, // and issue an error if one of them is not. - String seq = AnnotationUtils.getElementValue(anno, "subsequence", String.class, true); - String from = AnnotationUtils.getElementValue(anno, "from", String.class, true); - String to = AnnotationUtils.getElementValue(anno, "to", String.class, true); - - // check that each expression is parseable in this context - ClassTree enclosingClass = TreeUtils.enclosingClass(getCurrentPath()); - FlowExpressionContext context = - FlowExpressionContext.buildContextForClassDeclaration(enclosingClass, checker); - checkEffectivelyFinalAndParsable(seq, context, node); - checkEffectivelyFinalAndParsable(from, context, node); - checkEffectivelyFinalAndParsable(to, context, node); + String seq = atypeFactory.hasSubsequenceSubsequenceValue(anno); + String from = atypeFactory.hasSubsequenceFromValue(anno); + String to = atypeFactory.hasSubsequenceToValue(anno); + + // check that each expression is parsable at the declaration of this class + ClassTree enclosingClass = TreePathUtil.enclosingClass(getCurrentPath()); + checkEffectivelyFinalAndParsable(seq, enclosingClass, tree); + checkEffectivelyFinalAndParsable(from, enclosingClass, tree); + checkEffectivelyFinalAndParsable(to, enclosingClass, tree); } - return super.visitAnnotation(node, p); + return super.visitAnnotation(tree, p); } /** - * Determines if the Java expression named by s is effectively final at the current program - * location. + * Reports an error if the Java expression named by s is not effectively final when parsed at + * the declaration of the given class. + * + * @param s a Java expression + * @param classTree the expression is parsed with respect to this class + * @param whereToReportError the tree at which to possibly report an error */ private void checkEffectivelyFinalAndParsable( - String s, FlowExpressionContext context, Tree error) { - Receiver rec; + String s, ClassTree classTree, Tree whereToReportError) { + JavaExpression je; try { - rec = FlowExpressionParseUtil.parse(s, context, getCurrentPath(), false); - } catch (FlowExpressionParseException e) { - checker.report(error, e.getDiagMessage()); + je = + StringToJavaExpression.atTypeDecl( + s, TreeUtils.elementFromDeclaration(classTree), checker); + } catch (JavaExpressionParseException e) { + checker.report(whereToReportError, e.getDiagMessage()); return; } Element element = null; - if (rec instanceof LocalVariable) { - element = ((LocalVariable) rec).getElement(); - } else if (rec instanceof FieldAccess) { - element = ((FieldAccess) rec).getField(); - } else if (rec instanceof ThisReference || rec instanceof ValueLiteral) { + if (je instanceof LocalVariable) { + element = ((LocalVariable) je).getElement(); + } else if (je instanceof FieldAccess) { + element = ((FieldAccess) je).getField(); + } else if (je instanceof ThisReference || je instanceof ValueLiteral) { return; } if (element == null || !ElementUtils.isEffectivelyFinal(element)) { - checker.reportError(error, NOT_FINAL, rec); + checker.reportError(whereToReportError, NOT_FINAL, je); } } @@ -143,10 +155,13 @@ private void checkEffectivelyFinalAndParsable( * Checks if this array access is legal. Uses the common assignment check and a simple MinLen * check of its own. The MinLen check is needed because the common assignment check always * returns false when the upper bound qualifier is @UpperBoundUnknown. + * + * @param indexTree the array index + * @param arrTree the array */ private void visitAccess(ExpressionTree indexTree, ExpressionTree arrTree) { - String arrName = FlowExpressions.internalReprOf(this.atypeFactory, arrTree).toString(); + String arrName = JavaExpression.fromTree(arrTree).toString(); LessThanLengthOf lhsQual = (LessThanLengthOf) UBQualifier.createUBQualifier(arrName, "0"); if (relaxedCommonAssignmentCheck(lhsQual, indexTree) || checkMinLen(indexTree, arrTree)) { return; @@ -159,7 +174,9 @@ private void visitAccess(ExpressionTree indexTree, ExpressionTree arrTree) { // 3. If neither of the above, issue an error that names the upper bound type. AnnotatedTypeMirror indexType = atypeFactory.getAnnotatedType(indexTree); - UBQualifier qualifier = UBQualifier.createUBQualifier(indexType, atypeFactory.UNKNOWN); + UBQualifier qualifier = + UBQualifier.createUBQualifier( + indexType, atypeFactory.UNKNOWN, (UpperBoundChecker) checker); ValueAnnotatedTypeFactory valueFactory = atypeFactory.getValueAnnotatedTypeFactory(); Long valMax = ValueCheckerUtils.getMaxValue(indexTree, valueFactory); @@ -189,8 +206,13 @@ private void visitAccess(ExpressionTree indexTree, ExpressionTree arrTree) { } @Override - protected void commonAssignmentCheck( - Tree varTree, ExpressionTree valueTree, @CompilerMessageKey String errorKey) { + protected boolean commonAssignmentCheck( + Tree varTree, + ExpressionTree valueTree, + @CompilerMessageKey String errorKey, + Object... extraArgs) { + + boolean result = true; // 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). @@ -202,13 +224,13 @@ protected void commonAssignmentCheck( anm = atypeFactory.getAnnotationMirrorFromJavaExpressionString( subSeq.to, varTree, getCurrentPath()); - } catch (FlowExpressionParseException e) { + } catch (JavaExpressionParseException e) { anm = null; } boolean ltelCheckFailed = true; if (anm != null) { - UBQualifier qual = UBQualifier.createUBQualifier(anm); + UBQualifier qual = UBQualifier.createUBQualifier(anm, (UpperBoundChecker) checker); ltelCheckFailed = !qual.isLessThanOrEqualTo(subSeq.array); } @@ -223,6 +245,7 @@ protected void commonAssignmentCheck( subSeq.array, subSeq.array, subSeq.array); + result = false; } else { checker.reportWarning( valueTree, @@ -237,27 +260,34 @@ protected void commonAssignmentCheck( } } - super.commonAssignmentCheck(varTree, valueTree, errorKey); + result = super.commonAssignmentCheck(varTree, valueTree, errorKey, extraArgs) && result; + return result; } @Override - protected void commonAssignmentCheck( + protected boolean commonAssignmentCheck( AnnotatedTypeMirror varType, ExpressionTree valueTree, - @CompilerMessageKey String errorKey) { + @CompilerMessageKey String errorKey, + Object... extraArgs) { AnnotatedTypeMirror valueType = atypeFactory.getAnnotatedType(valueTree); commonAssignmentCheckStartDiagnostic(varType, valueType, valueTree); + boolean result = true; + String diagnosticMessage = ""; if (!relaxedCommonAssignment(varType, valueTree)) { commonAssignmentCheckEndDiagnostic( "relaxedCommonAssignment did not succeed, now must call super", varType, valueType, valueTree); - super.commonAssignmentCheck(varType, valueTree, errorKey); - } else if (checker.hasOption("showchecks")) { - commonAssignmentCheckEndDiagnostic( - true, "relaxedCommonAssignment", varType, valueType, valueTree); + result = super.commonAssignmentCheck(varType, valueTree, errorKey, extraArgs); + if (!result && showchecks) { + diagnosticMessage = "relaxedCommonAssignment()=>false and super()=>false"; + } } + commonAssignmentCheckEndDiagnostic( + result, diagnosticMessage, varType, valueType, valueTree); + return result; } /** @@ -289,7 +319,7 @@ protected void commonAssignmentCheck( * @return true if the assignment is legal based on special Upper Bound rules */ private boolean relaxedCommonAssignment(AnnotatedTypeMirror varType, ExpressionTree valueExp) { - if (valueExp.getKind() == Kind.NEW_ARRAY && varType.getKind() == TypeKind.ARRAY) { + if (valueExp.getKind() == Tree.Kind.NEW_ARRAY && varType.getKind() == TypeKind.ARRAY) { List expressions = ((NewArrayTree) valueExp).getInitializers(); if (expressions == null || expressions.isEmpty()) { @@ -298,7 +328,8 @@ private boolean relaxedCommonAssignment(AnnotatedTypeMirror varType, ExpressionT // The qualifier we need for an array is in the component type, not varType. AnnotatedTypeMirror componentType = ((AnnotatedArrayType) varType).getComponentType(); UBQualifier qualifier = - UBQualifier.createUBQualifier(componentType, atypeFactory.UNKNOWN); + UBQualifier.createUBQualifier( + componentType, atypeFactory.UNKNOWN, (UpperBoundChecker) checker); if (!qualifier.isLessThanLengthQualifier()) { return false; } @@ -310,56 +341,48 @@ private boolean relaxedCommonAssignment(AnnotatedTypeMirror varType, ExpressionT return true; } - UBQualifier qualifier = UBQualifier.createUBQualifier(varType, atypeFactory.UNKNOWN); + UBQualifier qualifier = + UBQualifier.createUBQualifier( + varType, atypeFactory.UNKNOWN, (UpperBoundChecker) checker); return qualifier.isLessThanLengthQualifier() && relaxedCommonAssignmentCheck((LessThanLengthOf) qualifier, valueExp); } /** * Fetches a receiver and an offset from a String using the passed type factory. Returns null if - * there is a parse exception. This wraps - * GenericAnnotatedTypeFactory#getReceiverFromJavaExpressionString. + * there is a parse exception. This wraps GenericAnnotatedTypeFactory#parseJavaExpressionString. * - *

    This is useful for expressions like "n+1", for which {@link - * #getReceiverFromJavaExpressionString} returns null because the whole expression is not a - * receiver. + *

    This is useful for expressions like "n+1", for which {@link #parseJavaExpressionString} + * returns null because the whole expression is not a receiver. */ - static Pair getReceiverAndOffsetFromJavaExpressionString( + static @Nullable IPair getExpressionAndOffsetFromJavaExpressionString( String s, UpperBoundAnnotatedTypeFactory atypeFactory, TreePath currentPath) { - Pair p = AnnotatedTypeFactory.getExpressionAndOffset(s); + IPair p = AnnotatedTypeFactory.getExpressionAndOffset(s); - Receiver rec = getReceiverFromJavaExpressionString(p.first, atypeFactory, currentPath); - if (rec == null) { + JavaExpression je = parseJavaExpressionString(p.first, atypeFactory, currentPath); + if (je == null) { return null; } - return Pair.of(rec, p.second); + return IPair.of(je, p.second); } /** * Fetches a receiver from a String using the passed type factory. Returns null if there is a - * parse exception -- that is, if the string does not represent an expression for a Receiver. - * For example, the expression "n+1" does not represent a Receiver. + * parse exception -- that is, if the string does not represent an expression for a + * JavaExpression. For example, the expression "n+1" does not represent a JavaExpression. * - *

    This wraps GenericAnnotatedTypeFactory#getReceiverFromJavaExpressionString. + *

    This wraps GenericAnnotatedTypeFactory#parseJavaExpressionString. */ - static Receiver getReceiverFromJavaExpressionString( + static @Nullable JavaExpression parseJavaExpressionString( String s, UpperBoundAnnotatedTypeFactory atypeFactory, TreePath currentPath) { - Receiver rec; + JavaExpression result; try { - rec = atypeFactory.getReceiverFromJavaExpressionString(s, currentPath); - } catch (FlowExpressionParseException e) { - rec = null; + result = atypeFactory.parseJavaExpressionString(s, currentPath); + } catch (JavaExpressionParseException e) { + result = null; } - return rec; - } - - /** - * Given a Java expression, returns the additive inverse, as a String. Assumes that - * FlowExpressions do not contain multiplication. - */ - private String negateString(String s, FlowExpressionContext context) { - return Subsequence.negateString(s, getCurrentPath(), context); + return result; } /* @@ -387,7 +410,9 @@ private boolean relaxedCommonAssignmentCheck( LessThanLengthOf varLtlQual, ExpressionTree valueExp) { AnnotatedTypeMirror expType = atypeFactory.getAnnotatedType(valueExp); - UBQualifier expQual = UBQualifier.createUBQualifier(expType, atypeFactory.UNKNOWN); + UBQualifier expQual = + UBQualifier.createUBQualifier( + expType, atypeFactory.UNKNOWN, (UpperBoundChecker) checker); UBQualifier lessThanQual = atypeFactory.fromLessThan(valueExp, getCurrentPath()); if (lessThanQual != null) { @@ -405,9 +430,8 @@ private boolean relaxedCommonAssignmentCheck( // Take advantage of information available on a HasSubsequence(a, from, to) annotation // on the lhs qualifier (varLtlQual): - // this allows us to show that iff varLtlQual includes LTL(b), - // b has HSS, and expQual includes LTL(a, -from), then the LTL(b) can be removed from - // varLtlQual. + // this allows us to show that iff varLtlQual includes LTL(b), b has HSS, and expQual + // includes LTL(a, -from), then the LTL(b) can be removed from varLtlQual. UBQualifier newLHS = processSubsequenceForLHS(varLtlQual, expQual); if (newLHS.isUnknown()) { @@ -467,18 +491,15 @@ private UBQualifier processSubsequenceForLHS(LessThanLengthOf varLtlQual, UBQual // check is lhsSeq is an actual LTL if (varLtlQual.hasSequenceWithOffset(lhsSeq, 0)) { - Receiver rec = - getReceiverFromJavaExpressionString(lhsSeq, atypeFactory, getCurrentPath()); - FlowExpressionContext context = Subsequence.getContextFromReceiver(rec, checker); + JavaExpression lhsSeqExpr = + parseJavaExpressionString(lhsSeq, atypeFactory, getCurrentPath()); Subsequence subSeq = - Subsequence.getSubsequenceFromReceiver( - rec, atypeFactory, getCurrentPath(), context); + Subsequence.getSubsequenceFromReceiver(lhsSeqExpr, atypeFactory); if (subSeq != null) { String from = subSeq.from; String a = subSeq.array; - - if (expQual.hasSequenceWithOffset(a, negateString(from, context))) { + if (expQual.hasSequenceWithOffset(a, Subsequence.negateString(from))) { // This cast is safe because LTLs cannot contain duplicates. // Note that this updates newLHS on each iteration from its old value, // so even if there are multiple HSS arrays the result will be correct. diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnalysis.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnalysis.java new file mode 100644 index 000000000000..1c87a27f488a --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnalysis.java @@ -0,0 +1,49 @@ +package org.checkerframework.checker.initialization; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.flow.CFAbstractAnalysis; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.javacutil.AnnotationMirrorSet; + +import javax.lang.model.type.TypeMirror; + +/** + * The analysis class for the initialization type system (serves as factory for the transfer + * function, stores, and abstract values. + */ +public class InitializationAnalysis + extends CFAbstractAnalysis { + + /** + * Creates a new {@code InitializationAnalysis}. + * + * @param checker the checker + * @param factory the factory + */ + protected InitializationAnalysis( + BaseTypeChecker checker, InitializationParentAnnotatedTypeFactory factory) { + super(checker, factory); + } + + @Override + public InitializationStore createEmptyStore(boolean sequentialSemantics) { + return new InitializationStore(this, sequentialSemantics); + } + + @Override + public InitializationStore createCopiedStore(InitializationStore s) { + return new InitializationStore(s); + } + + @Override + public @Nullable CFValue createAbstractValue( + AnnotationMirrorSet annotations, TypeMirror underlyingType) { + return defaultCreateAbstractValue(this, annotations, underlyingType); + } + + @Override + public InitializationParentAnnotatedTypeFactory getTypeFactory() { + return (InitializationParentAnnotatedTypeFactory) super.getTypeFactory(); + } +} 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 76fcda52282e..13b1c96ee37a 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationAnnotatedTypeFactory.java @@ -1,878 +1,298 @@ package org.checkerframework.checker.initialization; import com.sun.source.tree.ClassTree; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.LiteralTree; import com.sun.source.tree.MethodTree; -import com.sun.source.tree.NewClassTree; import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.tree.JCTree; + +import org.checkerframework.checker.initialization.qual.HoldsForDefaultValue; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.dataflow.analysis.TransferResult; +import org.checkerframework.dataflow.cfg.node.ClassNameNode; +import org.checkerframework.dataflow.cfg.node.FieldAccessNode; +import org.checkerframework.dataflow.cfg.node.ImplicitThisNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.node.ReturnNode; +import org.checkerframework.framework.flow.CFAbstractStore; +import org.checkerframework.framework.flow.CFAbstractValue; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.framework.qual.MonotonicQualifier; +import org.checkerframework.framework.qual.PolymorphicQualifier; +import org.checkerframework.framework.source.SourceChecker; +import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; +import org.checkerframework.framework.util.AnnotatedTypes; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreePathUtil; +import org.checkerframework.javacutil.TreeUtils; +import org.plumelib.util.IPair; + import java.lang.annotation.Annotation; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; import java.util.Set; + import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.Modifier; -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 javax.lang.model.type.TypeMirror; -import javax.lang.model.util.Types; -import org.checkerframework.checker.initialization.qual.FBCBottom; -import org.checkerframework.checker.initialization.qual.Initialized; -import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; -import org.checkerframework.checker.initialization.qual.UnderInitialization; -import org.checkerframework.checker.initialization.qual.UnknownInitialization; -import org.checkerframework.checker.nullness.NullnessAnnotatedTypeFactory; -import org.checkerframework.checker.nullness.NullnessChecker; -import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.flow.CFAbstractAnalysis; -import org.checkerframework.framework.flow.CFAbstractValue; -import org.checkerframework.framework.qual.Unused; -import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; -import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; -import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; -import org.checkerframework.framework.type.typeannotator.TypeAnnotator; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory; -import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.ElementUtils; -import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypesUtils; /** - * The annotated type factory for the freedom-before-commitment type-system. The - * freedom-before-commitment type-system and this class are abstract and need to be combined with - * another type-system whose safe initialization should be tracked. For an example, see the {@link - * NullnessChecker}. + * The annotated type factory for the freedom-before-commitment type system. When using the + * freedom-before-commitment type system as a subchecker, you must ensure that the parent checker + * hooks into it properly. See {@link InitializationChecker} for further information. */ -public abstract class InitializationAnnotatedTypeFactory< - Value extends CFAbstractValue, - Store extends InitializationStore, - Transfer extends InitializationTransfer, - Flow extends CFAbstractAnalysis> - extends GenericAnnotatedTypeFactory { - - /** {@link UnknownInitialization}. */ - protected final AnnotationMirror UNKNOWN_INITIALIZATION; - - /** {@link Initialized}. */ - protected final AnnotationMirror INITIALIZED; - - /** {@link UnderInitialization} or null. */ - protected final AnnotationMirror UNDER_INITALIZATION; - - /** {@link NotOnlyInitialized} or null. */ - protected final AnnotationMirror NOT_ONLY_INITIALIZED; - - /** {@link FBCBottom}. */ - protected final AnnotationMirror FBCBOTTOM; - - /** Cache for the initialization annotations. */ - protected final Set> initAnnos; - - /** - * String representation of all initialization annotations. - * - *

    {@link UnknownInitialization} {@link UnderInitialization} {@link Initialized} {@link - * FBCBottom} - * - *

    This is used to quickly check of an AnnotationMirror is one of the initialization - * annotations without having to repeatedly convert them to strings. - */ - protected final Set initAnnoNames; +public class InitializationAnnotatedTypeFactory extends InitializationParentAnnotatedTypeFactory { /** * Create a new InitializationAnnotatedTypeFactory. * * @param checker the checker to which the new type factory belongs */ - protected InitializationAnnotatedTypeFactory(BaseTypeChecker checker) { - super(checker, true); - - INITIALIZED = AnnotationBuilder.fromClass(elements, Initialized.class); - UNDER_INITALIZATION = AnnotationBuilder.fromClass(elements, UnderInitialization.class); - NOT_ONLY_INITIALIZED = AnnotationBuilder.fromClass(elements, NotOnlyInitialized.class); - FBCBOTTOM = AnnotationBuilder.fromClass(elements, FBCBottom.class); - UNKNOWN_INITIALIZATION = AnnotationBuilder.fromClass(elements, UnknownInitialization.class); - - Set> tempInitAnnos = new LinkedHashSet<>(4); - tempInitAnnos.add(UnderInitialization.class); - tempInitAnnos.add(Initialized.class); - tempInitAnnos.add(UnknownInitialization.class); - tempInitAnnos.add(FBCBottom.class); - - initAnnos = Collections.unmodifiableSet(tempInitAnnos); - - Set tempInitAnnoNames = new HashSet<>(4); - tempInitAnnoNames.add(AnnotationUtils.annotationName(UNKNOWN_INITIALIZATION)); - tempInitAnnoNames.add(AnnotationUtils.annotationName(UNDER_INITALIZATION)); - tempInitAnnoNames.add(AnnotationUtils.annotationName(INITIALIZED)); - tempInitAnnoNames.add(AnnotationUtils.annotationName(FBCBOTTOM)); - - initAnnoNames = Collections.unmodifiableSet(tempInitAnnoNames); - - // No call to postInit() because this class is abstract. - // Its subclasses must call postInit(). - } - - public Set> getInitializationAnnotations() { - return initAnnos; - } - - /** - * Is the annotation {@code anno} an initialization qualifier? - * - * @param anno the annotation to check - * @return true if the argument is an initialization qualifier - */ - protected boolean isInitializationAnnotation(AnnotationMirror anno) { - assert anno != null; - return initAnnoNames.contains(AnnotationUtils.annotationName(anno)); + public InitializationAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + postInit(); } - /* - * The following method can be used to appropriately configure the - * commitment type-system. - */ - - /** - * Returns the list of annotations that is forbidden for the constructor return type. - * - * @return the list of annotations that is forbidden for the constructor return type - */ - public Set> getInvalidConstructorReturnTypeAnnotations() { - return getInitializationAnnotations(); - } - - /** - * Returns the annotation that makes up the invariant of this commitment type system, such as - * {@code @NonNull}. - * - * @return the invariant annotation for this type system - */ - public abstract AnnotationMirror getFieldInvariantAnnotation(); - - /** - * Returns whether or not {@code field} has the invariant annotation. - * - *

    This method is a convenience method for {@link - * #hasFieldInvariantAnnotation(AnnotatedTypeMirror, VariableElement)}. - * - *

    If the {@code field} is a type variable, this method returns true if any possible - * instantiation of the type parameter could have the invariant annotation. See {@link - * NullnessAnnotatedTypeFactory#hasFieldInvariantAnnotation(VariableTree)} for an example. - * - * @param field field that might have invariant annotation - * @return whether or not field has the invariant annotation - */ - protected final boolean hasFieldInvariantAnnotation(VariableTree field) { - AnnotatedTypeMirror type = getAnnotatedType(field); - VariableElement fieldElement = TreeUtils.elementFromDeclaration(field); - return hasFieldInvariantAnnotation(type, fieldElement); - } - - /** - * Returns whether or not {@code type} has the invariant annotation. - * - *

    If the {@code type} is a type variable, this method returns true if any possible - * instantiation of the type parameter could have the invariant annotation. See {@link - * NullnessAnnotatedTypeFactory#hasFieldInvariantAnnotation(VariableTree)} for an example. - * - * @param type of field that might have invariant annotation - * @param fieldElement the field element, which can be used to check annotations on the - * declaration - * @return whether or not the type has the invariant annotation - */ - protected abstract boolean hasFieldInvariantAnnotation( - AnnotatedTypeMirror type, VariableElement fieldElement); - - /** - * Creates a {@link UnderInitialization} annotation with the given type as its type frame - * argument. - * - * @param typeFrame the type down to which some value has been initialized - * @return an {@link UnderInitialization} annotation with the given argument - */ - public AnnotationMirror createFreeAnnotation(TypeMirror typeFrame) { - assert typeFrame != null; - AnnotationBuilder builder = new AnnotationBuilder(processingEnv, UnderInitialization.class); - builder.setValue("value", typeFrame); - return builder.build(); - } - - /** - * Creates a {@link UnderInitialization} annotation with the given type frame. - * - * @param typeFrame the type down to which some value has been initialized - * @return an {@link UnderInitialization} annotation with the given argument - */ - public AnnotationMirror createFreeAnnotation(Class typeFrame) { - assert typeFrame != null; - AnnotationBuilder builder = new AnnotationBuilder(processingEnv, UnderInitialization.class); - builder.setValue("value", typeFrame); - return builder.build(); - } - - /** - * Creates a {@link UnknownInitialization} annotation with a given type frame. - * - * @param typeFrame the type down to which some value has been initialized - * @return an {@link UnknownInitialization} annotation with the given argument - */ - public AnnotationMirror createUnclassifiedAnnotation(Class typeFrame) { - assert typeFrame != null; - AnnotationBuilder builder = - new AnnotationBuilder(processingEnv, UnknownInitialization.class); - builder.setValue("value", typeFrame); - return builder.build(); - } - - /** - * Creates an {@link UnknownInitialization} annotation with a given type frame. - * - * @param typeFrame the type down to which some value has been initialized - * @return an {@link UnknownInitialization} annotation with the given argument - */ - public AnnotationMirror createUnclassifiedAnnotation(TypeMirror typeFrame) { - assert typeFrame != null; - AnnotationBuilder builder = - new AnnotationBuilder(processingEnv, UnknownInitialization.class); - builder.setValue("value", typeFrame); - return builder.build(); - } - - /** - * Returns the type frame (that is, the argument) of a given initialization annotation. - * - * @param annotation a {@link UnderInitialization} or {@link UnknownInitialization} annotation - * @return the annotation's argument - */ - public TypeMirror getTypeFrameFromAnnotation(AnnotationMirror annotation) { - TypeMirror name = - AnnotationUtils.getElementValue(annotation, "value", TypeMirror.class, true); - return name; - } - - /** - * Is {@code anno} the {@link UnderInitialization} annotation (with any type frame)? - * - * @param anno the annotation to check - * @return true if {@code anno} is {@link UnderInitialization} - */ - public boolean isFree(AnnotationMirror anno) { - return areSameByClass(anno, UnderInitialization.class); - } - - /** - * Is {@code anno} the {@link UnknownInitialization} annotation (with any type frame)? - * - * @param anno the annotation to check - * @return true if {@code anno} is {@link UnknownInitialization} - */ - public boolean isUnclassified(AnnotationMirror anno) { - return areSameByClass(anno, UnknownInitialization.class); - } - - /** - * Is {@code anno} the bottom annotation? - * - * @param anno the annotation to check - * @return true if {@code anno} is {@link FBCBottom} - */ - public boolean isFbcBottom(AnnotationMirror anno) { - return AnnotationUtils.areSame(anno, FBCBOTTOM); - } - - /** - * Is {@code anno} the {@link Initialized} annotation? - * - * @param anno the annotation to check - * @return true if {@code anno} is {@link Initialized} - */ - public boolean isCommitted(AnnotationMirror anno) { - return AnnotationUtils.areSame(anno, INITIALIZED); - } - - /** - * Does {@code anno} have the annotation {@link UnderInitialization} (with any type frame)? - * - * @param anno the annotation to check - * @return true if {@code anno} has {@link UnderInitialization} - */ - public boolean isFree(AnnotatedTypeMirror anno) { - return anno.hasEffectiveAnnotation(UnderInitialization.class); + @Override + public InitializationChecker getChecker() { + return (InitializationChecker) super.getChecker(); } /** - * Does {@code anno} have the annotation {@link UnknownInitialization} (with any type frame)? + * Gets the factory of the {@link InitializationFieldAccessSubchecker}, whose flow-analysis + * results we reuse to avoid performing the same flow analysis twice. * - * @param anno the annotation to check - * @return true if {@code anno} has {@link UnknownInitialization} - */ - public boolean isUnclassified(AnnotatedTypeMirror anno) { - return anno.hasEffectiveAnnotation(UnknownInitialization.class); - } - - /** - * Does {@code anno} have the bottom annotation? + *

    If type checking has not yet started, the subcheckers are uninitialized, and this returns + * {@code null}. More concretely, this method only returns a non-null value after {@link + * SourceChecker#initChecker()} has been called on all subcheckers. Since the flow analysis is + * initialized in {@link AnnotatedTypeFactory#postInit()}, and the type factory is created after + * all subcheckers have been initialized, this method will always return a non-null value unless + * a subclass attempts to use it for some purpose other than accessing the flow analysis. * - * @param anno the annotation to check - * @return true if {@code anno} has {@link FBCBottom} + * @return the factory of the {@link InitializationFieldAccessSubchecker}, or {@code null} if + * not yet initialized + * @see #createFlowAnalysis() + * @see #performFlowAnalysis(ClassTree) + * @see #getRegularExitStore(Tree) + * @see #getExceptionalExitStore(Tree) + * @see #getReturnStatementStores(MethodTree) */ - public boolean isFbcBottom(AnnotatedTypeMirror anno) { - return anno.hasEffectiveAnnotation(FBCBottom.class); + protected @Nullable InitializationFieldAccessAnnotatedTypeFactory getFieldAccessFactory() { + InitializationChecker checker = getChecker(); + BaseTypeChecker targetChecker = checker.getSubchecker(checker.getTargetCheckerClass()); + return targetChecker.getTypeFactoryOfSubcheckerOrNull( + InitializationFieldAccessSubchecker.class); } - /** - * Does {@code anno} have the annotation {@link Initialized}? - * - * @param anno the annotation to check - * @return true if {@code anno} has {@link Initialized} - */ - public boolean isCommitted(AnnotatedTypeMirror anno) { - return anno.hasEffectiveAnnotation(Initialized.class); + @Override + protected InitializationAnalysis createFlowAnalysis() { + return getFieldAccessFactory().getAnalysis(); } - /** Are all fields committed-only? */ - protected boolean areAllFieldsCommittedOnly(ClassTree classTree) { - for (Tree member : classTree.getMembers()) { - if (member.getKind() != Tree.Kind.VARIABLE) { - continue; - } - VariableTree var = (VariableTree) member; - VariableElement varElt = TreeUtils.elementFromDeclaration(var); - // var is not committed-only - if (getDeclAnnotation(varElt, NotOnlyInitialized.class) != null) { - // var is not static -- need a check of initializer blocks, - // not of constructor which is where this is used - if (!varElt.getModifiers().contains(Modifier.STATIC)) { - return false; - } - } - } - return true; + @Override + protected void performFlowAnalysis(ClassTree classTree) { + flowResult = getFieldAccessFactory().getFlowResult(); } - /** - * {@inheritDoc} - * - *

    In most cases, subclasses want to call this method first because it may clear all - * annotations and use the hierarchy's root annotations. - */ @Override - public void postAsMemberOf( - AnnotatedTypeMirror type, AnnotatedTypeMirror owner, Element element) { - super.postAsMemberOf(type, owner, element); - - if (element.getKind().isField()) { - Collection declaredFieldAnnotations = - getDeclAnnotations(element); - AnnotatedTypeMirror fieldAnnotations = getAnnotatedType(element); - computeFieldAccessType( - type, declaredFieldAnnotations, owner, fieldAnnotations, element); - } + public InitializationStore getRegularExitStore(Tree tree) { + return getFieldAccessFactory().getRegularExitStore(tree); } - /** - * Controls which hierarchies' qualifiers are changed based on the receiver type and the - * declared annotations for a field. - * - * @see #computeFieldAccessType - * @see #getAnnotatedTypeLhs(Tree) - */ - private boolean computingAnnotatedTypeMirrorOfLHS = false; - @Override - public AnnotatedTypeMirror getAnnotatedTypeLhs(Tree lhsTree) { - boolean oldComputingAnnotatedTypeMirrorOfLHS = computingAnnotatedTypeMirrorOfLHS; - computingAnnotatedTypeMirrorOfLHS = true; - AnnotatedTypeMirror result = super.getAnnotatedTypeLhs(lhsTree); - computingAnnotatedTypeMirrorOfLHS = oldComputingAnnotatedTypeMirrorOfLHS; - return result; + public InitializationStore getExceptionalExitStore(Tree tree) { + return getFieldAccessFactory().getExceptionalExitStore(tree); } @Override - public AnnotatedDeclaredType getSelfType(Tree tree) { - AnnotatedDeclaredType selfType = super.getSelfType(tree); - - TreePath path = getPath(tree); - Tree topLevelMember = findTopLevelClassMemberForTree(path); - if (topLevelMember != null) { - if (topLevelMember.getKind() != Kind.METHOD - || TreeUtils.isConstructor((MethodTree) topLevelMember)) { - - setSelfTypeInInitializationCode(tree, selfType, path); - } - } - - return selfType; + public List>> + getReturnStatementStores(MethodTree methodTree) { + return getFieldAccessFactory().getReturnStatementStores(methodTree); } /** - * 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. + * {@inheritDoc} * - *

    Are there any other type of top level objects? - */ - private Tree findTopLevelClassMemberForTree(TreePath path) { - ClassTree enclosingClass = TreeUtils.enclosingClass(path); - if (enclosingClass != null) { - - List classMembers = enclosingClass.getMembers(); - TreePath searchPath = path; - while (searchPath.getParentPath() != null - && searchPath.getParentPath().getLeaf() != enclosingClass) { - searchPath = searchPath.getParentPath(); - if (classMembers.contains(searchPath.getLeaf())) { - return searchPath.getLeaf(); - } - } - } - return null; - } - - /** - * Side-effects argument {@code selfType} to make it @Initialized or @UnderInitialization, - * depending on whether all fields have been set. + *

    This implementaiton also takes the target checker into account. + * + * @see #getUninitializedFields(InitializationStore, CFAbstractStore, TreePath, boolean, + * Collection) */ + @Override protected void setSelfTypeInInitializationCode( - Tree tree, AnnotatedDeclaredType selfType, TreePath path) { - ClassTree enclosingClass = TreeUtils.enclosingClass(path); + Tree tree, AnnotatedTypeMirror.AnnotatedDeclaredType selfType, TreePath path) { + ClassTree enclosingClass = TreePathUtil.enclosingClass(path); Type classType = ((JCTree) enclosingClass).type; - AnnotationMirror annotation = null; + AnnotationMirror annotation; - // If all fields are committed-only, and they are all initialized, + // If all fields are initialized-only, and they are all initialized, // then: - // - if the class is final, this is @Initialized - // - otherwise, this is @UnderInitialization(CurrentClass) as - // there might still be subclasses that need initialization. - if (areAllFieldsCommittedOnly(enclosingClass)) { - Store store = getStoreBefore(tree); - if (store != null - && getUninitializedInvariantFields(store, path, false, Collections.emptyList()) + // - if the class is final, this is @Initialized + // - otherwise, this is @UnderInitialization(CurrentClass) as + // there might still be subclasses that need initialization. + if (areAllFieldsInitializedOnly(enclosingClass)) { + GenericAnnotatedTypeFactory targetFactory = + checker.getTypeFactoryOfSubcheckerOrNull( + ((InitializationChecker) checker).getTargetCheckerClass()); + InitializationStore initStore = getStoreBefore(tree); + CFAbstractStore targetStore = targetFactory.getStoreBefore(tree); + if (initStore != null + && targetStore != null + && getUninitializedFields( + initStore, targetStore, path, false, Collections.emptyList()) + .isEmpty()) { + if (classType.isFinal()) { + annotation = INITIALIZED; + } else { + annotation = createUnderInitializationAnnotation(classType); + } + } else if (initStore != null + && getUninitializedFields(initStore, path, false, Collections.emptyList()) .isEmpty()) { if (classType.isFinal()) { annotation = INITIALIZED; } else { - annotation = createFreeAnnotation(classType); + annotation = createUnderInitializationAnnotation(classType); } + } else { + annotation = null; } + } else { + annotation = null; } if (annotation == null) { - annotation = getFreeOrRawAnnotationOfSuperType(classType); + annotation = getUnderInitializationAnnotationOfSuperType(classType); } selfType.replaceAnnotation(annotation); } /** - * Returns a {@link UnderInitialization} annotation that has the superclass of {@code type} as - * type frame. - */ - protected AnnotationMirror getFreeOrRawAnnotationOfSuperType(TypeMirror type) { - // Find supertype if possible. - AnnotationMirror annotation; - List superTypes = types.directSupertypes(type); - TypeMirror superClass = null; - for (TypeMirror superType : superTypes) { - ElementKind kind = types.asElement(superType).getKind(); - if (kind == ElementKind.CLASS) { - superClass = superType; - break; - } - } - // Create annotation. - if (superClass != null) { - annotation = createFreeAnnotation(superClass); - } else { - // Use Object as a valid super-class. - annotation = createFreeAnnotation(Object.class); - } - return annotation; - } - - /** - * Returns the (non-static) fields that have the invariant annotation and are not yet - * initialized in a given store. + * Returns the fields that are not yet initialized in a given store, taking into account the + * target checker. + * + *

    A field f is initialized if + * + *

      + *
    • f is initialized in the initialization store, i.e., it has been assigned; + *
    • the value of f in the target store has a non-top qualifier that does not have the + * meta-annotation {@link HoldsForDefaultValue}; or + *
    • the declared qualifier of f in the target hierarchy either has the meta-annotation + * {@link HoldsForDefaultValue} or is a top qualifier. + *
    + * + *

    See {@link #getUninitializedFields(InitializationStore, TreePath, boolean, Collection)} + * for a method that does not require the target checker. + * + * @param initStore a store for the initialization checker + * @param targetStore a store for the target checker corresponding to initStore + * @param path the current path, used to determine the current class + * @param isStatic whether to report static fields or instance fields + * @param receiverAnnotations the annotations on the receiver + * @return the fields that are not yet initialized in a given store */ - public List getUninitializedInvariantFields( - Store store, + public List getUninitializedFields( + InitializationStore initStore, + CFAbstractStore targetStore, TreePath path, boolean isStatic, - List receiverAnnotations) { - ClassTree currentClass = TreeUtils.enclosingClass(path); - List fields = InitializationChecker.getAllFields(currentClass); - List violatingFields = new ArrayList<>(); - for (VariableTree field : fields) { - if (isUnused(field, receiverAnnotations)) { - continue; // don't consider unused fields - } - VariableElement fieldElem = TreeUtils.elementFromDeclaration(field); - if (ElementUtils.isStatic(fieldElem) == isStatic) { - // Does this field need to satisfy the invariant? - if (hasFieldInvariantAnnotation(field)) { - // Has the field been initialized? - if (!store.isFieldInitialized(fieldElem)) { - violatingFields.add(field); - } - } - } + Collection receiverAnnotations) { + List uninitializedFields = + super.getUninitializedFields(initStore, path, isStatic, receiverAnnotations); + + GenericAnnotatedTypeFactory factory = + checker.getTypeFactoryOfSubcheckerOrNull( + ((InitializationChecker) checker).getTargetCheckerClass()); + + if (factory == null) { + throw new BugInCF( + "Did not find target type factory for checker " + + ((InitializationChecker) checker).getTargetCheckerClass()); } - return violatingFields; - } - /** - * Returns the (non-static) fields that have the invariant annotation and are initialized in a - * given store. - */ - public List getInitializedInvariantFields(Store store, TreePath path) { - // TODO: Instead of passing the TreePath around, can we use - // getCurrentClassTree? - ClassTree currentClass = TreeUtils.enclosingClass(path); - List fields = InitializationChecker.getAllFields(currentClass); - List initializedFields = new ArrayList<>(); - for (VariableTree field : fields) { - VariableElement fieldElem = TreeUtils.elementFromDeclaration(field); - if (!ElementUtils.isStatic(fieldElem)) { - // Does this field need to satisfy the invariant? - if (hasFieldInvariantAnnotation(field)) { - // Has the field been initialized? - if (store.isFieldInitialized(fieldElem)) { - initializedFields.add(field); - } - } - } - } - return initializedFields; - } - - /** Returns whether the field {@code f} is unused, given the annotations on the receiver. */ - private boolean isUnused( - VariableTree field, Collection receiverAnnos) { - if (receiverAnnos.isEmpty()) { - return false; - } - - AnnotationMirror unused = - getDeclAnnotation(TreeUtils.elementFromDeclaration(field), Unused.class); - if (unused == null) { - return false; + // Remove primitives + if (!((InitializationChecker) checker).checkPrimitives()) { + uninitializedFields.removeIf(var -> getAnnotatedType(var).getKind().isPrimitive()); } - Name when = AnnotationUtils.getElementValueClassName(unused, "when", false); - for (AnnotationMirror anno : receiverAnnos) { - Name annoName = ((TypeElement) anno.getAnnotationType().asElement()).getQualifiedName(); - if (annoName.contentEquals(when)) { - return true; - } - } + // Filter out fields which are initialized according to subchecker + uninitializedFields.removeIf( + var -> { + ClassTree enclosingClass = TreePathUtil.enclosingClass(getPath(var)); + Node receiver; + if (ElementUtils.isStatic(TreeUtils.elementFromDeclaration(var))) { + receiver = new ClassNameNode(enclosingClass); + } else { + receiver = + new ImplicitThisNode( + TreeUtils.elementFromDeclaration(enclosingClass).asType()); + } + VariableElement varElement = TreeUtils.elementFromDeclaration(var); + FieldAccessNode fa = new FieldAccessNode(var, varElement, receiver); + CFAbstractValue value = targetStore.getValue(fa); + return isInitialized(factory, value, varElement); + }); - return false; + return uninitializedFields; } /** - * Return true if the type is initialized with respect to the given frame -- that is, all of the - * fields of the frame are initialized. + * Determines whether the specified variable's current value is initialized. * - * @param type the type whose initialization type qualifiers to check - * @param frame a class in {@code type}'s class hierarchy - * @return true if the type is initialized for the given frame - */ - public boolean isInitializedForFrame(AnnotatedTypeMirror type, TypeMirror frame) { - AnnotationMirror initializationAnno = - type.getEffectiveAnnotationInHierarchy(UNKNOWN_INITIALIZATION); - TypeMirror typeFrame = getTypeFrameFromAnnotation(initializationAnno); - Types types = processingEnv.getTypeUtils(); - return types.isSubtype(typeFrame, types.erasure(frame)); - } - - /** - * Determine the type of a field access (implicit or explicit) based on the receiver type and - * the declared annotations for the field. + *

    Returns {@code true} iff the variable's current value is initialized. This holds for + * variables whose value has a non-top qualifier that does not have the meta-annotation {@link + * HoldsForDefaultValue} (e.g., variables with a {@code NonNull} value), as well as variables + * whose declaration has a qualifier that has the meta-annotation {@link HoldsForDefaultValue} + * (e.g., variables whose declared type is {@code Nullable}). * - * @param type type of the field access expression - * @param declaredFieldAnnotations annotations on the element - * @param receiverType inferred annotations of the receiver + * @param factory the parent checker's factory + * @param value the variable's current value + * @param var the variable to check + * @return whether the specified variable is yet to be initialized */ - private void computeFieldAccessType( - AnnotatedTypeMirror type, - Collection declaredFieldAnnotations, - AnnotatedTypeMirror receiverType, - AnnotatedTypeMirror fieldAnnotations, - Element element) { - // not necessary for primitive fields - if (TypesUtils.isPrimitive(type.getUnderlyingType())) { - return; - } - // not necessary if there is an explicit UnknownInitialization - // annotation on the field - if (AnnotationUtils.containsSameByName( - fieldAnnotations.getAnnotations(), UNKNOWN_INITIALIZATION)) { - return; - } - if (isUnclassified(receiverType) || isFree(receiverType)) { - - TypeMirror fieldDeclarationType = element.getEnclosingElement().asType(); - boolean isInitializedForFrame = - isInitializedForFrame(receiverType, fieldDeclarationType); - if (isInitializedForFrame) { - // The receiver is initialized for this frame. - // Change the type of the field to @UnknownInitialization so that - // anything can be assigned to this field. - type.replaceAnnotation(UNKNOWN_INITIALIZATION); - } else if (computingAnnotatedTypeMirrorOfLHS) { - // The receiver is not initialized for this frame, but the type of a lhs is being - // computed. - // Change the type of the field to @UnknownInitialization so that - // anything can be assigned to this field. - type.replaceAnnotation(UNKNOWN_INITIALIZATION); - } else { - // The receiver is not initialized for this frame and the type being computed is not - // a LHS. - // Replace all annotations with the top annotation for that hierarchy. - type.clearAnnotations(); - type.addAnnotations(qualHierarchy.getTopAnnotations()); - } - - if (!AnnotationUtils.containsSame(declaredFieldAnnotations, NOT_ONLY_INITIALIZED)) { - // add root annotation for all other hierarchies, and - // Committed for the commitment hierarchy - type.replaceAnnotation(INITIALIZED); - } - } - } - - @Override - protected TypeAnnotator createTypeAnnotator() { - return new ListTypeAnnotator( - super.createTypeAnnotator(), new CommitmentTypeAnnotator(this)); - } - - @Override - protected TreeAnnotator createTreeAnnotator() { - return new ListTreeAnnotator( - super.createTreeAnnotator(), new CommitmentTreeAnnotator(this)); - } - - protected class CommitmentTypeAnnotator extends TypeAnnotator { - public CommitmentTypeAnnotator( - InitializationAnnotatedTypeFactory atypeFactory) { - super(atypeFactory); - } - - @Override - public Void visitExecutable(AnnotatedExecutableType t, Void p) { - Void result = super.visitExecutable(t, p); - Element elem = t.getElement(); - if (elem.getKind() == ElementKind.CONSTRUCTOR) { - AnnotatedDeclaredType returnType = (AnnotatedDeclaredType) t.getReturnType(); - DeclaredType underlyingType = returnType.getUnderlyingType(); - returnType.replaceAnnotation(getFreeOrRawAnnotationOfSuperType(underlyingType)); - } - return result; - } - } - - protected class CommitmentTreeAnnotator extends TreeAnnotator { - - public CommitmentTreeAnnotator( - InitializationAnnotatedTypeFactory atypeFactory) { - super(atypeFactory); - } - - @Override - public Void visitMethod(MethodTree node, AnnotatedTypeMirror p) { - Void result = super.visitMethod(node, p); - if (TreeUtils.isConstructor(node)) { - assert p instanceof AnnotatedExecutableType; - AnnotatedExecutableType exeType = (AnnotatedExecutableType) p; - DeclaredType underlyingType = - (DeclaredType) exeType.getReturnType().getUnderlyingType(); - AnnotationMirror a = getFreeOrRawAnnotationOfSuperType(underlyingType); - exeType.getReturnType().replaceAnnotation(a); - } - return result; - } - - @Override - public Void visitNewClass(NewClassTree node, AnnotatedTypeMirror p) { - super.visitNewClass(node, p); - boolean allCommitted = true; - Type type = ((JCTree) node).type; - for (ExpressionTree a : node.getArguments()) { - final AnnotatedTypeMirror t = getAnnotatedType(a); - allCommitted &= (isCommitted(t) || isFbcBottom(t)); - } - if (!allCommitted) { - p.replaceAnnotation(createFreeAnnotation(type)); - return null; - } - p.replaceAnnotation(INITIALIZED); - return null; - } - - @Override - public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) { - if (tree.getKind() != Tree.Kind.NULL_LITERAL) { - type.addAnnotation(INITIALIZED); - } - return super.visitLiteral(tree, type); - } - } - - /** - * The {@link QualifierHierarchy} for the initialization type system. Type systems extending the - * Initialization Checker should call methods {@link - * InitializationQualifierHierarchy#isSubtypeInitialization(AnnotationMirror, AnnotationMirror)} - * and {@link InitializationQualifierHierarchy#leastUpperBoundInitialization(AnnotationMirror, - * AnnotationMirror)} for appropriate qualifiers. See protected subclass - * NullnessQualifierHierarchy within class {@link - * org.checkerframework.checker.nullness.NullnessChecker} for an example. - */ - protected abstract class InitializationQualifierHierarchy extends MultiGraphQualifierHierarchy { - - /** - * Create an InitializationQualifierHierarchy. - * - * @param f a factory to create to create a {@link - * org.checkerframework.framework.util.GraphQualifierHierarchy} - * @param arg seems to be ignored - */ - protected InitializationQualifierHierarchy(MultiGraphFactory f, Object... arg) { - super(f, arg); - } - - /** - * Subtype testing for initialization annotations. Will return false if either qualifier is - * not an initialization annotation. Subclasses should override isSubtype and call this - * method for initialization qualifiers. - */ - public boolean isSubtypeInitialization(AnnotationMirror rhs, AnnotationMirror lhs) { - if (!isInitializationAnnotation(rhs) || !isInitializationAnnotation(lhs)) { - return false; - } - - // 't' is always a subtype of 't' - if (AnnotationUtils.areSame(rhs, lhs)) { - return true; - } - // @Initialized is only a supertype of @FBCBottom. - if (isCommitted(lhs)) { - return isFbcBottom(rhs); - } - - // @FBCBottom is a supertype of nothing. - if (isFbcBottom(lhs)) { - return false; - } - // @FBCBottom is a subtype of everything. - if (isFbcBottom(rhs)) { - return true; + public static boolean isInitialized( + GenericAnnotatedTypeFactory factory, + CFAbstractValue value, + VariableElement var) { + AnnotatedTypeMirror declType = factory.getAnnotatedType(var); + + Set topAnnotations = + factory.getQualifierHierarchy().getTopAnnotations(); + + for (Class invariant : factory.getSupportedTypeQualifiers()) { + // Skip default-value, monotonic, polymorphic, and top qualifiers + if (invariant.getAnnotation(HoldsForDefaultValue.class) != null + || invariant.getAnnotation(MonotonicQualifier.class) != null + || invariant.getAnnotation(PolymorphicQualifier.class) != null + || topAnnotations.stream() + .anyMatch( + annotation -> factory.areSameByClass(annotation, invariant))) { + continue; } - boolean unc1 = isUnclassified(rhs); - boolean unc2 = isUnclassified(lhs); - boolean free1 = isFree(rhs); - boolean free2 = isFree(lhs); - // @Initialized is only a subtype of @UnknownInitialization. - if (isCommitted(rhs)) { - return unc2; - } - // @UnknownInitialization is not a subtype of @UnderInitialization. - if (unc1 && free2) { + boolean hasInvariantInStore = + value != null + && value.getAnnotations().stream() + .anyMatch( + annotation -> + factory.areSameByClass(annotation, invariant)); + boolean hasInvariantAtDeclaration = + AnnotatedTypes.findEffectiveLowerBoundAnnotations( + factory.getQualifierHierarchy(), declType) + .stream() + .anyMatch(annotation -> factory.areSameByClass(annotation, invariant)); + + if (hasInvariantAtDeclaration && !hasInvariantInStore) { return false; } - // Now, either both annotations are @UnderInitialization, both annotations are - // @UnknownInitialization or anno1 is @UnderInitialization and anno2 is - // @UnknownInitialization. - assert (free1 && free2) || (unc1 && unc2) || (free1 && unc2); - // Thus, we only need to look at the type frame. - TypeMirror frame1 = getTypeFrameFromAnnotation(rhs); - TypeMirror frame2 = getTypeFrameFromAnnotation(lhs); - return types.isSubtype(frame1, frame2); } - /** - * Compute the least upper bound of two initialization qualifiers. Returns null if one of - * the qualifiers is not in the initialization hierarachy. Subclasses should override - * leastUpperBound and call this method for initialization qualifiers. - * - * @param anno1 an initialization qualifier - * @param anno2 an initialization qualifier - * @return the lub of anno1 and anno2 - */ - protected AnnotationMirror leastUpperBoundInitialization( - AnnotationMirror anno1, AnnotationMirror anno2) { - if (!isInitializationAnnotation(anno1) || !isInitializationAnnotation(anno2)) { - return null; - } - - // Handle the case where one is a subtype of the other. - if (isSubtypeInitialization(anno1, anno2)) { - return anno2; - } else if (isSubtypeInitialization(anno2, anno1)) { - return anno1; - } - boolean unc1 = isUnclassified(anno1); - boolean unc2 = isUnclassified(anno2); - boolean free1 = isFree(anno1); - boolean free2 = isFree(anno2); - - // Handle @Initialized. - if (isCommitted(anno1)) { - assert free2; - return createUnclassifiedAnnotation(getTypeFrameFromAnnotation(anno2)); - } else if (isCommitted(anno2)) { - assert free1; - return createUnclassifiedAnnotation(getTypeFrameFromAnnotation(anno1)); - } - - if (free1 && free2) { - return createFreeAnnotation( - lubTypeFrame( - getTypeFrameFromAnnotation(anno1), - getTypeFrameFromAnnotation(anno2))); - } - - assert (unc1 || free1) && (unc2 || free2); - return createUnclassifiedAnnotation( - lubTypeFrame( - getTypeFrameFromAnnotation(anno1), getTypeFrameFromAnnotation(anno2))); - } - - /** Returns the least upper bound of two types. */ - protected TypeMirror lubTypeFrame(TypeMirror a, TypeMirror b) { - if (types.isSubtype(a, b)) { - return b; - } else if (types.isSubtype(b, a)) { - return a; - } - - return TypesUtils.leastUpperBound(a, b, processingEnv); - } - - @Override - public AnnotationMirror greatestLowerBound(AnnotationMirror anno1, AnnotationMirror anno2) { - return super.greatestLowerBound(anno1, anno2); - } + return true; } } diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationChecker.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationChecker.java index 477ccba52b8c..a1b145647fcf 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationChecker.java @@ -3,34 +3,115 @@ import com.sun.source.tree.ClassTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; + +import org.checkerframework.checker.initialization.qual.HoldsForDefaultValue; +import org.checkerframework.checker.initialization.qual.Initialized; +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.checker.nullness.NullnessNoInitAnnotatedTypeFactory; +import org.checkerframework.checker.nullness.NullnessNoInitSubchecker; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseTypeChecker; + import java.util.ArrayList; import java.util.List; -import java.util.SortedSet; -import org.checkerframework.common.basetype.BaseTypeChecker; +import java.util.NavigableSet; +import java.util.Set; /** * Tracks whether a value is initialized (all its fields are set), and checks that values are * initialized before being used. Implements the freedom-before-commitment scheme for * initialization, augmented by type frames. * + *

    Because there is a cyclic dependency between this type system and the target type system, + * using this checker is more complex than for others. Specifically: + * + *

      + *
    1. Any target type system must provide two checkers: the target checker does all of the + * checking of the target type system (e.g., nullness), while the target checker's parent + * belongs to a subclass of the {@code InitializationChecker}. You can look at the {@link + * NullnessChecker} for an example: For the nullness type system, the {@link + * NullnessNoInitSubchecker} is the target checker which actually checks {@link NonNull} and + * related qualifiers, while the {@link NullnessChecker} is a subclass of this checker and + * thus checks {@link Initialized} and related qualifiers. The parent-child relationship + * between the checkers is necessary because this checker is dependent on the target checker + * to know which fields should be checked for initialization, and when such a field is + * initialized: A field is checked for initialization if its declared type is not the top type + * and does not have the meta-annotation {@link HoldsForDefaultValue} (e.g., {@link NonNull}). + * Such a field becomes initialized as soon as its refined type agrees with its declared type + * (which can happen either by assigning the field or by a contract annotation like {@link + * EnsuresNonNull}). + *
    2. The target checker must add the {@link InitializationFieldAccessSubchecker} as a subchecker + * and the {@link InitializationFieldAccessTreeAnnotator} as a tree annotator. This is + * necessary to give possibly uninitialized fields the top type of the target hierarchy (e.g., + * {@link Nullable}), ensuring that all fields are initialized before being used. This needs + * to be a separate checker because the target checker cannot access any type information from + * its parent, which is only initialized after all subcheckers have finished. + *
    3. The target checker must override all necessary methods in the target checker's type factory + * to take the type information from the InitializationFieldAccessSubchecker into account. You + * can look at {@link NullnessNoInitAnnotatedTypeFactory} for examples. + *
    4. Any subclass of the {@code InitializationChecker} should support the command-line option + * {@code -AassumeInitialized} via {@code @SupportedOptions({"assumeInitialized"})}, so + * initialization checking can be turned off. This gives users of, e.g., the {@link + * NullnessChecker} an easy way to turn off initialization checking without having to directly + * call the {@link NullnessNoInitSubchecker}. + *
    + * + *

    If you want to modify the freedom-before-commitment scheme in your subclass, note that the + * InitializationChecker does not use the default convention where, e.g., the annotated type factory + * for {@code NameChecker} is {@code NameAnnotatedTypeFactory}. Instead every subclass of this + * checker always uses the {@link InitializationAnnotatedTypeFactory} unless this behavior is + * overridden. Note also that the flow-sensitive type refinement for this type system is performed + * by the {@link InitializationFieldAccessSubchecker}; this checker performs no refinement, instead + * reusing the results from that one. + * * @checker_framework.manual #initialization-checker Initialization Checker */ public abstract class InitializationChecker extends BaseTypeChecker { - /** Create a new InitializationChecker. */ - protected InitializationChecker() {} + /** Default constructor for InitializationChecker. */ + public InitializationChecker() {} + + /** + * Whether to check primitives for initialization. + * + * @return whether to check primitives for initialization + */ + public abstract boolean checkPrimitives(); + + /** + * The checker for the target type system for which to check initialization. + * + * @return the checker for the target type system. + */ + public abstract Class getTargetCheckerClass(); @Override - public SortedSet getSuppressWarningsPrefixes() { - SortedSet result = super.getSuppressWarningsPrefixes(); - // The SuppressesWarnings string "initialization" is not useful here: it suppresses *all* - // warnings, not just those related to initialization. Instead, if the user writes - // @SuppressWarnings("initialization"), let that match keys containing that string. + public NavigableSet getSuppressWarningsPrefixes() { + NavigableSet result = super.getSuppressWarningsPrefixes(); + // "fbc" is for backward compatibility only; you should use + // "initialization" instead. result.add("fbc"); + // The default prefix "initialization" must be added manually because this checker class + // is abstract and its subclasses are not named "InitializationChecker". + result.add("initialization"); return result; } - /** Returns a list of all fields of the given class. */ + @Override + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); + checkers.add(getTargetCheckerClass()); + return checkers; + } + + /** + * Returns a list of all fields of the given class. + * + * @param clazz the class + * @return a list of all fields of {@code clazz} + */ public static List getAllFields(ClassTree clazz) { List fields = new ArrayList<>(); for (Tree t : clazz.getMembers()) { @@ -41,4 +122,23 @@ public static List getAllFields(ClassTree clazz) { } return fields; } + + @Override + public InitializationAnnotatedTypeFactory getTypeFactory() { + return (InitializationAnnotatedTypeFactory) super.getTypeFactory(); + } + + @Override + protected InitializationVisitor createSourceVisitor() { + return new InitializationVisitor(this); + } + + @Override + protected boolean messageKeyMatches( + String messageKey, String messageKeyInSuppressWarningsString) { + // Also support the shorter keys used by typetools + return super.messageKeyMatches(messageKey, messageKeyInSuppressWarningsString) + || super.messageKeyMatches( + messageKey.replace(".invalid", ""), messageKeyInSuppressWarningsString); + } } diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessAnnotatedTypeFactory.java new file mode 100644 index 000000000000..4f04ab3340b6 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessAnnotatedTypeFactory.java @@ -0,0 +1,62 @@ +package org.checkerframework.checker.initialization; + +import com.sun.source.tree.ClassTree; + +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.dataflow.analysis.AnalysisResult; +import org.checkerframework.framework.flow.CFValue; + +/** The type factory for the {@link InitializationFieldAccessSubchecker}. */ +public class InitializationFieldAccessAnnotatedTypeFactory + extends InitializationParentAnnotatedTypeFactory { + + /** + * Create a new InitializationFieldAccessAnnotatedTypeFactory. + * + * @param checker the checker to which the new type factory belongs + */ + public InitializationFieldAccessAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + postInit(); + } + + @Override + protected InitializationAnalysis createFlowAnalysis() { + return new InitializationAnalysis(checker, this); + } + + @Override + protected void performFlowAnalysis(ClassTree classTree) { + // Only perform the analysis if initialization checking is turned on. + if (!assumeInitialized) { + super.performFlowAnalysis(classTree); + } + } + + /** + * Returns the flow analysis. + * + * @return the flow analysis + * @see #getFlowResult() + */ + /*package-private*/ InitializationAnalysis getAnalysis() { + return analysis; + } + + /** + * Returns the result of the flow analysis. Invariant: + * + *

    +     *  scannedClasses.get(c) == FINISHED for some class c ⇒ flowResult != null
    +     * 
    + * + * Note that flowResult contains analysis results for Trees from multiple classes which are + * produced by multiple calls to performFlowAnalysis. + * + * @return the result of the flow analysis + * @see #getAnalysis() + */ + /*package-private*/ AnalysisResult getFlowResult() { + return flowResult; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessSubchecker.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessSubchecker.java new file mode 100644 index 000000000000..3e8191360105 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessSubchecker.java @@ -0,0 +1,37 @@ +package org.checkerframework.checker.initialization; + +import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; +import org.checkerframework.common.basetype.BaseTypeChecker; + +/** + * Part of the freedom-before-commitment type system. + * + *

    This checker does not actually do any type checking. It exists to provide its parent checker + * (the {@link InitializationChecker#getTargetCheckerClass()}) with declared initialization + * qualifiers via the {@link InitializationFieldAccessTreeAnnotator}. + * + *

    Additionally, this checker performs the flow-sensitive type refinement for the fbc type + * system, which is necessary to avoid reporting follow-up errors related to initialization (see the + * AssignmentDuringInitialization test case). To avoid performing the same type refinement twice, + * the InitializationChecker performs no refinement, instead reusing the results from this checker. + * + * @see InitializationChecker + */ +public class InitializationFieldAccessSubchecker extends BaseTypeChecker { + + /** Default constructor for InitializationFieldAccessSubchecker. */ + public InitializationFieldAccessSubchecker() {} + + // Suppress all errors and warnings, since they are also reported by the InitializationChecker + + @Override + public void reportError(Object source, @CompilerMessageKey String messageKey, Object... args) { + // do nothing + } + + @Override + public void reportWarning( + Object source, @CompilerMessageKey String messageKey, Object... args) { + // do nothing + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessTreeAnnotator.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessTreeAnnotator.java new file mode 100644 index 000000000000..e2ddd5f094c2 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessTreeAnnotator.java @@ -0,0 +1,155 @@ +package org.checkerframework.checker.initialization; + +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.Tree; + +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; +import org.checkerframework.framework.type.treeannotator.TreeAnnotator; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.TreeUtils; + +import javax.lang.model.element.Element; +import javax.lang.model.type.TypeMirror; + +/** + * Part of the freedom-before-commitment type system. + * + *

    This annotator should be added to {@link GenericAnnotatedTypeFactory#createTreeAnnotator} for + * the target checker. It ensures that the fields of an uninitialized receiver have the top type in + * the parent checker's hierarchy. + * + * @see InitializationChecker#getTargetCheckerClass() + */ +public class InitializationFieldAccessTreeAnnotator extends TreeAnnotator { + + /** The value of the assumeInitialized option. */ + protected final boolean assumeInitialized; + + /** + * Creates a new CommitmentFieldAccessTreeAnnotator. + * + * @param atypeFactory the type factory belonging to the init checker's parent + */ + public InitializationFieldAccessTreeAnnotator( + GenericAnnotatedTypeFactory atypeFactory) { + super(atypeFactory); + assumeInitialized = atypeFactory.getChecker().hasOption("assumeInitialized"); + } + + @Override + public Void visitIdentifier(IdentifierTree tree, AnnotatedTypeMirror p) { + super.visitIdentifier(tree, p); + computeFieldAccessType(tree, p); + return null; + } + + @Override + public Void visitMemberSelect(MemberSelectTree tree, AnnotatedTypeMirror p) { + super.visitMemberSelect(tree, p); + computeFieldAccessType(tree, p); + return null; + } + + /** + * Adapts the type in the target checker hierarchy of a field access depending on the field's + * declared type and the receiver's initialization type. + * + * @param tree the field access + * @param type the field access's unadapted type + */ + private void computeFieldAccessType(ExpressionTree tree, AnnotatedTypeMirror type) { + GenericAnnotatedTypeFactory factory = + (GenericAnnotatedTypeFactory) atypeFactory; + + // Don't adapt anything if initialization checking is turned off. + if (assumeInitialized) { + return; + } + + // Don't adapt anything if "tree" is not actually a field access. + + // Don't adapt uses of the identifiers "this" or "super" that are not field accesses + // (e.g., constructor calls or uses of an outer this). + if (tree instanceof IdentifierTree) { + IdentifierTree identTree = (IdentifierTree) tree; + if (identTree.getName().contentEquals("this") + || identTree.getName().contentEquals("super")) { + return; + } + } + + // Don't adapt method accesses. + if (type instanceof AnnotatedTypeMirror.AnnotatedExecutableType) { + return; + } + + // Don't adapt trees that do not have a (explicit or implicit) receiver (e.g., local + // variables). + InitializationFieldAccessAnnotatedTypeFactory initFactory = + atypeFactory + .getChecker() + .getTypeFactoryOfSubcheckerOrNull( + InitializationFieldAccessSubchecker.class); + if (initFactory == null) { + throw new BugInCF("Did not find InitializationFieldAccessSubchecker!"); + } + AnnotatedTypeMirror receiver = initFactory.getReceiverType(tree); + if (receiver == null) { + return; + } + + // Don't adapt trees whose receiver is initialized. + if (!initFactory.isUnknownInitialization(receiver) + && !initFactory.isUnderInitialization(receiver)) { + return; + } + + // Don't adapt trees with an explicit UnknownInitialization annotation on the field + Element element = TreeUtils.elementFromUse(tree); + AnnotatedTypeMirror fieldAnnotations = factory.getAnnotatedType(element); + if (AnnotationUtils.containsSameByName( + fieldAnnotations.getAnnotations(), initFactory.UNKNOWN_INITIALIZATION)) { + return; + } + + TypeMirror fieldOwnerType = element.getEnclosingElement().asType(); + boolean isReceiverInitToOwner = initFactory.isInitializedForFrame(receiver, fieldOwnerType); + + // If the field has been initialized, don't clear annotations. + // This is ok even if the field was initialized with a non-invariant + // value because in that case, there must have been an error before. + // E.g.: + // { f1 = f2; + // f2 = f1; } + // Here, we will get an error for the first assignment, but we won't get another + // error for the second assignment. + // See the AssignmentDuringInitialization test case. + Tree fieldDeclarationTree = initFactory.declarationFromElement(element); + InitializationStore store = initFactory.getStoreBefore(tree); + // If the field declaration is null (because the field is declared in bytecode), + // or the store is null (because flow-sensitive refinement is turned off), + // the field is considered uninitialized. + // Fields of objects other than this are not tracked and thus also considered uninitialized. + // Otherwise, check if the field is initialized in the given store. + boolean isFieldInitialized = + fieldDeclarationTree != null + && store != null + && TreeUtils.isSelfAccess(tree) + && initFactory + .getInitializedFields(store, initFactory.getPath(tree)) + .contains(fieldDeclarationTree); + if (!isReceiverInitToOwner + && !isFieldInitialized + && !factory.isComputingAnnotatedTypeMirrorOfLhs()) { + // The receiver is not initialized for this frame and the type being computed is + // not a LHS. + // Replace all annotations with the top annotation for that hierarchy. + type.clearAnnotations(); + type.addAnnotations(factory.getQualifierHierarchy().getTopAnnotations()); + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessVisitor.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessVisitor.java new file mode 100644 index 000000000000..65b6ba51ab81 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationFieldAccessVisitor.java @@ -0,0 +1,36 @@ +package org.checkerframework.checker.initialization; + +import com.sun.source.tree.ClassTree; + +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; + +/** The visitor for the {@link InitializationFieldAccessSubchecker}. */ +public class InitializationFieldAccessVisitor + extends BaseTypeVisitor { + + /** The value of the assumeInitialized option. */ + private final boolean assumeInitialized; + + /** + * Create an InitializationFieldAccessVisitor. + * + * @param checker the initialization field-access checker + */ + public InitializationFieldAccessVisitor(BaseTypeChecker checker) { + super(checker); + assumeInitialized = checker.hasOption("assumeInitialized"); + } + + @Override + public void processClassTree(ClassTree classTree) { + // As stated in the documentation for the InitializationFieldAccessChecker + // and InitializationChecker, this checker performs the flow analysis + // (which is handled in the BaseTypeVisitor), but does not perform + // any type checking. + // Thus, this method does nothing but scan through the members. + if (!assumeInitialized) { + scan(classTree.getMembers(), null); + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationParentAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationParentAnnotatedTypeFactory.java new file mode 100644 index 000000000000..4fbef7f8fe43 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationParentAnnotatedTypeFactory.java @@ -0,0 +1,959 @@ +package org.checkerframework.checker.initialization; + +import com.sun.source.tree.BinaryTree; +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.NewArrayTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.UnaryTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePath; +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.tree.JCTree; + +import org.checkerframework.checker.initialization.qual.FBCBottom; +import org.checkerframework.checker.initialization.qual.Initialized; +import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; +import org.checkerframework.checker.initialization.qual.PolyInitialized; +import org.checkerframework.checker.initialization.qual.UnderInitialization; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.flow.CFAbstractAnalysis; +import org.checkerframework.framework.flow.CFAbstractStore; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.framework.qual.Unused; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +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.GenericAnnotatedTypeFactory; +import org.checkerframework.framework.type.MostlyNoElementQualifierHierarchy; +import org.checkerframework.framework.type.QualifierHierarchy; +import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; +import org.checkerframework.framework.type.treeannotator.LiteralTreeAnnotator; +import org.checkerframework.framework.type.treeannotator.PropagationTreeAnnotator; +import org.checkerframework.framework.type.treeannotator.TreeAnnotator; +import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; +import org.checkerframework.framework.type.typeannotator.TypeAnnotator; +import org.checkerframework.framework.util.QualifierKind; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreePathUtil; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +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 javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; + +/** + * Superclass for {@link InitializationFieldAccessAnnotatedTypeFactory} and {@link + * InitializationAnnotatedTypeFactory} to contain common functionality. + */ +public abstract class InitializationParentAnnotatedTypeFactory + extends GenericAnnotatedTypeFactory< + CFValue, InitializationStore, InitializationTransfer, InitializationAnalysis> { + + /** {@link UnknownInitialization}. */ + protected final AnnotationMirror UNKNOWN_INITIALIZATION; + + /** {@link Initialized}. */ + protected final AnnotationMirror INITIALIZED; + + /** {@link UnderInitialization} or null. */ + protected final AnnotationMirror UNDER_INITALIZATION; + + /** {@link NotOnlyInitialized} or null. */ + protected final AnnotationMirror NOT_ONLY_INITIALIZED; + + /** {@link PolyInitialized}. */ + protected final AnnotationMirror POLY_INITIALIZED; + + /** {@link FBCBottom}. */ + protected final AnnotationMirror FBCBOTTOM; + + /** The java.lang.Object type. */ + protected final TypeMirror objectTypeMirror; + + /** The Unused.when field/element. */ + protected final ExecutableElement unusedWhenElement; + + /** The UnderInitialization.value field/element. */ + protected final ExecutableElement underInitializationValueElement; + + /** The UnknownInitialization.value field/element. */ + protected final ExecutableElement unknownInitializationValueElement; + + /** The value of the assumeInitialized option. */ + protected final boolean assumeInitialized; + + /** + * Create a new InitializationParentAnnotatedTypeFactory. + * + *

    Don't forget to call {@link #postInit()} in the concrete subclass. + * + * @param checker the checker to which the new type factory belongs + */ + public InitializationParentAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker, true); + + UNKNOWN_INITIALIZATION = AnnotationBuilder.fromClass(elements, UnknownInitialization.class); + INITIALIZED = AnnotationBuilder.fromClass(elements, Initialized.class); + UNDER_INITALIZATION = AnnotationBuilder.fromClass(elements, UnderInitialization.class); + NOT_ONLY_INITIALIZED = AnnotationBuilder.fromClass(elements, NotOnlyInitialized.class); + POLY_INITIALIZED = AnnotationBuilder.fromClass(elements, PolyInitialized.class); + FBCBOTTOM = AnnotationBuilder.fromClass(elements, FBCBottom.class); + + objectTypeMirror = + processingEnv.getElementUtils().getTypeElement("java.lang.Object").asType(); + unusedWhenElement = TreeUtils.getMethod(Unused.class, "when", 0, processingEnv); + underInitializationValueElement = + TreeUtils.getMethod(UnderInitialization.class, "value", 0, processingEnv); + unknownInitializationValueElement = + TreeUtils.getMethod(UnknownInitialization.class, "value", 0, processingEnv); + + assumeInitialized = checker.hasOption("assumeInitialized"); + } + + @Override + public void postAsMemberOf( + AnnotatedTypeMirror type, AnnotatedTypeMirror owner, Element element) { + super.postAsMemberOf(type, owner, element); + + if (element.getKind().isField()) { + Collection declaredFieldAnnotations = + getDeclAnnotations(element); + AnnotatedTypeMirror fieldAnnotations = getAnnotatedType(element); + computeFieldAccessInitializationType( + type, declaredFieldAnnotations, owner, fieldAnnotations); + } + } + + /** + * Adapts the initialization type of a field access (implicit or explicit) based on the receiver + * type and the declared annotations for the field. + * + *

    To adapt the type in the target checker's hierarchy, see the {@link + * InitializationFieldAccessTreeAnnotator} instead. + * + * @param type type of the field access expression + * @param declaredFieldAnnotations declared annotations on the field + * @param receiverType inferred annotations of the receiver + * @param fieldType inferred annotations of the field + */ + private void computeFieldAccessInitializationType( + AnnotatedTypeMirror type, + Collection declaredFieldAnnotations, + AnnotatedTypeMirror receiverType, + AnnotatedTypeMirror fieldType) { + // Primitive values have no fields and are thus always @Initialized. + if (TypesUtils.isPrimitive(type.getUnderlyingType())) { + return; + } + // not necessary if there is an explicit UnknownInitialization + // annotation on the field + if (AnnotationUtils.containsSameByName( + fieldType.getAnnotations(), UNKNOWN_INITIALIZATION)) { + return; + } + + if (isUnknownInitialization(receiverType) || isUnderInitialization(receiverType)) { + if (AnnotationUtils.containsSame(declaredFieldAnnotations, NOT_ONLY_INITIALIZED)) { + // A field declared @NotOnlyInitialized with an uninitialized receiver has + // @UnknownInitialization + type.replaceAnnotation(UNKNOWN_INITIALIZATION); + } else { + // A field declared @NotOnlyInitialized with an initialized receiver is + // @Initialized + type.replaceAnnotation(INITIALIZED); + } + } + } + + @Override + protected Set> createSupportedTypeQualifiers() { + Set> result = new HashSet<>(); + result.add(UnknownInitialization.class); + result.add(UnderInitialization.class); + result.add(Initialized.class); + result.add(FBCBottom.class); + result.add(PolyInitialized.class); + return result; + } + + @Override + public InitializationTransfer createFlowTransferFunction( + CFAbstractAnalysis analysis) { + return new InitializationTransfer((InitializationAnalysis) analysis); + } + + /** + * Returns {@code true}. Initialization cannot be undone, i.e., an @Initialized object always + * stays @Initialized, an @UnderInitialization(A) object always stays @UnderInitialization(A) + * (though it may additionally become @Initialized), etc. + */ + @Override + public boolean isImmutable(TypeMirror type) { + return true; + } + + /** + * Creates a {@link UnderInitialization} annotation with the given type as its type frame + * argument. + * + * @param typeFrame the type down to which some value has been initialized + * @return an {@link UnderInitialization} annotation with the given argument + */ + public AnnotationMirror createUnderInitializationAnnotation(TypeMirror typeFrame) { + assert typeFrame != null; + AnnotationBuilder builder = new AnnotationBuilder(processingEnv, UnderInitialization.class); + builder.setValue("value", typeFrame); + return builder.build(); + } + + @Override + public @Nullable AnnotatedDeclaredType getSelfType(Tree tree) { + AnnotatedDeclaredType selfType = super.getSelfType(tree); + + if (assumeInitialized) { + return selfType; + } + + TreePath path = getPath(tree); + AnnotatedDeclaredType enclosing = selfType; + while (path != null && enclosing != null) { + TreePath topLevelMemberPath = findTopLevelClassMemberForTree(path); + if (topLevelMemberPath != null && topLevelMemberPath.getLeaf() != null) { + Tree topLevelMember = topLevelMemberPath.getLeaf(); + if (topLevelMember.getKind() != Tree.Kind.METHOD + || TreeUtils.isConstructor((MethodTree) topLevelMember)) { + setSelfTypeInInitializationCode(tree, enclosing, topLevelMemberPath); + } + path = topLevelMemberPath.getParentPath(); + enclosing = enclosing.getEnclosingType(); + } else { + break; + } + } + + return selfType; + } + + /** + * In the first enclosing class, find the path to the top-level member that contains {@code + * path}. + * + * @param path the path whose leaf is the target + * @return path to a top-level member containing the leaf of {@code path} + */ + @SuppressWarnings("interning:not.interned") // AST node comparison + private @Nullable TreePath findTopLevelClassMemberForTree(TreePath path) { + if (TreeUtils.isClassTree(path.getLeaf())) { + path = path.getParentPath(); + if (path == null) { + return null; + } + } + ClassTree enclosingClass = TreePathUtil.enclosingClass(path); + if (enclosingClass != null) { + List classMembers = enclosingClass.getMembers(); + TreePath searchPath = path; + while (searchPath.getParentPath() != null + && searchPath.getParentPath().getLeaf() != enclosingClass) { + searchPath = searchPath.getParentPath(); + if (classMembers.contains(searchPath.getLeaf())) { + return searchPath; + } + } + } + return null; + } + + /** + * Side-effects argument {@code selfType} to make it @Initialized or @UnderInitialization, + * depending on whether all fields have been set. + * + * @param tree a tree + * @param selfType the type to side-effect + * @param path a path + */ + protected void setSelfTypeInInitializationCode( + Tree tree, AnnotatedDeclaredType selfType, TreePath path) { + ClassTree enclosingClass = TreePathUtil.enclosingClass(path); + Type classType = ((JCTree) enclosingClass).type; + AnnotationMirror annotation = null; + + // If all fields are initialized-only, and they are all initialized, + // then: + // - if the class is final, this is @Initialized + // - otherwise, this is @UnderInitialization(CurrentClass) as + // there might still be subclasses that need initialization. + if (areAllFieldsInitializedOnly(enclosingClass)) { + InitializationStore store = getStoreBefore(tree); + if (store != null + && getUninitializedFields(store, path, false, Collections.emptyList()) + .isEmpty()) { + if (classType.isFinal()) { + annotation = INITIALIZED; + } else { + annotation = createUnderInitializationAnnotation(classType); + } + } + } + + if (annotation == null) { + annotation = getUnderInitializationAnnotationOfSuperType(classType); + } + selfType.replaceAnnotation(annotation); + } + + /** + * Returns an {@link UnderInitialization} annotation that has the superclass of {@code type} as + * type frame. + * + * @param type a type + * @return true an {@link UnderInitialization} for the supertype of {@code type} + */ + protected AnnotationMirror getUnderInitializationAnnotationOfSuperType(TypeMirror type) { + // Find supertype if possible. + AnnotationMirror annotation; + List superTypes = types.directSupertypes(type); + TypeMirror superClass = null; + for (TypeMirror superType : superTypes) { + ElementKind kind = types.asElement(superType).getKind(); + if (kind == ElementKind.CLASS) { + superClass = superType; + break; + } + } + // Create annotation. + if (superClass != null) { + annotation = createUnderInitializationAnnotation(superClass); + } else { + // Use Object as a valid super-class. + annotation = createUnderInitializationAnnotation(Object.class); + } + return annotation; + } + + /** + * Returns whether the specified field is unused, given the specified annotations on the + * receiver. + * + * @param field the field to check + * @param receiverAnnos the annotations on the receiver + * @return whether {@code field} is unused given {@code receiverAnnos} + */ + protected boolean isUnused( + VariableTree field, Collection receiverAnnos) { + if (receiverAnnos.isEmpty()) { + return false; + } + + AnnotationMirror unused = + getDeclAnnotation(TreeUtils.elementFromDeclaration(field), Unused.class); + if (unused == null) { + return false; + } + + Name when = AnnotationUtils.getElementValueClassName(unused, unusedWhenElement); + for (AnnotationMirror anno : receiverAnnos) { + Name annoName = ((TypeElement) anno.getAnnotationType().asElement()).getQualifiedName(); + if (annoName.contentEquals(when)) { + return true; + } + } + + return false; + } + + /** + * Creates a {@link UnderInitialization} annotation with the given type frame. + * + * @param typeFrame the type down to which some value has been initialized + * @return an {@link UnderInitialization} annotation with the given argument + */ + public AnnotationMirror createUnderInitializationAnnotation(Class typeFrame) { + assert typeFrame != null; + AnnotationBuilder builder = new AnnotationBuilder(processingEnv, UnderInitialization.class); + builder.setValue("value", typeFrame); + return builder.build(); + } + + /** + * Are all fields initialized-only? + * + * @param classTree the class to query + * @return true if all fields are initialized-only + */ + protected boolean areAllFieldsInitializedOnly(ClassTree classTree) { + for (Tree member : classTree.getMembers()) { + if (member.getKind() != Tree.Kind.VARIABLE) { + continue; + } + VariableTree var = (VariableTree) member; + VariableElement varElt = TreeUtils.elementFromDeclaration(var); + // var is not initialized-only + if (getDeclAnnotation(varElt, NotOnlyInitialized.class) != null) { + // var is not static -- need a check of initializer blocks, + // not of constructor which is where this is used + if (!varElt.getModifiers().contains(Modifier.STATIC)) { + return false; + } + } + } + return true; + } + + /** + * Returns the fields that are possibly uninitialized in a given store, without taking into + * account the target checker. + * + *

    I.e., this method returns all fields that have not been assigned, without considering + * fields that may be considered initialized by the target checker even though they have not + * been explicitly assigned. See {@link + * InitializationAnnotatedTypeFactory#getUninitializedFields( InitializationStore, + * CFAbstractStore, TreePath, boolean, Collection)} for a method that does take the target + * checker into account. + * + * @param store a store + * @param path the current path, used to determine the current class + * @param isStatic whether to report static fields or instance fields + * @param receiverAnnotations the annotations on the receiver + * @return the fields that are not yet initialized in a given store + */ + public List getUninitializedFields( + InitializationStore store, + TreePath path, + boolean isStatic, + Collection receiverAnnotations) { + ClassTree currentClass = TreePathUtil.enclosingClass(path); + List fields = InitializationChecker.getAllFields(currentClass); + List uninit = new ArrayList<>(); + for (VariableTree field : fields) { + if (isUnused(field, receiverAnnotations)) { + continue; // don't consider unused fields + } + VariableElement fieldElem = TreeUtils.elementFromDeclaration(field); + if (ElementUtils.isStatic(fieldElem) == isStatic) { + if (!store.isFieldInitialized(fieldElem)) { + uninit.add(field); + } + } + } + return uninit; + } + + /** + * Returns the fields that are initialized in the given store. + * + * @param store a store + * @param path the current path; used to compute the current class + * @return the fields that are initialized in the given store + */ + public List getInitializedFields(InitializationStore store, TreePath path) { + // TODO: Instead of passing the TreePath around, can we use + // getCurrentClassTree? + ClassTree currentClass = TreePathUtil.enclosingClass(path); + List fields = InitializationChecker.getAllFields(currentClass); + List initializedFields = new ArrayList<>(); + for (VariableTree field : fields) { + VariableElement fieldElem = TreeUtils.elementFromDeclaration(field); + if (!ElementUtils.isStatic(fieldElem)) { + if (store.isFieldInitialized(fieldElem)) { + initializedFields.add(field); + } + } + } + return initializedFields; + } + + @Override + public boolean isNotFullyInitializedReceiver(MethodTree methodTree) { + if (super.isNotFullyInitializedReceiver(methodTree)) { + return true; + } + AnnotatedDeclaredType receiverType = getAnnotatedType(methodTree).getReceiverType(); + if (receiverType != null) { + return isUnknownInitialization(receiverType) || isUnderInitialization(receiverType); + } else { + // There is no receiver e.g. in static methods. + return false; + } + } + + /** + * Creates a {@link UnknownInitialization} annotation with a given type frame. + * + * @param typeFrame the type down to which some value has been initialized + * @return an {@link UnknownInitialization} annotation with the given argument + */ + public AnnotationMirror createUnknownInitializationAnnotation(Class typeFrame) { + assert typeFrame != null; + AnnotationBuilder builder = + new AnnotationBuilder(processingEnv, UnknownInitialization.class); + builder.setValue("value", typeFrame); + return builder.build(); + } + + /** + * Creates an {@link UnknownInitialization} annotation with a given type frame. + * + * @param typeFrame the type down to which some value has been initialized + * @return an {@link UnknownInitialization} annotation with the given argument + */ + public AnnotationMirror createUnknownInitializationAnnotation(TypeMirror typeFrame) { + assert typeFrame != null; + AnnotationBuilder builder = + new AnnotationBuilder(processingEnv, UnknownInitialization.class); + builder.setValue("value", typeFrame); + return builder.build(); + } + + /** + * Is {@code anno} the {@link UnderInitialization} annotation (with any type frame)? + * + * @param anno the annotation to check + * @return true if {@code anno} is {@link UnderInitialization} + */ + public boolean isUnderInitialization(AnnotationMirror anno) { + return areSameByClass(anno, UnderInitialization.class); + } + + /** + * Is {@code anno} the {@link UnknownInitialization} annotation (with any type frame)? + * + * @param anno the annotation to check + * @return true if {@code anno} is {@link UnknownInitialization} + */ + public boolean isUnknownInitialization(AnnotationMirror anno) { + return areSameByClass(anno, UnknownInitialization.class); + } + + /** + * Is {@code anno} the bottom annotation? + * + * @param anno the annotation to check + * @return true if {@code anno} is {@link FBCBottom} + */ + public boolean isFbcBottom(AnnotationMirror anno) { + return AnnotationUtils.areSame(anno, FBCBOTTOM); + } + + /** + * Is {@code anno} the {@link Initialized} annotation? + * + * @param anno the annotation to check + * @return true if {@code anno} is {@link Initialized} + */ + public boolean isInitialized(AnnotationMirror anno) { + return AnnotationUtils.areSame(anno, INITIALIZED); + } + + /** + * Does {@code anno} have the annotation {@link UnderInitialization} (with any type frame)? + * + * @param anno the annotation to check + * @return true if {@code anno} has {@link UnderInitialization} + */ + public boolean isUnderInitialization(AnnotatedTypeMirror anno) { + return anno.hasEffectiveAnnotation(UnderInitialization.class); + } + + /** + * Does {@code anno} have the annotation {@link UnknownInitialization} (with any type frame)? + * + * @param anno the annotation to check + * @return true if {@code anno} has {@link UnknownInitialization} + */ + public boolean isUnknownInitialization(AnnotatedTypeMirror anno) { + return anno.hasEffectiveAnnotation(UnknownInitialization.class); + } + + /** + * Does {@code anno} have the bottom annotation? + * + * @param anno the annotation to check + * @return true if {@code anno} has {@link FBCBottom} + */ + public boolean isFbcBottom(AnnotatedTypeMirror anno) { + return anno.hasEffectiveAnnotation(FBCBottom.class); + } + + /** + * Does {@code anno} have the annotation {@link Initialized}? + * + * @param anno the annotation to check + * @return true if {@code anno} has {@link Initialized} + */ + public boolean isInitialized(AnnotatedTypeMirror anno) { + return anno.hasEffectiveAnnotation(Initialized.class); + } + + /** + * Return true if the type is initialized with respect to the given frame -- that is, all of the + * fields of the frame are initialized. + * + * @param type the type whose initialization type qualifiers to check + * @param frame a class in {@code type}'s class hierarchy + * @return true if the type is initialized for the given frame + */ + public boolean isInitializedForFrame(AnnotatedTypeMirror type, TypeMirror frame) { + if (isInitialized(type)) { + return true; + } + + AnnotationMirror initializationAnno = + type.getEffectiveAnnotationInHierarchy(UNKNOWN_INITIALIZATION); + if (initializationAnno == null) { + initializationAnno = type.getEffectiveAnnotationInHierarchy(UNDER_INITALIZATION); + } + + TypeMirror typeFrame = getTypeFrameFromAnnotation(initializationAnno); + Types types = processingEnv.getTypeUtils(); + return types.isSubtype(typeFrame, types.erasure(frame)); + } + + /** + * Returns the type frame (that is, the argument) of a given initialization annotation. + * + * @param annotation a {@link UnderInitialization} or {@link UnknownInitialization} annotation + * @return the annotation's argument + */ + public TypeMirror getTypeFrameFromAnnotation(AnnotationMirror annotation) { + if (AnnotationUtils.areSameByName( + annotation, + "org.checkerframework.checker.initialization.qual.UnderInitialization")) { + return AnnotationUtils.getElementValue( + annotation, + underInitializationValueElement, + TypeMirror.class, + objectTypeMirror); + } else { + return AnnotationUtils.getElementValue( + annotation, + unknownInitializationValueElement, + TypeMirror.class, + objectTypeMirror); + } + } + + @Override + protected QualifierHierarchy createQualifierHierarchy() { + return new InitializationQualifierHierarchy(); + } + + @Override + protected TypeAnnotator createTypeAnnotator() { + return new ListTypeAnnotator( + super.createTypeAnnotator(), new CommitmentTypeAnnotator(this)); + } + + /** + * Returns {@code false}. Redundancy in only the initialization hierarchy is ok and may even be + * caused by implicit default annotations. The parent checker should determine whether to warn + * about redundancy. + */ + @Override + public boolean shouldWarnIfStubRedundantWithBytecode() { + return false; + } + + @Override + protected TreeAnnotator createTreeAnnotator() { + // Don't call super.createTreeAnnotator because we want our CommitmentTreeAnnotator + // instead of the default PropagationTreeAnnotator + List treeAnnotators = new ArrayList<>(2); + treeAnnotators.add(new LiteralTreeAnnotator(this).addStandardLiteralQualifiers()); + if (dependentTypesHelper.hasDependentAnnotations()) { + treeAnnotators.add(dependentTypesHelper.createDependentTypesTreeAnnotator()); + } + treeAnnotators.add(new CommitmentTreeAnnotator(this)); + return new ListTreeAnnotator(treeAnnotators); + } + + /** + * This type annotator adds the correct UnderInitialization annotation to super constructors. + */ + protected class CommitmentTypeAnnotator extends TypeAnnotator { + + /** + * Creates a new CommitmentTypeAnnotator. + * + * @param atypeFactory this factory + */ + public CommitmentTypeAnnotator(InitializationParentAnnotatedTypeFactory atypeFactory) { + super(atypeFactory); + } + + @Override + public Void visitExecutable(AnnotatedExecutableType t, Void p) { + Void result = super.visitExecutable(t, p); + Element elem = t.getElement(); + if (elem.getKind() == ElementKind.CONSTRUCTOR) { + AnnotatedDeclaredType returnType = (AnnotatedDeclaredType) t.getReturnType(); + DeclaredType underlyingType = returnType.getUnderlyingType(); + returnType.replaceAnnotation( + getUnderInitializationAnnotationOfSuperType(underlyingType)); + } + return result; + } + } + + /** + * This tree annotator modifies the propagation tree annotator to add propagation rules for the + * freedom-before-commitment system. + */ + protected class CommitmentTreeAnnotator extends PropagationTreeAnnotator { + + /** + * Creates a new CommitmentTreeAnnotator. + * + * @param initializationAnnotatedTypeFactory this factory + */ + public CommitmentTreeAnnotator( + InitializationParentAnnotatedTypeFactory initializationAnnotatedTypeFactory) { + super(initializationAnnotatedTypeFactory); + } + + @Override + public Void visitMethod(MethodTree tree, AnnotatedTypeMirror p) { + Void result = super.visitMethod(tree, p); + if (TreeUtils.isConstructor(tree)) { + assert p instanceof AnnotatedExecutableType; + AnnotatedExecutableType exeType = (AnnotatedExecutableType) p; + DeclaredType underlyingType = + (DeclaredType) exeType.getReturnType().getUnderlyingType(); + AnnotationMirror a = getUnderInitializationAnnotationOfSuperType(underlyingType); + exeType.getReturnType().replaceAnnotation(a); + } + return result; + } + + @Override + public Void visitNewClass(NewClassTree tree, AnnotatedTypeMirror p) { + super.visitNewClass(tree, p); + boolean allInitialized = true; + Type type = ((JCTree) tree).type; + for (ExpressionTree a : tree.getArguments()) { + AnnotatedTypeMirror t = getAnnotatedType(a); + allInitialized &= (isInitialized(t) || isFbcBottom(t)); + } + if (!allInitialized) { + p.replaceAnnotation(createUnderInitializationAnnotation(type)); + return null; + } + p.replaceAnnotation(INITIALIZED); + return null; + } + + @Override + public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) { + if (tree.getKind() != Tree.Kind.NULL_LITERAL) { + type.addAnnotation(INITIALIZED); + } + return super.visitLiteral(tree, type); + } + + @Override + public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { + // The most precise element type for `new Object[] {null}` is @FBCBottom, but + // the most useful element type is @Initialized (which is also accurate). + AnnotatedArrayType arrayType = (AnnotatedArrayType) type; + AnnotatedTypeMirror componentType = arrayType.getComponentType(); + if (componentType.hasEffectiveAnnotation(FBCBOTTOM)) { + componentType.replaceAnnotation(INITIALIZED); + } + return null; + } + + @Override + public Void visitMemberSelect( + MemberSelectTree tree, AnnotatedTypeMirror annotatedTypeMirror) { + if (TreeUtils.isArrayLengthAccess(tree)) { + annotatedTypeMirror.replaceAnnotation(INITIALIZED); + } + return super.visitMemberSelect(tree, annotatedTypeMirror); + } + + /* The result of a binary or unary operator is either primitive or a String. + * Primitives have no fields and are thus always @Initialized. + * Since all String constructors return @Initialized strings, Strings + * are also always @Initialized. */ + + @Override + public Void visitBinary(BinaryTree tree, AnnotatedTypeMirror type) { + return null; + } + + @Override + public Void visitUnary(UnaryTree tree, AnnotatedTypeMirror type) { + return null; + } + } + + /** The {@link QualifierHierarchy} for the initialization type system. */ + protected class InitializationQualifierHierarchy extends MostlyNoElementQualifierHierarchy { + + /** Qualifier kind for the @{@link UnknownInitialization} annotation. */ + private final QualifierKind UNKNOWN_INIT; + + /** Qualifier kind for the @{@link UnderInitialization} annotation. */ + private final QualifierKind UNDER_INIT; + + /** Create an InitializationQualifierHierarchy. */ + protected InitializationQualifierHierarchy() { + super( + InitializationParentAnnotatedTypeFactory.this.getSupportedTypeQualifiers(), + elements, + InitializationParentAnnotatedTypeFactory.this); + UNKNOWN_INIT = getQualifierKind(UNKNOWN_INITIALIZATION); + UNDER_INIT = getQualifierKind(UNDER_INITALIZATION); + } + + @Override + public boolean isSubtypeWithElements( + AnnotationMirror subAnno, + QualifierKind subKind, + AnnotationMirror superAnno, + QualifierKind superKind) { + if (!subKind.isSubtypeOf(superKind)) { + return false; + } else if ((subKind == UNDER_INIT && superKind == UNDER_INIT) + || (subKind == UNDER_INIT && superKind == UNKNOWN_INIT) + || (subKind == UNKNOWN_INIT && superKind == UNKNOWN_INIT)) { + // Thus, we only need to look at the type frame. + TypeMirror frame1 = getTypeFrameFromAnnotation(subAnno); + TypeMirror frame2 = getTypeFrameFromAnnotation(superAnno); + return types.isSubtype(frame1, frame2); + } else { + return true; + } + } + + @Override + protected AnnotationMirror leastUpperBoundWithElements( + AnnotationMirror anno1, + QualifierKind qual1, + AnnotationMirror anno2, + QualifierKind qual2, + QualifierKind lubKind) { + // Handle the case where one is a subtype of the other. + if (isSubtypeWithElements(anno1, qual1, anno2, qual2)) { + return anno2; + } else if (isSubtypeWithElements(anno2, qual2, anno1, qual1)) { + return anno1; + } + boolean unknowninit1 = isUnknownInitialization(anno1); + boolean unknowninit2 = isUnknownInitialization(anno2); + boolean underinit1 = isUnderInitialization(anno1); + boolean underinit2 = isUnderInitialization(anno2); + + // Handle @Initialized. + if (isInitialized(anno1)) { + assert underinit2; + return createUnknownInitializationAnnotation(getTypeFrameFromAnnotation(anno2)); + } else if (isInitialized(anno2)) { + assert underinit1; + return createUnknownInitializationAnnotation(getTypeFrameFromAnnotation(anno1)); + } + + if (underinit1 && underinit2) { + return createUnderInitializationAnnotation( + lubTypeFrame( + getTypeFrameFromAnnotation(anno1), + getTypeFrameFromAnnotation(anno2))); + } + + assert (unknowninit1 || underinit1) && (unknowninit2 || underinit2); + return createUnknownInitializationAnnotation( + lubTypeFrame( + getTypeFrameFromAnnotation(anno1), getTypeFrameFromAnnotation(anno2))); + } + + /** + * Returns the least upper bound of two Java basetypes (without annotations). + * + * @param a the first argument + * @param b the second argument + * @return the lub of the two arguments + */ + protected TypeMirror lubTypeFrame(TypeMirror a, TypeMirror b) { + if (types.isSubtype(a, b)) { + return b; + } else if (types.isSubtype(b, a)) { + return a; + } + + return TypesUtils.leastUpperBound(a, b, processingEnv); + } + + @Override + protected AnnotationMirror greatestLowerBoundWithElements( + AnnotationMirror anno1, + QualifierKind qual1, + AnnotationMirror anno2, + QualifierKind qual2, + QualifierKind glbKind) { + // Handle the case where one is a subtype of the other. + if (isSubtypeWithElements(anno1, qual1, anno2, qual2)) { + return anno1; + } else if (isSubtypeWithElements(anno2, qual2, anno1, qual1)) { + return anno2; + } + boolean unknowninit1 = isUnknownInitialization(anno1); + boolean unknowninit2 = isUnknownInitialization(anno2); + boolean underinit1 = isUnderInitialization(anno1); + boolean underinit2 = isUnderInitialization(anno2); + + // Handle @Initialized. + if (isInitialized(anno1)) { + assert underinit2; + return FBCBOTTOM; + } else if (isInitialized(anno2)) { + assert underinit1; + return FBCBOTTOM; + } + + TypeMirror typeFrame = + TypesUtils.greatestLowerBound( + getTypeFrameFromAnnotation(anno1), + getTypeFrameFromAnnotation(anno2), + processingEnv); + if (typeFrame.getKind() == TypeKind.ERROR + || typeFrame.getKind() == TypeKind.INTERSECTION) { + return FBCBOTTOM; + } + + if (underinit1 && underinit2) { + return createUnderInitializationAnnotation(typeFrame); + } + + assert (unknowninit1 || underinit1) && (unknowninit2 || underinit2); + return createUnderInitializationAnnotation(typeFrame); + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationStore.java b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationStore.java index d28e612bfa49..09a38400b19d 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationStore.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationStore.java @@ -1,25 +1,19 @@ package org.checkerframework.checker.initialization; -import java.util.HashMap; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizer; +import org.checkerframework.dataflow.expression.ClassName; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.ThisReference; +import org.checkerframework.framework.flow.CFAbstractStore; +import org.checkerframework.framework.flow.CFValue; +import org.plumelib.util.ToStringComparator; + import java.util.HashSet; -import java.util.Map; import java.util.Set; -import javax.lang.model.element.AnnotationMirror; + import javax.lang.model.element.Element; import javax.lang.model.element.VariableElement; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.ClassName; -import org.checkerframework.dataflow.analysis.FlowExpressions.FieldAccess; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; -import org.checkerframework.dataflow.analysis.FlowExpressions.ThisReference; -import org.checkerframework.dataflow.cfg.CFGVisualizer; -import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; -import org.checkerframework.framework.flow.CFAbstractAnalysis; -import org.checkerframework.framework.flow.CFAbstractStore; -import org.checkerframework.framework.flow.CFAbstractValue; -import org.checkerframework.framework.type.AnnotatedTypeFactory; -import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.javacutil.AnnotationUtils; /** * A store that extends {@code CFAbstractStore} and additionally tracks which fields of the 'self' @@ -27,18 +21,21 @@ * * @see InitializationTransfer */ -public class InitializationStore, S extends InitializationStore> - extends CFAbstractStore { +public class InitializationStore extends CFAbstractStore { /** The set of fields that are initialized. */ protected final Set initializedFields; - /** The set of fields that have 'invariant' annotation. */ - protected final Map invariantFields; - public InitializationStore(CFAbstractAnalysis analysis, boolean sequentialSemantics) { + /** + * Creates a new InitializationStore. + * + * @param analysis the analysis class this store belongs to + * @param sequentialSemantics should the analysis use sequential Java semantics? + */ + public InitializationStore(InitializationAnalysis analysis, boolean sequentialSemantics) { super(analysis, sequentialSemantics); - initializedFields = new HashSet<>(); - invariantFields = new HashMap<>(); + // The initialCapacity for the two maps is set to 4, an arbitrary, small value. + initializedFields = new HashSet<>(4); } /** @@ -48,80 +45,38 @@ public InitializationStore(CFAbstractAnalysis analysis, boolean sequent * initialized. */ @Override - public void insertValue(Receiver r, V value) { - if (value == null) { - // No need to insert a null abstract value because it represents - // top and top is also the default value. + public void insertValue(JavaExpression je, CFValue value, boolean permitNondeterministic) { + if (!shouldInsert(je, value, permitNondeterministic)) { return; } - InitializationAnnotatedTypeFactory atypeFactory = - (InitializationAnnotatedTypeFactory) analysis.getTypeFactory(); - QualifierHierarchy qualifierHierarchy = atypeFactory.getQualifierHierarchy(); - AnnotationMirror invariantAnno = atypeFactory.getFieldInvariantAnnotation(); - - // Remember fields that have the 'invariant' annotation in the store. - if (r instanceof FieldAccess) { - FieldAccess fieldAccess = (FieldAccess) r; - if (!fieldValues.containsKey(r)) { - Set declaredAnnos = - atypeFactory.getAnnotatedType(fieldAccess.getField()).getAnnotations(); - if (AnnotationUtils.containsSame(declaredAnnos, invariantAnno)) { - if (!invariantFields.containsKey(fieldAccess)) { - invariantFields.put( - fieldAccess, - analysis.createSingleAnnotationValue(invariantAnno, r.getType())); - } - } - } - } + super.insertValue(je, value, permitNondeterministic); - super.insertValue(r, value); - - for (AnnotationMirror a : value.getAnnotations()) { - if (qualifierHierarchy.isSubtype(a, invariantAnno)) { - if (r instanceof FieldAccess) { - FieldAccess fa = (FieldAccess) r; - if (fa.getReceiver() instanceof ThisReference - || fa.getReceiver() instanceof ClassName) { - addInitializedField(fa.getField()); - } - } + if (je instanceof FieldAccess) { + FieldAccess fa = (FieldAccess) je; + if (fa.getReceiver() instanceof ThisReference + || fa.getReceiver() instanceof ClassName) { + addInitializedField(fa.getField()); } } } /** - * {@inheritDoc} + * A copy constructor. * - *

    Additionally, the {@link InitializationStore} keeps all field values for fields that have - * the 'invariant' annotation. + * @param other the store to copy */ - @Override - public void updateForMethodCall( - MethodInvocationNode n, AnnotatedTypeFactory atypeFactory, V val) { - // Remove invariant annotated fields to avoid performance issue reported in #1438. - for (FieldAccess invariantField : invariantFields.keySet()) { - fieldValues.remove(invariantField); - } - - super.updateForMethodCall(n, atypeFactory, val); - - // Add invariant annotation again. - fieldValues.putAll(invariantFields); - } - - /** A copy constructor. */ - public InitializationStore(S other) { + public InitializationStore(InitializationStore other) { super(other); initializedFields = new HashSet<>(other.initializedFields); - invariantFields = new HashMap<>(other.invariantFields); } /** * Mark the field identified by the element {@code field} as initialized if it belongs to the * current class, or is static (in which case there is no aliasing issue and we can just add all * static fields). + * + * @param field a field that is initialized */ public void addInitializedField(FieldAccess field) { boolean fieldOnThisReference = field.getReceiver() instanceof ThisReference; @@ -134,6 +89,8 @@ public void addInitializedField(FieldAccess field) { /** * Mark the field identified by the element {@code f} as initialized (the caller needs to ensure * that the field belongs to the current class, or is a static field). + * + * @param f a field that is initialized */ public void addInitializedField(VariableElement f) { initializedFields.add(f); @@ -145,93 +102,116 @@ public boolean isFieldInitialized(Element f) { } @Override - protected boolean supersetOf(CFAbstractStore o) { + protected boolean supersetOf(CFAbstractStore o) { if (!(o instanceof InitializationStore)) { return false; } - @SuppressWarnings("unchecked") - S other = (S) o; + InitializationStore other = (InitializationStore) o; + for (Element field : other.initializedFields) { if (!initializedFields.contains(field)) { return false; } } - for (FieldAccess invariantField : other.invariantFields.keySet()) { - if (!invariantFields.containsKey(invariantField)) { - return false; - } - } + return super.supersetOf(other); + } - Map removedFieldValues = new HashMap<>(); - Map removedOtherFieldValues = new HashMap<>(); - try { - // Remove invariant annotated fields to avoid performance issue reported in #1438. - for (FieldAccess invariantField : invariantFields.keySet()) { - V v = fieldValues.remove(invariantField); - removedFieldValues.put(invariantField, v); - } - for (FieldAccess invariantField : other.invariantFields.keySet()) { - V v = other.fieldValues.remove(invariantField); - removedOtherFieldValues.put(invariantField, v); - } + @Override + public InitializationStore leastUpperBound(InitializationStore other) { + InitializationStore result = super.leastUpperBound(other); - return super.supersetOf(other); - } finally { - // Restore removed values. - fieldValues.putAll(removedFieldValues); - other.fieldValues.putAll(removedOtherFieldValues); - } + result.initializedFields.addAll(other.initializedFields); + result.initializedFields.retainAll(initializedFields); + + return result; } + /* + * TODO: implement a meaningful `isDeclaredInitialized`. + * @Override - public S leastUpperBound(S other) { - // Remove invariant annotated fields to avoid performance issue reported in #1438. - Map removedFieldValues = new HashMap<>(); - Map removedOtherFieldValues = new HashMap<>(); - for (FieldAccess invariantField : invariantFields.keySet()) { - V v = fieldValues.remove(invariantField); - removedFieldValues.put(invariantField, v); - } - for (FieldAccess invariantField : other.invariantFields.keySet()) { - V v = other.fieldValues.remove(invariantField); - removedOtherFieldValues.put(invariantField, v); + protected CFValue newFieldValueAfterMethodCall( + FieldAccess fieldAccess, + GenericAnnotatedTypeFactory atypeFactory, + CFValue value) { + if (isDeclaredInitialized(fieldAccess)) { + return value; } - S result = super.leastUpperBound(other); - - // Restore removed values. - fieldValues.putAll(removedFieldValues); - other.fieldValues.putAll(removedOtherFieldValues); + return super.newFieldValueAfterMethodCall(fieldAccess, atypeFactory, value); + } + */ - // Set intersection for initializedFields. - result.initializedFields.addAll(other.initializedFields); - result.initializedFields.retainAll(initializedFields); + /* + * Determine whether the given field is declared as {@link Initialized} (taking into account + * viewpoint adaption for {@link NotOnlyInitialized}). + * + * @param fieldAccess the field to check + * @return whether the given field is declared as {@link Initialized} (taking into account + * viewpoint adaption for {@link NotOnlyInitialized}) + * + protected boolean isDeclaredInitialized(FieldAccess fieldAccess) { + // Returning false is the conservative answer, but not much faster than asking the ATF. + return false; + /* + InitializationParentAnnotatedTypeFactory atypeFactory = + (InitializationParentAnnotatedTypeFactory) analysis.getTypeFactory(); + AnnotatedTypeMirror declField = atypeFactory.getAnnotatedType(fieldAccess.getField()); + if (!declField.hasAnnotation(atypeFactory.INITIALIZED)) { + return false; + } - // Set intersection for invariantFields. - for (Map.Entry e : invariantFields.entrySet()) { - if (other.invariantFields.containsKey(e.getKey())) { - result.invariantFields.put(e.getKey(), e.getValue()); + AnnotatedTypeMirror receiverType; + if (thisValue != null + && thisValue.getUnderlyingType().getKind() != TypeKind.ERROR + && thisValue.getUnderlyingType().getKind() != TypeKind.NULL) { + receiverType = + AnnotatedTypeMirror.createType( + thisValue.getUnderlyingType(), atypeFactory, false); + for (AnnotationMirror anno : thisValue.getAnnotations()) { + receiverType.replaceAnnotation(anno); } + } else if (!fieldAccess.isStatic()) { + receiverType = + AnnotatedTypeMirror.createType( + fieldAccess.getReceiver().getType(), atypeFactory, false) + .getErased(); + receiverType.addAnnotations(atypeFactory.getQualifierHierarchy().getTopAnnotations()); + } else { + receiverType = null; } - // Add invariant annotation. - result.fieldValues.putAll(result.invariantFields); - return result; + if (receiverType != null) { + return receiverType.hasAnnotation(atypeFactory.INITIALIZED); + } else { + // The field is static and INITIALIZED, so there is nothing else to check. + return true; + } } + */ @Override - protected String internalVisualize(CFGVisualizer viz) { - return super.internalVisualize(viz) - + viz.visualizeStoreKeyVal("initialized fields", initializedFields) - + viz.visualizeStoreKeyVal("invariant fields", invariantFields); - } + protected String internalVisualize(CFGVisualizer viz) { + String superVisualize = super.internalVisualize(viz); + + String initializedVisualize = + viz.visualizeStoreKeyVal( + "initialized fields", ToStringComparator.sorted(initializedFields)); - public Map getFieldValues() { - return fieldValues; + if (superVisualize.isEmpty()) { + return String.join(viz.getSeparator(), initializedVisualize); + } else { + return String.join(viz.getSeparator(), superVisualize, initializedVisualize); + } } - public CFAbstractAnalysis getAnalysis() { - return analysis; + /** + * Returns the analysis associated with this store. + * + * @return the analysis associated with this store + */ + public InitializationAnalysis getAnalysis() { + return (InitializationAnalysis) analysis; } } 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 b539d07bb12f..b18487a80aa0 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationTransfer.java @@ -2,93 +2,70 @@ import com.sun.source.tree.ClassTree; import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.MethodTree; import com.sun.tools.javac.code.Symbol; -import java.util.ArrayList; -import java.util.List; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; -import 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; + import org.checkerframework.dataflow.analysis.RegularTransferResult; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.node.AssignmentNode; -import org.checkerframework.dataflow.cfg.node.FieldAccessNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; -import org.checkerframework.dataflow.cfg.node.ThisLiteralNode; -import org.checkerframework.framework.flow.CFAbstractAnalysis; +import org.checkerframework.dataflow.cfg.node.ThisNode; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.flow.CFAbstractTransfer; -import org.checkerframework.framework.flow.CFAbstractValue; -import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; +import java.util.ArrayList; +import java.util.List; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; + /** * A transfer function that extends {@link CFAbstractTransfer} and tracks {@link * InitializationStore}s. In addition to the features of {@link CFAbstractTransfer}, this transfer - * function also track which fields of the current class ('self' receiver) have been initialized. + * function also tracks which fields of the current class ('self' receiver) have been initialized. * *

    More precisely, the following refinements are performed: * *

      - *
    1. After the call to a constructor ("this()" call), all non-null fields of the current class - * can safely be considered initialized. - *
    2. After a method call with a postcondition that ensures a field to be non-null, that field - * can safely be considered initialized (this is done in {@link - * InitializationStore#insertValue(FlowExpressions.Receiver, CFAbstractValue)}). - *
    3. All non-null fields with an initializer can be considered initialized (this is done in - * {@link InitializationStore#insertValue(FlowExpressions.Receiver, CFAbstractValue)}). - *
    4. After the call to a super constructor ("super()" call), all non-null fields of the super - * class can safely be considered initialized. + *
    5. After the call to a constructor ("this()" call), all fields of the current class can safely + * be considered initialized. + *
    6. After the call to a super constructor ("super()" call), all fields of the super class can + * safely be considered initialized. *
    - * - * @see InitializationStore - * @param the type of the transfer function */ -public class InitializationTransfer< - V extends CFAbstractValue, - T extends InitializationTransfer, - S extends InitializationStore> - extends CFAbstractTransfer { +public class InitializationTransfer + extends CFAbstractTransfer { - protected final InitializationAnnotatedTypeFactory atypeFactory; + /** The initialization type factory */ + protected final InitializationParentAnnotatedTypeFactory atypeFactory; - public InitializationTransfer(CFAbstractAnalysis analysis) { + /** + * Create a new InitializationTransfer for the given analysis. + * + * @param analysis init analysi.s + */ + public InitializationTransfer(InitializationAnalysis analysis) { super(analysis); - this.atypeFactory = - (InitializationAnnotatedTypeFactory) analysis.getTypeFactory(); - } - - @Override - protected boolean isNotFullyInitializedReceiver(MethodTree methodTree) { - if (super.isNotFullyInitializedReceiver(methodTree)) { - return true; - } - final AnnotatedDeclaredType receiverType = - analysis.getTypeFactory().getAnnotatedType(methodTree).getReceiverType(); - if (receiverType != null) { - return atypeFactory.isUnclassified(receiverType) || atypeFactory.isFree(receiverType); - } else { - // There is no receiver e.g. in static methods. - return false; - } + this.atypeFactory = analysis.getTypeFactory(); } /** * 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); @@ -97,24 +74,24 @@ protected List initializedFieldsAfterCall( String methodString = tree.getMethodSelect().toString(); // Case 1: After a call to the constructor of the same class, all - // invariant fields are guaranteed to be initialized. - if (isConstructor && receiver instanceof ThisLiteralNode && methodString.equals("this")) { - ClassTree clazz = TreeUtils.enclosingClass(analysis.getTypeFactory().getPath(tree)); + // fields are guaranteed to be initialized. + if (isConstructor && receiver instanceof ThisNode && methodString.equals("this")) { + ClassTree clazz = TreePathUtil.enclosingClass(analysis.getTypeFactory().getPath(tree)); TypeElement clazzElem = TreeUtils.elementFromDeclaration(clazz); - markInvariantFieldsAsInitialized(result, clazzElem); + markFieldsAsInitialized(result, clazzElem); } // Case 4: After a call to the constructor of the super class, all - // invariant fields of any super class are guaranteed to be initialized. - if (isConstructor && receiver instanceof ThisLiteralNode && methodString.equals("super")) { - ClassTree clazz = TreeUtils.enclosingClass(analysis.getTypeFactory().getPath(tree)); + // fields of any super class are guaranteed to be initialized. + if (isConstructor && receiver instanceof ThisNode && methodString.equals("super")) { + ClassTree clazz = TreePathUtil.enclosingClass(analysis.getTypeFactory().getPath(tree)); TypeElement clazzElem = TreeUtils.elementFromDeclaration(clazz); TypeMirror superClass = clazzElem.getSuperclass(); while (superClass != null && superClass.getKind() != TypeKind.NONE) { clazzElem = (TypeElement) analysis.getTypes().asElement(superClass); superClass = clazzElem.getSuperclass(); - markInvariantFieldsAsInitialized(result, clazzElem); + markFieldsAsInitialized(result, clazzElem); } } @@ -122,81 +99,51 @@ protected List initializedFieldsAfterCall( } /** - * Adds all the fields of the class {@code clazzElem} that have the 'invariant annotation' to - * the set of initialized fields {@code result}. + * Adds all the fields of the class {@code clazzElem} to the list of initialized fields {@code + * result}. + * + * @param result the list of initialized fields + * @param clazzElem the class whose fields to add */ - protected void markInvariantFieldsAsInitialized( - List result, TypeElement clazzElem) { + protected void markFieldsAsInitialized(List result, TypeElement clazzElem) { List fields = ElementFilter.fieldsIn(clazzElem.getEnclosedElements()); for (VariableElement field : fields) { if (((Symbol) field).type.tsym.completer != Symbol.Completer.NULL_COMPLETER || ((Symbol) field).type.getKind() == TypeKind.ERROR) { - // If the type is not completed yet, we might run - // into trouble. Skip the field. + // If the type is not completed yet, we might run into trouble. Skip the field. // TODO: is there a nicer solution? - // This was raised by Issue 244. + // This was raised by Issue #244. continue; } - AnnotatedTypeMirror fieldType = atypeFactory.getAnnotatedType(field); - if (atypeFactory.hasFieldInvariantAnnotation(fieldType, field)) { - result.add(field); - } + result.add(field); } } @Override - public TransferResult visitAssignment(AssignmentNode n, TransferInput in) { - TransferResult result = super.visitAssignment(n, in); - assert result instanceof RegularTransferResult; - Receiver expr = FlowExpressions.internalReprOf(analysis.getTypeFactory(), n.getTarget()); - - // If this is an assignment to a field of 'this', then mark the field as - // initialized. - if (!expr.containsUnknown()) { - if (expr instanceof FieldAccess) { - FieldAccess fa = (FieldAccess) expr; - result.getRegularStore().addInitializedField(fa); - } - } - return result; - } + public TransferResult visitAssignment( + AssignmentNode n, TransferInput in) { + TransferResult result = super.visitAssignment(n, in); + JavaExpression lhs = JavaExpression.fromNode(n.getTarget()); - /** - * If an invariant field is initialized and has the invariant annotation, than it has at least - * the invariant annotation. Note that only fields of the 'this' receiver are tracked for - * initialization. - */ - @Override - public TransferResult visitFieldAccess(FieldAccessNode n, TransferInput p) { - TransferResult result = super.visitFieldAccess(n, p); - assert !result.containsTwoStores(); - S store = result.getRegularStore(); - if (store.isFieldInitialized(n.getElement()) - && n.getReceiver() instanceof ThisLiteralNode) { - AnnotatedTypeMirror fieldAnno = - analysis.getTypeFactory().getAnnotatedType(n.getElement()); - // Only if the field has the type system's invariant annotation, - // such as @NonNull. - if (fieldAnno.hasAnnotation(atypeFactory.getFieldInvariantAnnotation())) { - AnnotationMirror inv = atypeFactory.getFieldInvariantAnnotation(); - V oldResultValue = result.getResultValue(); - V refinedResultValue = - analysis.createSingleAnnotationValue( - inv, oldResultValue.getUnderlyingType()); - V newResultValue = refinedResultValue.mostSpecific(oldResultValue, null); - result.setResultValue(newResultValue); - } + // If this is an assignment to a field of 'this', then mark the field as initialized. + if (!lhs.containsUnknown() && lhs instanceof FieldAccess) { + FieldAccess fa = (FieldAccess) lhs; + // Only a ternary expression may cause a conditional transfer result, e.g. + // condExpr#num0 = (obj instanceof List) + // In such cases, the LHS is never a FieldAccess, so we can assert that result + // is a regular transfer result. This is important because otherwise the + // addInitializedField would be called on a temporary, merged store. + assert result instanceof RegularTransferResult; + result.getRegularStore().addInitializedField(fa); } return result; } @Override - public TransferResult visitMethodInvocation( - MethodInvocationNode n, TransferInput in) { - TransferResult result = super.visitMethodInvocation(n, in); - assert result instanceof ConditionalTransferResult; - List newlyInitializedFields = - initializedFieldsAfterCall(n, (ConditionalTransferResult) result); + public TransferResult visitMethodInvocation( + MethodInvocationNode n, TransferInput in) { + TransferResult result = super.visitMethodInvocation(n, in); + 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 44c370f48fd6..037c703fbf0a 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/initialization/InitializationVisitor.java @@ -1,6 +1,8 @@ package org.checkerframework.checker.initialization; +import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.BlockTree; +import com.sun.source.tree.CatchTree; import com.sun.source.tree.ClassTree; import com.sun.source.tree.CompilationUnitTree; import com.sun.source.tree.ExpressionTree; @@ -8,61 +10,49 @@ import com.sun.source.tree.MethodTree; import com.sun.source.tree.NewClassTree; import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; -import com.sun.source.tree.TypeCastTree; import com.sun.source.tree.VariableTree; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -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 com.sun.source.util.TreePath; + import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.nullness.NullnessChecker; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; -import org.checkerframework.dataflow.analysis.FlowExpressions.ClassName; -import org.checkerframework.dataflow.analysis.FlowExpressions.FieldAccess; -import org.checkerframework.dataflow.analysis.FlowExpressions.LocalVariable; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; -import org.checkerframework.dataflow.analysis.FlowExpressions.ThisReference; +import org.checkerframework.framework.flow.CFAbstractAnalysis.FieldInitialValue; import org.checkerframework.framework.flow.CFAbstractStore; -import org.checkerframework.framework.flow.CFAbstractValue; +import org.checkerframework.framework.flow.CFValue; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.framework.util.AnnotationFormatter; -import org.checkerframework.framework.util.DefaultAnnotationFormatter; +import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; +import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; -import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreeUtils; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.VariableElement; + +/* NO-AFU + import org.checkerframework.common.wholeprograminference.WholeProgramInference; +*/ + /** * The visitor for the freedom-before-commitment type-system. The freedom-before-commitment * type-system and this class are abstract and need to be combined with another type-system whose * safe initialization should be tracked. For an example, see the {@link NullnessChecker}. */ -public class InitializationVisitor< - Factory extends InitializationAnnotatedTypeFactory, - Value extends CFAbstractValue, - Store extends InitializationStore> - extends BaseTypeVisitor { - - protected final AnnotationFormatter annoFormatter; +public class InitializationVisitor extends BaseTypeVisitor { // Error message keys - private static final @CompilerMessageKey String COMMITMENT_INVALID_CAST = - "initialization.invalid.cast"; - private static final @CompilerMessageKey String COMMITMENT_FIELDS_UNINITIALIZED = - "initialization.fields.uninitialized"; - private static final @CompilerMessageKey String COMMITMENT_STATIC_FIELDS_UNINITIALIZED = - "initialization.static.fields.uninitialized"; private static final @CompilerMessageKey String COMMITMENT_INVALID_FIELD_TYPE = "initialization.invalid.field.type"; private static final @CompilerMessageKey String COMMITMENT_INVALID_CONSTRUCTOR_RETURN_TYPE = @@ -73,17 +63,40 @@ public class InitializationVisitor< private static final @CompilerMessageKey String COMMITMENT_INVALID_FIELD_WRITE_INITIALIZED = "initialization.invalid.field.write.initialized"; + /** List of fields in the current compilation unit that have been initialized. */ + protected final List initializedFields; + + /** The value of the assumeInitialized option. */ + protected final boolean assumeInitialized; + + /** + * Create an InitializationVisitor. + * + * @param checker the initialization checker + */ public InitializationVisitor(BaseTypeChecker checker) { super(checker); - annoFormatter = new DefaultAnnotationFormatter(); initializedFields = new ArrayList<>(); + assumeInitialized = checker.hasOption("assumeInitialized"); + } + + @Override + protected InitializationAnnotatedTypeFactory createTypeFactory() { + return new InitializationAnnotatedTypeFactory(checker); + } + + @Override + public void visit(TreePath path) { + // This visitor does nothing if init checking is turned off. + if (!assumeInitialized) { + super.visit(path); + } } @Override public void setRoot(CompilationUnitTree root) { // Clean up the cache of initialized fields once per compilation unit. - // Alternatively, but harder to determine, this could be done once per - // top-level class. + // Alternatively, but harder to determine, this could be done once per top-level class. initializedFields.clear(); super.setRoot(root); } @@ -91,8 +104,7 @@ public void setRoot(CompilationUnitTree root) { @Override protected void checkConstructorInvocation( AnnotatedDeclaredType dt, AnnotatedExecutableType constructor, NewClassTree src) { - // receiver annotations for constructors are forbidden, therefore no - // check is necessary + // Receiver annotations for constructors are forbidden, therefore no check is necessary. // TODO: nested constructors can have receivers! } @@ -109,310 +121,289 @@ protected void checkThisOrSuperConstructorCall( } @Override - protected void commonAssignmentCheck( - Tree varTree, ExpressionTree valueExp, @CompilerMessageKey String errorKey) { + protected boolean commonAssignmentCheck( + 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 - // MemberSelectTree + // cast is safe: a field access can only be an IdentifierTree or MemberSelectTree ExpressionTree lhs = (ExpressionTree) varTree; ExpressionTree y = valueExp; - Element el = TreeUtils.elementFromUse(lhs); + VariableElement el = TreeUtils.variableElementFromUse(lhs); AnnotatedTypeMirror xType = atypeFactory.getReceiverType(lhs); AnnotatedTypeMirror yType = atypeFactory.getAnnotatedType(y); // the special FBC rules do not apply if there is an explicit // UnknownInitialization annotation - Set fieldAnnotations = - atypeFactory.getAnnotatedType(TreeUtils.elementFromUse(lhs)).getAnnotations(); + AnnotationMirrorSet fieldAnnotations = + atypeFactory.getAnnotatedType(el).getAnnotations(); if (!AnnotationUtils.containsSameByName( fieldAnnotations, atypeFactory.UNKNOWN_INITIALIZATION)) { if (!ElementUtils.isStatic(el) - && !(atypeFactory.isCommitted(yType) - || atypeFactory.isFree(xType) + && !(atypeFactory.isInitialized(yType) + || atypeFactory.isUnderInitialization(xType) || atypeFactory.isFbcBottom(yType))) { @CompilerMessageKey String err; - if (atypeFactory.isCommitted(xType)) { + if (atypeFactory.isInitialized(xType)) { err = COMMITMENT_INVALID_FIELD_WRITE_INITIALIZED; } else { err = COMMITMENT_INVALID_FIELD_WRITE_UNKNOWN_INITIALIZATION; } checker.reportError(varTree, err, varTree); - return; // prevent issuing another errow about subtyping - } - } - } - super.commonAssignmentCheck(varTree, valueExp, errorKey); - } - - @Override - public Void visitVariable(VariableTree node, Void p) { - // is this a field (and not a local variable)? - if (TreeUtils.elementFromDeclaration(node).getKind().isField()) { - Set annotationMirrors = - atypeFactory.getAnnotatedType(node).getExplicitAnnotations(); - // Fields cannot have commitment annotations. - for (Class c : atypeFactory.getInitializationAnnotations()) { - for (AnnotationMirror a : annotationMirrors) { - if (atypeFactory.isUnclassified(a)) { - continue; // unclassified is allowed - } - if (atypeFactory.areSameByClass(a, c)) { - checker.reportError(node, COMMITMENT_INVALID_FIELD_TYPE, node); - break; - } - } - } - } - return super.visitVariable(node, p); - } - - @Override - protected boolean checkContract( - Receiver expr, - AnnotationMirror necessaryAnnotation, - AnnotationMirror inferredAnnotation, - CFAbstractStore store) { - // also use the information about initialized fields to check contracts - final AnnotationMirror invariantAnno = atypeFactory.getFieldInvariantAnnotation(); - - if (!atypeFactory.getQualifierHierarchy().isSubtype(invariantAnno, necessaryAnnotation) - || !(expr instanceof FieldAccess)) { - return super.checkContract(expr, necessaryAnnotation, inferredAnnotation, store); - } - - FieldAccess fa = (FieldAccess) expr; - if (fa.getReceiver() instanceof ThisReference || fa.getReceiver() instanceof ClassName) { - @SuppressWarnings("unchecked") - Store s = (Store) store; - if (s.isFieldInitialized(fa.getField())) { - AnnotatedTypeMirror fieldType = atypeFactory.getAnnotatedType(fa.getField()); - // is this an invariant-field? - if (AnnotationUtils.containsSame(fieldType.getAnnotations(), invariantAnno)) { - return true; + return false; // prevent issuing another error about subtyping } } - } else { - Set recvAnnoSet; - @SuppressWarnings("unchecked") - Value value = (Value) store.getValue(fa.getReceiver()); - - if (value != null) { - recvAnnoSet = value.getAnnotations(); - } else if (fa.getReceiver() instanceof LocalVariable) { - Element elem = ((LocalVariable) fa.getReceiver()).getElement(); - AnnotatedTypeMirror recvType = atypeFactory.getAnnotatedType(elem); - recvAnnoSet = recvType.getAnnotations(); - } else { - // Is there anything better we could do? - return false; - } - - boolean isRecvCommitted = false; - for (AnnotationMirror anno : recvAnnoSet) { - if (atypeFactory.isCommitted(anno)) { - isRecvCommitted = true; - } - } - - AnnotatedTypeMirror fieldType = atypeFactory.getAnnotatedType(fa.getField()); - // The receiver is fully initialized and the field type - // has the invariant type. - if (isRecvCommitted - && AnnotationUtils.containsSame(fieldType.getAnnotations(), invariantAnno)) { - return true; - } } - return super.checkContract(expr, necessaryAnnotation, inferredAnnotation, store); + return super.commonAssignmentCheck(varTree, valueExp, errorKey, extraArgs); } @Override - public Void visitTypeCast(TypeCastTree node, Void p) { - AnnotatedTypeMirror exprType = atypeFactory.getAnnotatedType(node.getExpression()); - AnnotatedTypeMirror castType = atypeFactory.getAnnotatedType(node); - AnnotationMirror exprAnno = null, castAnno = null; - - // find commitment annotation - for (Class a : atypeFactory.getInitializationAnnotations()) { - if (castType.hasAnnotation(a)) { - assert castAnno == null; - castAnno = castType.getAnnotation(a); - } - if (exprType.hasAnnotation(a)) { - assert exprAnno == null; - exprAnno = exprType.getAnnotation(a); - } - } - - // TODO: this is most certainly unsafe!! (and may be hiding some problems) - // If we don't find a commitment annotation, then we just assume that - // the subtyping is alright. - // The case that has come up is with wildcards not getting a type for - // some reason, even though the default is @Initialized. - boolean isSubtype; - if (exprAnno == null || castAnno == null) { - isSubtype = true; - } else { - assert exprAnno != null && castAnno != null; - isSubtype = atypeFactory.getQualifierHierarchy().isSubtype(exprAnno, castAnno); - } - - if (!isSubtype) { - checker.reportError( - node, - COMMITMENT_INVALID_CAST, - annoFormatter.formatAnnotationMirror(exprAnno), - annoFormatter.formatAnnotationMirror(castAnno)); - return p; // suppress cast.unsafe warning - } - - return super.visitTypeCast(node, p); + protected void checkExceptionParameter(CatchTree node) { + // TODO Issue 363 + // https://github.com/eisop/checker-framework/issues/363 } - protected final List initializedFields; - @Override - public void processClassTree(ClassTree node) { + public void processClassTree(ClassTree tree) { // go through all members and look for initializers. // save all fields that are initialized and do not report errors about // them later when checking constructors. - for (Tree member : node.getMembers()) { + for (Tree member : tree.getMembers()) { if (member.getKind() == Tree.Kind.BLOCK && !((BlockTree) member).isStatic()) { BlockTree block = (BlockTree) member; - Store store = atypeFactory.getRegularExitStore(block); + InitializationStore store = atypeFactory.getRegularExitStore(block); // Add field values for fields with an initializer. - for (Pair t : store.getAnalysis().getFieldValues()) { - store.addInitializedField(t.first); + for (FieldInitialValue fieldInitialValue : + store.getAnalysis().getFieldInitialValues()) { + if (fieldInitialValue.initializer != null) { + store.addInitializedField(fieldInitialValue.fieldDecl.getField()); + } } - final List init = - atypeFactory.getInitializedInvariantFields(store, getCurrentPath()); + List init = + atypeFactory.getInitializedFields(store, getCurrentPath()); initializedFields.addAll(init); } } - super.processClassTree(node); + super.processClassTree(tree); // Warn about uninitialized static fields. - if (node.getKind() == Kind.CLASS) { - boolean isStatic = true; + Tree.Kind nodeKind = tree.getKind(); + // Skip interfaces (and annotations, which are interfaces). In an interface, every static + // field must be initialized. Java forbids uninitialized variables and static initializer + // blocks. + if (nodeKind != Tree.Kind.INTERFACE && nodeKind != Tree.Kind.ANNOTATION_TYPE) { // See GenericAnnotatedTypeFactory.performFlowAnalysis for why we use // the regular exit store of the class here. - Store store = atypeFactory.getRegularExitStore(node); - // Add field values for fields with an initializer. - for (Pair t : store.getAnalysis().getFieldValues()) { - store.addInitializedField(t.first); + InitializationStore store = atypeFactory.getRegularExitStore(tree); + if (store != null) { + // Add field values for fields with an initializer. + for (FieldInitialValue fieldInitialValue : + store.getAnalysis().getFieldInitialValues()) { + if (fieldInitialValue.initializer != null) { + store.addInitializedField(fieldInitialValue.fieldDecl.getField()); + } + } } + List receiverAnnotations = Collections.emptyList(); - checkFieldsInitialized(node, isStatic, store, receiverAnnotations); + checkFieldsInitialized(tree, true, store, receiverAnnotations); } } @Override - public Void visitMethod(MethodTree node, Void p) { - if (TreeUtils.isConstructor(node)) { + public Void visitMethod(MethodTree tree, Void p) { + if (TreeUtils.isConstructor(tree)) { Collection returnTypeAnnotations = - AnnotationUtils.getExplicitAnnotationsOnConstructorResult(node); + AnnotationUtils.getExplicitAnnotationsOnConstructorResult(tree); // check for invalid constructor return type - for (Class c : - atypeFactory.getInvalidConstructorReturnTypeAnnotations()) { + for (Class c : atypeFactory.getSupportedTypeQualifiers()) { for (AnnotationMirror a : returnTypeAnnotations) { if (atypeFactory.areSameByClass(a, c)) { - checker.reportError(node, COMMITMENT_INVALID_CONSTRUCTOR_RETURN_TYPE, node); + checker.reportError(tree, COMMITMENT_INVALID_CONSTRUCTOR_RETURN_TYPE, tree); break; } } } - // Check that all fields have been initialized at the end of the - // constructor. + // Check that all fields have been initialized at the end of the constructor. boolean isStatic = false; - Store store = atypeFactory.getRegularExitStore(node); - List receiverAnnotations = getAllReceiverAnnotations(node); - checkFieldsInitialized(node, isStatic, store, receiverAnnotations); + + InitializationStore store = atypeFactory.getRegularExitStore(tree); + List receiverAnnotations = getAllReceiverAnnotations(tree); + checkFieldsInitialized(tree, isStatic, store, receiverAnnotations); } - return super.visitMethod(node, p); + return super.visitMethod(tree, p); } - /** Returns the full list of annotations on the receiver. */ - private List getAllReceiverAnnotations(MethodTree node) { + /** + * The assignment/variable/method invocation tree currently being checked. + * + *

    In the case that the right-hand side is an object, this is used by {@link + * #reportCommonAssignmentError(AnnotatedTypeMirror, AnnotatedTypeMirror, Tree, String, + * Object...)} to get the correct store value for the right-hand side's fields and check whether + * they are initialized according to the target checker. + */ + protected Tree commonAssignmentTree; + + @Override + public Void visitVariable(VariableTree tree, Void p) { + Tree oldCommonAssignmentTree = commonAssignmentTree; + commonAssignmentTree = tree; + // is this a field (and not a local variable)? + if (TreeUtils.elementFromDeclaration(tree).getKind().isField()) { + Set annotationMirrors = + atypeFactory.getAnnotatedType(tree).getExplicitAnnotations(); + // Fields cannot have commitment annotations. + for (Class c : atypeFactory.getSupportedTypeQualifiers()) { + for (AnnotationMirror a : annotationMirrors) { + if (atypeFactory.isUnknownInitialization(a)) { + continue; // unknown initialization is allowed + } + if (atypeFactory.areSameByClass(a, c)) { + checker.reportError(tree, COMMITMENT_INVALID_FIELD_TYPE, tree); + break; + } + } + } + } + super.visitVariable(tree, p); + commonAssignmentTree = oldCommonAssignmentTree; + return null; + } + + @Override + public Void visitAssignment(AssignmentTree node, Void p) { + Tree oldCommonAssignmentTree = commonAssignmentTree; + commonAssignmentTree = node; + super.visitAssignment(node, p); + commonAssignmentTree = oldCommonAssignmentTree; + return null; + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree node, Void p) { + Tree oldCommonAssignmentTree = commonAssignmentTree; + commonAssignmentTree = node; + super.visitMethodInvocation(node, p); + commonAssignmentTree = oldCommonAssignmentTree; + return null; + } + + /** + * Returns the full list of annotations on the receiver. + * + * @param tree a method declaration + * @return all the annotations on the method's receiver + */ + private List getAllReceiverAnnotations(MethodTree tree) { // TODO: get access to a Types instance and use it to get receiver type // Or, extend ExecutableElement with such a method. // Note that we cannot use the receiver type from AnnotatedExecutableType, because that // would only have the nullness annotations; here we want to see all annotations on the // receiver. - List rcvannos = null; - if (TreeUtils.isConstructor(node)) { + if (TreeUtils.isConstructor(tree)) { com.sun.tools.javac.code.Symbol meth = - (com.sun.tools.javac.code.Symbol) TreeUtils.elementFromDeclaration(node); - rcvannos = meth.getRawTypeAttributes(); - if (rcvannos == null) { - rcvannos = Collections.emptyList(); - } + (com.sun.tools.javac.code.Symbol) TreeUtils.elementFromDeclaration(tree); + return meth.getRawTypeAttributes(); } - return rcvannos; + return Collections.emptyList(); } /** - * Checks that all fields (all static fields if {@code staticFields} is true) are initialized in - * the given store. + * Checks that all fields (all static fields if {@code staticFields} is true) are initialized at + * the end of a given constructor or static class initializer. + * + * @param tree a {@link ClassTree} if {@code staticFields} is true; a {@link MethodTree} for a + * constructor if {@code staticFields} is false. This is where errors are reported, if they + * are not reported at the fields themselves + * @param staticFields whether to check static fields or instance fields + * @param initExitStore the initialization exit store for the constructor or static initializer + * @param receiverAnnotations the annotations on the receiver */ - // TODO: the code for checking if fields are initialized should be re-written, - // as the current version contains quite a few ugly parts, is hard to understand, - // and it is likely that it does not take full advantage of the information - // about initialization we compute in - // GenericAnnotatedTypeFactory.initializationStaticStore and - // GenericAnnotatedTypeFactory.initializationStore. protected void checkFieldsInitialized( - Tree blockNode, + Tree tree, boolean staticFields, - Store store, + InitializationStore initExitStore, List receiverAnnotations) { - // If the store is null, then the constructor cannot terminate - // successfully - if (store == null) { + // If the store is null, then the constructor cannot terminate successfully + if (initExitStore == null) { return; } - String COMMITMENT_FIELDS_UNINITIALIZED_KEY = - (staticFields - ? COMMITMENT_STATIC_FIELDS_UNINITIALIZED - : COMMITMENT_FIELDS_UNINITIALIZED); - - List violatingFields = - atypeFactory.getUninitializedInvariantFields( - store, getCurrentPath(), staticFields, receiverAnnotations); - - if (staticFields) { - // TODO: Why is nothing done for static fields? - // Do we need the following? - // violatingFields.removeAll(store.initializedFields); - } else { - // remove fields that have already been initialized by an - // initializer block - violatingFields.removeAll(initializedFields); + // Compact canonical record constructors do not generate visible assignments in the source, + // but by definition they assign to all the record's fields so we don't need to + // check for uninitialized fields in them: + if (tree.getKind() == Tree.Kind.METHOD + && TreeUtils.isCompactCanonicalRecordConstructor((MethodTree) tree)) { + return; } - // Remove fields with a relevant @SuppressWarnings annotation. - Iterator itor = violatingFields.iterator(); - while (itor.hasNext()) { - VariableTree f = itor.next(); - Element e = TreeUtils.elementFromTree(f); - if (checker.shouldSuppressWarnings(e, COMMITMENT_FIELDS_UNINITIALIZED_KEY)) { - itor.remove(); + GenericAnnotatedTypeFactory targetFactory = + checker.getTypeFactoryOfSubcheckerOrNull( + ((InitializationChecker) checker).getTargetCheckerClass()); + // The target checker's store corresponding to initExitStore + CFAbstractStore targetExitStore = targetFactory.getRegularExitStore(tree); + List uninitializedFields = + atypeFactory.getUninitializedFields( + initExitStore, + targetExitStore, + getCurrentPath(), + staticFields, + receiverAnnotations); + uninitializedFields.removeAll(initializedFields); + + // If we are checking initialization of a class's static fields or of a default constructor, + // we issue an error for every uninitialized field at the respective field declaration. + // If we are checking a non-default constructor, we issue a single error at the constructor + // declaration. + boolean errorAtField = staticFields || TreeUtils.isSynthetic((MethodTree) tree); + + String errorMsg = + (staticFields + ? "initialization.static.field.uninitialized" + : errorAtField + ? "initialization.field.uninitialized" + : "initialization.fields.uninitialized"); + + // Remove fields with a relevant @SuppressWarnings annotation + uninitializedFields.removeIf( + f -> checker.shouldSuppressWarnings(TreeUtils.elementFromDeclaration(f), errorMsg)); + + if (!uninitializedFields.isEmpty()) { + if (errorAtField) { + // Issue each error at the relevant field + for (VariableTree f : uninitializedFields) { + checker.reportError(f, errorMsg, f.getName()); + } + } else { + // Issue all the errors at the relevant constructor + StringJoiner fieldsString = new StringJoiner(", "); + for (VariableTree f : uninitializedFields) { + fieldsString.add(f.getName()); + } + checker.reportError(tree, errorMsg, fieldsString); } } - if (!violatingFields.isEmpty()) { - StringBuilder fieldsString = new StringBuilder(); - boolean first = true; - for (VariableTree f : violatingFields) { - if (!first) { - fieldsString.append(", "); - } - first = false; - fieldsString.append(f.getName()); - } - checker.reportError(blockNode, COMMITMENT_FIELDS_UNINITIALIZED_KEY, fieldsString); + /* NO-AFU + // Support -Ainfer command-line argument. + WholeProgramInference wpi = atypeFactory.getWholeProgramInference(); + if (wpi != null) { + // For each uninitialized field, treat it as if the default value is assigned to it. + List uninitFields = new ArrayList<>(violatingFields); + uninitFields.addAll(nonviolatingFields); + for (VariableTree fieldTree : uninitFields) { + Element elt = TreeUtils.elementFromDeclaration(fieldTree); + wpi.updateFieldFromType( + fieldTree, + elt, + fieldTree.getName().toString(), + atypeFactory.getDefaultValueAnnotatedType(elt.asType())); + } } + */ } } diff --git a/checker/src/main/java/org/checkerframework/checker/initialization/messages.properties b/checker/src/main/java/org/checkerframework/checker/initialization/messages.properties index 04c924c4412d..89a7cd980ded 100644 --- a/checker/src/main/java/org/checkerframework/checker/initialization/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/initialization/messages.properties @@ -3,6 +3,6 @@ initialization.invalid.field.write.unknown=storing values that are possibly unde initialization.invalid.field.write.in.constructor=storing possibly-uninitialized values that come from outside the constructor is not permitted initialization.invalid.constructor.return.type=the newly created object in a constructor must be @UnderInitialization initialization.invalid.field.type=initialization annotations are not allowed on fields, except for @UnknownInitialization -initialization.invalid.cast=cast between initialization types %s and %s is not allowed +initialization.field.uninitialized=the default constructor does not initialize field %s initialization.fields.uninitialized=the constructor does not initialize fields: %s -initialization.static.fields.uninitialized=static fields not initialized: %s +initialization.static.field.uninitialized=static field %s not initialized 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 ffd7f193972d..06da92f8c2ed 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/InterningAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/interning/InterningAnnotatedTypeFactory.java @@ -2,18 +2,15 @@ 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; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.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; @@ -29,11 +26,18 @@ import org.checkerframework.framework.type.typeannotator.DefaultQualifierForUseTypeAnnotator; import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; import org.checkerframework.framework.type.typeannotator.TypeAnnotator; -import org.checkerframework.framework.util.AnnotationMirrorSet; import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + /** * An {@link AnnotatedTypeFactory} that accounts for the properties of the Interned type system. * This type factory will add the {@link Interned} annotation to a type if the input: @@ -47,19 +51,34 @@ *

  • 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 {@code @}{@link + * FindDistinct} declaration annotation. (TODO: That isn't a good implementation, because it is not + * accurate: the value might be equals() to some other Java value. More seriously, it permits too + * much. Writing {@code @FindDistinct} should permit equality tests on the given formal parameter, + * but should not (for example) permit the formal parameter to be assigned into an + * {@code @InternedDistinct} location.) + * + *

    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 { /** The {@link UnknownInterned} annotation. */ 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); + + /** A set containing just {@link #INTERNED}. */ + final AnnotationMirrorSet INTERNED_SET = AnnotationMirrorSet.singleton(INTERNED); + /** * Creates a new {@link InterningAnnotatedTypeFactory} that operates on a particular AST. * @@ -69,7 +88,7 @@ public InterningAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); // If you update the following, also update ../../../../../docs/manual/interning-checker.tex - addAliasedAnnotation("com.sun.istack.internal.Interned", INTERNED); + addAliasedTypeAnnotation("com.sun.istack.internal.Interned", INTERNED); this.postInit(); } @@ -98,11 +117,12 @@ public Void visitExecutable(AnnotatedExecutableType type, Void p) { // Annotate method returns, not constructors. scan(type.getReturnType(), p); } - if (type.getReceiverType() != null + AnnotatedTypeMirror receiverType = type.getReceiverType(); + if (receiverType != null // Intern method may be called on UnknownInterned object, so its receiver should // not be annotated as @Interned. - && typeFactory.getDeclAnnotation(methodElt, InternMethod.class) == null) { - scanAndReduce(type.getReceiverType(), p, null); + && atypeFactory.getDeclAnnotation(methodElt, InternMethod.class) == null) { + scanAndReduce(receiverType, p, null); } scanAndReduce(type.getParameterTypes(), p, null); scanAndReduce(type.getThrownTypes(), p, null); @@ -112,10 +132,10 @@ public Void visitExecutable(AnnotatedExecutableType type, Void p) { } @Override - public Set getTypeDeclarationBounds(TypeMirror typeMirror) { + public AnnotationMirrorSet getTypeDeclarationBounds(TypeMirror typeMirror) { if (typeMirror.getKind() == TypeKind.DECLARED && ((DeclaredType) typeMirror).asElement().getKind() == ElementKind.ENUM) { - return AnnotationMirrorSet.singleElementSet(INTERNED); + return INTERNED_SET; } return super.getTypeDeclarationBounds(typeMirror); } @@ -130,18 +150,10 @@ protected TypeAnnotator createTypeAnnotator() { return new ListTypeAnnotator(new InterningTypeAnnotator(this), super.createTypeAnnotator()); } - @Override - public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, boolean useFlow) { - Element element = TreeUtils.elementFromTree(tree); - if (!type.isAnnotatedInHierarchy(INTERNED) && ElementUtils.isCompileTimeConstant(element)) { - type.addAnnotation(INTERNED); - } - super.addComputedTypeAnnotations(tree, type, useFlow); - } - @Override public void addComputedTypeAnnotations(Element element, AnnotatedTypeMirror type) { - if (!type.isAnnotatedInHierarchy(INTERNED) && ElementUtils.isCompileTimeConstant(element)) { + if (!type.hasAnnotationInHierarchy(INTERNED) + && ElementUtils.isCompileTimeConstant(element)) { type.addAnnotation(INTERNED); } super.addComputedTypeAnnotations(element, type); @@ -155,35 +167,45 @@ private class InterningTreeAnnotator extends TreeAnnotator { } @Override - public Void visitBinary(BinaryTree node, AnnotatedTypeMirror type) { - if (TreeUtils.isCompileTimeString(node)) { + public Void visitBinary(BinaryTree tree, AnnotatedTypeMirror type) { + if (TreeUtils.isCompileTimeString(tree)) { type.replaceAnnotation(INTERNED); - } else if (TreeUtils.isStringConcatenation(node)) { + } else if (TreeUtils.isStringConcatenation(tree)) { type.replaceAnnotation(TOP); } else if (type.getKind().isPrimitive() - || node.getKind() == Tree.Kind.EQUAL_TO - || node.getKind() == Tree.Kind.NOT_EQUAL_TO) { + || tree.getKind() == Tree.Kind.EQUAL_TO + || tree.getKind() == Tree.Kind.NOT_EQUAL_TO) { type.replaceAnnotation(INTERNED); } else { type.replaceAnnotation(TOP); } - return super.visitBinary(node, type); + return super.visitBinary(tree, type); } /* Compound assignments never result in an interned result. */ @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMirror type) { + public Void visitCompoundAssignment(CompoundAssignmentTree tree, AnnotatedTypeMirror type) { type.replaceAnnotation(TOP); - return super.visitCompoundAssignment(node, type); + return super.visitCompoundAssignment(tree, type); } @Override - public Void visitTypeCast(TypeCastTree node, AnnotatedTypeMirror type) { - if (TreeUtils.typeOf(node.getType()).getKind().isPrimitive()) { + public Void visitTypeCast(TypeCastTree tree, AnnotatedTypeMirror type) { + if (TreeUtils.typeOf(tree.getType()).getKind().isPrimitive()) { type.replaceAnnotation(INTERNED); } - return super.visitTypeCast(node, type); + return super.visitTypeCast(tree, type); + } + + @Override + public Void visitIdentifier(IdentifierTree tree, AnnotatedTypeMirror type) { + Element e = TreeUtils.elementFromUse(tree); + if (atypeFactory.getDeclAnnotation(e, FindDistinct.class) != null) { + // TODO: See note above about this being a poor implementation. + type.replaceAnnotation(INTERNED_DISTINCT); + } + return super.visitIdentifier(tree, type); } } diff --git a/checker/src/main/java/org/checkerframework/checker/interning/InterningChecker.java b/checker/src/main/java/org/checkerframework/checker/interning/InterningChecker.java index 063200c6a5f1..428fed8e423f 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/InterningChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/interning/InterningChecker.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.interning; -import javax.annotation.processing.SupportedOptions; import org.checkerframework.checker.interning.qual.Interned; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.qual.StubFiles; import org.checkerframework.framework.source.SupportedLintOptions; +import javax.annotation.processing.SupportedOptions; + /** * A type-checker plug-in for the {@link Interned} qualifier that finds (and verifies the absence * of) equality-testing and interning errors. 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 eb6285e2001b..eff70f42a864 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/interning/InterningVisitor.java @@ -17,23 +17,16 @@ import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; -import java.util.Comparator; -import java.util.List; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import 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.initialization.qual.UnknownInitialization; +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; import org.checkerframework.checker.interning.qual.UsesObjectEquals; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.signature.qual.CanonicalName; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeFactory.ParameterizedExecutableType; @@ -41,10 +34,26 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.framework.util.Heuristics; import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; +import java.util.Comparator; +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.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; + /** * Typechecks source code for interning violations. A type is considered interned if its primary * annotation is {@link Interned} or {@link InternedDistinct}. This visitor reports errors or @@ -52,7 +61,7 @@ * *

      *
    1. either argument to a "==" or "!=" comparison is not Interned (error - * "not.interned"). As a special case, the comparison is permitted if either arugment is + * "not.interned"). As a special case, the comparison is permitted if either argument is * InternedDistinct. *
    2. the receiver and argument for a call to an equals method are both Interned * (optional warning "unnecessary.equals") @@ -64,15 +73,17 @@ public final class InterningVisitor extends BaseTypeVisitorWhat the Interning + * Returns true if interning should be verified for the input expression. By default, all + * classes are checked for interning unless {@code -Acheckclass} is specified. + * + * @return true if interning should be verified for the input expression + * @see What the Interning * Checker checks */ private boolean shouldCheckExpression(ExpressionTree tree) { @@ -101,20 +114,20 @@ private boolean shouldCheckExpression(ExpressionTree tree) { /** Checks comparison operators, == and !=, for INTERNING violations. */ @Override - public Void visitBinary(BinaryTree node, Void p) { + public Void visitBinary(BinaryTree tree, Void p) { // No checking unless the operator is "==" or "!=". - if (!(node.getKind() == Tree.Kind.EQUAL_TO || node.getKind() == Tree.Kind.NOT_EQUAL_TO)) { - return super.visitBinary(node, p); + if (!(tree.getKind() == Tree.Kind.EQUAL_TO || tree.getKind() == Tree.Kind.NOT_EQUAL_TO)) { + return super.visitBinary(tree, p); } - ExpressionTree leftOp = node.getLeftOperand(); - ExpressionTree rightOp = node.getRightOperand(); + ExpressionTree leftOp = tree.getLeftOperand(); + ExpressionTree rightOp = tree.getRightOperand(); // Check passes if either arg is null. if (leftOp.getKind() == Tree.Kind.NULL_LITERAL || rightOp.getKind() == Tree.Kind.NULL_LITERAL) { - return super.visitBinary(node, p); + return super.visitBinary(tree, p); } AnnotatedTypeMirror left = atypeFactory.getAnnotatedType(leftOp); @@ -122,12 +135,12 @@ public Void visitBinary(BinaryTree node, Void p) { // If either argument is a primitive, check passes due to auto-unboxing if (left.getKind().isPrimitive() || right.getKind().isPrimitive()) { - return super.visitBinary(node, p); + return super.visitBinary(tree, p); } if (left.hasEffectiveAnnotation(INTERNED_DISTINCT) || right.hasEffectiveAnnotation(INTERNED_DISTINCT)) { - return super.visitBinary(node, p); + return super.visitBinary(tree, p); } // If shouldCheckExpression returns true for either the LHS or RHS, @@ -157,46 +170,41 @@ public Void visitBinary(BinaryTree node, Void p) { // with the interning check. if (!shouldCheckExpression(leftOp) && !shouldCheckExpression(rightOp)) { - return super.visitBinary(node, p); + return super.visitBinary(tree, p); } // Syntactic checks for legal uses of == - if (suppressInsideComparison(node)) { - return super.visitBinary(node, p); + if (suppressInsideComparison(tree)) { + return super.visitBinary(tree, p); } - if (suppressEarlyEquals(node)) { - return super.visitBinary(node, p); + if (suppressEarlyEquals(tree)) { + return super.visitBinary(tree, p); } - if (suppressEarlyCompareTo(node)) { - return super.visitBinary(node, p); + if (suppressEarlyCompareTo(tree)) { + return super.visitBinary(tree, p); } if (suppressEqualsIfClassIsAnnotated(left, right)) { - return super.visitBinary(node, p); + return super.visitBinary(tree, 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))) { - checker.reportError(leftOp, "not.interned", left); + || (leftElt != null + && atypeFactory.getDeclAnnotation(leftElt, UsesObjectEquals.class) + != null))) { + checker.reportError(leftOp, "not.interned"); } + + Element rightElt = TypesUtils.getTypeElement(right.getUnderlyingType()); if (!(right.hasEffectiveAnnotation(INTERNED) - || (rightElt != null && rightElt.getAnnotation(UsesObjectEquals.class) != null))) { - checker.reportError(rightOp, "not.interned", right); + || (rightElt != null + && atypeFactory.getDeclAnnotation(rightElt, UsesObjectEquals.class) + != null))) { + checker.reportError(rightOp, "not.interned"); } - return super.visitBinary(node, p); + return super.visitBinary(tree, p); } /** @@ -204,19 +212,50 @@ public Void visitBinary(BinaryTree node, Void p) { * equality is safe. */ @Override - public Void visitMethodInvocation(MethodInvocationTree node, Void p) { - if (isInvocationOfEquals(node)) { - AnnotatedTypeMirror recv = atypeFactory.getReceiverType(node); - AnnotatedTypeMirror comp = atypeFactory.getAnnotatedType(node.getArguments().get(0)); + public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { + if (isInvocationOfEquals(tree)) { + AnnotatedTypeMirror receiverType = atypeFactory.getReceiverType(tree); + assert receiverType != null : "@AssumeAssertion(nullness)"; + AnnotatedTypeMirror comp = atypeFactory.getAnnotatedType(tree.getArguments().get(0)); if (this.checker.getLintOption("dotequals", true) - && recv.hasEffectiveAnnotation(INTERNED) + && receiverType.hasEffectiveAnnotation(INTERNED) && comp.hasEffectiveAnnotation(INTERNED)) { - checker.reportWarning(node, "unnecessary.equals"); + checker.reportWarning(tree, "unnecessary.equals"); } } - return super.visitMethodInvocation(node, p); + return super.visitMethodInvocation(tree, p); + } + + // Ensure that method annotations are not written on methods they don't apply to. + @Override + public Void visitMethod(MethodTree tree, Void p) { + ExecutableElement methElt = TreeUtils.elementFromDeclaration(tree); + 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( + tree, + "invalid.method.annotation", + "@CompareToMethod", + "1 or 2", + methElt, + params); + } else if (hasEqualsMethodAnno && !(params == 1 || params == 2)) { + checker.reportError( + tree, "invalid.method.annotation", "@EqualsMethod", "1 or 2", methElt, params); + } else if (hasInternMethodAnno && !(params == 0)) { + checker.reportError( + tree, "invalid.method.annotation", "@InternMethod", "0", methElt, params); + } + + return super.visitMethod(tree, p); } /** @@ -224,8 +263,9 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { * 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 +286,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 +312,40 @@ 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 == paramName) + || (leftName == paramName && rightName.contentEquals("this"))) { + return true; + } + } + } + } + } + return false; + } + @Override protected void checkConstructorResult( AnnotatedExecutableType constructorType, ExecutableElement constructorElement) { @@ -286,7 +365,7 @@ public boolean validateTypeOf(Tree tree) { } else if (tree.getKind() == Tree.Kind.NEW_CLASS) { NewClassTree newClassTree = (NewClassTree) tree; TypeMirror typeMirror = TreeUtils.typeOf(newClassTree); - Set bounds = atypeFactory.getTypeDeclarationBounds(typeMirror); + AnnotationMirrorSet bounds = atypeFactory.getTypeDeclarationBounds(typeMirror); // Don't issue an invalid type warning for creations of objects of interned classes; // instead, issue an interned.object.creation if required. if (atypeFactory.containsSameByClass(bounds, Interned.class)) { @@ -305,7 +384,7 @@ public boolean validateTypeOf(Tree tree) { * * @param newInternedObject call to a constructor of an interned class * @param constructor declared type of the constructor - * @return false unless {@code newInternedObject} is immediately interned. + * @return false unless {@code newInternedObject} is immediately interned */ private boolean checkCreationOfInternedObject( NewClassTree newInternedObject, AnnotatedExecutableType constructor) { @@ -340,19 +419,24 @@ private boolean checkCreationOfInternedObject( // Helper methods // ********************************************************************** - /** Returns true if a class overrides Object.equals. */ - private boolean overridesEquals(ClassTree node) { - List members = node.getMembers(); + /** + * Returns the method that overrides Object.equals, or null. + * + * @param tree a class + * @return the class's implementation of equals, or null + */ + private @Nullable MethodTree equalsImplementation(ClassTree tree) { + List members = tree.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; } /** @@ -362,11 +446,11 @@ private boolean overridesEquals(ClassTree node) { * common idiom of writing an equals method with a non-Object parameter, in addition to the * equals method that overrides {@link Object#equals(Object)}. * - * @param node a method invocation node - * @return true iff {@code node} is a invocation of {@code equals()} + * @param tree a method invocation tree + * @return true iff {@code tree} is a invocation of {@code equals()} */ - private boolean isInvocationOfEquals(MethodInvocationTree node) { - ExecutableElement method = TreeUtils.elementFromUse(node); + public static boolean isInvocationOfEquals(MethodInvocationTree tree) { + ExecutableElement method = TreeUtils.elementFromUse(tree); return (method.getParameters().size() == 1 && method.getReturnType().getKind() == TypeKind.BOOLEAN // method symbols only have simple names @@ -389,69 +473,76 @@ private boolean isInvocationOfEquals(MethodInvocationTree node) { * statement returns zero, and the comparison tests "this" against the method's parameter *
    * - * @param node the comparison to check + * @param binaryTree the comparison to check * @return true if one of the supported heuristics is matched, false otherwise */ // TODO: handle != comparisons too! // TODO: handle more methods, such as early return from addAll when this == arg - private boolean suppressInsideComparison(final BinaryTree node) { + private boolean suppressInsideComparison(BinaryTree binaryTree) { // Only handle == binary trees - if (node.getKind() != Tree.Kind.EQUAL_TO) { + if (binaryTree.getKind() != Tree.Kind.EQUAL_TO) { return false; } - Tree left = node.getLeftOperand(); - Tree right = node.getRightOperand(); + ExpressionTree left = binaryTree.getLeftOperand(); + ExpressionTree right = binaryTree.getRightOperand(); // Only valid if we're comparing identifiers. if (!(left.getKind() == Tree.Kind.IDENTIFIER && right.getKind() == Tree.Kind.IDENTIFIER)) { 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 candidateTree; + while ((candidateTree = ppath.getLeaf()) != null) { + if (candidateTree.getKind() == Tree.Kind.IF) { + ifStatementTree = candidateTree; + } else if (candidateTree.getKind() == Tree.Kind.METHOD) { + methodTree = (MethodTree) candidateTree; + 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 = - TreeUtils.elementFromDeclaration(visitorState.getMethodTree()); - assert enclosingMethod != null; + ExecutableElement enclosingMethod = TreeUtils.elementFromDeclaration(methodTree); - final Element lhs = TreeUtils.elementFromUse((IdentifierTree) left); - final Element rhs = TreeUtils.elementFromUse((IdentifierTree) right); + Element lhs = TreeUtils.elementFromUse((IdentifierTree) left); + Element rhs = TreeUtils.elementFromUse((IdentifierTree) right); // Matcher to check for if statement that returns zero Heuristics.Matcher matcherIfReturnsZero = @@ -479,10 +570,17 @@ 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")) { - final boolean returnsZero = + if (overrides(enclosingMethod, Comparator.class, "compare") + || (hasCompareToMethodAnno && params == 2)) { + boolean returnsZero = new Heuristics.Within(new Heuristics.OfKind(Tree.Kind.IF, matcherIfReturnsZero)) .match(getCurrentPath()); @@ -490,22 +588,29 @@ 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)); - final boolean returnsZero = + } else if (overrides(enclosingMethod, Comparable.class, "compareTo") + || (hasCompareToMethodAnno && params == 1)) { + + boolean returnsZero = new Heuristics.Within(new Heuristics.OfKind(Tree.Kind.IF, matcherIfReturnsZero)) .match(getCurrentPath()); @@ -513,36 +618,15 @@ 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; - } - /** - * Returns true if two expressions originating from the same scope are identical, i.e. they are - * syntactically represented in the same way (modulo parentheses) and represent the same value. - * - *

    For example, given an expression (a == b) || a.equals(b) sameTree can be called to - * determine that the first 'a' and second 'a' refer to the same variable, which is the case - * since both expressions 'a' originate from the same scope. - * - *

    If the expression includes one or more method calls, assumes the method calls are - * deterministic. - * - * @param expr1 the first expression to compare - * @param expr2 the second expression to compare - expr2 must originate from the same scope as - * expr1 - * @return true if the expressions expr1 and expr2 are identical - */ - private static boolean sameTree(ExpressionTree expr1, ExpressionTree expr2) { - return TreeUtils.withoutParens(expr1) - .toString() - .equals(TreeUtils.withoutParens(expr2).toString()); + return false; } /** @@ -554,19 +638,20 @@ private static boolean sameTree(ExpressionTree expr1, ExpressionTree expr2) { * (a == b) || (a != null && a.equals(b)) * } * - * Returns true iff the given node fits this pattern. + * Returns true iff the given tree fits this pattern. * - * @return true iff the node fits a pattern such as (a == b || a.equals(b)) + * @param topBinaryTree the binary operation to check + * @return true iff the tree fits a pattern such as (a == b || a.equals(b)) */ - private boolean suppressEarlyEquals(final BinaryTree node) { + private boolean suppressEarlyEquals(BinaryTree topBinaryTree) { // Only handle == binary trees - if (node.getKind() != Tree.Kind.EQUAL_TO) { + if (topBinaryTree.getKind() != Tree.Kind.EQUAL_TO) { return false; } // should strip parens - final ExpressionTree left = TreeUtils.withoutParens(node.getLeftOperand()); - final ExpressionTree right = TreeUtils.withoutParens(node.getRightOperand()); + ExpressionTree left = TreeUtils.withoutParens(topBinaryTree.getLeftOperand()); + ExpressionTree right = TreeUtils.withoutParens(topBinaryTree.getRightOperand()); // looking for ((a == b || a.equals(b)) Heuristics.Matcher matcherEqOrEquals = @@ -581,10 +666,12 @@ private boolean isNeqNull( } ExpressionTree neqLeft = ((BinaryTree) e).getLeftOperand(); ExpressionTree neqRight = ((BinaryTree) e).getRightOperand(); - return (((sameTree(neqLeft, e1) || sameTree(neqLeft, e2)) + return (((TreeUtils.sameTree(neqLeft, e1) + || TreeUtils.sameTree(neqLeft, e2)) && neqRight.getKind() == Tree.Kind.NULL_LITERAL) // also check for "null != e1" and "null != e2" - || ((sameTree(neqRight, e1) || sameTree(neqRight, e2)) + || ((TreeUtils.sameTree(neqRight, e1) + || TreeUtils.sameTree(neqRight, e2)) && neqLeft.getKind() == Tree.Kind.NULL_LITERAL)); } @@ -594,7 +681,7 @@ public Boolean visitBinary(BinaryTree tree, Void p) { ExpressionTree rightTree = tree.getRightOperand(); if (tree.getKind() == Tree.Kind.CONDITIONAL_OR) { - if (sameTree(leftTree, node)) { + if (TreeUtils.sameTree(leftTree, topBinaryTree)) { // left is "a==b" // check right, which should be a.equals(b) or b.equals(a) or // similar @@ -659,10 +746,10 @@ public Boolean visitMethodInvocation(MethodInvocationTree tree, Void p) { // return false; // } - if (sameTree(receiver, left) && sameTree(arg, right)) { + if (TreeUtils.sameTree(receiver, left) && TreeUtils.sameTree(arg, right)) { return true; } - if (sameTree(receiver, right) && sameTree(arg, left)) { + if (TreeUtils.sameTree(receiver, right) && TreeUtils.sameTree(arg, left)) { return true; } @@ -679,26 +766,27 @@ public Boolean visitMethodInvocation(MethodInvocationTree tree, Void p) { /** * Pattern matches to prevent false positives of the form {@code (a == b || a.compareTo(b) == - * 0)}. Returns true iff the given node fits this pattern. + * 0)}. Returns true iff the given tree fits this pattern. * - * @return true iff the node fits the pattern (a == b || a.compareTo(b) == 0) + * @param topBinaryTree the binary operation to check + * @return true iff the tree fits the pattern (a == b || a.compareTo(b) == 0) */ - private boolean suppressEarlyCompareTo(final BinaryTree node) { + private boolean suppressEarlyCompareTo(BinaryTree topBinaryTree) { // Only handle == binary trees - if (node.getKind() != Tree.Kind.EQUAL_TO) { + if (topBinaryTree.getKind() != Tree.Kind.EQUAL_TO) { return false; } - Tree left = TreeUtils.withoutParens(node.getLeftOperand()); - Tree right = TreeUtils.withoutParens(node.getRightOperand()); + ExpressionTree left = TreeUtils.withoutParens(topBinaryTree.getLeftOperand()); + ExpressionTree right = TreeUtils.withoutParens(topBinaryTree.getRightOperand()); // Only valid if we're comparing identifiers. if (!(left.getKind() == Tree.Kind.IDENTIFIER && right.getKind() == Tree.Kind.IDENTIFIER)) { return false; } - final Element lhs = TreeUtils.elementFromUse((IdentifierTree) left); - final Element rhs = TreeUtils.elementFromUse((IdentifierTree) right); + Element lhs = TreeUtils.elementFromUse((IdentifierTree) left); + Element rhs = TreeUtils.elementFromUse((IdentifierTree) right); // looking for ((a == b || a.compareTo(b) == 0) Heuristics.Matcher matcherEqOrCompareTo = @@ -723,11 +811,14 @@ 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 - if (leftTree != node) { + if (leftTree != topBinaryTree) { return false; } if (rightTree.getKind() != Tree.Kind.EQUAL_TO) { @@ -810,7 +901,7 @@ private boolean classIsAnnotated(AnnotatedTypeMirror type) { if (tm.getKind() != TypeKind.DECLARED) { checker.message( - Kind.WARNING, + Diagnostic.Kind.WARNING, "InterningVisitor.classIsAnnotated: tm = %s (%s)", tm, tm.getClass()); @@ -818,13 +909,13 @@ private boolean classIsAnnotated(AnnotatedTypeMirror type) { Element classElt = ((DeclaredType) tm).asElement(); if (classElt == null) { checker.message( - Kind.WARNING, + Diagnostic.Kind.WARNING, "InterningVisitor.classIsAnnotated: classElt = null for tm = %s (%s)", tm, tm.getClass()); } if (classElt != null) { - Set bound = atypeFactory.getTypeDeclarationBounds(tm); + AnnotationMirrorSet bound = atypeFactory.getTypeDeclarationBounds(tm); return atypeFactory.containsSameByClass(bound, Interned.class); } return false; @@ -837,7 +928,7 @@ private boolean classIsAnnotated(AnnotatedTypeMirror type) { * @param scope the scope to search for the element corresponding to "this" in * @return the element corresponding to "this" in the given scope, or null if not found */ - private Element getThis(Scope scope) { + private @Nullable Element getThis(Scope scope) { for (Element e : scope.getLocalElements()) { if (e.getSimpleName().contentEquals("this")) { return e; @@ -847,7 +938,7 @@ private Element getThis(Scope scope) { } /** - * Determines whether or not the given element overrides the named method in the named class. + * Returns true if the given element overrides the named method in the named class. * * @param e an element for a method * @param clazz the class @@ -871,9 +962,15 @@ private boolean overrides(ExecutableElement e, Class clazz, String method) { return false; } - /** @see #typeToCheck */ - DeclaredType typeToCheck() { - String className = checker.getOption("checkclass"); + /** + * Returns the type to check. + * + * @return the type to check + */ + private @Nullable DeclaredType typeToCheck( + @UnknownInitialization(BaseTypeVisitor.class) InterningVisitor this) { + @SuppressWarnings("signature:assignment.type.incompatible") // user input + @CanonicalName String className = checker.getOption("checkclass"); if (className == null) { return null; } diff --git a/checker/src/main/java/org/checkerframework/checker/interning/javax-lang-model-element-name.astub b/checker/src/main/java/org/checkerframework/checker/interning/javax-lang-model-element-name.astub new file mode 100644 index 000000000000..68292315692f --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/interning/javax-lang-model-element-name.astub @@ -0,0 +1,13 @@ +// Name is interned within one instance of the Java compiler, but may differ between different +// instances. Use this stub file if your program interacts with only one instance of the compiler +// and doesn't make its own Name objects. + +package javax.lang.model.element; + +import org.checkerframework.checker.interning.qual.Interned; + +public @Interned interface Name extends java.lang.CharSequence {} + +package com.sun.tools.javac.util; + +public @Interned class Name implements javax.lang.model.element.Name {} diff --git a/checker/src/main/java/org/checkerframework/checker/interning/jdk8.astub b/checker/src/main/java/org/checkerframework/checker/interning/jdk8.astub index f4d8b0397529..b4011245a852 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/jdk8.astub +++ b/checker/src/main/java/org/checkerframework/checker/interning/jdk8.astub @@ -2,6 +2,9 @@ package java.util; +import org.checkerframework.checker.interning.qual.UsesObjectEquals; +import org.checkerframework.checker.interning.qual.Interned; + @UsesObjectEquals class XMLUtils {} /* 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 723998428959..b99a909b5bec 100644 --- a/checker/src/main/java/org/checkerframework/checker/interning/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/interning/messages.properties @@ -1,7 +1,8 @@ ### Error/warning messages for the Interning Checker -not.interned=attempting to use a non-@Interned comparison operand%nfound: %s +not.interned=attempting to use a non-@Interned comparison operand unnecessary.equals=use of .equals can be safely replaced by ==/!= 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/lock/LockAnalysis.java b/checker/src/main/java/org/checkerframework/checker/lock/LockAnalysis.java index 8c0a70728623..91bc9ac9cfc8 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/LockAnalysis.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/LockAnalysis.java @@ -1,15 +1,12 @@ package org.checkerframework.checker.lock; -import java.util.List; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.TypeMirror; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFStore; import org.checkerframework.framework.flow.CFValue; -import org.checkerframework.javacutil.Pair; +import org.checkerframework.javacutil.AnnotationMirrorSet; + +import javax.lang.model.type.TypeMirror; /** * The analysis class for the lock type system. @@ -19,11 +16,14 @@ */ public class LockAnalysis extends CFAbstractAnalysis { - public LockAnalysis( - BaseTypeChecker checker, - LockAnnotatedTypeFactory factory, - List> fieldValues) { - super(checker, factory, fieldValues); + /** + * Creates a new {@link LockAnalysis}. + * + * @param checker the checker + * @param factory the factory + */ + public LockAnalysis(BaseTypeChecker checker, LockAnnotatedTypeFactory factory) { + super(checker, factory); } @Override @@ -42,8 +42,7 @@ public LockStore createCopiedStore(LockStore s) { } @Override - public CFValue createAbstractValue( - Set annotations, TypeMirror underlyingType) { + public CFValue createAbstractValue(AnnotationMirrorSet annotations, TypeMirror underlyingType) { return defaultCreateAbstractValue(this, annotations, underlyingType); } } diff --git a/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java index a3c43ba142b3..c855abfce41e 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/LockAnnotatedTypeFactory.java @@ -3,23 +3,10 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.VariableTree; -import com.sun.source.util.TreePath; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.AnnotationValue; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.VariableElement; + +import org.checkerframework.checker.lock.qual.EnsuresLockHeld; +import org.checkerframework.checker.lock.qual.EnsuresLockHeldIf; import org.checkerframework.checker.lock.qual.GuardSatisfied; import org.checkerframework.checker.lock.qual.GuardedBy; import org.checkerframework.checker.lock.qual.GuardedByBottom; @@ -28,16 +15,18 @@ import org.checkerframework.checker.lock.qual.LockPossiblyHeld; import org.checkerframework.checker.lock.qual.LockingFree; import org.checkerframework.checker.lock.qual.MayReleaseLocks; +import org.checkerframework.checker.lock.qual.NewObject; import org.checkerframework.checker.lock.qual.ReleasesNoLocks; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.signature.qual.ClassGetName; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.ClassName; -import org.checkerframework.dataflow.analysis.FlowExpressions.FieldAccess; -import org.checkerframework.dataflow.analysis.FlowExpressions.LocalVariable; -import org.checkerframework.dataflow.analysis.FlowExpressions.MethodCall; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; -import org.checkerframework.dataflow.analysis.FlowExpressions.ThisReference; +import org.checkerframework.dataflow.expression.ClassName; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.LocalVariable; +import org.checkerframework.dataflow.expression.MethodCall; +import org.checkerframework.dataflow.expression.ThisReference; +import org.checkerframework.dataflow.expression.Unknown; import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.dataflow.qual.SideEffectFree; import org.checkerframework.dataflow.util.PurityUtils; @@ -46,21 +35,37 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; +import org.checkerframework.framework.type.MostlyNoElementQualifierHierarchy; import org.checkerframework.framework.type.QualifierHierarchy; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.framework.util.AnnotatedTypes; -import org.checkerframework.framework.util.FlowExpressionParseUtil; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionContext; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory; +import org.checkerframework.framework.util.QualifierKind; import org.checkerframework.framework.util.dependenttypes.DependentTypesError; import org.checkerframework.framework.util.dependenttypes.DependentTypesHelper; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.ElementUtils; -import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; +import org.plumelib.util.CollectionsPlume; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.util.Elements; /** * LockAnnotatedTypeFactory builds types with @LockHeld and @LockPossiblyHeld annotations. LockHeld @@ -83,45 +88,61 @@ public class LockAnnotatedTypeFactory /** The @{@link LockHeld} annotation. */ protected final AnnotationMirror LOCKHELD = AnnotationBuilder.fromClass(elements, LockHeld.class); + /** The @{@link LockPossiblyHeld} annotation. */ protected final AnnotationMirror LOCKPOSSIBLYHELD = AnnotationBuilder.fromClass(elements, LockPossiblyHeld.class); + /** The @{@link SideEffectFree} annotation. */ protected final AnnotationMirror SIDEEFFECTFREE = AnnotationBuilder.fromClass(elements, SideEffectFree.class); + /** The @{@link GuardedByUnknown} annotation. */ protected final AnnotationMirror GUARDEDBYUNKNOWN = AnnotationBuilder.fromClass(elements, GuardedByUnknown.class); - /** The @{@link GuardedByBottom} annotation. */ + + /** The @{@link GuardedBy} annotation. */ protected final AnnotationMirror GUARDEDBY = createGuardedByAnnotationMirror(new ArrayList()); + + /** The @{@link NewObject} annotation. */ + protected final AnnotationMirror NEWOBJECT = + AnnotationBuilder.fromClass(elements, NewObject.class); + /** The @{@link GuardedByBottom} annotation. */ protected final AnnotationMirror GUARDEDBYBOTTOM = AnnotationBuilder.fromClass(elements, GuardedByBottom.class); + /** The @{@link GuardSatisfied} annotation. */ protected final AnnotationMirror GUARDSATISFIED = AnnotationBuilder.fromClass(elements, GuardSatisfied.class); + /** The value() element/field of a @GuardedBy annotation. */ + protected final ExecutableElement guardedByValueElement = + TreeUtils.getMethod(GuardedBy.class, "value", 0, processingEnv); + + /** The value() element/field of a @GuardSatisfied annotation. */ + protected final ExecutableElement guardSatisfiedValueElement = + TreeUtils.getMethod(GuardSatisfied.class, "value", 0, processingEnv); + + /** The EnsuresLockHeld.value element/field. */ + protected final ExecutableElement ensuresLockHeldValueElement = + TreeUtils.getMethod(EnsuresLockHeld.class, "value", 0, processingEnv); + + /** The EnsuresLockHeldIf.expression element/field. */ + protected final ExecutableElement ensuresLockHeldIfExpressionElement = + TreeUtils.getMethod(EnsuresLockHeldIf.class, "expression", 0, processingEnv); + /** The net.jcip.annotations.GuardedBy annotation, or null if not on the classpath. */ - protected final Class jcipGuardedBy; + protected final @Nullable Class jcipGuardedBy; /** The javax.annotation.concurrent.GuardedBy annotation, or null if not on the classpath. */ - protected final Class javaxGuardedBy; + protected final @Nullable Class javaxGuardedBy; /** Create a new LockAnnotatedTypeFactory. */ public LockAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker, true); - // This alias is only true for the Lock Checker. All other checkers must - // ignore the @LockingFree annotation. - addAliasedDeclAnnotation(LockingFree.class, SideEffectFree.class, SIDEEFFECTFREE); - - // This alias is only true for the Lock Checker. All other checkers must - // ignore the @ReleasesNoLocks annotation. Note that ReleasesNoLocks is - // not truly side-effect-free even as far as the Lock Checker is concerned, - // so there is additional handling of this annotation in the Lock Checker. - addAliasedDeclAnnotation(ReleasesNoLocks.class, SideEffectFree.class, SIDEEFFECTFREE); - jcipGuardedBy = classForNameOrNull("net.jcip.annotations.GuardedBy"); javaxGuardedBy = classForNameOrNull("javax.annotation.concurrent.GuardedBy"); @@ -132,11 +153,11 @@ public LockAnnotatedTypeFactory(BaseTypeChecker checker) { /** * Returns the value of Class.forName, or null if Class.forName would throw an exception. * - * @param annotationClassName an annotation's fully-qualified name + * @param annotationClassName an annotation's name, in ClassGetName format * @return an annotation class or null */ @SuppressWarnings("unchecked") // cast to generic type - private Class classForNameOrNull( + private @Nullable Class classForNameOrNull( @ClassGetName String annotationClassName) { try { return (Class) Class.forName(annotationClassName); @@ -151,8 +172,8 @@ protected DependentTypesHelper createDependentTypesHelper() { @Override protected void reportErrors(Tree errorTree, List errors) { // If the error message is NOT_EFFECTIVELY_FINAL, then report - // lock.expression.not.final instead of expression.unparsable.type.invalid . - List superErrors = new ArrayList<>(); + // "lock.expression.not.final" instead of "expression.unparsable.type.invalid". + List superErrors = new ArrayList<>(errors.size()); for (DependentTypesError error : errors) { if (error.error.equals(NOT_EFFECTIVELY_FINAL)) { checker.reportError( @@ -165,37 +186,22 @@ protected void reportErrors(Tree errorTree, List errors) { } @Override - protected String standardizeString( - String expression, - FlowExpressionContext context, - TreePath localScope, - boolean useLocalScope) { - if (DependentTypesError.isExpressionError(expression)) { - return expression; - } + protected boolean shouldPassThroughExpression(String expression) { + // There is no expression to use to replace here, so just pass the expression + // along. + return super.shouldPassThroughExpression(expression) + || LockVisitor.SELF_RECEIVER_PATTERN.matcher(expression).matches(); + } - // Adds logic to parse expression, which only the Lock Checker uses. - if (LockVisitor.SELF_RECEIVER_PATTERN.matcher(expression).matches()) { - return expression; + @Override + protected @Nullable JavaExpression transform(JavaExpression javaExpr) { + if (javaExpr instanceof Unknown || isExpressionEffectivelyFinal(javaExpr)) { + return javaExpr; } - try { - FlowExpressions.Receiver result = - FlowExpressionParseUtil.parse( - expression, context, localScope, useLocalScope); - if (result == null) { - return new DependentTypesError(expression, " ").toString(); - } - if (!isExpressionEffectivelyFinal(result)) { - // If the expression isn't effectively final, then return the - // NOT_EFFECTIVELY_FINAL error string. - return new DependentTypesError(expression, NOT_EFFECTIVELY_FINAL) - .toString(); - } - return result.toString(); - } catch (FlowExpressionParseUtil.FlowExpressionParseException e) { - return new DependentTypesError(expression, e).toString(); - } + // If the expression isn't effectively final, then return the NOT_EFFECTIVELY_FINAL + // error string. + return createError(javaExpr.toString(), NOT_EFFECTIVELY_FINAL); } }; } @@ -218,18 +224,18 @@ protected String standardizeString( * @param expr expression * @return whether or not the expression is effectively final */ - boolean isExpressionEffectivelyFinal(Receiver expr) { + boolean isExpressionEffectivelyFinal(JavaExpression expr) { if (expr instanceof FieldAccess) { FieldAccess fieldAccess = (FieldAccess) expr; - Receiver recv = fieldAccess.getReceiver(); + JavaExpression receiver = fieldAccess.getReceiver(); // Don't call fieldAccess - return fieldAccess.isFinal() && isExpressionEffectivelyFinal(recv); + return fieldAccess.isFinal() && isExpressionEffectivelyFinal(receiver); } else if (expr instanceof LocalVariable) { return ElementUtils.isEffectivelyFinal(((LocalVariable) expr).getElement()); } else if (expr instanceof MethodCall) { MethodCall methodCall = (MethodCall) expr; - for (Receiver param : methodCall.getParameters()) { - if (!isExpressionEffectivelyFinal(param)) { + for (JavaExpression arg : methodCall.getArguments()) { + if (!isExpressionEffectivelyFinal(arg)) { return false; } } @@ -253,17 +259,18 @@ protected Set> createSupportedTypeQualifiers() { GuardedBy.class, GuardedByUnknown.class, GuardSatisfied.class, + NewObject.class, GuardedByBottom.class)); } @Override - public QualifierHierarchy createQualifierHierarchy(MultiGraphFactory factory) { - return new LockQualifierHierarchy(factory); + protected QualifierHierarchy createQualifierHierarchy() { + return new LockQualifierHierarchy(getSupportedTypeQualifiers(), elements); } @Override - protected LockAnalysis createFlowAnalysis(List> fieldValues) { - return new LockAnalysis(checker, this, fieldValues); + protected LockAnalysis createFlowAnalysis() { + return new LockAnalysis(checker, this); } @Override @@ -272,137 +279,142 @@ public LockTransfer createFlowTransferFunction( return new LockTransfer((LockAnalysis) analysis, (LockChecker) this.checker); } - class LockQualifierHierarchy extends MultiGraphQualifierHierarchy { + /** LockQualifierHierarchy. */ + class LockQualifierHierarchy extends MostlyNoElementQualifierHierarchy { - public LockQualifierHierarchy(MultiGraphFactory f) { - super(f, LOCKHELD); - } + /** Qualifier kind for the @{@link GuardedByUnknown} annotation. */ + private final QualifierKind GUARDEDBYUNKNOWN_KIND; - boolean isGuardedBy(AnnotationMirror am) { - return AnnotationUtils.areSameByName(am, GUARDEDBY); - } + /** Qualifier kind for the @{@link GuardedBy} annotation. */ + private final QualifierKind GUARDEDBY_KIND; - boolean isGuardSatisfied(AnnotationMirror am) { - return AnnotationUtils.areSameByName(am, GUARDSATISFIED); - } + /** Qualifier kind for the @{@link GuardSatisfied} annotation. */ + private final QualifierKind GUARDSATISFIED_KIND; - @Override - public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { + /** Qualifier kind for the @{@link NewObject} annotation. */ + private final QualifierKind NEWOBJECT_KIND; - boolean lhsIsGuardedBy = isGuardedBy(superAnno); - boolean rhsIsGuardedBy = isGuardedBy(subAnno); + /** Qualifier kind for the @{@link GuardedByBottom} annotation. */ + private final QualifierKind GUARDEDBYBOTTOM_KIND; - if (lhsIsGuardedBy && rhsIsGuardedBy) { - // Two @GuardedBy annotations are considered subtypes of each other if and only if - // their values match exactly. + /** + * Creates a LockQualifierHierarchy. + * + * @param qualifierClasses classes of annotations that are the qualifiers for this hierarchy + * @param elements element utils + */ + public LockQualifierHierarchy( + Collection> qualifierClasses, Elements elements) { + super(qualifierClasses, elements, LockAnnotatedTypeFactory.this); + GUARDEDBYUNKNOWN_KIND = getQualifierKind(GUARDEDBYUNKNOWN); + GUARDEDBY_KIND = getQualifierKind(GUARDEDBY); + GUARDSATISFIED_KIND = getQualifierKind(GUARDSATISFIED); + NEWOBJECT_KIND = getQualifierKind(NEWOBJECT); + GUARDEDBYBOTTOM_KIND = getQualifierKind(GUARDEDBYBOTTOM); + } - List lhsValues = + @Override + protected boolean isSubtypeWithElements( + AnnotationMirror subAnno, + QualifierKind subKind, + AnnotationMirror superAnno, + QualifierKind superKind) { + if (subKind == GUARDEDBY_KIND && superKind == GUARDEDBY_KIND) { + List subLocks = AnnotationUtils.getElementValueArray( - superAnno, "value", String.class, true); - List rhsValues = - AnnotationUtils.getElementValueArray(subAnno, "value", String.class, true); - - return rhsValues.containsAll(lhsValues) && lhsValues.containsAll(rhsValues); - } - - boolean lhsIsGuardSatisfied = isGuardSatisfied(superAnno); - boolean rhsIsGuardSatisfied = isGuardSatisfied(subAnno); - - if (lhsIsGuardSatisfied && rhsIsGuardSatisfied) { - // There are cases in which two expressions with identical @GuardSatisfied(...) - // annotations are not - // assignable. Those are handled elsewhere. - - // Two expressions with @GuardSatisfied annotations (without an index) are sometimes - // not assignable. - // For example, two method actual parameters with @GuardSatisfied annotations are - // assumed to refer to different guards. - - // This is largely handled in methodFromUse and in - // LockVisitor.visitMethodInvocation. - // Related behavior is handled in LockVisitor.visitMethod (issuing an error if a - // non-constructor method definition has a return type of @GuardSatisfied without an - // index). - - // Two expressions with @GuardSatisfied() annotations are assignable when comparing - // a formal receiver to an actual receiver (see - // LockVisitor.skipReceiverSubtypeCheck) or a formal parameter to an actual - // parameter (see LockVisitor.commonAssignmentCheck for the details on this rule). - + superAnno, + guardedByValueElement, + String.class, + Collections.emptyList()); + List superLocks = + AnnotationUtils.getElementValueArray( + subAnno, + guardedByValueElement, + String.class, + Collections.emptyList()); + return subLocks.containsAll(superLocks) && superLocks.containsAll(subLocks); + } else if (subKind == GUARDSATISFIED_KIND && superKind == GUARDSATISFIED_KIND) { return AnnotationUtils.areSame(superAnno, subAnno); } - - // Remove values from @GuardedBy annotations for further subtype checking. Remove - // indices from @GuardSatisfied annotations. - - if (lhsIsGuardedBy) { - superAnno = GUARDEDBY; - } else if (lhsIsGuardSatisfied) { - superAnno = GUARDSATISFIED; - } - - if (rhsIsGuardedBy) { - subAnno = GUARDEDBY; - } else if (rhsIsGuardSatisfied) { - subAnno = GUARDSATISFIED; - } - - return super.isSubtype(subAnno, superAnno); + throw new RuntimeException("Unexpected"); } @Override - public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror a2) { - AnnotationMirror a1top = getTopAnnotation(a1); - AnnotationMirror a2top = getTopAnnotation(a2); - - if (AnnotationUtils.areSame(a1top, LOCKPOSSIBLYHELD) - && AnnotationUtils.areSame(a2top, LOCKPOSSIBLYHELD)) { - return greatestLowerBoundInLockPossiblyHeldHierarchy(a1, a2); - } else if (AnnotationUtils.areSame(a1top, GUARDEDBYUNKNOWN) - && AnnotationUtils.areSame(a2top, GUARDEDBYUNKNOWN)) { - return greatestLowerBoundInGuardedByUnknownHierarchy(a1, a2); - } - - return null; - } - - private AnnotationMirror greatestLowerBoundInGuardedByUnknownHierarchy( - AnnotationMirror a1, AnnotationMirror a2) { - if (AnnotationUtils.areSame(a1, GUARDEDBYUNKNOWN)) { + protected AnnotationMirror leastUpperBoundWithElements( + AnnotationMirror a1, + QualifierKind qualifierKind1, + AnnotationMirror a2, + QualifierKind qualifierKind2, + QualifierKind lubKind) { + if (qualifierKind1 == GUARDEDBY_KIND && qualifierKind2 == GUARDEDBY_KIND) { + List locks1 = + AnnotationUtils.getElementValueArray( + a1, guardedByValueElement, String.class, Collections.emptyList()); + List locks2 = + AnnotationUtils.getElementValueArray( + a2, guardedByValueElement, String.class, Collections.emptyList()); + if (locks1.containsAll(locks2) && locks2.containsAll(locks1)) { + return a1; + } else { + return GUARDEDBYUNKNOWN; + } + } else if (qualifierKind1 == GUARDSATISFIED_KIND + && qualifierKind2 == GUARDSATISFIED_KIND) { + if (AnnotationUtils.areSame(a1, a2)) { + return a1; + } else { + return GUARDEDBYUNKNOWN; + } + } else if (qualifierKind1 == GUARDEDBYBOTTOM_KIND) { return a2; - } - - if (AnnotationUtils.areSame(a2, GUARDEDBYUNKNOWN)) { + } else if (qualifierKind2 == GUARDEDBYBOTTOM_KIND) { + return a1; + } else if (qualifierKind1 == NEWOBJECT_KIND) { + return a2; + } else if (qualifierKind2 == NEWOBJECT_KIND) { return a1; } + throw new TypeSystemError( + "leastUpperBoundWithElements(%s, %s, %s, %s, %s)", + a1, qualifierKind1, a2, qualifierKind2, lubKind); + } - if ((isGuardedBy(a1) && isGuardedBy(a2)) - || (isGuardSatisfied(a1) && isGuardSatisfied(a2))) { - // isSubtype(a1, a2) is symmetrical to isSubtype(a2, a1) since two - // @GuardedBy annotations are considered subtypes of each other - // if and only if their values match exactly, and two @GuardSatisfied - // annotations are considered subtypes of each other if and only if - // their indices match exactly. - - if (isSubtype(a1, a2)) { + // GLB never returns @NewObject unless one of the argumetns is @NewObject; it returns + // @GuardedByBottom instead, to prevent showing users the unexpected @NewObject type. + @Override + protected AnnotationMirror greatestLowerBoundWithElements( + AnnotationMirror a1, + QualifierKind qualifierKind1, + AnnotationMirror a2, + QualifierKind qualifierKind2, + QualifierKind glbKind) { + if (qualifierKind1 == GUARDEDBY_KIND && qualifierKind2 == GUARDEDBY_KIND) { + List locks1 = + AnnotationUtils.getElementValueArray( + a1, guardedByValueElement, String.class, Collections.emptyList()); + List locks2 = + AnnotationUtils.getElementValueArray( + a2, guardedByValueElement, String.class, Collections.emptyList()); + if (locks1.containsAll(locks2) && locks2.containsAll(locks1)) { return a1; + } else { + return GUARDEDBYBOTTOM; } - } - - return GUARDEDBYBOTTOM; - } - - private AnnotationMirror greatestLowerBoundInLockPossiblyHeldHierarchy( - AnnotationMirror a1, AnnotationMirror a2) { - if (AnnotationUtils.areSame(a1, LOCKPOSSIBLYHELD)) { + } else if (qualifierKind1 == GUARDSATISFIED_KIND + && qualifierKind2 == GUARDSATISFIED_KIND) { + if (AnnotationUtils.areSame(a1, a2)) { + return a1; + } else { + return GUARDEDBYBOTTOM; + } + } else if (qualifierKind1 == GUARDEDBYUNKNOWN_KIND) { return a2; - } - - if (AnnotationUtils.areSame(a2, LOCKPOSSIBLYHELD)) { + } else if (qualifierKind2 == GUARDEDBYUNKNOWN_KIND) { return a1; } - - return LOCKHELD; + throw new TypeSystemError( + "greatestLowerBoundWithElements(%s, %s, %s, %s, %s)", + a1, qualifierKind1, a2, qualifierKind2, glbKind); } } @@ -506,72 +518,70 @@ public static SideEffectAnnotation weakest() { * annotation is present, return RELEASESNOLOCKS as the default, and MAYRELEASELOCKS as the * conservative default. * - * @param element the method element + * @param methodElement the method element * @param issueErrorIfMoreThanOnePresent whether to issue an error if more than one side effect * annotation is present on the method + * @return the side effect annotation that is present on the given method */ - // package-private - SideEffectAnnotation methodSideEffectAnnotation( - Element element, boolean issueErrorIfMoreThanOnePresent) { - if (element != null) { - List sideEffectAnnotationPresent = new ArrayList<>(); - for (SideEffectAnnotation sea : SideEffectAnnotation.values()) { - if (getDeclAnnotationNoAliases(element, sea.getAnnotationClass()) != null) { - sideEffectAnnotationPresent.add(sea); - } + /*package-private*/ @Nullable SideEffectAnnotation methodSideEffectAnnotation( + ExecutableElement methodElement, boolean issueErrorIfMoreThanOnePresent) { + if (methodElement == null) { + // When there is not enough information to determine the correct side effect annotation, + // return the weakest one. + return SideEffectAnnotation.weakest(); + } + + Set sideEffectAnnotationPresent = + EnumSet.noneOf(SideEffectAnnotation.class); + for (SideEffectAnnotation sea : SideEffectAnnotation.values()) { + if (getDeclAnnotationNoAliases(methodElement, sea.getAnnotationClass()) != null) { + sideEffectAnnotationPresent.add(sea); } + } - int count = sideEffectAnnotationPresent.size(); + int count = sideEffectAnnotationPresent.size(); - if (count == 0) { - return defaults.applyConservativeDefaults(element) - ? SideEffectAnnotation.MAYRELEASELOCKS - : SideEffectAnnotation.RELEASESNOLOCKS; - } + if (count == 0) { + return defaults.applyConservativeDefaults(methodElement) + ? SideEffectAnnotation.MAYRELEASELOCKS + : SideEffectAnnotation.RELEASESNOLOCKS; + } - if (count > 1 && issueErrorIfMoreThanOnePresent) { - // TODO: Turn on after figuring out how this interacts with inherited annotations. - // checker.reportError(element, "multiple.sideeffect.annotations"); - } + if (count > 1 && issueErrorIfMoreThanOnePresent) { + // TODO: Turn on after figuring out how this interacts with inherited annotations. + // checker.reportError(methodElement, "multiple.sideeffect.annotations"); + } - SideEffectAnnotation weakest = sideEffectAnnotationPresent.get(0); - // At least one side effect annotation was found. Return the weakest. - for (SideEffectAnnotation sea : sideEffectAnnotationPresent) { - if (sea.isWeakerThan(weakest)) { - weakest = sea; - } + SideEffectAnnotation weakest = null; + // At least one side effect annotation was found. Return the weakest. + for (SideEffectAnnotation sea : sideEffectAnnotationPresent) { + if (weakest == null || sea.isWeakerThan(weakest)) { + weakest = sea; } - return weakest; } - - // When there is not enough information to determine the correct side effect annotation, - // return the weakest one. - return SideEffectAnnotation.weakest(); + return weakest; } /** - * Returns the index (that is, the {@code value} element) on the {@code @GuardSatisfied} - * annotation in the given AnnotatedTypeMirror. Assumes atm is non-null and contains a - * {@code @GuardSatisfied} annotation. + * Returns the index (that is, the {@code value} element) on the {@code @}{@link GuardSatisfied} + * annotation in the given AnnotatedTypeMirror. * - * @param atm an AnnotatedTypeMirror containing a GuardSatisfied annotation - * @return the index on the GuardSatisfied annotation + * @param atm an AnnotatedTypeMirror containing a {@link GuardSatisfied} annotation + * @return the index on the {@link GuardSatisfied} annotation */ - // package-private - int getGuardSatisfiedIndex(AnnotatedTypeMirror atm) { + /*package-private*/ int getGuardSatisfiedIndex(AnnotatedTypeMirror atm) { return getGuardSatisfiedIndex(atm.getAnnotation(GuardSatisfied.class)); } /** - * Returns the index (that is, the {@code value} element) on the given {@code @GuardSatisfied} - * annotation. Assumes am is non-null and is a GuardSatisfied annotation. + * Returns the index (that is, the {@code value} element) on the given {@code @}{@link + * GuardSatisfied} annotation. * - * @param am an AnnotationMirror for a GuardSatisfied annotation - * @return the index on the GuardSatisfied annotation + * @param am an AnnotationMirror for a {@link GuardSatisfied} annotation + * @return the index on the {@link GuardSatisfied} annotation */ - // package-private - int getGuardSatisfiedIndex(AnnotationMirror am) { - return AnnotationUtils.getElementValue(am, "value", Integer.class, true); + /*package-private*/ int getGuardSatisfiedIndex(AnnotationMirror am) { + return AnnotationUtils.getElementValueInt(am, guardSatisfiedValueElement, -1); } @Override @@ -579,7 +589,7 @@ public ParameterizedExecutableType methodFromUse( ExpressionTree tree, ExecutableElement methodElt, AnnotatedTypeMirror receiverType) { ParameterizedExecutableType mType = super.methodFromUse(tree, methodElt, receiverType); - if (tree.getKind() != Kind.METHOD_INVOCATION) { + if (tree.getKind() != Tree.Kind.METHOD_INVOCATION) { return mType; } @@ -627,13 +637,12 @@ && replaceAnnotationInGuardedByHierarchyIfGuardSatisfiedIndexMatches( List methodInvocationTreeArguments = ((MethodInvocationTree) tree).getArguments(); - List requiredArgs = - AnnotatedTypes.expandVarArgs(this, invokedMethod, methodInvocationTreeArguments); + List paramTypes = invokedMethod.getParameterTypes(); - for (int i = 0; i < requiredArgs.size(); i++) { + for (int i = 0; i < paramTypes.size(); i++) { if (replaceAnnotationInGuardedByHierarchyIfGuardSatisfiedIndexMatches( methodDefinitionReturn, - requiredArgs.get(i), + paramTypes.get(i), returnGuardSatisfiedIndex, getAnnotatedType(methodInvocationTreeArguments.get(i)) .getEffectiveAnnotationInHierarchy(GUARDEDBYUNKNOWN))) { @@ -663,7 +672,7 @@ && replaceAnnotationInGuardedByHierarchyIfGuardSatisfiedIndexMatches( */ private boolean replaceAnnotationInGuardedByHierarchyIfGuardSatisfiedIndexMatches( AnnotatedTypeMirror methodReturnAtm, - AnnotatedTypeMirror atm, + @Nullable AnnotatedTypeMirror atm, int matchingGuardSatisfiedIndex, AnnotationMirror annotationInGuardedByHierarchy) { if (atm == null @@ -692,7 +701,8 @@ public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { @Override public void addComputedTypeAnnotations(Tree tree, AnnotatedTypeMirror type, boolean useFlow) { if (tree.getKind() == Tree.Kind.VARIABLE) { - translateJcipAndJavaxAnnotations(TreeUtils.elementFromTree((VariableTree) tree), type); + translateJcipAndJavaxAnnotations( + TreeUtils.elementFromDeclaration((VariableTree) tree), type); } super.addComputedTypeAnnotations(tree, type, useFlow); @@ -729,8 +739,9 @@ private void translateJcipAndJavaxAnnotations(Element element, AnnotatedTypeMirr } // The version of javax.annotation.concurrent.GuardedBy included with the Checker Framework - // declares the type of value as an array of Strings where as the one included with FindBugs - // declares it as a String. So, the code below figures out which type should be used. + // declares the type of value as an array of Strings, whereas the one defined in JCIP and + // included with FindBugs declares it as a String. So, the code below figures out which type + // should be used. Map valmap = anno.getElementValues(); Object value = null; @@ -742,8 +753,10 @@ private void translateJcipAndJavaxAnnotations(Element element, AnnotatedTypeMirr } List lockExpressions; if (value instanceof List) { + @SuppressWarnings("unchecked") + List la = (List) value; lockExpressions = - AnnotationUtils.getElementValueArray(anno, "value", String.class, true); + CollectionsPlume.mapList((AnnotationValue a) -> (String) a.getValue(), la); } else if (value instanceof String) { lockExpressions = Collections.singletonList((String) value); } else { @@ -758,6 +771,8 @@ private void translateJcipAndJavaxAnnotations(Element element, AnnotatedTypeMirr } /** + * Returns an AnnotationMirror corresponding to @GuardedBy(values). + * * @param values a list of lock expressions * @return an AnnotationMirror corresponding to @GuardedBy(values) */ @@ -768,4 +783,12 @@ private AnnotationMirror createGuardedByAnnotationMirror(List values) { // Return the resulting AnnotationMirror return builder.build(); } + + @Override + public boolean isSideEffectFree(ExecutableElement method) { + SideEffectAnnotation seAnno = methodSideEffectAnnotation(method, false); + return seAnno == SideEffectAnnotation.RELEASESNOLOCKS + || seAnno == SideEffectAnnotation.LOCKINGFREE + || super.isSideEffectFree(method); + } } diff --git a/checker/src/main/java/org/checkerframework/checker/lock/LockStore.java b/checker/src/main/java/org/checkerframework/checker/lock/LockStore.java index 552090dd0b0f..cbf58ce5c44e 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/LockStore.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/LockStore.java @@ -1,25 +1,29 @@ package org.checkerframework.checker.lock; -import java.util.ArrayList; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; -import org.checkerframework.checker.lock.LockAnnotatedTypeFactory.SideEffectAnnotation; +import org.checkerframework.checker.lock.qual.LockHeld; +import org.checkerframework.checker.lock.qual.LockPossiblyHeld; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.ArrayAccess; -import org.checkerframework.dataflow.analysis.FlowExpressions.FieldAccess; -import org.checkerframework.dataflow.analysis.FlowExpressions.LocalVariable; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; -import org.checkerframework.dataflow.cfg.CFGVisualizer; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizer; +import org.checkerframework.dataflow.expression.ArrayAccess; +import org.checkerframework.dataflow.expression.ClassName; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.LocalVariable; +import org.checkerframework.dataflow.expression.MethodCall; +import org.checkerframework.dataflow.expression.ThisReference; import org.checkerframework.framework.flow.CFAbstractStore; import org.checkerframework.framework.flow.CFValue; -import org.checkerframework.framework.source.SourceChecker; -import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; import org.checkerframework.framework.type.QualifierHierarchy; +import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.AnnotationUtils; +import java.util.ArrayList; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; + /** * The Lock Store behaves like CFAbstractStore but requires the ability to insert exact annotations. * This is because we want to be able to insert @LockPossiblyHeld to replace @LockHeld, which @@ -35,8 +39,16 @@ public class LockStore extends CFAbstractStore { */ protected boolean inConstructorOrInitializer = false; + /** The type factory to use. */ private final LockAnnotatedTypeFactory atypeFactory; + /** + * Create a LockStore. + * + * @param analysis the analysis class this store belongs to + * @param sequentialSemantics should the analysis use sequential Java semantics (i.e., assume + * that only one thread is running at all times)? + */ public LockStore(LockAnalysis analysis, boolean sequentialSemantics) { super(analysis, sequentialSemantics); this.atypeFactory = (LockAnnotatedTypeFactory) analysis.getTypeFactory(); @@ -64,45 +76,45 @@ public LockStore leastUpperBound(LockStore other) { * Insert an annotation exactly, without regard to whether an annotation was already present. * This is only done for @LockPossiblyHeld. This is not sound for other type qualifiers. */ - public void insertLockPossiblyHeld(FlowExpressions.Receiver r) { - if (r.containsUnknown()) { + public void insertLockPossiblyHeld(JavaExpression je) { + if (je.containsUnknown()) { // Expressions containing unknown expressions are not stored. return; } - if (r instanceof FlowExpressions.LocalVariable) { - FlowExpressions.LocalVariable localVar = (FlowExpressions.LocalVariable) r; + if (je instanceof LocalVariable) { + LocalVariable localVar = (LocalVariable) je; CFValue current = localVariableValues.get(localVar); - CFValue value = changeLockAnnoToTop(r, current); + CFValue value = changeLockAnnoToTop(je, current); if (value != null) { localVariableValues.put(localVar, value); } - } else if (r instanceof FlowExpressions.FieldAccess) { - FlowExpressions.FieldAccess fieldAcc = (FlowExpressions.FieldAccess) r; + } else if (je instanceof FieldAccess) { + FieldAccess fieldAcc = (FieldAccess) je; CFValue current = fieldValues.get(fieldAcc); - CFValue value = changeLockAnnoToTop(r, current); + CFValue value = changeLockAnnoToTop(je, current); if (value != null) { fieldValues.put(fieldAcc, value); } - } else if (r instanceof FlowExpressions.MethodCall) { - FlowExpressions.MethodCall method = (FlowExpressions.MethodCall) r; + } else if (je instanceof MethodCall) { + MethodCall method = (MethodCall) je; CFValue current = methodValues.get(method); - CFValue value = changeLockAnnoToTop(r, current); + CFValue value = changeLockAnnoToTop(je, current); if (value != null) { methodValues.put(method, value); } - } else if (r instanceof FlowExpressions.ArrayAccess) { - FlowExpressions.ArrayAccess arrayAccess = (ArrayAccess) r; + } else if (je instanceof ArrayAccess) { + ArrayAccess arrayAccess = (ArrayAccess) je; CFValue current = arrayValues.get(arrayAccess); - CFValue value = changeLockAnnoToTop(r, current); + CFValue value = changeLockAnnoToTop(je, current); if (value != null) { arrayValues.put(arrayAccess, value); } - } else if (r instanceof FlowExpressions.ThisReference) { - thisValue = changeLockAnnoToTop(r, thisValue); - } else if (r instanceof FlowExpressions.ClassName) { - FlowExpressions.ClassName className = (FlowExpressions.ClassName) r; + } else if (je instanceof ThisReference) { + thisValue = changeLockAnnoToTop(je, thisValue); + } else if (je instanceof ClassName) { + ClassName className = (ClassName) je; CFValue current = classValues.get(className); - CFValue value = changeLockAnnoToTop(r, current); + CFValue value = changeLockAnnoToTop(je, current); if (value != null) { classValues.put(className, value); } @@ -116,19 +128,19 @@ public void insertLockPossiblyHeld(FlowExpressions.Receiver r) { * the LockPossiblyHeld hierarchy is set to LockPossiblyHeld. If currentValue is null, then a * new value is created where the annotation set is LockPossiblyHeld and GuardedByUnknown */ - private CFValue changeLockAnnoToTop(Receiver r, CFValue currentValue) { + private CFValue changeLockAnnoToTop(JavaExpression je, @Nullable CFValue currentValue) { if (currentValue == null) { - Set set = AnnotationUtils.createAnnotationSet(); + AnnotationMirrorSet set = new AnnotationMirrorSet(); set.add(atypeFactory.GUARDEDBYUNKNOWN); set.add(atypeFactory.LOCKPOSSIBLYHELD); - return analysis.createAbstractValue(set, r.getType()); + return analysis.createAbstractValue(set, je.getType()); } - QualifierHierarchy hierarchy = atypeFactory.getQualifierHierarchy(); - Set currentSet = currentValue.getAnnotations(); + QualifierHierarchy qualHierarchy = atypeFactory.getQualifierHierarchy(); + AnnotationMirrorSet currentSet = currentValue.getAnnotations(); AnnotationMirror gb = - hierarchy.findAnnotationInHierarchy(currentSet, atypeFactory.GUARDEDBYUNKNOWN); - Set newSet = AnnotationUtils.createAnnotationSet(); + qualHierarchy.findAnnotationInHierarchy(currentSet, atypeFactory.GUARDEDBYUNKNOWN); + AnnotationMirrorSet newSet = new AnnotationMirrorSet(); newSet.add(atypeFactory.LOCKPOSSIBLYHELD); if (gb != null) { newSet.add(gb); @@ -141,17 +153,16 @@ public void setInConstructorOrInitializer() { } @Override - public @Nullable CFValue getValue(FlowExpressions.Receiver expr) { + public @Nullable CFValue getValue(JavaExpression expr) { if (inConstructorOrInitializer) { // 'this' is automatically considered as being held in a constructor or initializer. // The class name, however, is not. - if (expr instanceof FlowExpressions.ThisReference) { + if (expr instanceof ThisReference) { initializeThisValue(atypeFactory.LOCKHELD, expr.getType()); - } else if (expr instanceof FlowExpressions.FieldAccess) { - FlowExpressions.FieldAccess fieldAcc = (FlowExpressions.FieldAccess) expr; - if (!fieldAcc.isStatic() - && fieldAcc.getReceiver() instanceof FlowExpressions.ThisReference) { + } else if (expr instanceof FieldAccess) { + FieldAccess fieldAcc = (FieldAccess) expr; + if (!fieldAcc.isStatic() && fieldAcc.getReceiver() instanceof ThisReference) { insertValue(fieldAcc.getReceiver(), atypeFactory.LOCKHELD); } } @@ -163,32 +174,26 @@ public void setInConstructorOrInitializer() { @Override protected String internalVisualize(CFGVisualizer viz) { return viz.visualizeStoreKeyVal("inConstructorOrInitializer", inConstructorOrInitializer) + + viz.getSeparator() + super.internalVisualize(viz); } - @Override - protected boolean isSideEffectFree( - AnnotatedTypeFactory atypeFactory, ExecutableElement method) { - LockAnnotatedTypeFactory lockAnnotatedTypeFactory = (LockAnnotatedTypeFactory) atypeFactory; - SourceChecker checker = lockAnnotatedTypeFactory.getContext().getChecker(); - return checker.hasOption("assumeSideEffectFree") - || checker.hasOption("assumePure") - || lockAnnotatedTypeFactory.methodSideEffectAnnotation(method, false) - == SideEffectAnnotation.RELEASESNOLOCKS - || super.isSideEffectFree(atypeFactory, method); - } - @Override public void updateForMethodCall( - MethodInvocationNode n, AnnotatedTypeFactory atypeFactory, CFValue val) { + MethodInvocationNode n, + GenericAnnotatedTypeFactory atypeFactory, + CFValue val) { super.updateForMethodCall(n, atypeFactory, val); ExecutableElement method = n.getTarget().getMethod(); - if (!isSideEffectFree(atypeFactory, method)) { + // The following behavior is similar to setting the sideEffectsUnrefineAliases field of + // Lockannotatedtypefactory, but it affects only the LockPosssiblyHeld type hierarchy (not + // the @GuardedBy hierarchy), so it cannot use that logic. + if (!atypeFactory.isSideEffectFree(method)) { // After the call to super.updateForMethodCall, only final fields are left in // fieldValues (if the method called is side-effecting). For the LockPossiblyHeld - // hierarchy, even a final field might be locked or unlocked by a side-effecting - // method. So, final fields must be set to @LockPossiblyHeld, but the annotation in - // the GuardedBy hierarchy should not be changed. + // hierarchy, even a final field might be locked or unlocked by a side-effecting method. + // So, final fields must be set to @LockPossiblyHeld, but the annotation in the + // GuardedBy hierarchy should not be changed. for (FieldAccess field : new ArrayList<>(fieldValues.keySet())) { CFValue newValue = changeLockAnnoToTop(field, fieldValues.get(field)); if (newValue != null) { @@ -212,36 +217,48 @@ public void updateForMethodCall( } } + /** + * Whether the specified value has the {@link LockHeld} annotation. + * + * @param value the value to check. + * @return whether the {@code value} has the {@link LockHeld} annotation + */ boolean hasLockHeld(CFValue value) { return AnnotationUtils.containsSame(value.getAnnotations(), atypeFactory.LOCKHELD); } + /** + * Whether the specified value has the {@link LockPossiblyHeld} annotation. + * + * @param value the value to check. + * @return whether the {@code value} has the {@link LockPossiblyHeld} annotation + */ boolean hasLockPossiblyHeld(CFValue value) { return AnnotationUtils.containsSame(value.getAnnotations(), atypeFactory.LOCKPOSSIBLYHELD); } @Override - public void insertValue(FlowExpressions.Receiver r, @Nullable CFValue value) { - if (value == null) { - // No need to insert a null abstract value because it represents - // top and top is also the default value. + public void insertValue( + JavaExpression je, @Nullable CFValue value, boolean permitNondeterministic) { + if (!shouldInsert(je, value, permitNondeterministic)) { return; } + // Even with concurrent semantics enabled, a @LockHeld value must always be // stored for fields and @Pure method calls. This is sound because: - // -Another thread can never release the lock on the current thread, and - // -Locks are assumed to be effectively final, hence another thread will not - // side effect the lock expression that has value @LockHeld. + // * Another thread can never release the lock on the current thread, and + // * Locks are assumed to be effectively final, hence another thread will not + // side effect the lock expression that has value @LockHeld. if (hasLockHeld(value)) { - if (r instanceof FlowExpressions.FieldAccess) { - FlowExpressions.FieldAccess fieldAcc = (FlowExpressions.FieldAccess) r; + if (je instanceof FieldAccess) { + FieldAccess fieldAcc = (FieldAccess) je; CFValue oldValue = fieldValues.get(fieldAcc); CFValue newValue = value.mostSpecific(oldValue, null); if (newValue != null) { fieldValues.put(fieldAcc, newValue); } - } else if (r instanceof FlowExpressions.MethodCall) { - FlowExpressions.MethodCall method = (FlowExpressions.MethodCall) r; + } else if (je instanceof MethodCall) { + MethodCall method = (MethodCall) je; CFValue oldValue = methodValues.get(method); CFValue newValue = value.mostSpecific(oldValue, null); if (newValue != null) { @@ -250,6 +267,6 @@ public void insertValue(FlowExpressions.Receiver r, @Nullable CFValue value) { } } - super.insertValue(r, value); + super.insertValue(je, value, permitNondeterministic); } } diff --git a/checker/src/main/java/org/checkerframework/checker/lock/LockTransfer.java b/checker/src/main/java/org/checkerframework/checker/lock/LockTransfer.java index 0c43456a7fa8..740c0ba594c4 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/LockTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/LockTransfer.java @@ -2,13 +2,7 @@ import com.sun.source.tree.ClassTree; import com.sun.source.tree.MethodTree; -import java.util.List; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Modifier; -import javax.lang.model.type.TypeMirror; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; + import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.UnderlyingAST; @@ -17,35 +11,60 @@ import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.cfg.node.SynchronizedNode; +import org.checkerframework.dataflow.expression.ClassName; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.flow.CFAbstractTransfer; import org.checkerframework.framework.flow.CFValue; import org.checkerframework.javacutil.TreeUtils; +import java.util.List; + +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeMirror; + /** * LockTransfer handles constructors, initializers, synchronized methods, and synchronized blocks. */ public class LockTransfer extends CFAbstractTransfer { + /** The type factory associated with this transfer function. */ private final LockAnnotatedTypeFactory atypeFactory; + /** + * Create a transfer function for the Lock Checker. + * + * @param analysis the analysis this transfer function belongs to + * @param checker the type-checker this transfer function belongs to + */ public LockTransfer(LockAnalysis analysis, LockChecker checker) { // Always run the Lock Checker with -AconcurrentSemantics turned on. - super(analysis, true /* useConcurrentSemantics */); + super(analysis, /* useConcurrentSemantics= */ true); this.atypeFactory = (LockAnnotatedTypeFactory) analysis.getTypeFactory(); } - /** Sets a given {@link Node} to @LockHeld in the given {@code store}. */ + /** + * Sets a given {@link Node} to @LockHeld in the given {@code store}. + * + * @param store the store to update + * @param node the node that should be @LockHeld + */ protected void makeLockHeld(LockStore store, Node node) { - Receiver internalRepr = FlowExpressions.internalReprOf(atypeFactory, node); + JavaExpression internalRepr = JavaExpression.fromNode(node); store.insertValue(internalRepr, atypeFactory.LOCKHELD); } - /** Sets a given {@link Node} to @LockPossiblyHeld in the given {@code store}. */ + /** + * Sets a given {@link Node} to @LockPossiblyHeld in the given {@code store}. + * + * @param store the store to update + * @param node the node that should be @LockPossiblyHeld + */ protected void makeLockPossiblyHeld(LockStore store, Node node) { - Receiver internalRepr = FlowExpressions.internalReprOf(atypeFactory, node); + JavaExpression internalRepr = JavaExpression.fromNode(node); - // insertValue cannot change an annotation to a less - // specific type (e.g. LockHeld to LockPossiblyHeld), - // so insertLockPossiblyHeld is called. + // insertValue cannot change an annotation to a less specific type (e.g. LockHeld to + // LockPossiblyHeld), so insertLockPossiblyHeld is called. store.insertLockPossiblyHeld(internalRepr); } @@ -79,41 +98,35 @@ public LockStore initialStore(UnderlyingAST underlyingAST, List and >= are irrelevant for visitBinary, since currently - // boxed primitives cannot be annotated with @GuardedBy(...), but they are left here - // in case that rule changes. - case LESS_THAN: - case LESS_THAN_EQUAL: - case GREATER_THAN: - case GREATER_THAN_EQUAL: - case INSTANCE_OF: - return true; - default: + @Override + public Void visitCompoundAssignment(CompoundAssignmentTree tree, AnnotatedTypeMirror type) { + if (TypesUtils.isString(type.getUnderlyingType())) { + type.replaceAnnotation(((LockAnnotatedTypeFactory) atypeFactory).GUARDEDBY); } - return false; + return super.visitCompoundAssignment(tree, type); } @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMirror type) { - if (TypesUtils.isString(type.getUnderlyingType())) { - type.replaceAnnotation(((LockAnnotatedTypeFactory) atypeFactory).GUARDEDBY); + public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { + if (!type.hasAnnotationInHierarchy(((LockAnnotatedTypeFactory) atypeFactory).NEWOBJECT)) { + type.replaceAnnotation(((LockAnnotatedTypeFactory) atypeFactory).NEWOBJECT); } - - return super.visitCompoundAssignment(node, type); + return super.visitNewArray(tree, type); } } 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 40f4e27dbdb4..6c8f71dcf9c1 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/LockVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/lock/LockVisitor.java @@ -12,25 +12,9 @@ import com.sun.source.tree.MethodTree; import com.sun.source.tree.SynchronizedTree; import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.concurrent.locks.Lock; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Modifier; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; + import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.lock.LockAnnotatedTypeFactory.SideEffectAnnotation; import org.checkerframework.checker.lock.qual.EnsuresLockHeld; @@ -41,11 +25,11 @@ import org.checkerframework.checker.lock.qual.GuardedByUnknown; import org.checkerframework.checker.lock.qual.Holding; import org.checkerframework.checker.lock.qual.LockHeld; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; -import org.checkerframework.dataflow.analysis.FlowExpressions.Unknown; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.Unknown; import org.checkerframework.dataflow.qual.Deterministic; import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.framework.flow.CFAbstractValue; @@ -53,17 +37,34 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.framework.util.AnnotatedTypes; -import org.checkerframework.framework.util.FlowExpressionParseUtil; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionContext; -import org.checkerframework.framework.util.FlowExpressionParseUtil.FlowExpressionParseException; +import org.checkerframework.framework.util.JavaExpressionParseUtil.JavaExpressionParseException; +import org.checkerframework.framework.util.StringToJavaExpression; import org.checkerframework.framework.util.dependenttypes.DependentTypesError; +import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.TypesUtils; +import org.plumelib.util.CollectionsPlume; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.locks.Lock; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; /** * The LockVisitor enforces the special type-checking rules described in the Lock Checker manual @@ -72,38 +73,48 @@ * @checker_framework.manual #lock-checker Lock Checker */ public class LockVisitor extends BaseTypeVisitor { - private final Class checkerGuardedByClass = GuardedBy.class; - private final Class checkerGuardSatisfiedClass = GuardSatisfied.class; + /** The class of GuardedBy */ + private static final Class checkerGuardedByClass = GuardedBy.class; + /** The class of GuardSatisfied */ + private static final Class checkerGuardSatisfiedClass = + GuardSatisfied.class; + + /** A pattern for spotting self receiver */ protected static final Pattern SELF_RECEIVER_PATTERN = Pattern.compile("^(\\.(.*))?$"); + /** + * Constructs a {@link LockVisitor}. + * + * @param checker the type checker to use + */ public LockVisitor(BaseTypeChecker checker) { super(checker); } @Override - public Void visitVariable(VariableTree node, Void p) { // visit a variable declaration + public Void visitVariable(VariableTree tree, Void p) { // visit a variable declaration // A user may not annotate a primitive type, a boxed primitive type or a String // with any qualifier from the @GuardedBy hierarchy. // They are immutable, so there is no need to guard them. - TypeMirror tm = TreeUtils.typeOf(node); + TypeMirror tm = TreeUtils.typeOf(tree); if (TypesUtils.isBoxedPrimitive(tm) || TypesUtils.isPrimitive(tm) || TypesUtils.isString(tm)) { - AnnotatedTypeMirror atm = atypeFactory.getAnnotatedType(node); + AnnotatedTypeMirror atm = atypeFactory.getAnnotatedType(tree); if (atm.hasExplicitAnnotationRelaxed(atypeFactory.GUARDSATISFIED) || atm.hasExplicitAnnotationRelaxed(atypeFactory.GUARDEDBY) || atm.hasExplicitAnnotation(atypeFactory.GUARDEDBYUNKNOWN) || atm.hasExplicitAnnotation(atypeFactory.GUARDEDBYBOTTOM)) { - checker.reportError(node, "immutable.type.guardedby"); + checker.reportError(tree, "immutable.type.guardedby"); } } - issueErrorIfMoreThanOneGuardedByAnnotationPresent(node); + issueErrorIfMoreThanOneGuardedByAnnotationPresent(tree); - return super.visitVariable(node, p); + return super.visitVariable(tree, p); } /** @@ -150,20 +161,20 @@ public LockAnnotatedTypeFactory createTypeFactory() { * issues an error if a synchronized method has a @LockingFree, @SideEffectFree, or @Pure * annotation. * - * @param node the MethodTree of the method definition to visit + * @param tree the MethodTree of the method definition to visit */ @Override - public Void visitMethod(MethodTree node, Void p) { - ExecutableElement methodElement = TreeUtils.elementFromDeclaration(node); + public Void visitMethod(MethodTree tree, Void p) { + ExecutableElement methodElement = TreeUtils.elementFromDeclaration(tree); - issueErrorIfMoreThanOneLockPreconditionMethodAnnotationPresent(methodElement, node); + issueErrorIfMoreThanOneLockPreconditionMethodAnnotationPresent(methodElement, tree); SideEffectAnnotation sea = atypeFactory.methodSideEffectAnnotation(methodElement, true); if (sea == SideEffectAnnotation.MAYRELEASELOCKS) { boolean issueGSwithMRLWarning = false; - VariableTree receiver = node.getReceiverParameter(); + VariableTree receiver = tree.getReceiverParameter(); if (receiver != null) { if (atypeFactory .getAnnotatedType(receiver) @@ -173,7 +184,7 @@ public Void visitMethod(MethodTree node, Void p) { } if (!issueGSwithMRLWarning) { // Skip loop if we already decided to issue the warning. - for (VariableTree vt : node.getParameters()) { + for (VariableTree vt : tree.getParameters()) { if (atypeFactory .getAnnotatedType(vt) .hasAnnotation(checkerGuardSatisfiedClass)) { @@ -184,30 +195,30 @@ public Void visitMethod(MethodTree node, Void p) { } if (issueGSwithMRLWarning) { - checker.reportError(node, "guardsatisfied.with.mayreleaselocks"); + checker.reportError(tree, "guardsatisfied.with.mayreleaselocks"); } } // Issue an error if a non-constructor method definition has a return type of // @GuardSatisfied without an index. if (methodElement != null && methodElement.getKind() != ElementKind.CONSTRUCTOR) { - AnnotatedTypeMirror returnTypeATM = atypeFactory.getAnnotatedType(node).getReturnType(); + AnnotatedTypeMirror returnTypeATM = atypeFactory.getAnnotatedType(tree).getReturnType(); if (returnTypeATM != null && returnTypeATM.hasAnnotation(GuardSatisfied.class)) { int returnGuardSatisfiedIndex = atypeFactory.getGuardSatisfiedIndex(returnTypeATM); if (returnGuardSatisfiedIndex == -1) { - checker.reportError(node, "guardsatisfied.return.must.have.index"); + checker.reportError(tree, "guardsatisfied.return.must.have.index"); } } } if (!sea.isWeakerThan(SideEffectAnnotation.LOCKINGFREE) && methodElement.getModifiers().contains(Modifier.SYNCHRONIZED)) { - checker.reportError(node, "lockingfree.synchronized.method", sea); + checker.reportError(tree, "lockingfree.synchronized.method", sea); } - return super.visitMethod(node, p); + return super.visitMethod(tree, p); } /** @@ -219,7 +230,7 @@ public Void visitMethod(MethodTree node, Void p) { *

  • {@code @javax.annotation.concurrent.GuardedBy} * * - * @param methodElement the ExecutableElement for the method call referred to by {@code node} + * @param methodElement the ExecutableElement for the method call referred to by {@code tree} * @param treeForErrorReporting the MethodTree used to report the error */ private void issueErrorIfMoreThanOneLockPreconditionMethodAnnotationPresent( @@ -292,7 +303,7 @@ protected boolean skipReceiverSubtypeCheck( } if (atypeFactory.areSameByClass(effectiveGb, checkerGuardedByClass)) { - Set annos = methodDefinitionReceiver.getAnnotations(); + AnnotationMirrorSet annos = methodDefinitionReceiver.getAnnotations(); AnnotationMirror guardSatisfied = atypeFactory.getAnnotationByClass(annos, checkerGuardSatisfiedClass); if (guardSatisfied != null) { @@ -310,10 +321,9 @@ protected boolean skipReceiverSubtypeCheck( } @Override - protected Set getExceptionParameterLowerBoundAnnotations() { - Set tops = - atypeFactory.getQualifierHierarchy().getTopAnnotations(); - Set annotationSet = AnnotationUtils.createAnnotationSet(); + protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { + AnnotationMirrorSet tops = qualHierarchy.getTopAnnotations(); + AnnotationMirrorSet annotationSet = new AnnotationMirrorSet(); for (AnnotationMirror anno : tops) { if (AnnotationUtils.areSame(anno, atypeFactory.GUARDEDBYUNKNOWN)) { annotationSet.add(atypeFactory.GUARDEDBY); @@ -327,62 +337,37 @@ protected Set getExceptionParameterLowerBoundAnnotat @Override protected void checkConstructorResult( AnnotatedExecutableType constructorType, ExecutableElement constructorElement) { - // Newly created objects are guarded by nothing, so allow @GuardBy({}) on constructor + // Newly created objects are guarded by nothing, so allow @GuardedBy({}) on constructor // results. AnnotationMirror anno = constructorType .getReturnType() .getAnnotationInHierarchy(atypeFactory.GUARDEDBYUNKNOWN); - if (!AnnotationUtils.areSame(anno, atypeFactory.GUARDEDBY)) { - super.checkConstructorResult(constructorType, constructorElement); + if (AnnotationUtils.areSame(anno, atypeFactory.GUARDEDBYUNKNOWN) + || AnnotationUtils.areSame(anno, atypeFactory.GUARDEDBYBOTTOM)) { + checker.reportWarning(constructorElement, "inconsistent.constructor.type", anno, null); } } @Override - protected void commonAssignmentCheck( + protected boolean commonAssignmentCheck( AnnotatedTypeMirror varType, AnnotatedTypeMirror valueType, Tree valueTree, - @CompilerMessageKey String errorKey) { - - Kind valueTreeKind = valueTree.getKind(); - - switch (valueTreeKind) { - case NEW_CLASS: - case NEW_ARRAY: - // Avoid issuing warnings for: @GuardedBy() Object o = new Object(); - // Do NOT do this if the LHS is @GuardedByBottom. - if (!varType.hasAnnotation(GuardedByBottom.class)) { - return; - } - break; - case INT_LITERAL: - case LONG_LITERAL: - case FLOAT_LITERAL: - case DOUBLE_LITERAL: - case BOOLEAN_LITERAL: - case CHAR_LITERAL: - case STRING_LITERAL: - // Avoid issuing warnings for: @GuardedBy() Object o; o = ; - // Do NOT do this if the LHS is @GuardedByBottom. - if (!varType.hasAnnotation(GuardedByBottom.class)) { - return; - } - break; - default: - } + @CompilerMessageKey String errorKey, + Object... extraArgs) { // In cases where assigning a value with a @GuardedBy annotation to a variable with a // @GuardSatisfied annotation is legal, this is our last chance to check that the - // appropriate locks are held before the information in the @GuardedBy annotation is - // lost in the assignment to the variable annotated with @GuardSatisfied. See the - // discussion of @GuardSatisfied in the "Type-checking rules" section of the - // Lock Checker manual chapter for more details. + // appropriate locks are held before the information in the @GuardedBy annotation is lost in + // the assignment to the variable annotated with @GuardSatisfied. See the discussion of + // @GuardSatisfied in the "Type-checking rules" section of the Lock Checker manual chapter + // for more details. + boolean result = true; if (varType.hasAnnotation(GuardSatisfied.class)) { if (valueType.hasAnnotation(GuardedBy.class)) { - checkLock(valueTree, valueType.getAnnotation(GuardedBy.class)); - return; + return checkLock(valueTree, valueType.getAnnotation(GuardedBy.class)); } else if (valueType.hasAnnotation(GuardSatisfied.class)) { // TODO: Find a cleaner, non-abstraction-breaking way to know whether method actual // parameters are being assigned to formal parameters. @@ -403,6 +388,7 @@ protected void commonAssignmentCheck( "guardsatisfied.assignment.disallowed", varType, valueType); + result = false; } } else { // The RHS can be @GuardSatisfied with a different index when matching method @@ -415,7 +401,7 @@ protected void commonAssignmentCheck( // Note: this matching of a @GS(index) to a @GS(differentIndex) is *only* // allowed when matching method formal parameters to actual parameters. - return; + return true; } } else if (!atypeFactory.getTypeHierarchy().isSubtype(valueType, varType)) { // Special case: replace the @GuardSatisfied primary annotation on the LHS with @@ -425,12 +411,15 @@ protected void commonAssignmentCheck( varType.deepCopy(); // TODO: Would shallowCopy be sufficient? varType2.replaceAnnotation(atypeFactory.GUARDEDBY); if (atypeFactory.getTypeHierarchy().isSubtype(valueType, varType2)) { - return; + return true; } } } - super.commonAssignmentCheck(varType, valueType, valueTree, errorKey); + result = + super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs) + && result; + return result; } @Override @@ -473,18 +462,18 @@ private void reportFailure( checker.reportError( overriderTree, messageKey, - overriderMeth, overriderTyp, - overriddenMeth, - overriddenTyp); + overriderMeth, + overriddenTyp, + overriddenMeth); } else { checker.reportError( overriderTree, messageKey, - overriderMeth, overriderTyp, - overriddenMeth, + overriderMeth, overriddenTyp, + overriddenMeth, overriderLocks, overriddenLocks); } @@ -498,7 +487,7 @@ private void reportFailure( protected boolean checkOverride( MethodTree overriderTree, AnnotatedDeclaredType enclosingType, - AnnotatedExecutableType overridden, + AnnotatedExecutableType overriddenMethodType, AnnotatedDeclaredType overriddenType) { boolean isValid = true; @@ -506,22 +495,23 @@ protected boolean checkOverride( SideEffectAnnotation seaOfOverriderMethod = atypeFactory.methodSideEffectAnnotation( TreeUtils.elementFromDeclaration(overriderTree), false); - SideEffectAnnotation seaOfOverridenMethod = - atypeFactory.methodSideEffectAnnotation(overridden.getElement(), false); + SideEffectAnnotation seaOfOverriddenMethod = + atypeFactory.methodSideEffectAnnotation(overriddenMethodType.getElement(), false); - if (seaOfOverriderMethod.isWeakerThan(seaOfOverridenMethod)) { + if (seaOfOverriderMethod.isWeakerThan(seaOfOverriddenMethod)) { isValid = false; reportFailure( "override.sideeffect.invalid", overriderTree, enclosingType, - overridden, + overriddenMethodType, overriddenType, null, null); } - return super.checkOverride(overriderTree, enclosingType, overridden, overriddenType) + return super.checkOverride( + overriderTree, enclosingType, overriddenMethodType, overriddenType) && isValid; } @@ -559,16 +549,17 @@ public boolean isValidUse( * with @MayReleaseLocks. Also check that matching @GuardSatisfied(index) on a method's formal * receiver/parameters matches those in corresponding locations on the method call site. * - * @param node the MethodInvocationTree of the method call being visited + * @param methodInvocationTree the MethodInvocationTree of the method call being visited */ @Override - public Void visitMethodInvocation(MethodInvocationTree node, Void p) { - ExecutableElement methodElement = TreeUtils.elementFromUse(node); + public Void visitMethodInvocation(MethodInvocationTree methodInvocationTree, Void p) { + ExecutableElement methodElement = TreeUtils.elementFromUse(methodInvocationTree); SideEffectAnnotation seaOfInvokedMethod = atypeFactory.methodSideEffectAnnotation(methodElement, false); - MethodTree enclosingMethod = TreeUtils.enclosingMethod(atypeFactory.getPath(node)); + MethodTree enclosingMethod = + TreePathUtil.enclosingMethod(atypeFactory.getPath(methodInvocationTree)); ExecutableElement enclosingMethodElement = null; if (enclosingMethod != null) { @@ -581,11 +572,11 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { if (seaOfInvokedMethod.isWeakerThan(seaOfContainingMethod)) { checker.reportError( - node, + methodInvocationTree, "method.guarantee.violated", seaOfContainingMethod.getNameOfSideEffectAnnotation(), - enclosingMethodElement.toString(), - methodElement.toString(), + enclosingMethodElement.getSimpleName(), + methodElement.getSimpleName(), seaOfInvokedMethod.getNameOfSideEffectAnnotation()); } } @@ -593,9 +584,9 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { if (methodElement != null) { // Handle releasing of explicit locks. Verify that the lock expression is effectively // final. - ExpressionTree recvTree = TreeUtils.getReceiverTree(node); + ExpressionTree receiverTree = TreeUtils.getReceiverTree(methodInvocationTree); - ensureReceiverOfExplicitUnlockCallIsEffectivelyFinal(methodElement, recvTree); + ensureReceiverOfExplicitUnlockCallIsEffectivelyFinal(methodElement, receiverTree); // Handle acquiring of explicit locks. Verify that the lock expression is effectively // final. @@ -608,12 +599,14 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { AnnotationMirror ensuresLockHeldAnno = atypeFactory.getDeclAnnotation(methodElement, EnsuresLockHeld.class); - List expressions = new ArrayList<>(); + List expressions = new ArrayList<>(); if (ensuresLockHeldAnno != null) { expressions.addAll( AnnotationUtils.getElementValueArray( - ensuresLockHeldAnno, "value", String.class, false)); + ensuresLockHeldAnno, + atypeFactory.ensuresLockHeldValueElement, + String.class)); } AnnotationMirror ensuresLockHeldIfAnno = @@ -622,18 +615,20 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { if (ensuresLockHeldIfAnno != null) { expressions.addAll( AnnotationUtils.getElementValueArray( - ensuresLockHeldIfAnno, "expression", String.class, false)); + ensuresLockHeldIfAnno, + atypeFactory.ensuresLockHeldIfExpressionElement, + String.class)); } for (String expr : expressions) { if (expr.equals("this")) { - // recvTree will be null for implicit this, or class name receivers. But they - // are also final. So nothing to be checked for them. - if (recvTree != null) { - ensureExpressionIsEffectivelyFinal(recvTree); + // receiverTree will be null for implicit this, or class name receivers. But + // they are also final. So nothing to be checked for them. + if (receiverTree != null) { + ensureExpressionIsEffectivelyFinal(receiverTree); } } else if (expr.equals("#1")) { - ExpressionTree firstParameter = node.getArguments().get(0); + ExpressionTree firstParameter = methodInvocationTree.getArguments().get(0); if (firstParameter != null) { ensureExpressionIsEffectivelyFinal(firstParameter); } @@ -644,11 +639,10 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { // Check that matching @GuardSatisfied(index) on a method's formal receiver/parameters // matches those in corresponding locations on the method call site. - ParameterizedExecutableType mType = atypeFactory.methodFromUse(node); + ParameterizedExecutableType mType = atypeFactory.methodFromUse(methodInvocationTree); AnnotatedExecutableType invokedMethod = mType.executableType; - List requiredArgs = - AnnotatedTypes.expandVarArgs(atypeFactory, invokedMethod, node.getArguments()); + List paramTypes = invokedMethod.getParameterTypes(); // Index on @GuardSatisfied at each location. -1 when no @GuardSatisfied annotation was // present. @@ -657,7 +651,7 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { // encountered we leave its index as -1. // The first element of the array is reserved for the receiver. int guardSatisfiedIndex[] = - new int[requiredArgs.size() + 1]; // + 1 for the receiver parameter type + new int[paramTypes.size() + 1]; // + 1 for the receiver parameter type // Retrieve receiver types from method definition and method call @@ -674,36 +668,39 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { && methodDefinitionReceiver.hasAnnotation(checkerGuardSatisfiedClass)) { guardSatisfiedIndex[0] = atypeFactory.getGuardSatisfiedIndex(methodDefinitionReceiver); - methodCallReceiver = atypeFactory.getReceiverType(node); + methodCallReceiver = atypeFactory.getReceiverType(methodInvocationTree); } } - // Retrieve formal parameter types from the method definition + // Retrieve formal parameter types from the method definition. - for (int i = 0; i < requiredArgs.size(); i++) { + for (int i = 0; i < paramTypes.size(); i++) { guardSatisfiedIndex[i + 1] = -1; - AnnotatedTypeMirror arg = requiredArgs.get(i); + AnnotatedTypeMirror paramType = paramTypes.get(i); - if (arg.hasAnnotation(checkerGuardSatisfiedClass)) { - guardSatisfiedIndex[i + 1] = atypeFactory.getGuardSatisfiedIndex(arg); + if (paramType.hasAnnotation(checkerGuardSatisfiedClass)) { + guardSatisfiedIndex[i + 1] = atypeFactory.getGuardSatisfiedIndex(paramType); } } - // Combine all of the actual parameters into one list of AnnotationMirrors + // Combine all of the actual parameters into one list of AnnotationMirrors. + ArrayList passedArgTypes = new ArrayList<>(guardSatisfiedIndex.length); + passedArgTypes.add(methodCallReceiver); + for (ExpressionTree argTree : methodInvocationTree.getArguments()) { + AnnotatedTypeMirror argType = atypeFactory.getAnnotatedType(argTree); + passedArgTypes.add(argType); + } ArrayList passedArgAnnotations = new ArrayList<>(guardSatisfiedIndex.length); - passedArgAnnotations.add( - methodCallReceiver == null - ? null - : methodCallReceiver.getAnnotationInHierarchy( - atypeFactory.GUARDEDBYUNKNOWN)); - for (ExpressionTree tree : node.getArguments()) { - passedArgAnnotations.add( - atypeFactory - .getAnnotatedType(tree) - .getAnnotationInHierarchy(atypeFactory.GUARDEDBYUNKNOWN)); + for (AnnotatedTypeMirror atm : passedArgTypes) { + if (atm != null) { + passedArgAnnotations.add( + atm.getAnnotationInHierarchy(atypeFactory.GUARDEDBYUNKNOWN)); + } else { + passedArgAnnotations.add(null); + } } // Perform the validity check and issue an error if not valid. @@ -732,17 +729,16 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { } } - if (bothAreGSwithNoIndex - || !(atypeFactory - .getQualifierHierarchy() - .isSubtype(arg1Anno, arg2Anno) - || atypeFactory - .getQualifierHierarchy() - .isSubtype(arg2Anno, arg1Anno))) { - // TODO: allow these strings to be localized + TypeMirror arg1TM = passedArgTypes.get(i).getUnderlyingType(); + TypeMirror arg2TM = passedArgTypes.get(j).getUnderlyingType(); - String formalParam1 = null; + if (bothAreGSwithNoIndex + || !(qualHierarchy.isSubtypeShallow( + arg1Anno, arg1TM, arg2Anno, arg2TM) + || qualHierarchy.isSubtypeShallow( + arg2Anno, arg2TM, arg1Anno, arg1TM))) { + String formalParam1; if (i == 0) { formalParam1 = "The receiver type"; } else { @@ -755,7 +751,7 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { "parameter #" + j; // j, not j-1, so the index is 1-based checker.reportError( - node, + methodInvocationTree, "guardsatisfied.parameters.must.match", formalParam1, formalParam2, @@ -770,7 +766,7 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { } } - return super.visitMethodInvocation(node, p); + return super.visitMethodInvocation(methodInvocationTree, p); } /** @@ -780,7 +776,7 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { * @param lockExpression the receiver tree for the method call to unlock(). Can be null. */ private void ensureReceiverOfExplicitUnlockCallIsEffectivelyFinal( - ExecutableElement methodElement, ExpressionTree lockExpression) { + ExecutableElement methodElement, @Nullable ExpressionTree lockExpression) { if (lockExpression == null) { // Implicit this, or class name receivers, are null. But they are also final. So nothing // to be checked for them. @@ -821,10 +817,10 @@ private void ensureReceiverOfExplicitUnlockCallIsEffectivelyFinal( *

    Additionally, a synchronized block may not be present in a method that has a @LockingFree * guarantee or stronger. An error is issued in this case. * - * @param node the SynchronizedTree for the synchronized block being visited + * @param tree the SynchronizedTree for the synchronized block being visited */ @Override - public Void visitSynchronized(SynchronizedTree node, Void p) { + public Void visitSynchronized(SynchronizedTree tree, Void p) { ProcessingEnvironment processingEnvironment = checker.getProcessingEnvironment(); javax.lang.model.util.Types types = processingEnvironment.getTypeUtils(); @@ -835,7 +831,7 @@ public Void visitSynchronized(SynchronizedTree node, Void p) { TypesUtils.typeFromClass( Lock.class, types, processingEnvironment.getElementUtils()); - ExpressionTree synchronizedExpression = node.getExpression(); + ExpressionTree synchronizedExpression = tree.getExpression(); ensureExpressionIsEffectivelyFinal(synchronizedExpression); @@ -844,10 +840,10 @@ public Void visitSynchronized(SynchronizedTree node, Void p) { atypeFactory.getAnnotatedType(synchronizedExpression).getUnderlyingType()); if (types.isSubtype(expressionType, lockInterfaceTypeMirror)) { - checker.reportError(node, "explicit.lock.synchronized"); + checker.reportError(tree, "explicit.lock.synchronized"); } - MethodTree enclosingMethod = TreeUtils.enclosingMethod(atypeFactory.getPath(node)); + MethodTree enclosingMethod = TreePathUtil.enclosingMethod(atypeFactory.getPath(tree)); ExecutableElement methodElement = null; if (enclosingMethod != null) { @@ -858,11 +854,11 @@ public Void visitSynchronized(SynchronizedTree node, Void p) { if (!seaOfContainingMethod.isWeakerThan(SideEffectAnnotation.LOCKINGFREE)) { checker.reportError( - node, "synchronized.block.in.lockingfree.method", seaOfContainingMethod); + tree, "synchronized.block.in.lockingfree.method", seaOfContainingMethod); } } - return super.visitSynchronized(node, p); + return super.visitSynchronized(tree, p); } /** @@ -876,17 +872,19 @@ public Void visitSynchronized(SynchronizedTree node, Void p) { * enforced to be @Deterministic. * * @param lockExpressionTree the expression tree of a synchronized block + * @return true if the check succeeds, false if an error message was issued */ - private void ensureExpressionIsEffectivelyFinal(final ExpressionTree lockExpressionTree) { - // This functionality could be implemented using a visitor instead, - // however with this design, it is easier to be certain that an error - // will always be issued if a tree kind is not recognized. + private boolean ensureExpressionIsEffectivelyFinal(ExpressionTree lockExpressionTree) { + // This functionality could be implemented using a visitor instead, however with this + // design, it is easier to be certain that an error will always be issued if a tree kind is + // not recognized. // Only the most common tree kinds for synchronized expressions are supported. // Traverse the expression using 'tree', as 'lockExpressionTree' is used for error // reporting. ExpressionTree tree = lockExpressionTree; + boolean result = true; while (true) { tree = TreeUtils.withoutParens(tree); @@ -894,27 +892,28 @@ private void ensureExpressionIsEffectivelyFinal(final ExpressionTree lockExpress case MEMBER_SELECT: if (!isTreeSymbolEffectivelyFinalOrUnmodifiable(tree)) { checker.reportError(tree, "lock.expression.not.final", lockExpressionTree); - return; + return false; } tree = ((MemberSelectTree) tree).getExpression(); break; case IDENTIFIER: if (!isTreeSymbolEffectivelyFinalOrUnmodifiable(tree)) { checker.reportError(tree, "lock.expression.not.final", lockExpressionTree); + return false; } - return; + return result; case METHOD_INVOCATION: Element elem = TreeUtils.elementFromUse(tree); if (atypeFactory.getDeclAnnotationNoAliases(elem, Deterministic.class) == null && atypeFactory.getDeclAnnotationNoAliases(elem, Pure.class) == null) { checker.reportError(tree, "lock.expression.not.final", lockExpressionTree); - return; + return false; } MethodInvocationTree methodInvocationTree = (MethodInvocationTree) tree; for (ExpressionTree argTree : methodInvocationTree.getArguments()) { - ensureExpressionIsEffectivelyFinal(argTree); + result = ensureExpressionIsEffectivelyFinal(argTree) && result; } tree = methodInvocationTree.getMethodSelect(); @@ -922,21 +921,32 @@ private void ensureExpressionIsEffectivelyFinal(final ExpressionTree lockExpress default: checker.reportError( tree, "lock.expression.possibly.not.final", lockExpressionTree); - return; + return false; } } } - private void ensureExpressionIsEffectivelyFinal( - final Receiver lockExpr, + /** + * Issues an error if the given expression is not effectively final. Returns true if the + * expression is effectively final, false if an error was issued. + * + * @param lockExpr an expression that might be effectively final + * @param expressionForErrorReporting how to print the expression in an error message + * @param treeForErrorReporting where to report the error + * @return true if the expression is effectively final, false if an error was issued + */ + private boolean ensureExpressionIsEffectivelyFinal( + JavaExpression lockExpr, String expressionForErrorReporting, Tree treeForErrorReporting) { - if (!atypeFactory.isExpressionEffectivelyFinal(lockExpr)) { + boolean result = atypeFactory.isExpressionEffectivelyFinal(lockExpr); + if (!result) { checker.reportError( treeForErrorReporting, "lock.expression.not.final", expressionForErrorReporting); } + return result; } @Override @@ -961,8 +971,8 @@ public Void visitAnnotation(AnnotationTree tree, Void p) { * Issues an error if a GuardSatisfied annotation is found in a location other than a method * return type or parameter (including the receiver). * - * @param annotationTree AnnotationTree used for error reporting and to help determine that an - * array parameter has no GuardSatisfied annotations except on the array type + * @param annotationTree an AnnotationTree used for error reporting and to help determine that + * an array parameter has no GuardSatisfied annotations except on the array type */ // TODO: Remove this method once @TargetLocations are enforced (i.e. once // issue https://github.com/typetools/checker-framework/issues/1919 is closed). @@ -1008,7 +1018,7 @@ private void issueErrorIfGuardSatisfiedAnnotationInUnsupportedLocation( } /** - * The flow expression parser requires a path for retrieving the scope that will be used to + * The JavaExpression parser requires a path for retrieving the scope that will be used to * resolve local variables. One would expect that simply providing the path to an AnnotationTree * would work, since the compiler (as called by the org.checkerframework.javacutil.Resolver * class) could walk up the path from the AnnotationTree to determine the scope. Unfortunately @@ -1027,13 +1037,13 @@ private void issueErrorIfGuardSatisfiedAnnotationInUnsupportedLocation( * @return a TreePath that can be passed to methods in the Resolver class to locate local * variables */ - private TreePath getPathForLocalVariableRetrieval(TreePath path) { + private @Nullable TreePath getPathForLocalVariableRetrieval(TreePath path) { assert path.getLeaf() instanceof AnnotationTree; // TODO: handle annotations in trees of kind NEW_CLASS (and add test coverage for this // scenario). // Currently an annotation in such a tree, such as "new @GuardedBy("foo") Object()", - // results in a constructor.invocation.invalid error. This must be fixed first. + // results in a "constructor.invocation.invalid" error. This must be fixed first. path = path.getParentPath(); @@ -1076,6 +1086,9 @@ private TreePath getPathForLocalVariableRetrieval(TreePath path) { /** * Returns true if the symbol for the given tree is final or effectively final. Package, class * and method symbols are unmodifiable and therefore considered final. + * + * @param tree the tree to test + * @return true if the symbol for the given tree is final or effectively final */ private boolean isTreeSymbolEffectivelyFinalOrUnmodifiable(Tree tree) { Element elem = TreeUtils.elementFromTree(tree); @@ -1087,15 +1100,17 @@ 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. + // If the identifier is a field accessed via an implicit this, then check the lock of this. + // (All other field accesses are checked in visitMemberSelect.) if (TreeUtils.isFieldAccess(tree)) { Tree parent = getCurrentPath().getParentPath().getLeaf(); // If the parent is not a member select, or if it is and the field is the expression, // then the field is accessed via an implicit this. - if (parent.getKind() != Kind.MEMBER_SELECT - || ((MemberSelectTree) parent).getExpression() == tree) { + if ((parent.getKind() != Tree.Kind.MEMBER_SELECT + || ((MemberSelectTree) parent).getExpression() == tree) + && !ElementUtils.isStatic(TreeUtils.elementFromUse(tree))) { AnnotationMirror guardedBy = atypeFactory .getSelfType(tree) @@ -1125,15 +1140,15 @@ public Void visitBinary(BinaryTree binaryTree, Void p) { } @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { - if (TreeUtils.isStringCompoundConcatenation(node)) { - ExpressionTree rightTree = node.getExpression(); + public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { + if (TreeUtils.isStringCompoundConcatenation(tree)) { + ExpressionTree rightTree = tree.getExpression(); if (!TypesUtils.isString(TreeUtils.typeOf(rightTree))) { checkPreconditionsForImplicitToStringCall(rightTree); } } - return super.visitCompoundAssignment(node, p); + return super.visitCompoundAssignment(tree, p); } /** @@ -1149,8 +1164,8 @@ public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { */ // TODO: If and when the de-sugared .toString() tree is accessible from BaseTypeVisitor, // the toString() method call should be visited instead of doing this. This would result - // in contracts.precondition.not.satisfied errors being issued instead of - // contracts.precondition.not.satisfied.field, so it would be clear that + // in "contracts.precondition.not.satisfied" errors being issued instead of + // "contracts.precondition.not.satisfied.field", so it would be clear that // the error refers to an implicit method call, not a dereference (field access). private void checkPreconditionsForImplicitToStringCall(ExpressionTree tree) { AnnotationMirror gbAnno = @@ -1164,58 +1179,82 @@ private void checkLockOfImplicitThis(Tree tree, AnnotationMirror gbAnno) { checkLockOfThisOrTree(tree, true, gbAnno); } - private void checkLock(Tree tree, AnnotationMirror gbAnno) { - checkLockOfThisOrTree(tree, false, gbAnno); + /** + * Checks the lock of the given tree. + * + * @param tree a tree whose lock to check + * @param gbAnno a {@code @GuardedBy} annotation + * @return true if the check succeeds, false if an error message was issued + */ + private boolean checkLock(Tree tree, AnnotationMirror gbAnno) { + return checkLockOfThisOrTree(tree, false, gbAnno); } - private void checkLockOfThisOrTree(Tree tree, boolean implicitThis, AnnotationMirror gbAnno) { + /** + * Helper method that checks the lock of either the implicit {@code this} or the given tree. + * + * @param tree a tree whose lock to check + * @param implicitThis true if checking the lock of the implicit {@code this} + * @param gbAnno a {@code @GuardedBy} annotation + * @return true if the check succeeds, false if an error message was issued + */ + private boolean checkLockOfThisOrTree( + Tree tree, boolean implicitThis, AnnotationMirror gbAnno) { if (gbAnno == null) { - throw new BugInCF("LockVisitor.checkLock: gbAnno cannot be null"); + throw new TypeSystemError("LockVisitor.checkLock: gbAnno cannot be null"); } if (atypeFactory.areSameByClass(gbAnno, GuardedByUnknown.class) || atypeFactory.areSameByClass(gbAnno, GuardedByBottom.class)) { checker.reportError(tree, "lock.not.held", "unknown lock " + gbAnno); - return; + return false; } else if (atypeFactory.areSameByClass(gbAnno, GuardSatisfied.class)) { - return; + return true; } List expressions = getLockExpressions(implicitThis, gbAnno, tree); if (expressions.isEmpty()) { - return; + return true; } + boolean result = true; LockStore store = atypeFactory.getStoreBefore(tree); for (LockExpression expression : expressions) { if (expression.error != null) { checker.reportError( tree, "expression.unparsable.type.invalid", expression.error.toString()); + result = false; } else if (expression.lockExpression == null) { checker.reportError( tree, "expression.unparsable.type.invalid", expression.expressionString); + result = false; } else if (!isLockHeld(expression.lockExpression, store)) { checker.reportError(tree, "lock.not.held", expression.lockExpression.toString()); + result = false; } if (expression.error != null && expression.lockExpression != null) { - ensureExpressionIsEffectivelyFinal( - expression.lockExpression, expression.expressionString, tree); + result = + ensureExpressionIsEffectivelyFinal( + expression.lockExpression, + expression.expressionString, + tree) + && result; } } + return result; } - private boolean isLockHeld(Receiver lock, LockStore store) { + private boolean isLockHeld(JavaExpression lockExpr, LockStore store) { if (store == null) { return false; } - CFAbstractValue value = store.getValue(lock); + CFAbstractValue value = store.getValue(lockExpr); if (value == null) { return false; } - Set annos = value.getAnnotations(); - QualifierHierarchy hierarchy = atypeFactory.getQualifierHierarchy(); + AnnotationMirrorSet annos = value.getAnnotations(); AnnotationMirror lockAnno = - hierarchy.findAnnotationInSameHierarchy(annos, atypeFactory.LOCKHELD); + qualHierarchy.findAnnotationInSameHierarchy(annos, atypeFactory.LOCKHELD); return lockAnno != null && atypeFactory.areSameByClass(lockAnno, LockHeld.class); } @@ -1223,42 +1262,45 @@ private List getLockExpressions( boolean implicitThis, AnnotationMirror gbAnno, Tree tree) { List expressions = - AnnotationUtils.getElementValueArray(gbAnno, "value", String.class, true); + AnnotationUtils.getElementValueArray( + gbAnno, + atypeFactory.guardedByValueElement, + String.class, + Collections.emptyList()); if (expressions.isEmpty()) { return Collections.emptyList(); } TreePath currentPath = getCurrentPath(); - List params = - FlowExpressions.getParametersOfEnclosingMethod(atypeFactory, currentPath); - - TypeMirror enclosingType = TreeUtils.typeOf(TreeUtils.enclosingClass(currentPath)); - Receiver pseudoReceiver = - FlowExpressions.internalReprOfPseudoReceiver(currentPath, enclosingType); - FlowExpressionContext exprContext = - new FlowExpressionContext(pseudoReceiver, params, atypeFactory.getContext()); - Receiver self; + + TypeMirror enclosingType = TreeUtils.typeOf(TreePathUtil.enclosingClass(currentPath)); + JavaExpression pseudoReceiver = + JavaExpression.getPseudoReceiver(currentPath, enclosingType); + + JavaExpression self; if (implicitThis) { self = pseudoReceiver; } else if (TreeUtils.isExpressionTree(tree)) { - self = FlowExpressions.internalReprOf(atypeFactory, (ExpressionTree) tree); + self = JavaExpression.fromTree((ExpressionTree) tree); } else { - self = new Unknown(TreeUtils.typeOf(tree)); + self = new Unknown(tree); } - List lockExpressions = new ArrayList<>(); - for (String expression : expressions) { - lockExpressions.add(parseExpressionString(expression, exprContext, currentPath, self)); - } - return lockExpressions; + return CollectionsPlume.mapList( + expression -> parseExpressionString(expression, currentPath, self), expressions); } + /** + * Parse a Java expression. + * + * @param expression the Java expression + * @param path the path to the expression + * @param itself the self expression + * @return the parsed expression + */ private LockExpression parseExpressionString( - String expression, - FlowExpressionContext flowExprContext, - TreePath path, - Receiver itself) { + String expression, TreePath path, JavaExpression itself) { LockExpression lockExpression = new LockExpression(expression); if (DependentTypesError.isExpressionError(expression)) { @@ -1280,23 +1322,9 @@ private LockExpression parseExpressionString( } return lockExpression; } else { - // TODO: The proper way to do this is to call - // flowExprContext.copyChangeToParsingMemberOfReceiver to set the receiver to - // the expression, and then call FlowExpressionParseUtil.parse on the - // remaining expression string with the new flow expression context. However, - // this currently results in a FlowExpressions.Receiver that has a different - // hash code than if the following flow expression is parsed directly, which - // results in our inability to check that a lock expression is held as it does - // not match anything in the store due to the hash code mismatch. For now, - // convert the "" portion to the node's string representation, and parse - // the entire string: - lockExpression.lockExpression = - FlowExpressionParseUtil.parse( - itself.toString() + "." + remainingExpression, - flowExprContext, - path, - true); + StringToJavaExpression.atPath( + itself.toString() + "." + remainingExpression, path, checker); if (!atypeFactory.isExpressionEffectivelyFinal(lockExpression.lockExpression)) { checker.reportError( path.getLeaf(), @@ -1307,10 +1335,10 @@ private LockExpression parseExpressionString( } } else { lockExpression.lockExpression = - FlowExpressionParseUtil.parse(expression, flowExprContext, path, true); + StringToJavaExpression.atPath(expression, path, checker); return lockExpression; } - } catch (FlowExpressionParseException ex) { + } catch (JavaExpressionParseException ex) { lockExpression.error = new DependentTypesError(expression, ex); return lockExpression; } @@ -1318,7 +1346,7 @@ private LockExpression parseExpressionString( private static class LockExpression { final String expressionString; - Receiver lockExpression = null; + JavaExpression lockExpression = null; DependentTypesError error = null; LockExpression(String expression) { diff --git a/checker/src/main/java/org/checkerframework/checker/lock/messages.properties b/checker/src/main/java/org/checkerframework/checker/lock/messages.properties index 95564c2d977d..65e83541fd04 100644 --- a/checker/src/main/java/org/checkerframework/checker/lock/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/lock/messages.properties @@ -1,6 +1,6 @@ ### Error messages for the Lock Checker contracts.precondition.not.satisfied=call to '%s' requires '%s' to be held -override.sideeffect.invalid=the side-effect annotation on an overrider method must be at least as strong as the one the overridden method.%n %s in %s%n cannot override%n %s in %s +override.sideeffect.invalid=the side-effect annotation on an overrider method must be at least as strong as the one the overridden method.%nmethod in %s%n %s%n cannot override method in %s%n %s multiple.sideeffect.annotations=method is annotated with multiple side effect annotations multiple.lock.precondition.annotations=only one @Holding, @net.jcip.annotations.GuardedBy or @javax.annotation.concurrent.GuardedBy annotation is allowed on a method multiple.guardedby.annotations=only one @org.checkerframework.checker.lock.qual.GuardedBy, @net.jcip.annotations.GuardedBy or @javax.annotation.concurrent.GuardedBy annotation is allowed on a variable declaration @@ -18,3 +18,6 @@ synchronized.block.in.lockingfree.method=A synchronized block cannot be written lock.expression.not.final=lock expression includes a non-final field or a call to a method that is not pure or deterministic.%n lock expression: %s lock.expression.possibly.not.final=could not determine that the lock expression(s) include only non-final fields or calls to methods that are pure or deterministic.%n lock expression: %s lock.not.held=required lock not held.%nrequired: %s + +# This overrides of a message string in BaseTypeChecker +inconsistent.constructor.type=Constructor result type should not be @GuardedByUnknown or @GuardedByBottom; found %s diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/CreatesMustCallForElementSupplier.java b/checker/src/main/java/org/checkerframework/checker/mustcall/CreatesMustCallForElementSupplier.java new file mode 100644 index 000000000000..a055574c6c7d --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/CreatesMustCallForElementSupplier.java @@ -0,0 +1,30 @@ +package org.checkerframework.checker.mustcall; + +import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; + +import javax.lang.model.element.ExecutableElement; + +/** + * This interface should be implemented by all type factories that can provide an {@link + * ExecutableElement} for {@link CreatesMustCallFor} and {@link CreatesMustCallFor.List}. This + * interface is needed so any type factory with these elements can be used to retrieve information + * about these annotations, not just the MustCallAnnotatedTypeFactory (in particular, the + * consistency checker needs to be able to call that method with both the CalledMethods type factory + * and the MustCall type factory). + */ +public interface CreatesMustCallForElementSupplier { + + /** + * Returns the CreatesMustCallFor.value field/element. + * + * @return the CreatesMustCallFor.value field/element + */ + ExecutableElement getCreatesMustCallForValueElement(); + + /** + * Returns the CreatesMustCallFor.List.value field/element. + * + * @return the CreatesMustCallFor.List.value field/element + */ + ExecutableElement getCreatesMustCallForListValueElement(); +} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/CreatesMustCallForToJavaExpression.java b/checker/src/main/java/org/checkerframework/checker/mustcall/CreatesMustCallForToJavaExpression.java new file mode 100644 index 000000000000..66eaeaf0d262 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/CreatesMustCallForToJavaExpression.java @@ -0,0 +1,210 @@ +package org.checkerframework.checker.mustcall; + +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.Tree; + +import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.Unknown; +import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; +import org.checkerframework.framework.util.JavaExpressionParseUtil; +import org.checkerframework.framework.util.StringToJavaExpression; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; + +import java.util.ArrayList; +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; + +/** + * Utility methods to convert targets of {@code @CreatesMustCallFor} annotations to {@link + * org.checkerframework.dataflow.expression.JavaExpression}s. + */ +public class CreatesMustCallForToJavaExpression { + + /** static utility methods only; don't create instances */ + private CreatesMustCallForToJavaExpression() {} + + /** + * Returns the elements of the @CreatesMustCallFor annotations on the invoked method, as + * JavaExpressions. Returns the empty set if the given method has no @CreatesMustCallFor + * annotation. + * + *

    If any expression is unparseable, this method reports an error and returns the empty set. + * + * @param n a method invocation + * @param atypeFactory the type factory to report errors and parse the expression string + * @param supplier a type factory that can supply the executable elements for CreatesMustCallFor + * and CreatesMustCallFor.List's value elements. Usually, you should just pass atypeFactory + * again. The arguments are different so that the given type factory's adherence to both + * protocols are checked by the type system. + * @return the arguments of the method's @CreatesMustCallFor annotation, or an empty list + */ + public static List getCreatesMustCallForExpressionsAtInvocation( + MethodInvocationNode n, + GenericAnnotatedTypeFactory atypeFactory, + CreatesMustCallForElementSupplier supplier) { + List results = new ArrayList<>(1); + List createsMustCallForAnnos = + getCreatesMustCallForAnnos(n.getTarget().getMethod(), atypeFactory, supplier); + for (AnnotationMirror createsMustCallFor : createsMustCallForAnnos) { + JavaExpression expr = + getCreatesMustCallForExpression( + createsMustCallFor, + n.getTree(), + n.getTarget().getMethod().getSimpleName(), + atypeFactory, + supplier, + (s) -> + StringToJavaExpression.atMethodInvocation( + s, n, atypeFactory.getChecker())); + if (expr != null && !results.contains(expr)) { + results.add(expr); + } + } + return results; + } + + /** + * Returns the elements of the @CreatesMustCallFor annotations on the method declaration, as + * JavaExpressions. Returns the empty set if the given method has no @CreatesMustCallFor + * annotation. + * + *

    If any expression is unparseable, this method reports an error and returns the empty set. + * + * @param tree a method declaration + * @param atypeFactory the type factory to report errors and parse the expression string + * @param supplier a type factory that can supply the executable elements for CreatesMustCallFor + * and CreatesMustCallFor.List's value elements. Usually, you should just pass atypeFactory + * again. The arguments are different so that the given type factory's adherence to both + * protocols are checked by the type system. + * @return the arguments of the method's @CreatesMustCallFor annotation, or an empty list + */ + public static List getCreatesMustCallForExpressionsAtMethodDeclaration( + MethodTree tree, + GenericAnnotatedTypeFactory atypeFactory, + CreatesMustCallForElementSupplier supplier) { + List results = new ArrayList<>(1); + ExecutableElement method = TreeUtils.elementFromDeclaration(tree); + List createsMustCallForAnnos = + getCreatesMustCallForAnnos(method, atypeFactory, supplier); + for (AnnotationMirror createsMustCallFor : createsMustCallForAnnos) { + JavaExpression expr = + getCreatesMustCallForExpression( + createsMustCallFor, + tree, + method.getSimpleName(), + atypeFactory, + supplier, + (s) -> + StringToJavaExpression.atMethodBody( + s, tree, atypeFactory.getChecker())); + if (expr != null && !results.contains(expr)) { + results.add(expr); + } + } + return results; + } + + /** + * Returns the {@code CreatesMustCallFor} annotations on a method + * + * @param method the method + * @param atypeFactory the type factory to use for looking up annotations + * @param supplier supplier to use to get elements + * @return the {@code CreatesMustCallFor} annotations + */ + private static List getCreatesMustCallForAnnos( + ExecutableElement method, + GenericAnnotatedTypeFactory atypeFactory, + CreatesMustCallForElementSupplier supplier) { + AnnotationMirror createsMustCallForList = + atypeFactory.getDeclAnnotation(method, CreatesMustCallFor.List.class); + List result = new ArrayList<>(); + if (createsMustCallForList != null) { + // Handle a set of CreatesMustCallFor annotations. + result.addAll( + AnnotationUtils.getElementValueArray( + createsMustCallForList, + supplier.getCreatesMustCallForListValueElement(), + AnnotationMirror.class)); + } + AnnotationMirror createsMustCallFor = + atypeFactory.getDeclAnnotation(method, CreatesMustCallFor.class); + if (createsMustCallFor != null) { + result.add(createsMustCallFor); + } + return result; + } + + /** + * Parses a single CreatesMustCallFor annotation. If the target expression cannot be parsed, + * issues a checker error. + * + * @param createsMustCallFor a @CreatesMustCallFor annotation + * @param tree the tree on which to report an error if annotation cannot be parsed + * @param methodName name to use in error message if annotation cannot be parsed + * @param atypeFactory the type factory + * @param supplier a type factory that can supply the executable elements for CreatesMustCallFor + * and CreatesMustCallFor.List's value elements. Usually, you should just pass atypeFactory + * again. The arguments are different so that the given type factory's adherence to both + * protocols are checked by the type system. + * @param converter function to be used to create JavaExpression + * @return the Java expression representing the target, or null if the target is unparseable + */ + private static @Nullable JavaExpression getCreatesMustCallForExpression( + AnnotationMirror createsMustCallFor, + Tree tree, + Name methodName, + GenericAnnotatedTypeFactory atypeFactory, + CreatesMustCallForElementSupplier supplier, + StringToJavaExpression converter) { + // Unfortunately, there is no way to avoid passing the default string "this" here. The + // default must be hard-coded into the client, such as here. That is the price for the + // efficiency of not having to query the annotation definition (such queries are expensive). + String targetStrWithoutAdaptation = + AnnotationUtils.getElementValue( + createsMustCallFor, + supplier.getCreatesMustCallForValueElement(), + String.class, + "this"); + // TODO: find a way to also check if the target is a known tempvar, and if so return that. + // That should improve the quality of the error messages we give. + JavaExpression targetExpr; + try { + targetExpr = converter.toJavaExpression(targetStrWithoutAdaptation); + if (targetExpr instanceof Unknown) { + issueUnparseableError(tree, methodName, atypeFactory, targetStrWithoutAdaptation); + return null; + } + } catch (JavaExpressionParseUtil.JavaExpressionParseException e) { + issueUnparseableError(tree, methodName, atypeFactory, targetStrWithoutAdaptation); + return null; + } + return targetExpr; + } + + /** + * Issues a createsmustcallfor.target.unparseable error. + * + * @param tree the tree on which to report the error + * @param methodName method name to use in error message + * @param atypeFactory the type factory to use to issue the error + * @param unparseable the unparseable string + */ + private static void issueUnparseableError( + Tree tree, + Name methodName, + GenericAnnotatedTypeFactory atypeFactory, + String unparseable) { + atypeFactory + .getChecker() + .reportError( + tree, "createsmustcallfor.target.unparseable", methodName, unparseable); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/IOUtils.astub b/checker/src/main/java/org/checkerframework/checker/mustcall/IOUtils.astub new file mode 100644 index 000000000000..8f35d8febbd0 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/IOUtils.astub @@ -0,0 +1,14 @@ +package org.apache.commons.io; + +import org.checkerframework.checker.mustcall.qual.*; + +class IOUtils { + @NotOwning static InputStream toBufferedInputStream(InputStream arg0) throws IOException; + @NotOwning static BufferedReader toBufferedReader(Reader arg0); + @NotOwning static InputStream toInputStream(CharSequence arg0); + @NotOwning static InputStream toInputStream(CharSequence arg0, Charset arg1); + @NotOwning static InputStream toInputStream(CharSequence arg0, String arg1) throws IOException; + @NotOwning static InputStream toInputStream(String arg0); + @NotOwning static InputStream toInputStream(String arg0, Charset arg1); + @NotOwning static InputStream toInputStream(String arg0, String arg1) throws IOException; +} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/JavaEE.astub b/checker/src/main/java/org/checkerframework/checker/mustcall/JavaEE.astub new file mode 100644 index 000000000000..4fbf47d2b9f9 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/JavaEE.astub @@ -0,0 +1,18 @@ +// DO NOT INCLUDE ANNOTATIONS THAT RELY ON MUSTCALL CREATION IN THIS FILE. +// This file is loaded regardless of the -AnoCreatesMustCallFor option. All +// assumptions that rely on the presence of mustcall creation (such as @MustCall({}) +// on unconnected sockets) MUST go in SocketCreatesMustCallFor.astub. + +import org.checkerframework.checker.mustcall.qual.*; + +package javax.servlet; + +// This interface doesn't appear in the annotated JDK, because it is part +// Java EE not Java SE. Therefore, this annotated version appears here in a +// stub file rather than in typetools/jdk. +interface ServletResponse { + // The link below justifies why these annotations are correct + // https://stackoverflow.com/questions/1159168/should-one-call-close-on-httpservletresponse-getoutputstream-getwriter + @NotOwning ServletOutputStream getOutputStream() throws IOException; + @NotOwning PrintWriter getWriter() throws IOException; +} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/JdkCompiler.astub b/checker/src/main/java/org/checkerframework/checker/mustcall/JdkCompiler.astub new file mode 100644 index 000000000000..dd29b986d4e8 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/JdkCompiler.astub @@ -0,0 +1,6 @@ +import org.checkerframework.checker.mustcall.qual.*; + +package com.sun.tools.javac.processing; + +@MustCall({}) +public class JavacProcessingEnvironment implements ProcessingEnvironment, Closeable {} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java new file mode 100644 index 000000000000..904b933d58b9 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java @@ -0,0 +1,618 @@ +package org.checkerframework.checker.mustcall; + +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.MemberReferenceTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.Tree; + +import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.MustCallAlias; +import org.checkerframework.checker.mustcall.qual.MustCallUnknown; +import org.checkerframework.checker.mustcall.qual.Owning; +import org.checkerframework.checker.mustcall.qual.PolyMustCall; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.framework.flow.CFStore; +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.AnnotatedExecutableType; +import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; +import org.checkerframework.framework.type.QualifierHierarchy; +import org.checkerframework.framework.type.QualifierUpperBounds; +import org.checkerframework.framework.type.SubtypeIsSubsetQualifierHierarchy; +import org.checkerframework.framework.type.poly.DefaultQualifierPolymorphism; +import org.checkerframework.framework.type.poly.QualifierPolymorphism; +import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; +import org.checkerframework.framework.type.treeannotator.TreeAnnotator; +import org.checkerframework.framework.type.typeannotator.DefaultQualifierForUseTypeAnnotator; +import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; +import org.checkerframework.framework.type.typeannotator.TypeAnnotator; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationMirrorMap; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; +import org.checkerframework.javacutil.TypesUtils; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +/** + * The annotated type factory for the Must Call Checker. Primarily responsible for the subtyping + * rules between @MustCall annotations. + */ +public class MustCallAnnotatedTypeFactory extends BaseAnnotatedTypeFactory + implements CreatesMustCallForElementSupplier { + + /** The {@code @}{@link MustCallUnknown} annotation. */ + public final AnnotationMirror TOP; + + /** + * The {@code @}{@link MustCall}{@code ()} annotation. It is the default in unannotated code. + */ + public final AnnotationMirror BOTTOM; + + /** The {@code @}{@link PolyMustCall} annotation. */ + public final AnnotationMirror POLY; + + /** + * Map from trees representing expressions to the temporary variables that represent them in the + * store. + * + *

    Consider the following code, adapted from Apache Zookeeper: + * + *

    +     *   sock = SocketChannel.open();
    +     *   sock.socket().setSoLinger(false, -1);
    +     * 
    + * + * This code is safe from resource leaks: sock is an unconnected socket and therefore has no + * must-call obligation. The expression sock.socket() similarly has no must-call obligation + * because it is a resource alias, but without a temporary variable that represents that + * expression in the store, the resource leak checker wouldn't be able to determine that. + * + *

    These temporary variables are only created once---here---but are used by all three parts + * of the resource leak checker by calling {@link #getTempVar(Node)}. The temporary variables + * are shared in the same way that subcheckers share CFG structure; see {@link + * #getSharedCFGForTree(Tree)}. + */ + /*package-private*/ final IdentityHashMap tempVars = + new IdentityHashMap<>(100); + + /** The MustCall.value field/element. */ + private final ExecutableElement mustCallValueElement = + TreeUtils.getMethod(MustCall.class, "value", 0, processingEnv); + + /** The InheritableMustCall.value field/element. */ + /*package-private*/ final ExecutableElement inheritableMustCallValueElement = + TreeUtils.getMethod(InheritableMustCall.class, "value", 0, processingEnv); + + /** The CreatesMustCallFor.List.value field/element. */ + private final ExecutableElement createsMustCallForListValueElement = + TreeUtils.getMethod(CreatesMustCallFor.List.class, "value", 0, processingEnv); + + /** The CreatesMustCallFor.value field/element. */ + private final ExecutableElement createsMustCallForValueElement = + TreeUtils.getMethod(CreatesMustCallFor.class, "value", 0, processingEnv); + + /** True if -AnoLightweightOwnership was passed on the command line. */ + private final boolean noLightweightOwnership; + + /* NO-AFU + * True if -AenableWpiForRlc (see {@link ResourceLeakChecker#ENABLE_WPI_FOR_RLC}) was passed on + * the command line. + * + private final boolean enableWpiForRlc; + */ + + /** + * Creates a MustCallAnnotatedTypeFactory. + * + * @param checker the checker associated with this type factory + */ + public MustCallAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + TOP = AnnotationBuilder.fromClass(elements, MustCallUnknown.class); + BOTTOM = createMustCall(Collections.emptyList()); + POLY = AnnotationBuilder.fromClass(elements, PolyMustCall.class); + addAliasedTypeAnnotation(InheritableMustCall.class, MustCall.class, true); + if (!checker.hasOption(MustCallChecker.NO_RESOURCE_ALIASES)) { + // In NO_RESOURCE_ALIASES mode, all @MustCallAlias annotations are ignored. + addAliasedTypeAnnotation(MustCallAlias.class, POLY); + } + noLightweightOwnership = checker.hasOption(MustCallChecker.NO_LIGHTWEIGHT_OWNERSHIP); + // enableWpiForRlc = checker.hasOption(ResourceLeakChecker.ENABLE_WPI_FOR_RLC); + this.postInit(); + } + + @Override + public void setRoot(@Nullable CompilationUnitTree root) { + super.setRoot(root); + // TODO: This should probably be guarded by isSafeToClearSharedCFG from + // GenericAnnotatedTypeFactory, but this works here because we know the Must Call Checker is + // always the first subchecker that's sharing tempvars. + tempVars.clear(); + } + + @Override + protected Set> createSupportedTypeQualifiers() { + // Explicitly name the qualifiers, in order to exclude @MustCallAlias. + return new LinkedHashSet<>( + Arrays.asList(MustCall.class, MustCallUnknown.class, PolyMustCall.class)); + } + + @Override + protected TreeAnnotator createTreeAnnotator() { + return new ListTreeAnnotator(super.createTreeAnnotator(), new MustCallTreeAnnotator(this)); + } + + @Override + protected TypeAnnotator createTypeAnnotator() { + return new ListTypeAnnotator(super.createTypeAnnotator(), new MustCallTypeAnnotator(this)); + } + + /** + * Returns a {@literal @}MustCall annotation that is like the input, but it does not have + * "close". Returns the argument annotation mirror (not a new one) if the argument doesn't have + * "close" as one of its elements. + * + *

    If the argument is null, returns bottom. + * + * @param anno a MustCall annotation + * @return a MustCall annotation that does not have "close" as one of its values, but is + * otherwise identical to anno + */ + public AnnotationMirror withoutClose(@Nullable AnnotationMirror anno) { + if (anno == null || AnnotationUtils.areSame(anno, BOTTOM)) { + return BOTTOM; + } else if (!AnnotationUtils.areSameByName( + anno, "org.checkerframework.checker.mustcall.qual.MustCall")) { + return anno; + } + List values = + AnnotationUtils.getElementValueArray(anno, mustCallValueElement, String.class); + // Use `removeAll` because `remove` only removes the first occurrence. + if (values.removeAll(Collections.singletonList("close"))) { + return createMustCall(values); + } else { + return anno; + } + } + + /** Treat non-owning method parameters as @MustCallUnknown (top) when the method is called. */ + @Override + public void methodFromUsePreSubstitution(ExpressionTree tree, AnnotatedExecutableType type) { + ExecutableElement declaration; + if (tree instanceof MethodInvocationTree) { + declaration = TreeUtils.elementFromUse((MethodInvocationTree) tree); + } else if (tree instanceof MemberReferenceTree) { + declaration = (ExecutableElement) TreeUtils.elementFromUse(tree); + } else { + throw new TypeSystemError("unexpected type of method tree: " + tree.getKind()); + } + changeNonOwningParameterTypesToTop(declaration, type); + super.methodFromUsePreSubstitution(tree, type); + } + + @Override + protected void constructorFromUsePreSubstitution( + NewClassTree tree, AnnotatedExecutableType type) { + ExecutableElement declaration = TreeUtils.elementFromUse(tree); + changeNonOwningParameterTypesToTop(declaration, type); + super.constructorFromUsePreSubstitution(tree, type); + } + + /** + * Class to implement the customized semantics of {@link MustCallAlias} (and {@link + * PolyMustCall}) annotations; see the {@link MustCallAlias} documentation for details. + */ + private class MustCallQualifierPolymorphism extends DefaultQualifierPolymorphism { + /** + * Creates a {@link MustCallQualifierPolymorphism}. + * + * @param env the processing environment + * @param factory the factory for the current checker + */ + public MustCallQualifierPolymorphism( + ProcessingEnvironment env, AnnotatedTypeFactory factory) { + super(env, factory); + } + + @Override + protected void replace( + AnnotatedTypeMirror type, AnnotationMirrorMap replacements) { + AnnotationMirrorMap realReplacements = replacements; + AnnotationMirror extantPolyAnnoReplacement = null; + TypeElement typeElement = TypesUtils.getTypeElement(type.getUnderlyingType()); + // only customize replacement for type elements + if (typeElement != null) { + assert replacements.size() == 1 && replacements.containsKey(POLY); + extantPolyAnnoReplacement = replacements.get(POLY); + if (AnnotationUtils.areSameByName( + extantPolyAnnoReplacement, MustCall.class.getCanonicalName())) { + List extentReplacementVals = + AnnotationUtils.getElementValueArray( + extantPolyAnnoReplacement, + getMustCallValueElement(), + String.class, + Collections.emptyList()); + // replacement only customized when parameter type has a non-empty must-call + // obligation + if (!extentReplacementVals.isEmpty()) { + AnnotationMirror inheritableMustCall = + getDeclAnnotation(typeElement, InheritableMustCall.class); + if (inheritableMustCall != null) { + List inheritableMustCallVals = + AnnotationUtils.getElementValueArray( + inheritableMustCall, + inheritableMustCallValueElement, + String.class, + Collections.emptyList()); + if (!inheritableMustCallVals.equals(extentReplacementVals)) { + // Use the must call values from the @InheritableMustCall annotation + // instead. + // This allows for wrapper types to have a must-call method with a + // different name than the must-call method for the wrapped type + AnnotationMirror mustCall = createMustCall(inheritableMustCallVals); + realReplacements = new AnnotationMirrorMap<>(); + realReplacements.put(POLY, mustCall); + } + } + } + } + } + super.replace(type, realReplacements); + } + } + + @Override + protected QualifierPolymorphism createQualifierPolymorphism() { + return new MustCallQualifierPolymorphism(processingEnv, this); + } + + /** + * Changes the type of each parameter not annotated as @Owning to @MustCallUnknown (top). Also + * replaces the component type of the varargs array, if applicable. + * + *

    This method is not responsible for handling receivers, which can never be owning. + * + * @param declaration a method or constructor declaration + * @param type the method or constructor's type + */ + private void changeNonOwningParameterTypesToTop( + ExecutableElement declaration, AnnotatedExecutableType type) { + // Formal parameters without a declared owning annotation are disregarded by the RLC + // _analysis_, as their @MustCall obligation is set to Top in this method. However, this + // computation is not desirable for RLC _inference_ in unannotated programs, where a goal is + // to infer and add @Owning annotations to formal parameters. + /* NO-AFU + if (getWholeProgramInference() != null && !isWpiEnabledForRLC()) { + return; + } + */ + List parameterTypes = type.getParameterTypes(); + for (int i = 0; i < parameterTypes.size(); i++) { + Element paramDecl = declaration.getParameters().get(i); + if (noLightweightOwnership || getDeclAnnotation(paramDecl, Owning.class) == null) { + AnnotatedTypeMirror paramType = parameterTypes.get(i); + if (!paramType.hasAnnotation(POLY)) { + paramType.replaceAnnotation(TOP); + } + } + } + if (declaration.isVarArgs()) { + // also modify the component type of a varargs array + AnnotatedTypeMirror varargsType = + ((AnnotatedArrayType) parameterTypes.get(parameterTypes.size() - 1)) + .getComponentType(); + if (!varargsType.hasAnnotation(POLY)) { + varargsType.replaceAnnotation(TOP); + } + } + } + + @Override + protected DefaultQualifierForUseTypeAnnotator createDefaultForUseTypeAnnotator() { + return new MustCallDefaultQualifierForUseTypeAnnotator(); + } + + /** + * Returns the {@link MustCall#value} element. For use with {@link + * AnnotationUtils#getElementValueArray}. + * + * @return the {@link MustCall#value} element + */ + public ExecutableElement getMustCallValueElement() { + return mustCallValueElement; + } + + /** Support @InheritableMustCall meaning @MustCall on all subtype elements. */ + private class MustCallDefaultQualifierForUseTypeAnnotator + extends DefaultQualifierForUseTypeAnnotator { + + /** Creates a {@code MustCallDefaultQualifierForUseTypeAnnotator}. */ + public MustCallDefaultQualifierForUseTypeAnnotator() { + super(MustCallAnnotatedTypeFactory.this); + } + + @Override + protected AnnotationMirrorSet getExplicitAnnos(Element element) { + AnnotationMirrorSet explict = super.getExplicitAnnos(element); + if (explict.isEmpty() && ElementUtils.isTypeElement(element)) { + AnnotationMirror inheritableMustCall = + getDeclAnnotation(element, InheritableMustCall.class); + if (inheritableMustCall != null) { + List mustCallVal = + AnnotationUtils.getElementValueArray( + inheritableMustCall, + inheritableMustCallValueElement, + String.class); + return AnnotationMirrorSet.singleton(createMustCall(mustCallVal)); + } + } + return explict; + } + } + + @Override + protected QualifierUpperBounds createQualifierUpperBounds() { + return new MustCallQualifierUpperBounds(); + } + + /** Support @InheritableMustCall meaning @MustCall on all subtypes. */ + private class MustCallQualifierUpperBounds extends QualifierUpperBounds { + + /** + * Creates a {@link QualifierUpperBounds} from the MustCall Checker the annotations that are + * in the type hierarchy. + */ + public MustCallQualifierUpperBounds() { + super(MustCallAnnotatedTypeFactory.this); + } + + @Override + protected AnnotationMirrorSet getAnnotationFromElement(Element element) { + AnnotationMirrorSet explict = super.getAnnotationFromElement(element); + if (!explict.isEmpty()) { + return explict; + } + AnnotationMirror inheritableMustCall = + getDeclAnnotation(element, InheritableMustCall.class); + if (inheritableMustCall != null) { + List mustCallVal = + AnnotationUtils.getElementValueArray( + inheritableMustCall, inheritableMustCallValueElement, String.class); + return AnnotationMirrorSet.singleton(createMustCall(mustCallVal)); + } + return AnnotationMirrorSet.emptySet(); + } + } + + /** + * Cache of the MustCall annotations that have actually been created. Most programs require few + * distinct MustCall annotations (e.g. MustCall() and MustCall("close")). + */ + private final Map, AnnotationMirror> mustCallAnnotations = new HashMap<>(10); + + /** + * Creates a {@link MustCall} annotation whose values are the given strings. + * + * @param val the methods that should be called + * @return an annotation indicating that the given methods should be called + */ + public AnnotationMirror createMustCall(List val) { + return mustCallAnnotations.computeIfAbsent(val, this::createMustCallImpl); + } + + /** + * Creates a {@link MustCall} annotation whose values are the given strings. + * + *

    This internal version bypasses the cache, and is only used for new annotations. + * + * @param methodList the methods that should be called + * @return an annotation indicating that the given methods should be called + */ + private AnnotationMirror createMustCallImpl(List methodList) { + AnnotationBuilder builder = new AnnotationBuilder(processingEnv, MustCall.class); + String[] methodArray = methodList.toArray(new String[methodList.size()]); + Arrays.sort(methodArray); + builder.setValue("value", methodArray); + return builder.build(); + } + + @Override + protected QualifierHierarchy createQualifierHierarchy() { + return new MustCallQualifierHierarchy( + this.getSupportedTypeQualifiers(), this.getProcessingEnv(), this); + } + + /** + * Fetches the store from the results of dataflow for {@code first}. If {@code afterFirstStore} + * is true, then the store after {@code first} is returned; if {@code afterFirstStore} is false, + * the store before {@code succ} is returned. + * + * @param afterFirstStore whether to use the store after the first block or the store before its + * successor, succ + * @param first a block + * @param succ first's successor + * @return the appropriate CFStore, populated with MustCall annotations, from the results of + * running dataflow + */ + public CFStore getStoreForBlock(boolean afterFirstStore, Block first, Block succ) { + return afterFirstStore ? flowResult.getStoreAfter(first) : flowResult.getStoreBefore(succ); + } + + /** + * Returns the CreatesMustCallFor.value field/element. + * + * @return the CreatesMustCallFor.value field/element + */ + @Override + public ExecutableElement getCreatesMustCallForValueElement() { + return createsMustCallForValueElement; + } + + /** + * Returns the CreatesMustCallFor.List.value field/element. + * + * @return the CreatesMustCallFor.List.value field/element + */ + @Override + public ExecutableElement getCreatesMustCallForListValueElement() { + return createsMustCallForListValueElement; + } + + /** + * The TreeAnnotator for the MustCall type system. + * + *

    This tree annotator treats non-owning method parameters as bottom, regardless of their + * declared type, when they appear in the body of the method. Doing so is safe because being + * non-owning means, by definition, that their must-call obligations are only relevant in the + * callee. (This behavior is disabled if the {@code -AnoLightweightOwnership} option is passed + * to the checker.) + * + *

    The tree annotator also changes the type of resource variables to remove "close" from + * their must-call types, because the try-with-resources statement guarantees that close() is + * called on all such variables. + */ + private class MustCallTreeAnnotator extends TreeAnnotator { + /** + * Create a MustCallTreeAnnotator. + * + * @param mustCallAnnotatedTypeFactory the type factory + */ + public MustCallTreeAnnotator(MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory) { + super(mustCallAnnotatedTypeFactory); + } + + @Override + public Void visitIdentifier(IdentifierTree tree, AnnotatedTypeMirror type) { + Element elt = TreeUtils.elementFromUse(tree); + // The following changes are not desired for RLC _inference_ in unannotated programs, + // where a goal is to infer and add @Owning annotations to formal parameters. Therefore, + // if WPI is enabled, they should not be executed. + if ( // NO-AFU getWholeProgramInference() == null + elt.getKind() == ElementKind.PARAMETER + && (noLightweightOwnership || getDeclAnnotation(elt, Owning.class) == null)) { + if (!type.hasAnnotation(POLY)) { + // Parameters that are not annotated with @Owning should be treated as bottom + // (to suppress warnings about them). An exception is polymorphic parameters, + // which might be @MustCallAlias (and so wouldn't be annotated with @Owning): + // these are not modified, to support verification of @MustCallAlias + // annotations. + type.replaceAnnotation(BOTTOM); + } + } + return super.visitIdentifier(tree, type); + } + } + + /** + * Return the temporary variable for node, if it exists. See {@code #tempVars}. + * + * @param node a CFG node + * @return the corresponding temporary variable, or null if there is not one + */ + public @Nullable LocalVariableNode getTempVar(Node node) { + return tempVars.get(node.getTree()); + } + + /* NO-AFU + * Checks if WPI is enabled for the Resource Leak Checker inference. See {@link + * ResourceLeakChecker#ENABLE_WPI_FOR_RLC}. + * + * @return returns true if WPI is enabled for the Resource Leak Checker + * + protected boolean isWpiEnabledForRLC() { + return enableWpiForRlc; + } + */ + + /** + * Returns true if the given type should never have a must-call obligation. + * + * @param type the type to check + * @return true if the given type should never have a must-call obligation + */ + public boolean shouldHaveNoMustCallObligation(TypeMirror type) { + return type.getKind().isPrimitive() + || TypesUtils.isClass(type) + || TypesUtils.isString(type); + } + + /** Qualifier hierarchy for the Must Call Checker. */ + private class MustCallQualifierHierarchy extends SubtypeIsSubsetQualifierHierarchy { + + /** + * Creates a SubtypeIsSubsetQualifierHierarchy from the given classes. + * + * @param qualifierClasses classes of annotations that are the qualifiers for this hierarchy + * @param processingEnv processing environment + * @param atypeFactory the associated type factory + */ + public MustCallQualifierHierarchy( + Collection> qualifierClasses, + ProcessingEnvironment processingEnv, + GenericAnnotatedTypeFactory atypeFactory) { + super(qualifierClasses, processingEnv, atypeFactory); + } + + @Override + public boolean isSubtypeShallow( + AnnotationMirror subQualifier, + TypeMirror subType, + AnnotationMirror superQualifier, + TypeMirror superType) { + if (shouldHaveNoMustCallObligation(subType) + || shouldHaveNoMustCallObligation(superType)) { + return true; + } + return super.isSubtypeShallow(subQualifier, subType, superQualifier, superType); + } + + @Override + public @Nullable AnnotationMirror leastUpperBoundShallow( + AnnotationMirror qualifier1, + TypeMirror tm1, + AnnotationMirror qualifier2, + TypeMirror tm2) { + boolean tm1NoMustCall = shouldHaveNoMustCallObligation(tm1); + boolean tm2NoMustCall = shouldHaveNoMustCallObligation(tm2); + if (tm1NoMustCall == tm2NoMustCall) { + return super.leastUpperBoundShallow(qualifier1, tm1, qualifier2, tm2); + } else if (tm1NoMustCall) { + return qualifier1; + } else { // if (tm2NoMustCall) { + return qualifier2; + } + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallChecker.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallChecker.java new file mode 100644 index 000000000000..85265b53d555 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallChecker.java @@ -0,0 +1,42 @@ +package org.checkerframework.checker.mustcall; + +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.qual.StubFiles; +import org.checkerframework.framework.source.SupportedOptions; + +/** + * This typechecker ensures that {@code @}{@link MustCall} annotations are consistent with one + * another. The Resource Leak Checker verifies that the given methods are actually called. + */ +@StubFiles({ + "IOUtils.astub", + "JavaEE.astub", + "JdkCompiler.astub", + "Reflection.astub", + "SocketCreatesMustCallFor.astub", +}) +@SupportedOptions({ + MustCallChecker.NO_CREATES_MUSTCALLFOR, + MustCallChecker.NO_LIGHTWEIGHT_OWNERSHIP, + MustCallChecker.NO_RESOURCE_ALIASES +}) +public class MustCallChecker extends BaseTypeChecker { + + /** + * Disables @CreatesMustCallFor support. Not of interest to most users. Not documented in the + * manual. + */ + public static final String NO_CREATES_MUSTCALLFOR = "noCreatesMustCallFor"; + + /** + * Disables @Owning/@NotOwning support. Not of interest to most users. Not documented in the + * manual. + */ + public static final String NO_LIGHTWEIGHT_OWNERSHIP = "noLightweightOwnership"; + + /** + * Disables @MustCallAlias support. Not of interest to most users. Not documented in the manual. + */ + public static final String NO_RESOURCE_ALIASES = "noResourceAliases"; +} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallNoCreatesMustCallForChecker.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallNoCreatesMustCallForChecker.java new file mode 100644 index 000000000000..53b04aade45a --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallNoCreatesMustCallForChecker.java @@ -0,0 +1,25 @@ +package org.checkerframework.checker.mustcall; + +import org.checkerframework.framework.qual.StubFiles; +import org.checkerframework.framework.source.SupportedOptions; +import org.checkerframework.framework.source.SuppressWarningsPrefix; + +/** + * This copy of the Must Call Checker is identical, except that it does not load the stub files that + * treat unconnected sockets as {@code @MustCall({})}. See SocketCreatesMustCallFor.astub. + * + *

    The only difference is the contents of the @StubFiles annotation. + */ +@StubFiles({ + "JavaEE.astub", + "Reflection.astub", +}) +@SuppressWarningsPrefix({ + // Preferred checkername, so that warnings are suppressed regardless of the option passed. + "mustcall", + // Also supported, but will only suppress warnings from this checker (and not from the regular + // Must Call Checker). + "mustcallnocreatesmustcallfor" +}) +@SupportedOptions({MustCallChecker.NO_CREATES_MUSTCALLFOR}) +public class MustCallNoCreatesMustCallForChecker extends MustCallChecker {} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallTransfer.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallTransfer.java new file mode 100644 index 000000000000..cc74d571f915 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallTransfer.java @@ -0,0 +1,341 @@ +package org.checkerframework.checker.mustcall; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePath; + +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.TransferInput; +import org.checkerframework.dataflow.analysis.TransferResult; +import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; +import org.checkerframework.dataflow.cfg.node.StringConversionNode; +import org.checkerframework.dataflow.cfg.node.SwitchExpressionNode; +import org.checkerframework.dataflow.cfg.node.TernaryExpressionNode; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.framework.flow.CFAnalysis; +import org.checkerframework.framework.flow.CFStore; +import org.checkerframework.framework.flow.CFTransfer; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.javacutil.TreePathUtil; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; +import org.checkerframework.javacutil.trees.TreeBuilder; + +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.type.TypeMirror; + +/** + * Transfer function for the must-call type system. Its primary purposes are (1) to create temporary + * variables for expressions (which allow those expressions to have refined information in the + * store, which the consistency checker can use), and (2) to reset refined information when a method + * annotated with @CreatesMustCallFor is called. + */ +public class MustCallTransfer extends CFTransfer { + + /** For building new AST nodes. */ + private final TreeBuilder treeBuilder; + + /** The type factory. */ + private final MustCallAnnotatedTypeFactory atypeFactory; + + /** + * A cache for the default type for java.lang.String, to avoid needing to look it up for every + * implicit string conversion. See {@link #getDefaultStringType(StringConversionNode)}. + */ + private @MonotonicNonNull AnnotationMirror defaultStringType; + + /** True if -AnoCreatesMustCallFor was passed on the command line. */ + private final boolean noCreatesMustCallFor; + + /* NO-AFU + * True if -AenableWpiForRlc was passed on the command line. See {@link + * ResourceLeakChecker#ENABLE_WPI_FOR_RLC}. + * + private final boolean enableWpiForRlc; + */ + + /** + * Create a MustCallTransfer. + * + * @param analysis the analysis + */ + public MustCallTransfer(CFAnalysis analysis) { + super(analysis); + atypeFactory = (MustCallAnnotatedTypeFactory) analysis.getTypeFactory(); + noCreatesMustCallFor = + atypeFactory.getChecker().hasOption(MustCallChecker.NO_CREATES_MUSTCALLFOR); + // enableWpiForRlc = + // atypeFactory.getChecker().hasOption(ResourceLeakChecker.ENABLE_WPI_FOR_RLC); + ProcessingEnvironment env = atypeFactory.getChecker().getProcessingEnvironment(); + treeBuilder = new TreeBuilder(env); + } + + @Override + public TransferResult visitStringConversion( + StringConversionNode n, TransferInput p) { + // Implicit String conversions should assume that the String's type is + // whatever the default for String is, not that the conversion is polymorphic. + TransferResult result = super.visitStringConversion(n, p); + LocalVariableNode temp = getOrCreateTempVar(n); + if (temp != null) { + AnnotationMirror defaultStringType = getDefaultStringType(n); + JavaExpression localExp = JavaExpression.fromNode(temp); + insertIntoStores(result, localExp, defaultStringType); + } + return result; + } + + /** + * Returns the default type for java.lang.String, which is cached in this class to avoid + * recomputing it. If the cache is currently unset, this method sets it. + * + * @param n a string conversion node, from which the type is computed if it is required + * @return the type of java.lang.String + */ + private AnnotationMirror getDefaultStringType(StringConversionNode n) { + if (this.defaultStringType == null) { + this.defaultStringType = + atypeFactory + .getAnnotatedType(TypesUtils.getTypeElement(n.getType())) + .getAnnotationInHierarchy(atypeFactory.TOP); + assert this.defaultStringType != null : "@AssumeAssertion(nullness): same hierarchy"; + } + return this.defaultStringType; + } + + @Override + public TransferResult visitMethodInvocation( + MethodInvocationNode n, TransferInput in) { + TransferResult result = super.visitMethodInvocation(n, in); + + updateStoreWithTempVar(result, n); + if (!noCreatesMustCallFor) { + List targetExprs = + CreatesMustCallForToJavaExpression.getCreatesMustCallForExpressionsAtInvocation( + n, atypeFactory, atypeFactory); + for (JavaExpression targetExpr : targetExprs) { + AnnotationMirror defaultType = + atypeFactory + .getAnnotatedType(TypesUtils.getTypeElement(targetExpr.getType())) + .getAnnotationInHierarchy(atypeFactory.TOP); + + if (result.containsTwoStores()) { + CFStore thenStore = result.getThenStore(); + lubWithStoreValue(thenStore, targetExpr, defaultType); + + CFStore elseStore = result.getElseStore(); + lubWithStoreValue(elseStore, targetExpr, defaultType); + } else { + CFStore store = result.getRegularStore(); + lubWithStoreValue(store, targetExpr, defaultType); + } + } + } + return result; + } + + /** + * Computes the LUB of the current value in the store for expr, if it exists, and defaultType. + * Inserts that LUB into the store as the new value for expr. + * + * @param store a CFStore + * @param expr an expression that might be in the store + * @param defaultType the default type of the expression's static type + */ + private void lubWithStoreValue( + CFStore store, JavaExpression expr, AnnotationMirror defaultType) { + CFValue value = store.getValue(expr); + CFValue defaultTypeAsCFValue = + analysis.createSingleAnnotationValue(defaultType, expr.getType()); + CFValue newValue = defaultTypeAsCFValue.leastUpperBound(value); + store.clearValue(expr); + store.insertValue(expr, newValue); + } + + /* NO-AFU + * See {@link ResourceLeakChecker#ENABLE_WPI_FOR_RLC}. + * + * @param tree a tree + * @return false if Resource Leak Checker is running as one of the upstream checkers and the + * -AenableWpiForRlc flag is not passed as a command line argument, otherwise returns the + * result of the super call + */ + /* NO-AFU + @Override + protected boolean shouldPerformWholeProgramInference(Tree tree) { + if (!isWpiEnabledForRLC() + && atypeFactory.getCheckerNames().contains(ResourceLeakChecker.class.getCanonicalName())) { + return false; + } + return super.shouldPerformWholeProgramInference(tree); + } + /* + + /* NO-AFU + * See {@link ResourceLeakChecker#ENABLE_WPI_FOR_RLC}. + * + * @param expressionTree a tree + * @param lhsTree its element + * @return false if Resource Leak Checker is running as one of the upstream checkers and the + * -AenableWpiForRlc flag is not passed as a command line argument, otherwise returns the + * result of the super call + */ + /* NO-AFU + @Override + protected boolean shouldPerformWholeProgramInference(Tree expressionTree, Tree lhsTree) { + if (!isWpiEnabledForRLC() + && atypeFactory.getCheckerNames().contains(ResourceLeakChecker.class.getCanonicalName())) { + return false; + } + return super.shouldPerformWholeProgramInference(expressionTree, lhsTree); + } + */ + + @Override + public TransferResult visitObjectCreation( + ObjectCreationNode node, TransferInput input) { + TransferResult result = super.visitObjectCreation(node, input); + updateStoreWithTempVar(result, node); + return result; + } + + @Override + public TransferResult visitTernaryExpression( + TernaryExpressionNode node, TransferInput input) { + TransferResult result = super.visitTernaryExpression(node, input); + if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { + // Add the synthetic variable created during CFG construction to the temporary + // variable map (rather than creating a redundant temp var) + atypeFactory.tempVars.put(node.getTree(), node.getTernaryExpressionVar()); + } + return result; + } + + @Override + public TransferResult visitSwitchExpressionNode( + SwitchExpressionNode node, TransferInput input) { + TransferResult result = super.visitSwitchExpressionNode(node, input); + if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { + // Add the synthetic variable created during CFG construction to the temporary + // variable map (rather than creating a redundant temp var) + atypeFactory.tempVars.put(node.getTree(), node.getSwitchExpressionVar()); + } + return result; + } + + /** + * This method either creates or looks up the temp var t for node, and then updates the store to + * give t the same type as {@code node}. + * + * @param node the node to be assigned to a temporary variable + * @param result the transfer result containing the store to be modified + */ + public void updateStoreWithTempVar(TransferResult result, Node node) { + // Must-call obligations on primitives are not supported. + if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { + LocalVariableNode temp = getOrCreateTempVar(node); + if (temp != null) { + JavaExpression localExp = JavaExpression.fromNode(temp); + AnnotationMirror anm = + atypeFactory + .getAnnotatedType(node.getTree()) + .getAnnotationInHierarchy(atypeFactory.TOP); + insertIntoStores(result, localExp, anm == null ? atypeFactory.TOP : anm); + } + } + } + + /** + * Either returns the temporary variable associated with node, or creates one if one does not + * exist. + * + * @param node a node, which must be an expression (not a statement) + * @return a temporary variable node representing {@code node} that can be placed into a store + */ + private @Nullable LocalVariableNode getOrCreateTempVar(Node node) { + LocalVariableNode localVariableNode = atypeFactory.tempVars.get(node.getTree()); + if (localVariableNode == null) { + VariableTree temp = createTemporaryVar(node); + if (temp != null) { + IdentifierTree identifierTree = treeBuilder.buildVariableUse(temp); + localVariableNode = new LocalVariableNode(identifierTree); + localVariableNode.setInSource(true); + atypeFactory.tempVars.put(node.getTree(), localVariableNode); + } + } + return localVariableNode; + } + + /** + * Creates a variable declaration for the given expression node, if possible. + * + *

    Note that error reporting code assumes that the names of temporary variables are not legal + * Java identifiers (see JLS 3.8). + * The temporary variable names generated here include an {@code '-'} character to make the + * names invalid. + * + * @param node an expression node + * @return a variable tree for the node, or null if an appropriate containing element cannot be + * located + */ + protected @Nullable VariableTree createTemporaryVar(Node node) { + ExpressionTree tree = (ExpressionTree) node.getTree(); + TypeMirror treeType = TreeUtils.typeOf(tree); + Element enclosingElement; + TreePath path = atypeFactory.getPath(tree); + if (path == null) { + enclosingElement = TreeUtils.elementFromUse(tree).getEnclosingElement(); + } else { + ClassTree classTree = TreePathUtil.enclosingClass(path); + enclosingElement = TreeUtils.elementFromDeclaration(classTree); + } + if (enclosingElement == null) { + return null; + } + // Declare and initialize a new, unique variable + VariableTree tmpVarTree = + treeBuilder.buildVariableDecl( + treeType, uniqueName("temp-var"), enclosingElement, tree); + return tmpVarTree; + } + + /** A unique identifier counter for node names. */ + private static AtomicLong uid = new AtomicLong(); + + /** + * Creates a unique, arbitrary string that can be used as a name for a temporary variable, using + * the given prefix. Can be used up to Long.MAX_VALUE times. + * + *

    Note that the correctness of the Resource Leak Checker depends on these names actually + * being unique, because {@code LocalVariableNode}s derived from them are used as keys in a map. + * + * @param prefix the prefix for the name + * @return a unique name that starts with the prefix + */ + protected String uniqueName(String prefix) { + return prefix + "-" + uid.getAndIncrement(); + } + + /* NO-AFU + * Checks if WPI is enabled for the Resource Leak Checker inference. See {@link + * ResourceLeakChecker#ENABLE_WPI_FOR_RLC}. + * + * @return returns true if WPI is enabled for the Resource Leak Checker + * + protected boolean isWpiEnabledForRLC() { + return enableWpiForRlc; + } + */ +} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallTypeAnnotator.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallTypeAnnotator.java new file mode 100644 index 000000000000..a60ac7db4463 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallTypeAnnotator.java @@ -0,0 +1,23 @@ +package org.checkerframework.checker.mustcall; + +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedPrimitiveType; +import org.checkerframework.framework.type.typeannotator.TypeAnnotator; + +/** Primitive types always have no must-call obligations. */ +public class MustCallTypeAnnotator extends TypeAnnotator { + + /** + * Create a MustCallTypeAnnotator. + * + * @param typeFactory the type factory + */ + protected MustCallTypeAnnotator(MustCallAnnotatedTypeFactory typeFactory) { + super(typeFactory); + } + + @Override + public Void visitPrimitive(AnnotatedPrimitiveType type, Void aVoid) { + type.replaceAnnotation(((MustCallAnnotatedTypeFactory) atypeFactory).BOTTOM); + return super.visitPrimitive(type, aVoid); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java new file mode 100644 index 000000000000..c0f96db13cae --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/MustCallVisitor.java @@ -0,0 +1,352 @@ +package org.checkerframework.checker.mustcall; + +import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.Tree; + +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.MustCallAlias; +import org.checkerframework.checker.mustcall.qual.NotOwning; +import org.checkerframework.checker.mustcall.qual.Owning; +import org.checkerframework.checker.mustcall.qual.PolyMustCall; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreePathUtil; +import org.checkerframework.javacutil.TreeUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +/** + * The visitor for the Must Call Checker. This visitor is similar to BaseTypeVisitor, but overrides + * methods that don't work well with the MustCall type hierarchy because it doesn't use the top type + * as the default type. + */ +public class MustCallVisitor extends BaseTypeVisitor { + + /** True if -AnoLightweightOwnership was passed on the command line. */ + private final boolean noLightweightOwnership; + + /** + * Creates a new MustCallVisitor. + * + * @param checker the type-checker associated with this visitor + */ + public MustCallVisitor(BaseTypeChecker checker) { + super(checker); + noLightweightOwnership = checker.hasOption(MustCallChecker.NO_LIGHTWEIGHT_OWNERSHIP); + } + + @Override + public Void visitReturn(ReturnTree tree, Void p) { + // Only check return types if ownership is being transferred. + if (!noLightweightOwnership) { + MethodTree enclosingMethod = TreePathUtil.enclosingMethod(this.getCurrentPath()); + // enclosingMethod is null if this return site is inside a lambda. TODO: handle lambdas + // more precisely? + if (enclosingMethod != null) { + ExecutableElement methodElt = TreeUtils.elementFromDeclaration(enclosingMethod); + AnnotationMirror notOwningAnno = + atypeFactory.getDeclAnnotation(methodElt, NotOwning.class); + if (notOwningAnno != null) { + // Skip return type subtyping check, because not-owning pointer means Object + // Construction Checker won't check anyway. + return null; + } + } + } + return super.visitReturn(tree, p); + } + + @Override + public Void visitAssignment(AssignmentTree tree, Void p) { + // This code implements the following rule: + // * It is always safe to assign a MustCallAlias parameter of a constructor + // to an owning field of the containing class. + // It is necessary to special case this because MustCallAlias is translated + // into @PolyMustCall, so the common assignment check will fail when assigning + // an @MustCallAlias parameter to an owning field: the parameter is polymorphic, + // but the field is not. + ExpressionTree lhs = tree.getVariable(); + ExpressionTree rhs = tree.getExpression(); + Element lhsElt = TreeUtils.elementFromTree(lhs); + Element rhsElt = TreeUtils.elementFromTree(rhs); + if (lhsElt != null && rhsElt != null) { + // Note that it is not necessary to check that the assignment is to a field of this, + // because that is implied by the other conditions: + // * if the field is final, then the only place it can be assigned to is in the + // constructor of the proper object (enforced by javac). + // * if the field is not final, then it cannot be assigned to in a constructor at all: + // the @CreatesMustCallFor annotation cannot be written on a constructor (it has + // @Target({ElementType.METHOD})), so this code relies on the standard rules for + // non-final owning field reassignment, which prevent it without an + // @CreatesMustCallFor annotation except in the constructor of the object containing + // the field. + boolean lhsIsOwningField = + lhs.getKind() == Tree.Kind.MEMBER_SELECT + && atypeFactory.getDeclAnnotation(lhsElt, Owning.class) != null; + boolean rhsIsMCA = + AnnotationUtils.containsSameByClass( + rhsElt.getAnnotationMirrors(), MustCallAlias.class); + boolean rhsIsConstructorParam = + rhsElt.getKind() == ElementKind.PARAMETER + && rhsElt.getEnclosingElement().getKind() == ElementKind.CONSTRUCTOR; + if (lhsIsOwningField && rhsIsMCA && rhsIsConstructorParam) { + // Do not execute common assignment check. + return null; + } + } + + return super.visitAssignment(tree, p); + } + + /** An empty string list. */ + private static final List emptyStringList = Collections.emptyList(); + + @Override + protected boolean validateType(Tree tree, AnnotatedTypeMirror type) { + if (TreeUtils.isClassTree(tree)) { + TypeElement classEle = TreeUtils.elementFromDeclaration((ClassTree) tree); + // If no @InheritableMustCall annotation is written here, `getDeclAnnotation()` gets one + // from stub files and supertypes. + AnnotationMirror anyInheritableMustCall = + atypeFactory.getDeclAnnotation(classEle, InheritableMustCall.class); + // An @InheritableMustCall annotation that is directly present. + AnnotationMirror directInheritableMustCall = + AnnotationUtils.getAnnotationByClass( + classEle.getAnnotationMirrors(), InheritableMustCall.class); + if (anyInheritableMustCall == null) { + if (!ElementUtils.isFinal(classEle)) { + // There is no @InheritableMustCall annotation on this or any superclass and + // this is a non-final class. + // If an explicit @MustCall annotation is present, issue a warning suggesting + // that @InheritableMustCall is probably what the programmer means, for + // usability. + if (atypeFactory.getDeclAnnotation(classEle, MustCall.class) != null) { + checker.reportWarning( + tree, + "mustcall.not.inheritable", + ElementUtils.getQualifiedName(classEle)); + } + } + } else { + // There is an @InheritableMustCall annotation on this, on a superclass, or in an + // annotation file. + // There are two possible problems: + // 1. There is an inconsistent @MustCall on this. + // 2. There is an explicit @InheritableMustCall here, and it is inconsistent with + // an @InheritableMustCall annotation on a supertype. + + // Check for problem 1. + AnnotationMirror explicitMustCall = + atypeFactory + .fromElement(classEle) + .getAnnotationInHierarchy(atypeFactory.TOP); + if (explicitMustCall != null) { + // There is a @MustCall annotation here. + + List inheritableMustCallVal = + AnnotationUtils.getElementValueArray( + anyInheritableMustCall, + atypeFactory.inheritableMustCallValueElement, + String.class, + emptyStringList); + AnnotationMirror inheritedMCAnno = + atypeFactory.createMustCall(inheritableMustCallVal); + + // Issue an error if there is an inconsistent, user-written @MustCall annotation + // here. + AnnotationMirror effectiveMCAnno = + type.getAnnotationInHierarchy(atypeFactory.TOP); + TypeMirror tm = type.getUnderlyingType(); + if (effectiveMCAnno != null + && !qualHierarchy.isSubtypeShallow( + inheritedMCAnno, effectiveMCAnno, tm)) { + + checker.reportError( + tree, + "inconsistent.mustcall.subtype", + ElementUtils.getQualifiedName(classEle), + effectiveMCAnno, + anyInheritableMustCall); + return false; + } + } + + // Check for problem 2. + if (directInheritableMustCall != null) { + + // `inheritedImcs` is inherited @InheritableMustCall annotations. + List inheritedImcs = new ArrayList<>(); + for (TypeElement elt : + ElementUtils.getDirectSuperTypeElements(classEle, elements)) { + AnnotationMirror imc = + atypeFactory.getDeclAnnotation(elt, InheritableMustCall.class); + if (imc != null) { + inheritedImcs.add(imc); + } + } + if (!inheritedImcs.isEmpty()) { + // There is an inherited @InheritableMustCall annotation, in addition to the + // one written explicitly here. + List inheritedMustCallVal = new ArrayList<>(); + for (AnnotationMirror inheritedImc : inheritedImcs) { + inheritedMustCallVal.addAll( + AnnotationUtils.getElementValueArray( + inheritedImc, + atypeFactory.inheritableMustCallValueElement, + String.class)); + } + AnnotationMirror inheritedMCAnno = + atypeFactory.createMustCall(inheritedMustCallVal); + + AnnotationMirror effectiveMCAnno = + type.getAnnotationInHierarchy(atypeFactory.TOP); + + TypeMirror tm = type.getUnderlyingType(); + + if (!qualHierarchy.isSubtypeShallow(inheritedMCAnno, effectiveMCAnno, tm)) { + + checker.reportError( + tree, + "inconsistent.mustcall.subtype", + ElementUtils.getQualifiedName(classEle), + effectiveMCAnno, + inheritedMCAnno); + return false; + } + } + } + } + } + return super.validateType(tree, type); + } + + @Override + public boolean isValidUse( + AnnotatedDeclaredType declarationType, AnnotatedDeclaredType useType, Tree tree) { + // MustCallAlias annotations are always permitted on type uses, despite not technically + // being a part of the type hierarchy. It's necessary to get the annotation from the element + // because MustCallAlias is aliased to PolyMustCall, which is what useType would contain. + // Note that isValidUse does not need to consider component types, on which it should be + // called separately. + Element elt = TreeUtils.elementFromTree(tree); + if (elt != null) { + if (AnnotationUtils.containsSameByClass( + elt.getAnnotationMirrors(), MustCallAlias.class)) { + return true; + } + // Need to check the type mirror for ajava-derived annotations and the element itself + // for human-written annotations from the source code. Getting to the ajava file + // directly at this point is impossible, so we approximate "the ajava file has an + // @MustCallAlias annotation" with "there is an @PolyMustCall annotation on the use + // type, but not in the source code". This only works because none of our inference + // techniques infer @PolyMustCall, so if @PolyMustCall is present but wasn't in the + // source, it must have been derived from an @MustCallAlias annotation (which we do + // infer). + boolean ajavaFileHasMustCallAlias = + useType.hasAnnotation(PolyMustCall.class) + && !AnnotationUtils.containsSameByClass( + elt.getAnnotationMirrors(), PolyMustCall.class); + if (ajavaFileHasMustCallAlias) { + return true; + } + } + return super.isValidUse(declarationType, useType, tree); + } + + @Override + protected boolean skipReceiverSubtypeCheck( + MethodInvocationTree tree, + AnnotatedTypeMirror methodDefinitionReceiver, + AnnotatedTypeMirror methodCallReceiver) { + // It does not make sense for receivers to have must-call obligations. If the receiver of a + // method were to have a non-empty must-call obligation, then actually this method should + // be part of the must-call annotation on the class declaration! So skipping this check is + // always sound. + return true; + } + + /** + * This method typically issues a warning if the result type of the constructor is not top, + * because in top-default type systems that indicates a potential problem. The Must Call Checker + * does not need this warning, because it expects the type of all constructors to be {@code + * MustCall({})} (by default) or some other {@code MustCall} type, not the top type. + * + *

    Instead, this method checks that the result type of a constructor is a supertype of the + * declared type on the class, if one exists. + * + * @param constructorType an AnnotatedExecutableType for the constructor + * @param constructorElement element that declares the constructor + */ + @Override + protected void checkConstructorResult( + AnnotatedExecutableType constructorType, ExecutableElement constructorElement) { + AnnotatedTypeMirror defaultType = + atypeFactory.getAnnotatedType( + ElementUtils.enclosingTypeElement(constructorElement)); + AnnotationMirror defaultAnno = defaultType.getAnnotationInHierarchy(atypeFactory.TOP); + AnnotatedTypeMirror resultType = constructorType.getReturnType(); + AnnotationMirror resultAnno = resultType.getAnnotationInHierarchy(atypeFactory.TOP); + if (!qualHierarchy.isSubtypeShallow( + defaultAnno, + defaultType.getUnderlyingType(), + resultAnno, + resultType.getUnderlyingType())) { + checker.reportError( + constructorElement, "inconsistent.constructor.type", resultAnno, defaultAnno); + } + } + + /** + * Change the default for exception parameter lower bounds to bottom (the default), to prevent + * false positives. This is unsound; see the discussion on + * https://github.com/typetools/checker-framework/issues/3839. + * + *

    TODO: change checking of throws clauses to require that the thrown exception + * is @MustCall({}). This would probably eliminate most of the same false positives, without + * adding undue false positives. + * + * @return a set containing only the @MustCall({}) annotation + */ + @Override + protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { + return new AnnotationMirrorSet(atypeFactory.BOTTOM); + } + + /** + * Does not issue any warnings. + * + *

    This implementation prevents recursing into annotation arguments. Annotation arguments are + * literals, which don't have must-call obligations. + * + *

    Annotation arguments are treated as return locations for the purposes of defaulting, + * rather than parameter locations. This causes them to default incorrectly when the annotation + * is defined in bytecode. See https://github.com/typetools/checker-framework/issues/3178 for an + * explanation of why this is necessary to avoid false positives. + */ + @Override + public Void visitAnnotation(AnnotationTree tree, Void p) { + return null; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/Reflection.astub b/checker/src/main/java/org/checkerframework/checker/mustcall/Reflection.astub new file mode 100644 index 000000000000..ed22694ed0b4 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/Reflection.astub @@ -0,0 +1,21 @@ +// Contains assumptions about reflection. In particular, this +// makes the results of reflection the default (i.e. bottom) type +// in the Must Call type system. This choice is technically unsound: +// if e.g. the java.net.Socket constructor is invoked via reflection, +// then our checker will fail to track it. For usability, we found it +// much more useful to ignore these warnings than to issue them. +// If you wish to see warnings related to reflection, remove this stub file +// when running the checker. + +// TODO: make this configurable so that users can easily turn this assumption off, +// e.g. via a command-line option. For now, keep this as a stub file rather than +// moving it to the annotated JDK so that re-compiling the checker without this +// assumption is easy if we ever need to do it. + +package java.lang.reflect; + +import org.checkerframework.checker.mustcall.qual.MustCall; + +class Constructor { + @MustCall({}) T newInstance(Object... initArgs); +} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/SocketCreatesMustCallFor.astub b/checker/src/main/java/org/checkerframework/checker/mustcall/SocketCreatesMustCallFor.astub new file mode 100644 index 000000000000..8dd1f7f624aa --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/SocketCreatesMustCallFor.astub @@ -0,0 +1,41 @@ +// This version of the standard assumptions for sockets is intended for use with obligation creation support (i.e. +// without the -AnoCreatesMustCallFor argument to the checker). It includes @MustCall({}) annotations for no-argument +// socket constructors, which would be unsound if obligation creation support was disabled. The checker chooses +// whether to use it automatically, when invoked via the Resource Leak Checker. + +package java.net; + + +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + + +class Socket implements Closeable { + @MustCall({}) Socket(); + @MustCall({}) Socket(Proxy arg0); +} + +class ServerSocket implements Closeable { + @MustCall({}) ServerSocket() throws IOException; +} + +package javax.net; + +class SocketFactory { + @Owning @MustCall({}) Socket createSocket() throws IOException; +} + +class ServerSocketFactory { + @Owning @MustCall({}) ServerSocket createServerSocket() throws IOException; +} + +package java.nio.channels; + +class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel { + static @MustCall({}) SocketChannel open() throws IOException; +} + +class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel { + static @MustCall({}) ServerSocketChannel open() throws IOException; +} diff --git a/checker/src/main/java/org/checkerframework/checker/mustcall/messages.properties b/checker/src/main/java/org/checkerframework/checker/mustcall/messages.properties new file mode 100644 index 000000000000..d179aed103cf --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/mustcall/messages.properties @@ -0,0 +1,3 @@ +inconsistent.mustcall.subtype=%s is annotated as %s, but one of its supertypes is annotated %s +createsmustcallfor.target.unparseable=The method %s is annotated as @CreatesMustCallFor, but the target (%s) was unparseable in the current context. Rewrite your code so that the relevant expression is a local variable. +mustcall.not.inheritable=@MustCall annotation is not inherited by subclasses. Use @InheritableMustCall instead. diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/CollectionToArrayHeuristics.java b/checker/src/main/java/org/checkerframework/checker/nullness/CollectionToArrayHeuristics.java index ad0e46de384a..a7108d5eec0d 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/CollectionToArrayHeuristics.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/CollectionToArrayHeuristics.java @@ -5,14 +5,7 @@ import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.NewArrayTree; import com.sun.source.tree.Tree; -import java.util.Collection; -import java.util.List; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; + import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.value.qual.ArrayLen; import org.checkerframework.framework.type.AnnotatedTypeMirror; @@ -24,6 +17,16 @@ import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; +import java.util.Collection; +import java.util.List; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + /** * Determines the nullness type of calls to {@link java.util.Collection#toArray()}. * @@ -35,19 +38,27 @@ public class CollectionToArrayHeuristics { /** The processing environment. */ private final ProcessingEnvironment processingEnv; + /** The checker, used for issuing diagnostic messages. */ private final BaseTypeChecker checker; + /** The type factory. */ - private final NullnessAnnotatedTypeFactory atypeFactory; + private final NullnessNoInitAnnotatedTypeFactory atypeFactory; + + /** Whether to trust {@code @ArrayLen(0)} annotations. */ + private final boolean trustArrayLenZero; + + /** The Collection type. */ + private final AnnotatedDeclaredType collectionType; /** The Collection.toArray(T[]) method. */ private final ExecutableElement collectionToArrayE; + /** The Collection.size() method. */ private final ExecutableElement size; - /** The Collection type. */ - private final AnnotatedDeclaredType collectionType; - /** Whether to trust {@code @ArrayLen(0)} annotations. */ - private final boolean trustArrayLenZero; + + /** The ArrayLen.value field/element. */ + private final ExecutableElement arrayLenValueElement; /** * Create a CollectionToArrayHeuristics. @@ -56,19 +67,17 @@ public class CollectionToArrayHeuristics { * @param factory the type factory */ public CollectionToArrayHeuristics( - BaseTypeChecker checker, NullnessAnnotatedTypeFactory factory) { + BaseTypeChecker checker, NullnessNoInitAnnotatedTypeFactory factory) { this.processingEnv = checker.getProcessingEnvironment(); this.checker = checker; this.atypeFactory = factory; - this.collectionToArrayE = - TreeUtils.getMethod( - java.util.Collection.class.getName(), "toArray", processingEnv, "T[]"); - this.size = - TreeUtils.getMethod(java.util.Collection.class.getName(), "size", 0, processingEnv); this.collectionType = - factory.fromElement( - processingEnv.getElementUtils().getTypeElement("java.util.Collection")); + factory.fromElement(ElementUtils.getTypeElement(processingEnv, Collection.class)); + this.collectionToArrayE = + TreeUtils.getMethod("java.util.Collection", "toArray", processingEnv, "T[]"); + this.size = TreeUtils.getMethod("java.util.Collection", "size", 0, processingEnv); + this.arrayLenValueElement = TreeUtils.getMethod(ArrayLen.class, "value", 0, processingEnv); this.trustArrayLenZero = checker.getLintOption( @@ -101,9 +110,9 @@ public void handle(MethodInvocationTree tree, AnnotatedExecutableType method) { if (receiverIsNonNull && !argIsHandled) { if (argument.getKind() != Tree.Kind.NEW_ARRAY) { - checker.reportWarning(tree, "toArray.nullable.elements.not.newarray"); + checker.reportWarning(tree, "toarray.nullable.elements.not.newarray"); } else { - checker.reportWarning(tree, "toArray.nullable.elements.mismatched.size"); + checker.reportWarning(tree, "toarray.nullable.elements.mismatched.size"); } } } @@ -175,10 +184,10 @@ private boolean isArrayLenZeroFieldAccess(ExpressionTree argument) { if (t.getKind() == TypeKind.ARRAY) { List ams = t.getAnnotationMirrors(); for (AnnotationMirror am : ams) { - if (AnnotationUtils.areSameByClass(am, ArrayLen.class)) { + if (atypeFactory.areSameByClass(am, ArrayLen.class)) { List lens = AnnotationUtils.getElementValueArray( - am, "value", Integer.class, false); + am, arrayLenValueElement, Integer.class); if (lens.size() == 1 && lens.get(0) == 0) { return true; } diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnalysis.java b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnalysis.java index 7125cb110a0b..a6f18ffccb2f 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnalysis.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnalysis.java @@ -1,31 +1,24 @@ package org.checkerframework.checker.nullness; -import java.util.List; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.TypeMirror; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFAbstractValue; -import org.checkerframework.javacutil.Pair; +import org.checkerframework.javacutil.AnnotationMirrorSet; -/** Boiler plate code to glue together all the parts the KeyFor dataflow classes. */ -public class KeyForAnalysis extends CFAbstractAnalysis { +import javax.lang.model.type.TypeMirror; - public KeyForAnalysis( - BaseTypeChecker checker, - KeyForAnnotatedTypeFactory factory, - List> fieldValues, - int maxCountBeforeWidening) { - super(checker, factory, fieldValues, maxCountBeforeWidening); - } +/** Boilerplate code to glue together all the parts the KeyFor dataflow classes. */ +public class KeyForAnalysis extends CFAbstractAnalysis { - public KeyForAnalysis( - BaseTypeChecker checker, - KeyForAnnotatedTypeFactory factory, - List> fieldValues) { - super(checker, factory, fieldValues); + /** + * Creates a new {@code KeyForAnalysis}. + * + * @param checker the checker + * @param factory the factory + */ + public KeyForAnalysis(BaseTypeChecker checker, KeyForAnnotatedTypeFactory factory) { + super(checker, factory); } @Override @@ -39,10 +32,10 @@ public KeyForStore createCopiedStore(KeyForStore store) { } @Override - public KeyForValue createAbstractValue( - Set annotations, TypeMirror underlyingType) { + public @Nullable KeyForValue createAbstractValue( + AnnotationMirrorSet annotations, TypeMirror underlyingType) { - if (!CFAbstractValue.validateSet(annotations, underlyingType, qualifierHierarchy)) { + if (!CFAbstractValue.validateSet(annotations, underlyingType, atypeFactory)) { return null; } return new KeyForValue(this, annotations, underlyingType); diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnnotatedTypeFactory.java index 1f968444b1c3..f358b435bc2d 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnnotatedTypeFactory.java @@ -3,79 +3,98 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.NewClassTree; import com.sun.source.tree.Tree; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.AnnotationValue; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.TypeKind; + import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.KeyForBottom; import org.checkerframework.checker.nullness.qual.PolyKeyFor; import org.checkerframework.checker.nullness.qual.UnknownKeyFor; +import org.checkerframework.checker.signature.qual.CanonicalName; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.util.NodeUtils; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.DefaultTypeHierarchy; import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.framework.type.TypeHierarchy; +import org.checkerframework.framework.type.SubtypeIsSupersetQualifierHierarchy; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.framework.util.GraphQualifierHierarchy; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreeUtils; +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; + public class KeyForAnnotatedTypeFactory extends GenericAnnotatedTypeFactory< KeyForValue, KeyForStore, KeyForTransfer, KeyForAnalysis> { - /** The @{@link KeyFor} annotation. */ - protected final AnnotationMirror KEYFOR = AnnotationBuilder.fromClass(elements, KeyFor.class); /** The @{@link UnknownKeyFor} annotation. */ protected final AnnotationMirror UNKNOWNKEYFOR = AnnotationBuilder.fromClass(elements, UnknownKeyFor.class); + /** The @{@link KeyForBottom} annotation. */ protected final AnnotationMirror KEYFORBOTTOM = AnnotationBuilder.fromClass(elements, KeyForBottom.class); /** The canonical name of the KeyFor class. */ - protected final String KEYFOR_NAME = KeyFor.class.getCanonicalName(); + protected static final @CanonicalName String KEYFOR_NAME = KeyFor.class.getCanonicalName(); /** The Map.containsKey method. */ private final ExecutableElement mapContainsKey = TreeUtils.getMethod("java.util.Map", "containsKey", 1, processingEnv); + /** The Map.get method. */ private final ExecutableElement mapGet = TreeUtils.getMethod("java.util.Map", "get", 1, processingEnv); + /** The Map.put method. */ private final ExecutableElement mapPut = TreeUtils.getMethod("java.util.Map", "put", 2, processingEnv); + /** The KeyFor.value field/element. */ + protected final ExecutableElement keyForValueElement = + TreeUtils.getMethod(KeyFor.class, "value", 0, processingEnv); + + /** Moves annotations from one side of a pseudo-assignment to the other. */ private final KeyForPropagator keyForPropagator = new KeyForPropagator(UNKNOWNKEYFOR); - /** Create a new KeyForAnnotatedTypeFactory. */ + /** + * If true, assume the argument to Map.get is always a key for the receiver map. This is set by + * the `-AassumeKeyFor` command-line argument. However, if the Nullness Checker is being run, + * then `-AassumeKeyFor` disables the Map Key Checker. + */ + private final boolean assumeKeyFor; + + /** + * Creates a new KeyForAnnotatedTypeFactory. + * + * @param checker the associated checker + */ public KeyForAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker, true); + assumeKeyFor = checker.hasOption("assumeKeyFor"); + // Add compatibility annotations: - addAliasedAnnotation( + addAliasedTypeAnnotation( "org.checkerframework.checker.nullness.compatqual.KeyForDecl", KeyFor.class, true); - addAliasedAnnotation( + addAliasedTypeAnnotation( "org.checkerframework.checker.nullness.compatqual.KeyForType", KeyFor.class, true); + // While strictly required for soundness, this leads to too many false positives. Printing + // a key or putting it in a map erases all knowledge of what maps it was a key for. + // TODO: Revisit when side effect annotations are more precise. + // sideEffectsUnrefineAliases = true; + this.postInit(); } @@ -93,15 +112,6 @@ public ParameterizedExecutableType constructorFromUse(NewClassTree tree) { return result; } - @Override - protected TypeHierarchy createTypeHierarchy() { - return new KeyForTypeHierarchy( - checker, - getQualifierHierarchy(), - checker.getBooleanOption("ignoreRawTypeArguments", true), - checker.hasOption("invariantArrays")); - } - @Override protected TreeAnnotator createTreeAnnotator() { return new ListTreeAnnotator( @@ -109,43 +119,10 @@ protected TreeAnnotator createTreeAnnotator() { new KeyForPropagationTreeAnnotator(this, keyForPropagator)); } - // TODO: work on removing this class - protected static class KeyForTypeHierarchy extends DefaultTypeHierarchy { - - public KeyForTypeHierarchy( - BaseTypeChecker checker, - QualifierHierarchy qualifierHierarchy, - boolean ignoreRawTypes, - boolean invariantArrayComponents) { - super(checker, qualifierHierarchy, ignoreRawTypes, invariantArrayComponents); - } - - @Override - protected boolean isSubtype( - AnnotatedTypeMirror subtype, AnnotatedTypeMirror supertype, AnnotationMirror top) { - // TODO: THIS IS FROM THE OLD TYPE HIERARCHY. WE SHOULD FIX DATA-FLOW/PROPAGATION TO DO - // THE RIGHT THING - if (supertype.getKind() == TypeKind.TYPEVAR && subtype.getKind() == TypeKind.TYPEVAR) { - // TODO: Investigate whether there is a nicer and more proper way to - // get assignments between two type variables working. - if (supertype.getAnnotations().isEmpty()) { - return true; - } - } - - // Otherwise Covariant would cause trouble. - if (subtype.hasAnnotation(KeyForBottom.class)) { - return true; - } - return super.isSubtype(subtype, supertype, top); - } - } - @Override - protected KeyForAnalysis createFlowAnalysis( - List> fieldValues) { + protected KeyForAnalysis createFlowAnalysis() { // Explicitly call the constructor instead of using reflection. - return new KeyForAnalysis(checker, this, fieldValues); + return new KeyForAnalysis(checker, this); } @Override @@ -155,10 +132,13 @@ public KeyForTransfer createFlowTransferFunction( return new KeyForTransfer((KeyForAnalysis) analysis); } - /* + /** * Given a string array 'values', returns an AnnotationMirror corresponding to @KeyFor(values) + * + * @param values the values for the {@code @KeyFor} annotation + * @return a {@code @KeyFor} annotation with the given values */ - public AnnotationMirror createKeyForAnnotationMirrorWithValue(LinkedHashSet values) { + public AnnotationMirror createKeyForAnnotationMirrorWithValue(Set values) { // Create an AnnotationBuilder with the ArrayList AnnotationBuilder builder = new AnnotationBuilder(getProcessingEnv(), KeyFor.class); builder.setValue("value", values.toArray()); @@ -167,14 +147,14 @@ public AnnotationMirror createKeyForAnnotationMirrorWithValue(LinkedHashSet values = new LinkedHashSet<>(); - values.add(value); - return createKeyForAnnotationMirrorWithValue(values); + return createKeyForAnnotationMirrorWithValue(Collections.singleton(value)); } /** @@ -185,11 +165,18 @@ public AnnotationMirror createKeyForAnnotationMirrorWithValue(String value) { * @return whether or not the expression is a key for the map */ public boolean isKeyForMap(String mapExpression, ExpressionTree tree) { + // This test only has an effect if the Map Key Checker is being run on its own. If the + // Nullness Checker is being run, then -AassumeKeyFor disables the Map Key Checker. + if (assumeKeyFor) { + return true; + } Collection maps = null; AnnotatedTypeMirror type = getAnnotatedType(tree); - AnnotationMirror keyForAnno = type.getAnnotation(KeyFor.class); + AnnotationMirror keyForAnno = type.getEffectiveAnnotation(KeyFor.class); if (keyForAnno != null) { - maps = AnnotationUtils.getElementValueArray(keyForAnno, "value", String.class, false); + maps = + AnnotationUtils.getElementValueArray( + keyForAnno, keyForValueElement, String.class); } else { KeyForValue value = getInferredValueFor(tree); if (value != null) { @@ -201,108 +188,9 @@ public boolean isKeyForMap(String mapExpression, ExpressionTree tree) { } @Override - public QualifierHierarchy createQualifierHierarchy(MultiGraphFactory factory) { - return new KeyForQualifierHierarchy(factory); - } - - private final class KeyForQualifierHierarchy extends GraphQualifierHierarchy { - - public KeyForQualifierHierarchy(MultiGraphFactory factory) { - super(factory, KEYFORBOTTOM); - } - - private List extractValues(AnnotationMirror anno) { - Map valMap = - anno.getElementValues(); - - List res; - if (valMap.isEmpty()) { - res = new ArrayList<>(); - } else { - res = AnnotationUtils.getElementValueArray(anno, "value", String.class, true); - } - return res; - } - - @Override - public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { - if (AnnotationUtils.areSameByName(superAnno, KEYFOR_NAME) - && AnnotationUtils.areSameByName(subAnno, KEYFOR_NAME)) { - List lhsValues = extractValues(superAnno); - List rhsValues = extractValues(subAnno); - - return rhsValues.containsAll(lhsValues); - } - // Ignore annotation values to ensure that annotation is in supertype map. - if (AnnotationUtils.areSameByName(superAnno, KEYFOR_NAME)) { - superAnno = KEYFOR; - } - if (AnnotationUtils.areSameByName(subAnno, KEYFOR_NAME)) { - subAnno = KEYFOR; - } - // TODO: the erased TypeMirror will be used. Can we store that already here? - return super.isSubtype(subAnno, superAnno); - } - - @Override - public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2) { - if (AnnotationUtils.areSameByName(a1, UNKNOWNKEYFOR)) { - return a1; - } else if (AnnotationUtils.areSameByName(a2, UNKNOWNKEYFOR)) { - return a2; - } else if (AnnotationUtils.areSameByName(a1, KEYFORBOTTOM)) { - return a2; - } else if (AnnotationUtils.areSameByName(a2, KEYFORBOTTOM)) { - return a1; - } else if (AnnotationUtils.areSameByName(a1, KEYFOR) - && AnnotationUtils.areSameByName(a2, KEYFOR)) { - List a1Values = extractValues(a1); - List a2Values = extractValues(a2); - LinkedHashSet set = new LinkedHashSet<>(a1Values); - set.retainAll(a2Values); - return createKeyForAnnotationMirrorWithValue(set); - } - // a1 or a2 is @PolyKeyFor. - // Ignore annotation values to ensure that annotation is in supertype map. - if (AnnotationUtils.areSameByName(a1, KEYFOR)) { - a1 = KEYFOR; - } - if (AnnotationUtils.areSameByName(a2, KEYFOR)) { - a2 = KEYFOR; - } - // Let super handle @PolyKeyFor. - return super.leastUpperBound(a1, a2); - } - - @Override - public AnnotationMirror greatestLowerBound(AnnotationMirror a1, AnnotationMirror a2) { - if (AnnotationUtils.areSameByName(a1, UNKNOWNKEYFOR)) { - return a2; - } else if (AnnotationUtils.areSameByName(a2, UNKNOWNKEYFOR)) { - return a1; - } else if (AnnotationUtils.areSameByName(a1, KEYFORBOTTOM)) { - return a1; - } else if (AnnotationUtils.areSameByName(a2, KEYFORBOTTOM)) { - return a2; - } else if (AnnotationUtils.areSameByName(a1, KEYFOR) - && AnnotationUtils.areSameByName(a2, KEYFOR)) { - List a1Values = extractValues(a1); - List a2Values = extractValues(a2); - LinkedHashSet set = new LinkedHashSet<>(a1Values); - set.addAll(a2Values); - return createKeyForAnnotationMirrorWithValue(set); - } - // a1 or a2 is @PolyKeyFor. - // Ignore annotation values to ensure that annotation is in supertype map. - if (AnnotationUtils.areSameByName(a1, KEYFOR)) { - a1 = KEYFOR; - } - if (AnnotationUtils.areSameByName(a2, KEYFOR)) { - a2 = KEYFOR; - } - // Let super handle @PolyKeyFor. - return super.greatestLowerBound(a1, a2); - } + protected QualifierHierarchy createQualifierHierarchy() { + return new SubtypeIsSupersetQualifierHierarchy( + getSupportedTypeQualifiers(), processingEnv, KeyForAnnotatedTypeFactory.this); } /** Returns true if the node is an invocation of Map.containsKey. */ diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java index 40a3ee930312..abacdbcbbfeb 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagationTreeAnnotator.java @@ -3,8 +3,7 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.NewClassTree; import com.sun.source.tree.VariableTree; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.type.TypeKind; + import org.checkerframework.checker.nullness.KeyForPropagator.PropagationDirection; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeMirror; @@ -12,6 +11,9 @@ import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.javacutil.TreeUtils; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; + /** * For the following initializations we wish to propagate the annotations from the left-hand side to * the right-hand side or vice versa: @@ -53,11 +55,7 @@ public KeyForPropagationTreeAnnotator( super(atypeFactory); this.keyForPropagator = propagationTreeAnnotator; keySetMethod = - TreeUtils.getMethod( - java.util.Map.class.getName(), - "keySet", - 0, - atypeFactory.getProcessingEnv()); + TreeUtils.getMethod("java.util.Map", "keySet", 0, atypeFactory.getProcessingEnv()); } /** @@ -80,12 +78,11 @@ public Void visitVariable(VariableTree variableTree, AnnotatedTypeMirror type) { // This should only happen on Map.keySet(); if (type.getKind() == TypeKind.DECLARED) { - final ExpressionTree initializer = variableTree.getInitializer(); + ExpressionTree initializer = variableTree.getInitializer(); if (isCallToKeyset(initializer)) { - final AnnotatedDeclaredType variableType = (AnnotatedDeclaredType) type; - final AnnotatedTypeMirror initializerType = - atypeFactory.getAnnotatedType(initializer); + AnnotatedDeclaredType variableType = (AnnotatedDeclaredType) type; + AnnotatedTypeMirror initializerType = atypeFactory.getAnnotatedType(initializer); // Propagate just for declared (class) types, not for array types, boxed primitives, // etc. @@ -104,9 +101,9 @@ public Void visitVariable(VariableTree variableTree, AnnotatedTypeMirror type) { /** Transfers annotations to type if the left hand side is a variable declaration. */ @Override - public Void visitNewClass(NewClassTree node, AnnotatedTypeMirror type) { + public Void visitNewClass(NewClassTree tree, AnnotatedTypeMirror type) { keyForPropagator.propagateNewClassTree( - node, type, (KeyForAnnotatedTypeFactory) atypeFactory); - return super.visitNewClass(node, type); + tree, type, (KeyForAnnotatedTypeFactory) atypeFactory); + return super.visitNewClass(tree, type); } } diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java index f22bf7fd6087..0cee0aee4abc 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForPropagator.java @@ -2,23 +2,26 @@ import com.sun.source.tree.NewClassTree; import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; import com.sun.source.util.TreePath; + +import org.checkerframework.checker.nullness.qual.UnknownKeyFor; +import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.AnnotatedTypeReplacer; +import org.checkerframework.framework.util.TypeArgumentMapper; +import org.checkerframework.javacutil.TreePathUtil; +import org.checkerframework.javacutil.TreeUtils; +import org.plumelib.util.IPair; + import java.util.List; import java.util.Set; -import javax.annotation.processing.ProcessingEnvironment; + import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeKind; import javax.lang.model.util.Types; -import org.checkerframework.framework.type.AnnotatedTypeFactory; -import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; -import org.checkerframework.framework.type.AnnotatedTypeReplacer; -import org.checkerframework.framework.util.TypeArgumentMapper; -import org.checkerframework.framework.util.typeinference.TypeArgInferenceUtil; -import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.Pair; /** * KeyForPropagator is used to move nested KeyFor annotations in type arguments from one side of a @@ -28,7 +31,7 @@ * @see org.checkerframework.checker.nullness.KeyForPropagationTreeAnnotator */ public class KeyForPropagator { - public static enum PropagationDirection { + public enum PropagationDirection { // transfer FROM the super type to the subtype TO_SUBTYPE, @@ -41,11 +44,23 @@ public static enum PropagationDirection { BOTH } - // The top type of the KeyFor hierarchy, this class will replace @UnknownKeyFor annotations. - // It will also add annotations when they are misisng for types that require primary annotation - // (i.e. not TypeVars, Wildcards, Intersections, or Unions). + /** + * The top type of the KeyFor hierarchy. + * + *

    This class will replace @UnknownKeyFor annotations. It will also add annotations when they + * are missing for types that require primary annotation (i.e. not TypeVars, Wildcards, + * Intersections, or Unions). + */ private final AnnotationMirror UNKNOWN_KEYFOR; + /** Instance of {@link KeyForPropagationReplacer}. */ + private final KeyForPropagationReplacer replacer = new KeyForPropagationReplacer(); + + /** + * Creates a KeyForPropagator + * + * @param unknownKeyfor an {@link UnknownKeyFor} annotation + */ public KeyForPropagator(AnnotationMirror unknownKeyfor) { this.UNKNOWN_KEYFOR = unknownKeyfor; } @@ -81,14 +96,13 @@ public KeyForPropagator(AnnotationMirror unknownKeyfor) { * String, @KeyFor("b") List<@KeyFor("c") String>>} */ public void propagate( - final AnnotatedDeclaredType subtype, - final AnnotatedDeclaredType supertype, + AnnotatedDeclaredType subtype, + AnnotatedDeclaredType supertype, PropagationDirection direction, - final AnnotatedTypeFactory typeFactory) { - final TypeElement subtypeElement = (TypeElement) subtype.getUnderlyingType().asElement(); - final TypeElement supertypeElement = - (TypeElement) supertype.getUnderlyingType().asElement(); - final Types types = typeFactory.getProcessingEnv().getTypeUtils(); + AnnotatedTypeFactory typeFactory) { + TypeElement subtypeElement = (TypeElement) subtype.getUnderlyingType().asElement(); + TypeElement supertypeElement = (TypeElement) supertype.getUnderlyingType().asElement(); + Types types = typeFactory.getProcessingEnv().getTypeUtils(); // Note: The right hand side of this or expression will cover raw types if (subtype.getTypeArguments().isEmpty()) { @@ -96,25 +110,22 @@ public void propagate( } // else // this can happen for two reasons: - // 1) the subclass introduced NEW type arguments when the superclass had none - // 2) the supertype was RAW. + // 1) the subclass introduced NEW type arguments when the superclass had none + // 2) the supertype was RAW. // In either case, there is no reason to propagate if (supertype.getTypeArguments().isEmpty()) { return; } - Set> typeParamMappings = + Set> typeParamMappings = TypeArgumentMapper.mapTypeArgumentIndices(subtypeElement, supertypeElement, types); - KeyForPropagationReplacer replacer = - new KeyForPropagationReplacer(typeFactory.getProcessingEnv()); - - final List subtypeArgs = subtype.getTypeArguments(); - final List supertypeArgs = supertype.getTypeArguments(); + List subtypeArgs = subtype.getTypeArguments(); + List supertypeArgs = supertype.getTypeArguments(); - for (final Pair path : typeParamMappings) { - final AnnotatedTypeMirror subtypeArg = subtypeArgs.get(path.first); - final AnnotatedTypeMirror supertypeArg = supertypeArgs.get(path.second); + for (IPair path : typeParamMappings) { + AnnotatedTypeMirror subtypeArg = subtypeArgs.get(path.first); + AnnotatedTypeMirror supertypeArg = supertypeArgs.get(path.second); if (subtypeArg.getKind() == TypeKind.WILDCARD || supertypeArg.getKind() == TypeKind.WILDCARD) { @@ -151,19 +162,24 @@ public void propagateNewClassTree( NewClassTree newClassTree, AnnotatedTypeMirror type, KeyForAnnotatedTypeFactory atypeFactory) { - Pair context = - atypeFactory.getVisitorState().getAssignmentContext(); - if (type.getKind() != TypeKind.DECLARED || context == null || context.first == null) { + if (type.getKind() != TypeKind.DECLARED || TreeUtils.isDiamondTree(newClassTree)) { return; } TreePath path = atypeFactory.getPath(newClassTree); if (path == null) { return; } - AnnotatedTypeMirror assignedTo = TypeArgInferenceUtil.assignedTo(atypeFactory, path); - if (assignedTo == null) { + Tree assignmentContext = TreePathUtil.getAssignmentContext(path); + AnnotatedTypeMirror assignedTo; + if (assignmentContext instanceof VariableTree) { + if (TreeUtils.isVariableTreeDeclaredUsingVar((VariableTree) assignmentContext)) { + return; + } + assignedTo = atypeFactory.getAnnotatedTypeLhs(assignmentContext); + } else { return; } + // array types and boxed primitives etc don't require propagation if (assignedTo.getKind() == TypeKind.DECLARED) { propagate( @@ -175,33 +191,19 @@ public void propagateNewClassTree( } /** - * An annotated type replacer that replaces @KeyFor annotations and only if the type that is - * receiving an annotation has an @UnknownKeyFor annotation or NO key for annotations. + * An {@link AnnotatedTypeReplacer} that copies the annotation in KeyFor hierarchy from the + * first types to the second type, if the second type is annotated with @UnknownKeyFor or has no + * annotation in the KeyFor hierarchy. */ private class KeyForPropagationReplacer extends AnnotatedTypeReplacer { - - /** The processing environment. */ - private final ProcessingEnvironment env; - - /** - * Create a new replacer. - * - * @param env the processing environment - */ - private KeyForPropagationReplacer(ProcessingEnvironment env) { - this.env = env; - } - @Override protected void replaceAnnotations(AnnotatedTypeMirror from, AnnotatedTypeMirror to) { - final AnnotationMirror fromKeyFor = from.getAnnotationInHierarchy(UNKNOWN_KEYFOR); - final AnnotationMirror toKeyFor = to.getAnnotationInHierarchy(UNKNOWN_KEYFOR); - - boolean toNeedsAnnotation = - toKeyFor == null || AnnotationUtils.areSame(toKeyFor, UNKNOWN_KEYFOR); - if (fromKeyFor != null && toNeedsAnnotation) { - AnnotationBuilder annotationBuilder = new AnnotationBuilder(env, fromKeyFor); - to.replaceAnnotation(annotationBuilder.build()); + AnnotationMirror fromKeyFor = from.getAnnotationInHierarchy(UNKNOWN_KEYFOR); + if (fromKeyFor != null) { + if (to.hasAnnotation(UNKNOWN_KEYFOR) + || to.getAnnotationInHierarchy(UNKNOWN_KEYFOR) == null) { + to.replaceAnnotation(fromKeyFor); + } } } } diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForSubchecker.java b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForSubchecker.java index 3ab7b9c2ce5a..1aa71bcf0c2f 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForSubchecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForSubchecker.java @@ -2,6 +2,8 @@ import org.checkerframework.common.basetype.BaseTypeChecker; +import javax.annotation.processing.SupportedOptions; + /** * A type-checker for determining which values are keys for which maps. Typically used as part of * the compound checker for the nullness type system. @@ -9,4 +11,5 @@ * @checker_framework.manual #map-key-checker Map Key Checker * @checker_framework.manual #nullness-checker Nullness Checker */ +@SupportedOptions({"assumeKeyFor"}) public class KeyForSubchecker extends BaseTypeChecker {} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForTransfer.java b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForTransfer.java index 23dd2bb57753..3061400b3062 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForTransfer.java @@ -1,17 +1,22 @@ package org.checkerframework.checker.nullness; -import java.util.LinkedHashSet; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; import org.checkerframework.checker.nullness.qual.KeyFor; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.framework.flow.CFAbstractTransfer; import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; /** * KeyForTransfer ensures that java.util.Map.put and containsKey cause the appropriate @KeyFor @@ -19,8 +24,20 @@ */ public class KeyForTransfer extends CFAbstractTransfer { + /** The KeyFor.value element/field. */ + protected final ExecutableElement keyForValueElement; + + /** + * Creates a new KeyForTransfer. + * + * @param analysis the analysis + */ public KeyForTransfer(KeyForAnalysis analysis) { super(analysis); + + ProcessingEnvironment processingEnv = + ((KeyForAnnotatedTypeFactory) analysis.getTypeFactory()).getProcessingEnv(); + keyForValueElement = TreeUtils.getMethod(KeyFor.class, "value", 0, processingEnv); } /* @@ -39,14 +56,14 @@ public TransferResult visitMethodInvocation( if (factory.isMapContainsKey(node) || factory.isMapPut(node)) { Node receiver = node.getTarget().getReceiver(); - Receiver internalReceiver = FlowExpressions.internalReprOf(factory, receiver); - String mapName = internalReceiver.toString(); - Receiver keyReceiver = FlowExpressions.internalReprOf(factory, node.getArgument(0)); + JavaExpression receiverJe = JavaExpression.fromNode(receiver); + String mapName = receiverJe.toString(); + JavaExpression keyExpr = JavaExpression.fromNode(node.getArgument(0)); LinkedHashSet keyForMaps = new LinkedHashSet<>(); keyForMaps.add(mapName); - final KeyForValue previousKeyValue = in.getValueOfSubNode(node.getArgument(0)); + KeyForValue previousKeyValue = in.getValueOfSubNode(node.getArgument(0)); if (previousKeyValue != null) { for (AnnotationMirror prevAm : previousKeyValue.getAnnotations()) { if (prevAm != null && factory.areSameByClass(prevAm, KeyFor.class)) { @@ -58,23 +75,30 @@ public TransferResult visitMethodInvocation( AnnotationMirror am = factory.createKeyForAnnotationMirrorWithValue(keyForMaps); if (factory.isMapContainsKey(node)) { - result.getThenStore().insertValue(keyReceiver, am); - } else { // method is Map.put - result.getThenStore().insertValue(keyReceiver, am); - result.getElseStore().insertValue(keyReceiver, am); + // method is Map.containsKey + result.getThenStore().insertValue(keyExpr, am); + } else { + // method is Map.put + result.getThenStore().insertValue(keyExpr, am); + result.getElseStore().insertValue(keyExpr, am); } } return result; } - /** @return the String value of a KeyFor, this will throw an exception */ - private Set getKeys(final AnnotationMirror keyFor) { + /** + * Returns the elements/arguments of a {@code @KeyFor} annotation. + * + * @param keyFor a {@code @KeyFor} annotation + * @return the elements/arguments of a {@code @KeyFor} annotation + */ + private Set getKeys(AnnotationMirror keyFor) { if (keyFor.getElementValues().isEmpty()) { - return new LinkedHashSet<>(); + return Collections.emptySet(); } return new LinkedHashSet<>( - AnnotationUtils.getElementValueArray(keyFor, "value", String.class, true)); + AnnotationUtils.getElementValueArray(keyFor, keyForValueElement, String.class)); } } diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForValue.java b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForValue.java index 587b09ac541d..3f6c231f503d 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/KeyForValue.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/KeyForValue.java @@ -1,16 +1,22 @@ package org.checkerframework.checker.nullness; import com.sun.source.tree.ExpressionTree; + +import org.checkerframework.checker.nullness.qual.KeyFor; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.flow.CFAbstractAnalysis; +import org.checkerframework.framework.flow.CFAbstractValue; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; +import org.plumelib.util.CollectionsPlume; + import java.util.LinkedHashSet; import java.util.List; import java.util.Set; + import javax.lang.model.element.AnnotationMirror; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.KeyFor; -import org.checkerframework.framework.flow.CFAbstractAnalysis; -import org.checkerframework.framework.flow.CFAbstractValue; -import org.checkerframework.javacutil.AnnotationUtils; /** * KeyForValue holds additional information about which maps this value is a key for. This extra @@ -18,14 +24,18 @@ * type. For example, * *

    - * {@code Map map = ...;}
    - * {@code  T method(T param) { }
    - *   if (map.contains(param) {
      *     {@code @NonNull Object o = map.get(param);}
    + * 
    + * + *
    
    + * Map<T, Object> map = ...;
    + * <T> T method(T param) {
    + *   if (map.contains(param)) {
    + *     @NonNull Object o = map.get(param);
      *     return param;
      *   }
      * }
    - * }
    + * * * Inside the if statement, {@code param} is a key for "map". This would normally be represented as * {@code @KeyFor("map") T}, but this is not a subtype of {@code T}, so the type cannot be refined. @@ -38,22 +48,30 @@ public class KeyForValue extends CFAbstractValue { * this value is a key. Otherwise, it's null. */ // Cannot be final because lub re-assigns; add a new constructor to do this cleanly? - private Set keyForMaps; + private @Nullable Set keyForMaps; - /** Create an instance. */ + /** + * Create a KeyForValue. + * + * @param analysis the analysis + * @param annotations the annotations + * @param underlyingType the underlying type + */ public KeyForValue( CFAbstractAnalysis analysis, - Set annotations, + AnnotationMirrorSet annotations, TypeMirror underlyingType) { super(analysis, annotations, underlyingType); - AnnotationMirror keyfor = - analysis.getTypeFactory().getAnnotationByClass(annotations, KeyFor.class); + KeyForAnnotatedTypeFactory atypeFactory = + (KeyForAnnotatedTypeFactory) analysis.getTypeFactory(); + AnnotationMirror keyfor = atypeFactory.getAnnotationByClass(annotations, KeyFor.class); if (keyfor != null && (underlyingType.getKind() == TypeKind.TYPEVAR || underlyingType.getKind() == TypeKind.WILDCARD)) { - keyForMaps = new LinkedHashSet<>(); List list = - AnnotationUtils.getElementValueArray(keyfor, "value", String.class, true); + AnnotationUtils.getElementValueArray( + keyfor, atypeFactory.keyForValueElement, String.class); + keyForMaps = new LinkedHashSet<>(list.size()); keyForMaps.addAll(list); } else { keyForMaps = null; @@ -64,28 +82,31 @@ public KeyForValue( * If the underlying type is a type variable or a wildcard, then this is a set of maps for which * this value is a key. Otherwise, it's null. */ - public Set getKeyForMaps() { + public @Nullable Set getKeyForMaps() { return keyForMaps; } @Override - public KeyForValue leastUpperBound(KeyForValue other) { - KeyForValue lub = super.leastUpperBound(other); + protected KeyForValue upperBound( + @Nullable KeyForValue other, TypeMirror upperBoundTypeMirror, boolean shouldWiden) { + KeyForValue upperBound = super.upperBound(other, upperBoundTypeMirror, shouldWiden); + if (other == null || other.keyForMaps == null || this.keyForMaps == null) { - return lub; + return upperBound; } // Lub the keyForMaps by intersecting the sets. - lub.keyForMaps = new LinkedHashSet<>(); - lub.keyForMaps.addAll(this.keyForMaps); - lub.keyForMaps.retainAll(other.keyForMaps); - if (lub.keyForMaps.isEmpty()) { - lub.keyForMaps = null; + upperBound.keyForMaps = new LinkedHashSet<>(this.keyForMaps.size()); + upperBound.keyForMaps.addAll(this.keyForMaps); + upperBound.keyForMaps.retainAll(other.keyForMaps); + if (upperBound.keyForMaps.isEmpty()) { + upperBound.keyForMaps = null; } - return lub; + return upperBound; } @Override - public KeyForValue mostSpecific(KeyForValue other, KeyForValue backup) { + public @Nullable KeyForValue mostSpecific( + @Nullable KeyForValue other, @Nullable KeyForValue backup) { KeyForValue mostSpecific = super.mostSpecific(other, backup); if (mostSpecific == null) { if (other == null) { @@ -117,7 +138,7 @@ private void addKeyFor(Set newKeyForMaps) { return; } if (keyForMaps == null) { - keyForMaps = new LinkedHashSet<>(); + keyForMaps = new LinkedHashSet<>(CollectionsPlume.mapCapacity(newKeyForMaps.size())); } keyForMaps.addAll(newKeyForMaps); } diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnalysis.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnalysis.java deleted file mode 100644 index ce08388f0538..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnalysis.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.checkerframework.checker.nullness; - -import java.util.List; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.TypeMirror; -import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.flow.CFAbstractAnalysis; -import org.checkerframework.framework.flow.CFAbstractValue; -import org.checkerframework.javacutil.Pair; - -/** - * The analysis class for the non-null type system (serves as factory for the transfer function, - * stores and abstract values. - */ -public class NullnessAnalysis - extends CFAbstractAnalysis { - - public NullnessAnalysis( - BaseTypeChecker checker, - NullnessAnnotatedTypeFactory factory, - List> fieldValues) { - super(checker, factory, fieldValues); - } - - @Override - public NullnessStore createEmptyStore(boolean sequentialSemantics) { - return new NullnessStore(this, sequentialSemantics); - } - - @Override - public NullnessStore createCopiedStore(NullnessStore s) { - return new NullnessStore(s); - } - - @Override - public NullnessValue createAbstractValue( - Set annotations, TypeMirror underlyingType) { - if (!CFAbstractValue.validateSet(annotations, underlyingType, qualifierHierarchy)) { - return null; - } - return new NullnessValue(this, annotations, underlyingType); - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnnotatedTypeFactory.java deleted file mode 100644 index b1f91db5088c..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnnotatedTypeFactory.java +++ /dev/null @@ -1,588 +0,0 @@ -package org.checkerframework.checker.nullness; - -import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.CompoundAssignmentTree; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.IdentifierTree; -import com.sun.source.tree.MemberSelectTree; -import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.MethodTree; -import com.sun.source.tree.NewArrayTree; -import com.sun.source.tree.NewClassTree; -import com.sun.source.tree.ReturnTree; -import com.sun.source.tree.Tree; -import com.sun.source.tree.TypeCastTree; -import com.sun.source.tree.UnaryTree; -import com.sun.source.tree.VariableTree; -import com.sun.source.util.TreePath; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.VariableElement; -import org.checkerframework.checker.initialization.InitializationAnnotatedTypeFactory; -import org.checkerframework.checker.initialization.qual.FBCBottom; -import org.checkerframework.checker.initialization.qual.Initialized; -import org.checkerframework.checker.initialization.qual.UnderInitialization; -import org.checkerframework.checker.initialization.qual.UnknownInitialization; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.nullness.qual.PolyNull; -import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.flow.CFAbstractAnalysis; -import org.checkerframework.framework.type.AnnotatedTypeFactory; -import org.checkerframework.framework.type.AnnotatedTypeFormatter; -import org.checkerframework.framework.type.AnnotatedTypeMirror; -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.AnnotatedWildcardType; -import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; -import org.checkerframework.framework.type.treeannotator.LiteralTreeAnnotator; -import org.checkerframework.framework.type.treeannotator.PropagationTreeAnnotator; -import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.framework.type.typeannotator.DefaultForTypeAnnotator; -import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator; -import org.checkerframework.framework.type.typeannotator.PropagationTypeAnnotator; -import org.checkerframework.framework.type.typeannotator.TypeAnnotator; -import org.checkerframework.framework.util.AnnotatedTypes; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory; -import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.Pair; -import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypesUtils; - -/** The annotated type factory for the nullness type-system. */ -public class NullnessAnnotatedTypeFactory - extends InitializationAnnotatedTypeFactory< - NullnessValue, NullnessStore, NullnessTransfer, NullnessAnalysis> { - - /** The @{@link NonNull} annotation. */ - protected final AnnotationMirror NONNULL = AnnotationBuilder.fromClass(elements, NonNull.class); - /** The @{@link Nullable} annotation. */ - protected final AnnotationMirror NULLABLE = - AnnotationBuilder.fromClass(elements, Nullable.class); - /** The @{@link PolyNull} annotation. */ - protected final AnnotationMirror POLYNULL = - AnnotationBuilder.fromClass(elements, PolyNull.class); - /** The @{@link MonotonicNonNull} annotation. */ - protected final AnnotationMirror MONOTONIC_NONNULL = - AnnotationBuilder.fromClass(elements, MonotonicNonNull.class); - - /** Handles invocations of {@link java.lang.System#getProperty(String)}. */ - protected final SystemGetPropertyHandler systemGetPropertyHandler; - - /** Determines the nullness type of calls to {@link java.util.Collection#toArray()}. */ - protected final CollectionToArrayHeuristics collectionToArrayHeuristics; - - /** The Class.getCanonicalName() method. */ - protected final ExecutableElement classGetCanonicalName; - - /** Cache for the nullness annotations. */ - protected final Set> nullnessAnnos; - - // List is in alphabetical order. If you update it, also update - // ../../../../../../../../docs/manual/nullness-checker.tex . - /** Aliases for {@code @Nonnull}. */ - private static final List NONNULL_ALIASES = - Arrays.asList( - // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/annotation/NonNull.java - "android.annotation.NonNull", - // https://android.googlesource.com/platform/frameworks/support/+/master/annotations/src/main/java/android/support/annotation/NonNull.java - "android.support.annotation.NonNull", - // https://android.googlesource.com/platform/frameworks/support/+/master/annotations/src/main/java/androidx/annotation/NonNull.java - "androidx.annotation.NonNull", - // https://android.googlesource.com/platform/tools/metalava/+/master/stub-annotations/src/main/java/androidx/annotation/RecentlyNonNull.java - "androidx.annotation.RecentlyNonNull", - "com.sun.istack.internal.NotNull", - // http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/NonNull.html - "edu.umd.cs.findbugs.annotations.NonNull", - // https://github.com/ReactiveX/RxJava/blob/2.x/src/main/java/io/reactivex/annotations/NonNull.java - "io.reactivex.annotations.NonNull", - // https://github.com/ReactiveX/RxJava/blob/3.x/src/main/java/io/reactivex/rxjava3/annotations/NonNull.java - "io.reactivex.rxjava3.annotations.NonNull", - // https://jcp.org/en/jsr/detail?id=305 - "javax.annotation.Nonnull", - // https://javaee.github.io/javaee-spec/javadocs/javax/validation/constraints/NotNull.html - "javax.validation.constraints.NotNull", - // https://github.com/rzwitserloot/lombok/blob/master/src/core/lombok/NonNull.java - "lombok.NonNull", - // https://search.maven.org/search?q=a:checker-compat-qual - "org.checkerframework.checker.nullness.compatqual.NonNullDecl", - "org.checkerframework.checker.nullness.compatqual.NonNullType", - // https://help.eclipse.org/neon/index.jsp?topic=/org.eclipse.jdt.doc.isv/reference/api/org/eclipse/jdt/annotation/NonNull.html - "org.eclipse.jdt.annotation.NonNull", - // https://github.com/eclipse/jgit/blob/master/org.eclipse.jgit/src/org/eclipse/jgit/annotations/NonNull.java - "org.eclipse.jgit.annotations.NonNull", - // https://github.com/JetBrains/intellij-community/blob/master/platform/annotations/java8/src/org/jetbrains/annotations/NotNull.java - "org.jetbrains.annotations.NotNull", - // http://svn.code.sf.net/p/jmlspecs/code/JMLAnnotations/trunk/src/org/jmlspecs/annotation/NonNull.java - "org.jmlspecs.annotation.NonNull", - // http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NonNull.html - "org.netbeans.api.annotations.common.NonNull", - // https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/lang/NonNull.java - "org.springframework.lang.NonNull"); - - // List is in alphabetical order. If you update it, also update - // ../../../../../../../../docs/manual/nullness-checker.tex . - /** Aliases for {@code @Nullable}. */ - private static final List NULLABLE_ALIASES = - Arrays.asList( - // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/annotation/Nullable.java - "android.annotation.Nullable", - // https://android.googlesource.com/platform/frameworks/support/+/master/annotations/src/main/java/android/support/annotation/Nullable.java - "android.support.annotation.Nullable", - // https://android.googlesource.com/platform/frameworks/support/+/master/annotations/src/main/java/androidx/annotation/Nullable.java - "androidx.annotation.Nullable", - // https://android.googlesource.com/platform/tools/metalava/+/master/stub-annotations/src/main/java/androidx/annotation/RecentlyNullable.java - "androidx.annotation.RecentlyNullable", - "com.sun.istack.internal.Nullable", - // http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/CheckForNull.html - "edu.umd.cs.findbugs.annotations.CheckForNull", - // http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/Nullable.html - "edu.umd.cs.findbugs.annotations.Nullable", - // http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/PossiblyNull.html - "edu.umd.cs.findbugs.annotations.PossiblyNull", - // http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/UnknownNullness.html - "edu.umd.cs.findbugs.annotations.UnknownNullness", - // https://github.com/ReactiveX/RxJava/blob/2.x/src/main/java/io/reactivex/annotations/Nullable.java - "io.reactivex.annotations.Nullable", - // https://github.com/ReactiveX/RxJava/blob/3.x/src/main/java/io/reactivex/rxjava3/annotations/Nullable.java - "io.reactivex.rxjava3.annotations.Nullable", - // https://jcp.org/en/jsr/detail?id=305 - "javax.annotation.CheckForNull", - "javax.annotation.Nullable", - // https://search.maven.org/search?q=a:checker-compat-qual - "org.checkerframework.checker.nullness.compatqual.NullableDecl", - "org.checkerframework.checker.nullness.compatqual.NullableType", - // https://help.eclipse.org/neon/index.jsp?topic=/org.eclipse.jdt.doc.isv/reference/api/org/eclipse/jdt/annotation/Nullable.html - "org.eclipse.jdt.annotation.Nullable", - // https://github.com/eclipse/jgit/blob/master/org.eclipse.jgit/src/org/eclipse/jgit/annotations/Nullable.java - "org.eclipse.jgit.annotations.Nullable", - // https://github.com/JetBrains/intellij-community/blob/master/platform/annotations/java8/src/org/jetbrains/annotations/Nullable.java - "org.jetbrains.annotations.Nullable", - // http://svn.code.sf.net/p/jmlspecs/code/JMLAnnotations/trunk/src/org/jmlspecs/annotation/Nullable.java - "org.jmlspecs.annotation.Nullable", - // http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/CheckForNull.html - "org.netbeans.api.annotations.common.CheckForNull", - // http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NullAllowed.html - "org.netbeans.api.annotations.common.NullAllowed", - // http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NullUnknown.html - "org.netbeans.api.annotations.common.NullUnknown", - // https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/lang/Nullable.java - "org.springframework.lang.Nullable"); - - /** - * Creates a NullnessAnnotatedTypeFactory. - * - * @param checker the associated {@link NullnessChecker} - */ - public NullnessAnnotatedTypeFactory(BaseTypeChecker checker) { - super(checker); - - Set> tempNullnessAnnos = new LinkedHashSet<>(); - tempNullnessAnnos.add(NonNull.class); - tempNullnessAnnos.add(MonotonicNonNull.class); - tempNullnessAnnos.add(Nullable.class); - tempNullnessAnnos.add(PolyNull.class); - nullnessAnnos = Collections.unmodifiableSet(tempNullnessAnnos); - - NONNULL_ALIASES.forEach(annotation -> addAliasedAnnotation(annotation, NONNULL)); - NULLABLE_ALIASES.forEach(annotation -> addAliasedAnnotation(annotation, NULLABLE)); - - // Add compatibility annotations: - addAliasedAnnotation( - "org.checkerframework.checker.nullness.compatqual.PolyNullDecl", POLYNULL); - addAliasedAnnotation( - "org.checkerframework.checker.nullness.compatqual.MonotonicNonNullDecl", - MONOTONIC_NONNULL); - addAliasedAnnotation( - "org.checkerframework.checker.nullness.compatqual.PolyNullType", POLYNULL); - addAliasedAnnotation( - "org.checkerframework.checker.nullness.compatqual.MonotonicNonNullType", - MONOTONIC_NONNULL); - - boolean permitClearProperty = - checker.getLintOption( - NullnessChecker.LINT_PERMITCLEARPROPERTY, - NullnessChecker.LINT_DEFAULT_PERMITCLEARPROPERTY); - systemGetPropertyHandler = - new SystemGetPropertyHandler(processingEnv, this, permitClearProperty); - - classGetCanonicalName = - TreeUtils.getMethod( - java.lang.Class.class.getName(), "getCanonicalName", 0, processingEnv); - - postInit(); - - // do this last, as it might use the factory again. - this.collectionToArrayHeuristics = new CollectionToArrayHeuristics(checker, this); - } - - @Override - protected Set> createSupportedTypeQualifiers() { - return new LinkedHashSet<>( - Arrays.asList( - Nullable.class, - MonotonicNonNull.class, - NonNull.class, - UnderInitialization.class, - Initialized.class, - UnknownInitialization.class, - FBCBottom.class, - PolyNull.class)); - } - - /** - * For types of left-hand side of an assignment, this method replaces {@link PolyNull} with - * {@link Nullable} if the org.checkerframework.dataflow analysis has determined that this is - * allowed soundly. For example: - * - *
     @PolyNull String foo(@PolyNull String param) {
    -     *    if (param == null) {
    -     *        //  @PolyNull is really @Nullable, so change
    -     *        // the type of param to @Nullable.
    -     *        param = null;
    -     *    }
    -     *    return param;
    -     * }
    -     * 
    - * - * @param lhsType type to replace whose polymorphic qualifier will be replaced - * @param context tree used to get dataflow value - */ - protected void replacePolyQualifier(AnnotatedTypeMirror lhsType, Tree context) { - if (lhsType.hasAnnotation(PolyNull.class)) { - NullnessValue inferred = getInferredValueFor(context); - if (inferred != null && inferred.isPolyNullNull) { - lhsType.replaceAnnotation(NULLABLE); - } - } - } - - @Override - public List getUninitializedInvariantFields( - NullnessStore store, - TreePath path, - boolean isStatic, - List receiverAnnotations) { - List candidates = - super.getUninitializedInvariantFields(store, path, isStatic, receiverAnnotations); - List result = new ArrayList<>(); - for (VariableTree c : candidates) { - AnnotatedTypeMirror type = getAnnotatedType(c); - boolean isPrimitive = TypesUtils.isPrimitive(type.getUnderlyingType()); - if (!isPrimitive) { - // primitives do not need to be initialized - result.add(c); - } - } - return result; - } - - @Override - protected NullnessAnalysis createFlowAnalysis( - List> fieldValues) { - return new NullnessAnalysis(checker, this, fieldValues); - } - - @Override - public NullnessTransfer createFlowTransferFunction( - CFAbstractAnalysis analysis) { - return new NullnessTransfer((NullnessAnalysis) analysis); - } - - /** @return an AnnotatedTypeFormatter that does not print the qualifiers on null literals */ - @Override - protected AnnotatedTypeFormatter createAnnotatedTypeFormatter() { - boolean printVerboseGenerics = checker.hasOption("printVerboseGenerics"); - return new NullnessAnnotatedTypeFormatter( - printVerboseGenerics, - // -AprintVerboseGenerics implies -AprintAllQualifiers - printVerboseGenerics || checker.hasOption("printAllQualifiers")); - } - - @Override - public ParameterizedExecutableType methodFromUse(MethodInvocationTree tree) { - ParameterizedExecutableType mType = super.methodFromUse(tree); - AnnotatedExecutableType method = mType.executableType; - - // Special cases for method invocations with specific arguments. - systemGetPropertyHandler.handle(tree, method); - collectionToArrayHeuristics.handle(tree, method); - // `MyClass.class.getCanonicalName()` is non-null. - if (TreeUtils.isMethodInvocation(tree, classGetCanonicalName, processingEnv)) { - ExpressionTree receiver = ((MemberSelectTree) tree.getMethodSelect()).getExpression(); - if (TreeUtils.isClassLiteral(receiver)) { - AnnotatedTypeMirror type = method.getReturnType(); - type.replaceAnnotation(NONNULL); - } - } - - return mType; - } - - @Override - public void adaptGetClassReturnTypeToReceiver( - final AnnotatedExecutableType getClassType, final AnnotatedTypeMirror receiverType) { - - super.adaptGetClassReturnTypeToReceiver(getClassType, receiverType); - - // Make the wildcard always @NonNull, regardless of the declared type. - - final AnnotatedDeclaredType returnAdt = - (AnnotatedDeclaredType) getClassType.getReturnType(); - final List typeArgs = returnAdt.getTypeArguments(); - final AnnotatedWildcardType classWildcardArg = (AnnotatedWildcardType) typeArgs.get(0); - classWildcardArg.getExtendsBoundField().replaceAnnotation(NONNULL); - } - - @Override - public AnnotatedTypeMirror getMethodReturnType(MethodTree m, ReturnTree r) { - AnnotatedTypeMirror result = super.getMethodReturnType(m, r); - replacePolyQualifier(result, r); - return result; - } - - @Override - protected TypeAnnotator createTypeAnnotator() { - DefaultForTypeAnnotator defaultForTypeAnnotator = new DefaultForTypeAnnotator(this); - defaultForTypeAnnotator.addAtmClass(AnnotatedTypeMirror.AnnotatedNoType.class, NONNULL); - defaultForTypeAnnotator.addAtmClass( - AnnotatedTypeMirror.AnnotatedPrimitiveType.class, NONNULL); - return new ListTypeAnnotator( - new PropagationTypeAnnotator(this), - defaultForTypeAnnotator, - new NullnessTypeAnnotator(this), - new CommitmentTypeAnnotator(this)); - } - - @Override - protected TreeAnnotator createTreeAnnotator() { - // Don't call super.createTreeAnnotator because the default tree annotators are incorrect - // for the Nullness Checker. - return new ListTreeAnnotator( - // DebugListTreeAnnotator(new Tree.Kind[] {Tree.Kind.CONDITIONAL_EXPRESSION}, - new NullnessPropagationTreeAnnotator(this), - new LiteralTreeAnnotator(this), - new NullnessTreeAnnotator(this), - new CommitmentTreeAnnotator(this)); - } - - /** - * Nullness doesn't call propagation on binary and unary because the result is - * always @Initialized (the default qualifier). - * - *

    Would this be valid to move into CommitmentTreeAnnotator. - */ - protected static class NullnessPropagationTreeAnnotator extends PropagationTreeAnnotator { - - /** Create the NullnessPropagationTreeAnnotator. */ - public NullnessPropagationTreeAnnotator(AnnotatedTypeFactory atypeFactory) { - super(atypeFactory); - } - - @Override - public Void visitBinary(BinaryTree node, AnnotatedTypeMirror type) { - return null; - } - - @Override - public Void visitUnary(UnaryTree node, AnnotatedTypeMirror type) { - return null; - } - - @Override - public Void visitTypeCast(TypeCastTree node, AnnotatedTypeMirror type) { - if (type.getKind().isPrimitive()) { - AnnotationMirror NONNULL = ((NullnessAnnotatedTypeFactory) atypeFactory).NONNULL; - // If a @Nullable expression is cast to a primitive, then an unboxing.of.nullable - // error is issued. Treat the cast as if it were annotated as @NonNull to avoid an - // type.invalid.annotations.on.use error. - if (!type.isAnnotatedInHierarchy(NONNULL)) { - type.addAnnotation(NONNULL); - } - } - return super.visitTypeCast(node, type); - } - } - - protected class NullnessTreeAnnotator extends TreeAnnotator - /*extends InitializationAnnotatedTypeFactory.CommitmentTreeAnnotator*/ { - - public NullnessTreeAnnotator(NullnessAnnotatedTypeFactory atypeFactory) { - super(atypeFactory); - } - - @Override - public Void visitMemberSelect(MemberSelectTree node, AnnotatedTypeMirror type) { - - Element elt = TreeUtils.elementFromUse(node); - assert elt != null; - return null; - } - - @Override - public Void visitVariable(VariableTree node, AnnotatedTypeMirror type) { - Element elt = TreeUtils.elementFromTree(node); - if (elt.getKind() == ElementKind.EXCEPTION_PARAMETER) { - if (!type.isAnnotatedInHierarchy(NONNULL)) { - // case 9. exception parameter - type.addAnnotation(NONNULL); - } - } - return null; - } - - @Override - public Void visitIdentifier(IdentifierTree node, AnnotatedTypeMirror type) { - - Element elt = TreeUtils.elementFromUse(node); - assert elt != null; - - if (elt.getKind() == ElementKind.EXCEPTION_PARAMETER) { - // TODO: It's surprising that we have to do this in - // both visitVariable and visitIdentifier. This should - // already be handled by applying the defaults anyway. - // case 9. exception parameter - type.replaceAnnotation(NONNULL); - } - - return null; - } - - // The result of a binary operation is always non-null. - @Override - public Void visitBinary(BinaryTree node, AnnotatedTypeMirror type) { - type.replaceAnnotation(NONNULL); - return null; - } - - // The result of a compound operation is always non-null. - @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMirror type) { - type.replaceAnnotation(NONNULL); - // Commitment will run after for initialization defaults - return null; - } - - // The result of a unary operation is always non-null. - @Override - public Void visitUnary(UnaryTree node, AnnotatedTypeMirror type) { - type.replaceAnnotation(NONNULL); - return null; - } - - // The result of newly allocated structures is always non-null. - @Override - public Void visitNewClass(NewClassTree node, AnnotatedTypeMirror type) { - type.replaceAnnotation(NONNULL); - return null; - } - - @Override - public Void visitNewArray(NewArrayTree node, AnnotatedTypeMirror type) { - // The result of newly allocated structures is always non-null. - if (!type.isAnnotatedInHierarchy(NONNULL)) { - type.replaceAnnotation(NONNULL); - } - - // The most precise element type for `new Object[] {null}` is @FBCBottom, but - // the most useful element type is @Initialized (which is also accurate). - AnnotatedArrayType arrayType = (AnnotatedArrayType) type; - AnnotatedTypeMirror componentType = arrayType.getComponentType(); - if (componentType.hasEffectiveAnnotation(FBCBOTTOM)) { - componentType.replaceAnnotation(INITIALIZED); - } - return null; - } - } - - protected class NullnessTypeAnnotator - extends InitializationAnnotatedTypeFactory< - NullnessValue, NullnessStore, NullnessTransfer, NullnessAnalysis> - .CommitmentTypeAnnotator { - - public NullnessTypeAnnotator(InitializationAnnotatedTypeFactory atypeFactory) { - super(atypeFactory); - } - } - - /** - * Returns the list of annotations of the non-null type system. - * - * @return the list of annotations of the non-null type system - */ - public Set> getNullnessAnnotations() { - return nullnessAnnos; - } - - @Override - public Set> getInvalidConstructorReturnTypeAnnotations() { - Set> l = - new HashSet<>(super.getInvalidConstructorReturnTypeAnnotations()); - l.addAll(getNullnessAnnotations()); - return l; - } - - @Override - public AnnotationMirror getFieldInvariantAnnotation() { - return NONNULL; - } - - /** - * {@inheritDoc} - * - *

    In other words, is the lower bound @NonNull? - * - * @param type of field that might have invariant annotation - * @return whether or not type has the invariant annotation - */ - @Override - protected boolean hasFieldInvariantAnnotation( - AnnotatedTypeMirror type, VariableElement fieldElement) { - AnnotationMirror invariant = getFieldInvariantAnnotation(); - Set lowerBounds = - AnnotatedTypes.findEffectiveLowerBoundAnnotations(qualHierarchy, type); - return AnnotationUtils.containsSame(lowerBounds, invariant); - } - - @Override - public QualifierHierarchy createQualifierHierarchy(MultiGraphFactory factory) { - return new NullnessQualifierHierarchy(factory, (Object[]) null); - } - - protected class NullnessQualifierHierarchy extends InitializationQualifierHierarchy { - - public NullnessQualifierHierarchy(MultiGraphFactory f, Object[] arg) { - super(f, arg); - } - - @Override - public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { - if (isInitializationAnnotation(subAnno) || isInitializationAnnotation(superAnno)) { - return this.isSubtypeInitialization(subAnno, superAnno); - } - return super.isSubtype(subAnno, superAnno); - } - - @Override - public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2) { - if (isInitializationAnnotation(a1) || isInitializationAnnotation(a2)) { - return this.leastUpperBoundInitialization(a1, a2); - } - return super.leastUpperBound(a1, a2); - } - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnnotatedTypeFormatter.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnnotatedTypeFormatter.java deleted file mode 100644 index a4b5bfd03236..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessAnnotatedTypeFormatter.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.checkerframework.checker.nullness; - -import java.util.Set; -import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedNullType; -import org.checkerframework.framework.type.DefaultAnnotatedTypeFormatter; -import org.checkerframework.framework.util.AnnotationFormatter; -import org.checkerframework.framework.util.DefaultAnnotationFormatter; - -/** A DefaultAnnotatedTypeFormatter that prints null literals without their annotations. */ -public class NullnessAnnotatedTypeFormatter extends DefaultAnnotatedTypeFormatter { - public NullnessAnnotatedTypeFormatter( - boolean printVerboseGenerics, boolean printInvisibleQualifiers) { - super( - new NullnessFormattingVisitor( - new DefaultAnnotationFormatter(), - printVerboseGenerics, - printInvisibleQualifiers)); - } - - protected static class NullnessFormattingVisitor extends FormattingVisitor { - - public NullnessFormattingVisitor( - AnnotationFormatter annoFormatter, - boolean printVerboseGenerics, - boolean defaultInvisiblesSetting) { - super(annoFormatter, printVerboseGenerics, defaultInvisiblesSetting); - } - - @Override - public String visitNull(AnnotatedNullType type, Set visiting) { - // The null literal will be understood as nullable by readers, therefore omit the - // annotations. - // Note: The visitTypeVariable will still print lower bounds with Null kind as "Void" - if (!currentPrintInvisibleSetting) { - return "null"; - } - - return super.visitNull(type, visiting); - } - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessChecker.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessChecker.java index b096be14fc0c..6783774d122a 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessChecker.java @@ -1,28 +1,43 @@ package org.checkerframework.checker.nullness; -import java.util.LinkedHashSet; -import java.util.SortedSet; import org.checkerframework.checker.initialization.InitializationChecker; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.source.SupportedLintOptions; +import javax.annotation.processing.SupportedOptions; + /** * An implementation of the nullness type-system, parameterized by an initialization type-system for - * safe initialization. It use freedom-before-commitment, augmented by type frames, as its - * initialization type system. + * safe initialization. It uses freedom-before-commitment, augmented by type frames (which are + * crucial to obtain acceptable precision), as its initialization type system. + * + *

    This checker uses the {@link NullnessNoInitSubchecker} to check for nullness and extends the + * {@link InitializationChecker} to also check that all non-null fields are properly initialized. + * + *

    You can use the following {@link SuppressWarnings} prefixes with this checker: * + *

      + *
    • {@code @SuppressWarnings("nullness")} suppresses warnings for both nullness and + * initialization annotations + *
    • {@code @SuppressWarnings("initialization")} suppresses warnings for initialization + * annotations only + *
    • {@code @SuppressWarnings("nullnessnoinit")} suppresses warnings for nullness annotations + * only + *
    + * + * @see KeyForSubchecker + * @see InitializationChecker + * @see NullnessNoInitSubchecker * @checker_framework.manual #nullness-checker Nullness Checker */ @SupportedLintOptions({ NullnessChecker.LINT_NOINITFORMONOTONICNONNULL, NullnessChecker.LINT_REDUNDANTNULLCOMPARISON, - // Temporary option to forbid non-null array component types, - // which is allowed by default. + // Temporary option to forbid non-null array component types, which is allowed by default. // Forbidding is sound and will eventually be the default. // Allowing is unsound, as described in Section 3.3.4, "Nullness and arrays": - // https://checkerframework.org/manual/#nullness-arrays + // https://eisop.github.io/cf/manual/#nullness-arrays // It is the default temporarily, until we improve the analysis to reduce false positives or we // learn what advice to give programmers about avoid false positive warnings. // See issue #986: https://github.com/typetools/checker-framework/issues/986 @@ -30,7 +45,13 @@ // Old name for soundArrayCreationNullness, for backward compatibility; remove in January 2021. "forbidnonnullarraycomponents", NullnessChecker.LINT_TRUSTARRAYLENZERO, - NullnessChecker.LINT_PERMITCLEARPROPERTY + NullnessChecker.LINT_PERMITCLEARPROPERTY, +}) +@SupportedOptions({ + "assumeKeyFor", + "assumeInitialized", + "jspecifyNullMarkedAlias", + "conservativeArgumentNullnessAfterInvocation" }) public class NullnessChecker extends InitializationChecker { @@ -41,7 +62,7 @@ public class NullnessChecker extends InitializationChecker { public static final boolean LINT_DEFAULT_NOINITFORMONOTONICNONNULL = false; /** - * Warn about redundant comparisons of expressions with {@code null}, if the expressions is + * Warn about redundant comparisons of an expression with {@code null}, if the expression is * known to be non-null. */ public static final String LINT_REDUNDANTNULLCOMPARISON = "redundantNullComparison"; @@ -67,30 +88,16 @@ public class NullnessChecker extends InitializationChecker { /** Default for {@link #LINT_PERMITCLEARPROPERTY}. */ public static final boolean LINT_DEFAULT_PERMITCLEARPROPERTY = false; - /* - @Override - public void initChecker() { - super.initChecker(); - } - */ - - @Override - protected LinkedHashSet> getImmediateSubcheckerClasses() { - LinkedHashSet> checkers = - super.getImmediateSubcheckerClasses(); - checkers.add(KeyForSubchecker.class); - return checkers; - } + /** Default constructor for NullnessChecker. */ + public NullnessChecker() {} @Override - public SortedSet getSuppressWarningsPrefixes() { - SortedSet result = super.getSuppressWarningsPrefixes(); - result.add("nullness"); - return result; + public boolean checkPrimitives() { + return false; } @Override - protected BaseTypeVisitor createSourceVisitor() { - return new NullnessVisitor(this); + public Class getTargetCheckerClass() { + return NullnessNoInitSubchecker.class; } } diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitAnalysis.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitAnalysis.java new file mode 100644 index 000000000000..f1ff4ca3c0a5 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitAnalysis.java @@ -0,0 +1,48 @@ +package org.checkerframework.checker.nullness; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.flow.CFAbstractAnalysis; +import org.checkerframework.framework.flow.CFAbstractValue; +import org.checkerframework.javacutil.AnnotationMirrorSet; + +import javax.lang.model.type.TypeMirror; + +/** + * The analysis class for the non-null type system (serves as factory for the transfer function, + * stores and abstract values. + */ +public class NullnessNoInitAnalysis + extends CFAbstractAnalysis< + NullnessNoInitValue, NullnessNoInitStore, NullnessNoInitTransfer> { + + /** + * Creates a new {@code NullnessAnalysis}. + * + * @param checker the checker + * @param factory the factory + */ + public NullnessNoInitAnalysis( + BaseTypeChecker checker, NullnessNoInitAnnotatedTypeFactory factory) { + super(checker, factory); + } + + @Override + public NullnessNoInitStore createEmptyStore(boolean sequentialSemantics) { + return new NullnessNoInitStore(this, sequentialSemantics); + } + + @Override + public NullnessNoInitStore createCopiedStore(NullnessNoInitStore s) { + return new NullnessNoInitStore(s); + } + + @Override + public @Nullable NullnessNoInitValue createAbstractValue( + AnnotationMirrorSet annotations, TypeMirror underlyingType) { + if (!CFAbstractValue.validateSet(annotations, underlyingType, atypeFactory)) { + return null; + } + return new NullnessNoInitValue(this, annotations, underlyingType); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitAnnotatedTypeFactory.java new file mode 100644 index 000000000000..35819c3aa379 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitAnnotatedTypeFactory.java @@ -0,0 +1,1041 @@ +package org.checkerframework.checker.nullness; + +import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.CompoundAssignmentTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewArrayTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.tree.UnaryTree; +import com.sun.source.tree.VariableTree; + +import org.checkerframework.checker.initialization.InitializationFieldAccessAnnotatedTypeFactory; +import org.checkerframework.checker.initialization.InitializationFieldAccessSubchecker; +import org.checkerframework.checker.initialization.InitializationFieldAccessTreeAnnotator; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.PolyNull; +import org.checkerframework.checker.signature.qual.FullyQualifiedName; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.LocalVariable; +import org.checkerframework.dataflow.expression.ThisReference; +import org.checkerframework.dataflow.util.NodeUtils; +import org.checkerframework.framework.flow.CFAbstractAnalysis; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.checkerframework.framework.qual.TypeUseLocation; +import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.framework.type.AnnotatedTypeFormatter; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +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.AnnotatedTypeVariable; +import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; +import org.checkerframework.framework.type.NoElementQualifierHierarchy; +import org.checkerframework.framework.type.QualifierHierarchy; +import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; +import org.checkerframework.framework.type.treeannotator.LiteralTreeAnnotator; +import org.checkerframework.framework.type.treeannotator.PropagationTreeAnnotator; +import org.checkerframework.framework.type.treeannotator.TreeAnnotator; +import org.checkerframework.framework.type.typeannotator.DefaultForTypeAnnotator; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeMirror; + +/** The annotated type factory for the nullness type-system. */ +public class NullnessNoInitAnnotatedTypeFactory + extends GenericAnnotatedTypeFactory< + NullnessNoInitValue, + NullnessNoInitStore, + NullnessNoInitTransfer, + NullnessNoInitAnalysis> { + + /** The @{@link NonNull} annotation. */ + protected final AnnotationMirror NONNULL = AnnotationBuilder.fromClass(elements, NonNull.class); + + /** The @{@link Nullable} annotation. */ + protected final AnnotationMirror NULLABLE = + AnnotationBuilder.fromClass(elements, Nullable.class); + + /** The @{@link PolyNull} annotation. */ + protected final AnnotationMirror POLYNULL = + AnnotationBuilder.fromClass(elements, PolyNull.class); + + /** The @{@link MonotonicNonNull} annotation. */ + protected final AnnotationMirror MONOTONIC_NONNULL = + AnnotationBuilder.fromClass(elements, MonotonicNonNull.class); + + /** Handles invocations of {@link java.lang.System#getProperty(String)}. */ + protected final SystemGetPropertyHandler systemGetPropertyHandler; + + /** Determines the nullness type of calls to {@link java.util.Collection#toArray()}. */ + protected final CollectionToArrayHeuristics collectionToArrayHeuristics; + + /** The Class.getCanonicalName() method. */ + protected final ExecutableElement classGetCanonicalName; + + /** The Arrays.copyOf() methods that operate on arrays of references. */ + private final List copyOfMethods; + + /** Cache for the nullness annotations. */ + protected final Set> nullnessAnnos; + + /** The Map.get method. */ + private final ExecutableElement mapGet = + TreeUtils.getMethod("java.util.Map", "get", 1, processingEnv); + + // List is in alphabetical order. If you update it, also update + // ../../../../../../../../docs/manual/nullness-checker.tex + // and make a pull request for variables NONNULL_ANNOTATIONS and BASE_COPYABLE_ANNOTATIONS in + // https://github.com/rzwitserloot/lombok/blob/master/src/core/lombok/core/handlers/HandlerUtil.java . + // Avoid changes to the string constants by ShadowJar relocate by using "start".toString() + + // "rest". + // Keep the original string constant in a comment to allow searching for it. + /** Aliases for {@code @Nonnull}. */ + @SuppressWarnings( + "signature:assignment.type.incompatible") // Class names intentionally obfuscated + private static final List<@FullyQualifiedName String> NONNULL_ALIASES = + Arrays.asList( + // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/annotation/NonNull.java + // https://developer.android.com/reference/androidx/annotation/NonNull + "android.annotation.NonNull", + // https://android.googlesource.com/platform/frameworks/support/+/master/annotations/src/main/java/android/support/annotation/NonNull.java + // https://developer.android.com/reference/android/support/annotation/NonNull + "android.support.annotation.NonNull", + // https://android.googlesource.com/platform/tools/metalava/+/9ad32fadc5a22e1357c82b447e33ec7fecdcd8c1/stub-annotations/src/main/java/android/support/annotation/RecentlyNonNull.java + "android.support.annotation.RecentlyNonNull", + // https://android.googlesource.com/platform/frameworks/support/+/master/annotations/src/main/java/androidx/annotation/NonNull.java + "androidx.annotation.NonNull", + // https://android.googlesource.com/platform/tools/metalava/+/master/stub-annotations/src/main/java/androidx/annotation/RecentlyNonNull.java + "androidx.annotation.RecentlyNonNull", + // https://android.googlesource.com/platform/sdk/+/66fcecc/common/src/com/android/annotations/NonNull.java + "com.android.annotations.NonNull", + // https://github.com/firebase/firebase-android-sdk/blob/master/firebase-database/src/main/java/com/google/firebase/database/annotations/NotNull.java + // "com.google.firebase.database.annotations.NotNull", + "com.go".toString() + "ogle.firebase.database.annotations.NotNull", + // https://github.com/firebase/firebase-admin-java/blob/master/src/main/java/com/google/firebase/internal/NonNull.java + // "com.google.firebase.internal.NonNull", + "com.go".toString() + "ogle.firebase.internal.NonNull", + // https://github.com/mongodb/mongo-java-driver/blob/master/driver-core/src/main/com/mongodb/lang/NonNull.java + "com.mongodb.lang.NonNull", + // https://github.com/eclipse-ee4j/jaxb-istack-commons/blob/master/istack-commons/runtime/src/main/java/com/sun/istack/NotNull.java + "com.sun.istack.NotNull", + // https://github.com/openjdk/jdk8/blob/master/jaxws/src/share/jaxws_classes/com/sun/istack/internal/NotNull.java + "com.sun.istack.internal.NotNull", + // https://github.com/pingidentity/ldapsdk/blob/master/src/com/unboundid/util/NotNull.java + "com.unboundid.util.NotNull", + // https://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/NonNull.html + "edu.umd.cs.findbugs.annotations.NonNull", + // https://github.com/micrometer-metrics/micrometer/blob/main/micrometer-core/src/main/java/io/micrometer/core/lang/NonNull.java + "io.micrometer.core.lang.NonNull", + // https://github.com/micronaut-projects/micronaut-core/blob/master/core/src/main/java/io/micronaut/core/annotation/NonNull.java + "io.micronaut.core.annotation.NonNull", + // https://github.com/ReactiveX/RxJava/blob/2.x/src/main/java/io/reactivex/annotations/NonNull.java + "io.reactivex.annotations.NonNull", + // https://github.com/ReactiveX/RxJava/blob/3.x/src/main/java/io/reactivex/rxjava3/annotations/NonNull.java + "io.reactivex.rxjava3.annotations.NonNull", + // https://github.com/jakartaee/common-annotations-api/blob/master/api/src/main/java/jakarta/annotation/Nonnull.java + "jakarta.annotation.Nonnull", + // https://jcp.org/en/jsr/detail?id=305; no documentation at + // https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.1/javax/annotation/Nonnull.html + "javax.annotation.Nonnull", + // https://javaee.github.io/javaee-spec/javadocs/javax/validation/constraints/NotNull.html + "javax.validation.constraints.NotNull", + // https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/util/NonNull.java + "libcore.util.NonNull", + // https://github.com/projectlombok/lombok/blob/master/src/core/lombok/NonNull.java + "lombok.NonNull", + // https://github.com/raphw/byte-buddy/blob/master/byte-buddy-agent/src/main/java/net/bytebuddy/agent/utility/nullability/NeverNull.java + "net.bytebuddy.agent.utility.nullability.NeverNull", + // https://github.com/raphw/byte-buddy/blob/master/byte-buddy-dep/src/main/java/net/bytebuddy/utility/nullability/NeverNull.java + "net.bytebuddy.utility.nullability.NeverNull", + // Removed in ANTLR 4.6. + // https://github.com/antlr/antlr4/blob/master/runtime/Java/src/org/antlr/v4/runtime/misc/NotNull.java + "org.antlr.v4.runtime.misc.NotNull", + // https://search.maven.org/artifact/org.checkerframework/checker-compat-qual/2.5.5/jar + "org.checkerframework.checker.nullness.compatqual.NonNullDecl", + "org.checkerframework.checker.nullness.compatqual.NonNullType", + // https://janino-compiler.github.io/janino/apidocs/org/codehaus/commons/nullanalysis/NotNull.html + "org.codehaus.commons.nullanalysis.NotNull", + // https://help.eclipse.org/neon/index.jsp?topic=/org.eclipse.jdt.doc.isv/reference/api/org/eclipse/jdt/annotation/NonNull.html + // https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/tree/org.eclipse.jdt.annotation/src/org/eclipse/jdt/annotation/NonNull.java + "org.eclipse.jdt.annotation.NonNull", + // https://github.com/eclipse/jgit/blob/master/org.eclipse.jgit/src/org/eclipse/jgit/annotations/NonNull.java + "org.eclipse.jgit.annotations.NonNull", + // https://github.com/eclipse/lsp4j/blob/main/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/validation/NonNull.java + "org.eclipse.lsp4j.jsonrpc.validation.NonNull", + // https://github.com/JetBrains/intellij-community/blob/master/platform/annotations/java8/src/org/jetbrains/annotations/NotNull.java + // https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html + "org.jetbrains.annotations.NotNull", + // http://svn.code.sf.net/p/jmlspecs/code/JMLAnnotations/trunk/src/org/jmlspecs/annotation/NonNull.java + "org.jmlspecs.annotation.NonNull", + // https://github.com/jspecify/jspecify/blob/main/src/main/java/org/jspecify/annotations/NonNull.java + "org.jspecify.annotations.NonNull", + // 2022-11-17: Deprecated old package location, remove after some grace period + // https://github.com/jspecify/jspecify/tree/main/src/main/java/org/jspecify/nullness + "org.jspecify.nullness.NonNull", + // http://bits.netbeans.org/dev/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NonNull.html + "org.netbeans.api.annotations.common.NonNull", + // https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/lang/NonNull.java + "org.springframework.lang.NonNull", + // https://github.com/reactor/reactor-core/blob/main/reactor-core/src/main/java/reactor/util/annotation/NonNull.java + "reactor.util.annotation.NonNull"); + + // List is in alphabetical order. If you update it, also update + // ../../../../../../../../docs/manual/nullness-checker.tex . + // See more comments with NONNULL_ALIASES above. + /** Aliases for {@code @Nullable}. */ + @SuppressWarnings( + "signature:assignment.type.incompatible") // Class names intentionally obfuscated + private static final List<@FullyQualifiedName String> NULLABLE_ALIASES = + Arrays.asList( + // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/annotation/Nullable.java + // https://developer.android.com/reference/androidx/annotation/Nullable + "android.annotation.Nullable", + // https://android.googlesource.com/platform/frameworks/support/+/master/annotations/src/main/java/android/support/annotation/Nullable.java + // https://developer.android.com/reference/android/support/annotation/Nullable + "android.support.annotation.Nullable", + // https://android.googlesource.com/platform/tools/metalava/+/9ad32fadc5a22e1357c82b447e33ec7fecdcd8c1/stub-annotations/src/main/java/android/support/annotation/RecentlyNullable.java + "android.support.annotation.RecentlyNullable", + // https://android.googlesource.com/platform/frameworks/support/+/master/annotations/src/main/java/androidx/annotation/Nullable.java + "androidx.annotation.Nullable", + // https://android.googlesource.com/platform/tools/metalava/+/master/stub-annotations/src/main/java/androidx/annotation/RecentlyNullable.java + "androidx.annotation.RecentlyNullable", + // https://android.googlesource.com/platform/sdk/+/66fcecc/common/src/com/android/annotations/Nullable.java + "com.android.annotations.Nullable", + // https://github.com/lpantano/java_seqbuster/blob/master/AdRec/src/adrec/com/beust/jcommander/internal/Nullable.java + "com.beust.jcommander.internal.Nullable", + // https://github.com/cloudendpoints/endpoints-java/blob/master/endpoints-framework/src/main/java/com/google/api/server/spi/config/Nullable.java + // "com.google.api.server.spi.config.Nullable", + "com.go".toString() + "ogle.api.server.spi.config.Nullable", + // https://github.com/firebase/firebase-android-sdk/blob/master/firebase-database/src/main/java/com/google/firebase/database/annotations/Nullable.java + // "com.google.firebase.database.annotations.Nullable", + "com.go".toString() + "ogle.firebase.database.annotations.Nullable", + // https://github.com/firebase/firebase-admin-java/blob/master/src/main/java/com/google/firebase/internal/Nullable.java + // "com.google.firebase.internal.Nullable", + "com.go".toString() + "ogle.firebase.internal.Nullable", + // https://gerrit.googlesource.com/gerrit/+/refs/heads/master/java/com/google/gerrit/common/Nullable.java + // "com.google.gerrit.common.Nullable", + "com.go".toString() + "ogle.gerrit.common.Nullable", + // + // "com.google.protobuf.Internal.ProtoMethodAcceptsNullParameter", + "com.go".toString() + "ogle.protobuf.Internal.ProtoMethodAcceptsNullParameter", + // + // "com.google.protobuf.Internal.ProtoMethodMayReturnNull", + "com.go".toString() + "ogle.protobuf.Internal.ProtoMethodMayReturnNull", + // https://github.com/mongodb/mongo-java-driver/blob/master/driver-core/src/main/com/mongodb/lang/Nullable.java + "com.mongodb.lang.Nullable", + // https://github.com/eclipse-ee4j/jaxb-istack-commons/blob/master/istack-commons/runtime/src/main/java/com/sun/istack/Nullable.java + "com.sun.istack.Nullable", + // https://github.com/openjdk/jdk8/blob/master/jaxws/src/share/jaxws_classes/com/sun/istack/internal/Nullable.java + "com.sun.istack.internal.Nullable", + // https://github.com/pingidentity/ldapsdk/blob/master/src/com/unboundid/util/Nullable.java + "com.unboundid.util.Nullable", + // https://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/CheckForNull.html + "edu.umd.cs.findbugs.annotations.CheckForNull", + // https://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/Nullable.html + "edu.umd.cs.findbugs.annotations.Nullable", + // https://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/PossiblyNull.html + "edu.umd.cs.findbugs.annotations.PossiblyNull", + // https://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/UnknownNullness.html + "edu.umd.cs.findbugs.annotations.UnknownNullness", + // https://github.com/micrometer-metrics/micrometer/blob/main/micrometer-core/src/main/java/io/micrometer/core/lang/Nullable.java + "io.micrometer.core.lang.Nullable", + // https://github.com/micronaut-projects/micronaut-core/blob/master/core/src/main/java/io/micronaut/core/annotation/Nullable.java + "io.micronaut.core.annotation.Nullable", + // https://github.com/ReactiveX/RxJava/blob/2.x/src/main/java/io/reactivex/annotations/Nullable.java + "io.reactivex.annotations.Nullable", + // https://github.com/ReactiveX/RxJava/blob/3.x/src/main/java/io/reactivex/rxjava3/annotations/Nullable.java + "io.reactivex.rxjava3.annotations.Nullable", + // https://github.com/eclipse-vertx/vertx-codegen/blob/master/src/main/java/io/vertx/codegen/annotations/Nullable.java + "io.vertx.codegen.annotations.Nullable", + // https://github.com/jakartaee/common-annotations-api/blob/master/api/src/main/java/jakarta/annotation/Nullable.java + "jakarta.annotation.Nullable", + // https://jcp.org/en/jsr/detail?id=305; no documentation at + // https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.1/javax/annotation/Nullable.html + "javax.annotation.CheckForNull", + "javax.annotation.Nullable", + // https://github.com/Pragmatists/JUnitParams/blob/master/src/main/java/junitparams/converters/Nullable.java + "junitparams.converters.Nullable", + // https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/util/Nullable.java + "libcore.util.Nullable", + // https://github.com/raphw/byte-buddy/blob/master/byte-buddy-agent/src/main/java/net/bytebuddy/agent/utility/nullability/AlwaysNull.java + "net.bytebuddy.agent.utility.nullability.AlwaysNull", + // https://github.com/raphw/byte-buddy/blob/master/byte-buddy-agent/src/main/java/net/bytebuddy/agent/utility/nullability/MaybeNull.java + "net.bytebuddy.agent.utility.nullability.MaybeNull", + // https://github.com/raphw/byte-buddy/blob/master/byte-buddy-agent/src/main/java/net/bytebuddy/agent/utility/nullability/UnknownNull.java + "net.bytebuddy.agent.utility.nullability.UnknownNull", + // https://github.com/raphw/byte-buddy/blob/master/byte-buddy-dep/src/main/java/net/bytebuddy/utility/nullability/AlwaysNull.java + "net.bytebuddy.utility.nullability.AlwaysNull", + // https://github.com/raphw/byte-buddy/blob/master/byte-buddy-dep/src/main/java/net/bytebuddy/utility/nullability/MaybeNull.java + "net.bytebuddy.utility.nullability.MaybeNull", + // https://github.com/raphw/byte-buddy/blob/master/byte-buddy-dep/src/main/java/net/bytebuddy/utility/nullability/UnknownNull.java + "net.bytebuddy.utility.nullability.UnknownNull", + // https://github.com/apache/avro/blob/master/lang/java/avro/src/main/java/org/apache/avro/reflect/Nullable.java + // "org.apache.avro.reflect.Nullable", + "org.apa".toString() + "che.avro.reflect.Nullable", + // https://github.com/apache/cxf/blob/master/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/ext/Nullable.java + // "org.apache.cxf.jaxrs.ext.Nullable", + "org.apa".toString() + "che.cxf.jaxrs.ext.Nullable", + // https://github.com/gatein/gatein-shindig/blob/master/java/common/src/main/java/org/apache/shindig/common/Nullable.java + // "org.apache.shindig.common.Nullable", + "org.apa".toString() + "che.shindig.common.Nullable", + // https://search.maven.org/search?q=a:checker-compat-qual + "org.checkerframework.checker.nullness.compatqual.NullableDecl", + "org.checkerframework.checker.nullness.compatqual.NullableType", + // https://janino-compiler.github.io/janino/apidocs/org/codehaus/commons/nullanalysis/Nullable.html + "org.codehaus.commons.nullanalysis.Nullable", + // https://help.eclipse.org/neon/index.jsp?topic=/org.eclipse.jdt.doc.isv/reference/api/org/eclipse/jdt/annotation/Nullable.html + // https://git.eclipse.org/c/jdt/eclipse.jdt.core.git/tree/org.eclipse.jdt.annotation/src/org/eclipse/jdt/annotation/Nullable.java + "org.eclipse.jdt.annotation.Nullable", + // https://github.com/eclipse/jgit/blob/master/org.eclipse.jgit/src/org/eclipse/jgit/annotations/Nullable.java + "org.eclipse.jgit.annotations.Nullable", + // https://github.com/JetBrains/intellij-community/blob/master/platform/annotations/java8/src/org/jetbrains/annotations/Nullable.java + // https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html + "org.jetbrains.annotations.Nullable", + // https://github.com/JetBrains/java-annotations/blob/master/java8/src/main/java/org/jetbrains/annotations/UnknownNullability.java + "org.jetbrains.annotations.UnknownNullability", + // http://svn.code.sf.net/p/jmlspecs/code/JMLAnnotations/trunk/src/org/jmlspecs/annotation/Nullable.java + "org.jmlspecs.annotation.Nullable", + // https://github.com/jspecify/jspecify/blob/main/src/main/java/org/jspecify/annotations/Nullable.java + "org.jspecify.annotations.Nullable", + // 2022-11-17: Deprecated old package location, remove after some grace period + // https://github.com/jspecify/jspecify/tree/main/src/main/java/org/jspecify/nullness + "org.jspecify.nullness.Nullable", + "org.jspecify.nullness.NullnessUnspecified", + // http://bits.netbeans.org/dev/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/CheckForNull.html + "org.netbeans.api.annotations.common.CheckForNull", + // http://bits.netbeans.org/dev/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NullAllowed.html + "org.netbeans.api.annotations.common.NullAllowed", + // http://bits.netbeans.org/dev/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NullUnknown.html + "org.netbeans.api.annotations.common.NullUnknown", + // https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/lang/Nullable.java + "org.springframework.lang.Nullable", + // https://github.com/reactor/reactor-core/blob/main/reactor-core/src/main/java/reactor/util/annotation/Nullable.java + "reactor.util.annotation.Nullable"); + + // List is in alphabetical order. If you update it, also update + // ../../../../../../../../docs/manual/nullness-checker.tex . + // See more comments with NONNULL_ALIASES above. + /** Aliases for {@code @PolyNull}. */ + @SuppressWarnings( + "signature:assignment.type.incompatible") // Class names intentionally obfuscated + private static final List<@FullyQualifiedName String> POLYNULL_ALIASES = + Arrays.asList( + // "com.google.protobuf.Internal.ProtoPassThroughNullness", + "com.go".toString() + "ogle.protobuf.Internal.ProtoPassThroughNullness"); + + /** + * Creates a NullnessAnnotatedTypeFactory. + * + * @param checker the associated {@link NullnessNoInitSubchecker} + */ + public NullnessNoInitAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + + Set> tempNullnessAnnos = new LinkedHashSet<>(4); + tempNullnessAnnos.add(NonNull.class); + tempNullnessAnnos.add(MonotonicNonNull.class); + tempNullnessAnnos.add(Nullable.class); + tempNullnessAnnos.add(PolyNull.class); + nullnessAnnos = Collections.unmodifiableSet(tempNullnessAnnos); + + NONNULL_ALIASES.forEach(annotation -> addAliasedTypeAnnotation(annotation, NONNULL)); + NULLABLE_ALIASES.forEach(annotation -> addAliasedTypeAnnotation(annotation, NULLABLE)); + POLYNULL_ALIASES.forEach(annotation -> addAliasedTypeAnnotation(annotation, POLYNULL)); + + // Add compatibility annotations: + addAliasedTypeAnnotation( + "org.checkerframework.checker.nullness.compatqual.PolyNullDecl", POLYNULL); + addAliasedTypeAnnotation( + "org.checkerframework.checker.nullness.compatqual.MonotonicNonNullDecl", + MONOTONIC_NONNULL); + addAliasedTypeAnnotation( + "org.checkerframework.checker.nullness.compatqual.PolyNullType", POLYNULL); + addAliasedTypeAnnotation( + "org.checkerframework.checker.nullness.compatqual.MonotonicNonNullType", + MONOTONIC_NONNULL); + + if (checker.getUltimateParentChecker().getBooleanOption("jspecifyNullMarkedAlias", true)) { + AnnotationMirror nullMarkedDefaultQual = + new AnnotationBuilder(processingEnv, DefaultQualifier.class) + .setValue("value", NonNull.class) + .setValue( + "locations", + new TypeUseLocation[] {TypeUseLocation.UPPER_BOUND}) + .setValue("applyToSubpackages", false) + .build(); + addAliasedDeclAnnotation( + "org.jspecify.annotations.NullMarked", + DefaultQualifier.class.getCanonicalName(), + nullMarkedDefaultQual); + + // 2022-11-17: Deprecated old package location, remove after some grace period + addAliasedDeclAnnotation( + "org.jspecify.nullness.NullMarked", + DefaultQualifier.class.getCanonicalName(), + nullMarkedDefaultQual); + } + + boolean permitClearProperty = + checker.getLintOption( + NullnessChecker.LINT_PERMITCLEARPROPERTY, + NullnessChecker.LINT_DEFAULT_PERMITCLEARPROPERTY); + systemGetPropertyHandler = + new SystemGetPropertyHandler(processingEnv, this, permitClearProperty); + + classGetCanonicalName = + TreeUtils.getMethod("java.lang.Class", "getCanonicalName", 0, processingEnv); + copyOfMethods = + Arrays.asList( + TreeUtils.getMethod( + "java.util.Arrays", "copyOf", processingEnv, "T[]", "int"), + TreeUtils.getMethod("java.util.Arrays", "copyOf", 3, processingEnv)); + + postInit(); + + // do this last, as it might use the factory again. + this.collectionToArrayHeuristics = new CollectionToArrayHeuristics(checker, this); + } + + @Override + public NullnessNoInitSubchecker getChecker() { + return (NullnessNoInitSubchecker) checker; + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new LinkedHashSet<>( + Arrays.asList( + Nullable.class, MonotonicNonNull.class, NonNull.class, PolyNull.class)); + } + + /** + * For types of left-hand side of an assignment, this method replaces {@link PolyNull} with + * {@link Nullable} (or with {@link NonNull} if the org.checkerframework.dataflow analysis has + * determined that this is allowed soundly. For example: + * + *
     @PolyNull String foo(@PolyNull String param) {
    +     *    if (param == null) {
    +     *        //  @PolyNull is really @Nullable, so change
    +     *        // the type of param to @Nullable.
    +     *        param = null;
    +     *    }
    +     *    return param;
    +     * }
    +     * 
    + * + * @param lhsType type to replace whose polymorphic qualifier will be replaced + * @param context tree used to get dataflow value + */ + protected void replacePolyQualifier(AnnotatedTypeMirror lhsType, Tree context) { + if (lhsType.hasAnnotation(PolyNull.class)) { + NullnessNoInitValue inferred = getInferredValueFor(context); + if (inferred != null) { + if (inferred.isPolyNullNonNull) { + lhsType.replaceAnnotation(NONNULL); + } else if (inferred.isPolyNullNull) { + lhsType.replaceAnnotation(NULLABLE); + } + } + } + } + + @Override + protected NullnessNoInitAnalysis createFlowAnalysis() { + return new NullnessNoInitAnalysis(checker, this); + } + + @Override + public NullnessNoInitTransfer createFlowTransferFunction( + CFAbstractAnalysis + analysis) { + return new NullnessNoInitTransfer((NullnessNoInitAnalysis) analysis); + } + + /** + * Returns an AnnotatedTypeFormatter that does not print the qualifiers on null literals. + * + * @return an AnnotatedTypeFormatter that does not print the qualifiers on null literals + */ + @Override + protected AnnotatedTypeFormatter createAnnotatedTypeFormatter() { + boolean printVerboseGenerics = checker.hasOption("printVerboseGenerics"); + return new NullnessNoInitAnnotatedTypeFormatter( + printVerboseGenerics, + // -AprintVerboseGenerics implies -AprintAllQualifiers + printVerboseGenerics || checker.hasOption("printAllQualifiers")); + } + + @Override + public ParameterizedExecutableType methodFromUse(MethodInvocationTree tree) { + ParameterizedExecutableType mType = super.methodFromUse(tree); + AnnotatedExecutableType method = mType.executableType; + + // Special cases for method invocations with specific arguments. + systemGetPropertyHandler.handle(tree, method); + collectionToArrayHeuristics.handle(tree, method); + // `MyClass.class.getCanonicalName()` is non-null. + if (TreeUtils.isMethodInvocation(tree, classGetCanonicalName, processingEnv)) { + ExpressionTree receiver = ((MemberSelectTree) tree.getMethodSelect()).getExpression(); + if (TreeUtils.isClassLiteral(receiver)) { + AnnotatedTypeMirror type = method.getReturnType(); + type.replaceAnnotation(NONNULL); + } + } + + return mType; + } + + @Override + public void adaptGetClassReturnTypeToReceiver( + AnnotatedExecutableType getClassType, + AnnotatedTypeMirror receiverType, + ExpressionTree tree) { + + super.adaptGetClassReturnTypeToReceiver(getClassType, receiverType, tree); + + // Make the captured wildcard always @NonNull, regardless of the declared type. + + AnnotatedDeclaredType returnAdt = (AnnotatedDeclaredType) getClassType.getReturnType(); + List typeArgs = returnAdt.getTypeArguments(); + AnnotatedTypeVariable classWildcardArg = (AnnotatedTypeVariable) typeArgs.get(0); + classWildcardArg.getUpperBound().replaceAnnotation(NONNULL); + } + + @Override + public AnnotatedTypeMirror getMethodReturnType(MethodTree m, ReturnTree r) { + AnnotatedTypeMirror result = super.getMethodReturnType(m, r); + replacePolyQualifier(result, r); + return result; + } + + @Override + public boolean isNotFullyInitializedReceiver(MethodTree methodDeclTree) { + InitializationFieldAccessAnnotatedTypeFactory initFactory = + getChecker() + .getTypeFactoryOfSubcheckerOrNull( + InitializationFieldAccessSubchecker.class); + if (initFactory == null) { + // init checker is deactivated. + return super.isNotFullyInitializedReceiver(methodDeclTree); + } + return initFactory.isNotFullyInitializedReceiver(methodDeclTree); + } + + @Override + public AnnotatedTypeMirror getAnnotatedTypeBefore(JavaExpression expr, ExpressionTree tree) { + InitializationFieldAccessAnnotatedTypeFactory initFactory = + getChecker() + .getTypeFactoryOfSubcheckerOrNull( + InitializationFieldAccessSubchecker.class); + if (initFactory == null) { + // init checker is deactivated. + return super.getAnnotatedTypeBefore(expr, tree); + } + if (expr instanceof FieldAccess) { + FieldAccess fa = (FieldAccess) expr; + JavaExpression receiver = fa.getReceiver(); + TypeMirror declaringClass = fa.getField().getEnclosingElement().asType(); + AnnotatedTypeMirror receiverType; + + if (receiver instanceof LocalVariable) { + Element receiverElem = ((LocalVariable) receiver).getElement(); + receiverType = initFactory.getAnnotatedType(receiverElem); + } else if (receiver instanceof ThisReference) { + receiverType = initFactory.getSelfType(tree); + } else { + return super.getAnnotatedTypeBefore(expr, tree); + } + + if (initFactory.isInitializedForFrame(receiverType, declaringClass)) { + AnnotatedTypeMirror declared = getAnnotatedType(fa.getField()); + AnnotatedTypeMirror refined = super.getAnnotatedTypeBefore(expr, tree); + AnnotatedTypeMirror res = AnnotatedTypeMirror.createType(fa.getType(), this, false); + // If the expression is initialized, then by definition, it has at least its + // declared annotation. + // Assuming the correctness of the Nullness Checker's type refinement, + // it also has its refined annotation. + // We thus use the GLB of those two annotations. + res.addAnnotations( + qualHierarchy.greatestLowerBoundsShallow( + declared.getAnnotations(), + declared.getUnderlyingType(), + refined.getAnnotations(), + refined.getUnderlyingType())); + return res; + } + } + + // Is there anything better we could do? + // Ideally, we would turn the expression string into a Tree or Element + // instead of a JavaExpression, so we could use + // atypeFactory.getAnnotatedType on the whole expression, + // but that doesn't seem possible. + return super.getAnnotatedTypeBefore(expr, tree); + } + + @Override + protected DefaultForTypeAnnotator createDefaultForTypeAnnotator() { + DefaultForTypeAnnotator defaultForTypeAnnotator = new DefaultForTypeAnnotator(this); + 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 TreeAnnotator createTreeAnnotator() { + // Don't call super.createTreeAnnotator because the default tree annotators are incorrect + // for the Nullness Checker. + List annotators = new ArrayList<>(3); + // annotators.add(new DebugListTreeAnnotator(new Tree.Kind[] + // {Tree.Kind.CONDITIONAL_EXPRESSION})); + annotators.add(new InitializationFieldAccessTreeAnnotator(this)); + annotators.add(new NullnessPropagationTreeAnnotator(this)); + annotators.add(new LiteralTreeAnnotator(this)); + return new ListTreeAnnotator(annotators); + } + + /** Adds nullness-specific propagation rules */ + protected class NullnessPropagationTreeAnnotator extends PropagationTreeAnnotator { + + /** + * Creates a NullnessPropagationTreeAnnotator. + * + * @param atypeFactory this factory + */ + public NullnessPropagationTreeAnnotator(AnnotatedTypeFactory atypeFactory) { + super(atypeFactory); + } + + @Override + public Void visitTypeCast(TypeCastTree tree, AnnotatedTypeMirror type) { + if (type.getKind().isPrimitive()) { + AnnotationMirror NONNULL = + ((NullnessNoInitAnnotatedTypeFactory) atypeFactory).NONNULL; + // If a @Nullable expression is cast to a primitive, then an unboxing.of.nullable + // error is issued. Treat the cast as if it were annotated as @NonNull to avoid an + // "type.invalid.annotations.on.use" error. + type.addMissingAnnotation(NONNULL); + } + return super.visitTypeCast(tree, type); + } + + @Override + public Void visitMemberSelect(MemberSelectTree tree, AnnotatedTypeMirror type) { + Element elt = TreeUtils.elementFromUse(tree); + assert elt != null; + + // Make primitive variable @NonNull in case the Initialization Checker + // considers it uninitialized. + if (TypesUtils.isPrimitive(type.getUnderlyingType())) { + type.replaceAnnotation(NONNULL); + } + + return null; + } + + @Override + public Void visitVariable(VariableTree tree, AnnotatedTypeMirror type) { + Element elt = TreeUtils.elementFromDeclaration(tree); + if (elt.getKind() == ElementKind.EXCEPTION_PARAMETER) { + // case 9. exception parameter + type.addMissingAnnotation(NONNULL); + } + return null; + } + + @Override + public Void visitIdentifier(IdentifierTree tree, AnnotatedTypeMirror type) { + + Element elt = TreeUtils.elementFromUse(tree); + assert elt != null; + + if (elt.getKind() == ElementKind.EXCEPTION_PARAMETER) { + // TODO: It's surprising that we have to do this in both visitVariable and + // visitIdentifier. This should already be handled by applying the defaults anyway. + // case 9. exception parameter + type.replaceAnnotation(NONNULL); + } + + // Make primitive variable @NonNull in case the Initialization Checker + // considers it uninitialized. + if (TypesUtils.isPrimitive(type.getUnderlyingType())) { + type.replaceAnnotation(NONNULL); + } + + return null; + } + + // The result of a binary operation is always non-null. + @Override + public Void visitBinary(BinaryTree tree, AnnotatedTypeMirror type) { + type.replaceAnnotation(NONNULL); + return null; + } + + // The result of a compound operation is always non-null. + @Override + public Void visitCompoundAssignment(CompoundAssignmentTree tree, AnnotatedTypeMirror type) { + super.visitCompoundAssignment(tree, type); + type.replaceAnnotation(NONNULL); + return null; + } + + // The result of a unary operation is always non-null. + @Override + public Void visitUnary(UnaryTree tree, AnnotatedTypeMirror type) { + type.replaceAnnotation(NONNULL); + return null; + } + + // The result of newly allocated structures is always non-null. + @Override + public Void visitNewClass(NewClassTree tree, AnnotatedTypeMirror type) { + type.replaceAnnotation(NONNULL); + return null; + } + + @Override + public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { + super.visitNewArray(tree, type); + + // The result of newly allocated structures is always non-null. + type.replaceAnnotation(NONNULL); + + return null; + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, AnnotatedTypeMirror type) { + if (TreeUtils.isMethodInvocation(tree, copyOfMethods, processingEnv)) { + List args = tree.getArguments(); + ExpressionTree lengthArg = args.get(1); + if (TreeUtils.isArrayLengthAccess(lengthArg)) { + // TODO: This syntactic test may not be not correct if the array expression has + // a side effect that affects the array length. This code could require that + // the expression has no method calls, assignments, etc. + ExpressionTree arrayArg = args.get(0); + if (TreeUtils.sameTree( + arrayArg, ((MemberSelectTree) lengthArg).getExpression())) { + AnnotatedArrayType arrayArgType = + (AnnotatedArrayType) getAnnotatedType(arrayArg); + AnnotatedTypeMirror arrayArgComponentType = arrayArgType.getComponentType(); + // Maybe this call is only necessary if argNullness is @NonNull. + ((AnnotatedArrayType) type) + .getComponentType() + .replaceAnnotations(arrayArgComponentType.getAnnotations()); + } + } + } + return super.visitMethodInvocation(tree, type); + } + } + + /** + * Returns the list of annotations of the non-null type system. + * + * @return the list of annotations of the non-null type system + */ + public Set> getNullnessAnnotations() { + return nullnessAnnos; + } + + @Override + protected QualifierHierarchy createQualifierHierarchy() { + return new NoElementQualifierHierarchy(getSupportedTypeQualifiers(), elements, this); + } + + /** + * Returns true if some annotation on the given type, or in the given list, is a nullness + * annotation such as @NonNull, @Nullable, @MonotonicNonNull, etc. + * + *

    This method ignores aliases of nullness annotations that are declaration annotations, + * because they may apply to inner types. + * + * @param annoTrees a list of annotations that the Java parser attached to the variable/method + * declaration; null if this type is not from such a location. This is a list of extra + * annotations to check, in addition to those on the type. + * @param typeTree the type whose annotations to test + * @return true if some annotation is a nullness annotation + */ + protected boolean containsNullnessAnnotation( + @Nullable List annoTrees, Tree typeTree) { + List annos = + TreeUtils.getExplicitAnnotationTrees(annoTrees, typeTree); + return containsNullnessAnnotation(annos); + } + + /** + * Returns true if some annotation in the given list is a nullness annotation such + * as @NonNull, @Nullable, @MonotonicNonNull, etc. + * + *

    This method ignores aliases of nullness annotations that are declaration annotations, + * because they may apply to inner types. + * + *

    Clients that are processing a field or variable definition, or a method return type, + * should call {@link #containsNullnessAnnotation(List, Tree)} instead. + * + * @param annoTrees a list of annotations to check + * @return true if some annotation is a nullness annotation + * @see #containsNullnessAnnotation(List, Tree) + */ + protected boolean containsNullnessAnnotation(List annoTrees) { + for (AnnotationTree annoTree : annoTrees) { + AnnotationMirror am = TreeUtils.annotationFromAnnotationTree(annoTree); + if (isNullnessAnnotation(am) && AnnotationUtils.isTypeUseAnnotation(am)) { + return true; + } + } + return false; + } + + /** + * Returns true if the given annotation is a nullness annotation such as {@code @NonNull}, + * {@code @Nullable}, {@code @MonotonicNonNull}, {@code @PolyNull}, or an alias thereof. + * + * @param am an annotation + * @return true if the given annotation is a nullness annotation + */ + protected boolean isNullnessAnnotation(AnnotationMirror am) { + return isNonNullOrAlias(am) + || isNullableOrAlias(am) + || AnnotationUtils.areSameByName(am, MONOTONIC_NONNULL) + || isPolyNullOrAlias(am); + } + + /** + * Returns true if the given annotation is {@code @NonNull} or an alias for it. + * + * @param am an annotation + * @return true if the given annotation is {@code @NonNull} or an alias for it + */ + protected boolean isNonNullOrAlias(AnnotationMirror am) { + AnnotationMirror canonical = canonicalAnnotation(am); + if (canonical != null) { + am = canonical; + } + return AnnotationUtils.areSameByName(am, NONNULL); + } + + /** + * Returns true if the given annotation is {@code @Nullable} or an alias for it. + * + * @param am an annotation + * @return true if the given annotation is {@code @Nullable} or an alias for it + */ + protected boolean isNullableOrAlias(AnnotationMirror am) { + AnnotationMirror canonical = canonicalAnnotation(am); + if (canonical != null) { + am = canonical; + } + return AnnotationUtils.areSameByName(am, NULLABLE); + } + + /** + * Returns true if the given annotation is {@code @PolyNull} or an alias for it. + * + * @param am an annotation + * @return true if the given annotation is {@code @PolyNull} or an alias for it + */ + protected boolean isPolyNullOrAlias(AnnotationMirror am) { + AnnotationMirror canonical = canonicalAnnotation(am); + if (canonical != null) { + am = canonical; + } + return AnnotationUtils.areSameByName(am, POLYNULL); + } + + // If a reference field has no initializer, then its default value is null. Treat that as + // @MonotonicNonNull rather than as @Nullable. + @Override + public AnnotatedTypeMirror getDefaultValueAnnotatedType(TypeMirror typeMirror) { + AnnotatedTypeMirror result = super.getDefaultValueAnnotatedType(typeMirror); + if (getAnnotationByClass(result.getAnnotations(), Nullable.class) != null) { + result.replaceAnnotation(MONOTONIC_NONNULL); + } + return result; + } + + /** A non-null reference to an object stays non-null under mutation. */ + @Override + public boolean isImmutable(TypeMirror type) { + return true; + } + + /* NO-AFU + // If + // 1. rhs is @Nullable + // 2. lhs is a field of this + // 3. in a constructor, initializer block, or field initializer + // then change rhs to @MonotonicNonNull. + @Override + public void wpiAdjustForUpdateField( + Tree lhsTree, Element element, String fieldName, AnnotatedTypeMirror rhsATM) { + // Synthetic variable names contain "#". Ignore them. + if (!rhsATM.hasAnnotation(Nullable.class) || fieldName.contains("#")) { + return; + } + TreePath lhsPath = getPath(lhsTree); + TypeElement enclosingClassOfLhs = + TreeUtils.elementFromDeclaration(TreePathUtil.enclosingClass(lhsPath)); + ClassSymbol enclosingClassOfField = ((VarSymbol) element).enclClass(); + if (enclosingClassOfLhs.equals(enclosingClassOfField) && TreePathUtil.inConstructor(lhsPath)) { + rhsATM.replaceAnnotation(MONOTONIC_NONNULL); + } + } + + // If + // 1. rhs is @MonotonicNonNull + // then change rhs to @Nullable + @Override + public void wpiAdjustForUpdateNonField(AnnotatedTypeMirror rhsATM) { + if (rhsATM.hasAnnotation(MonotonicNonNull.class)) { + rhsATM.replaceAnnotation(NULLABLE); + } + } + + @Override + public boolean wpiShouldInferTypesForReceivers() { + // All receivers must be non-null, or the dereference involved in + // the method call would fail (and cause an NPE). So, WPI should not + // infer non-null or nullable annotations on method receiver parameters. + return false; + } + + // This implementation overrides the superclass implementation to: + // * check for @MonotonicNonNull + // * output @RequiresNonNull rather than @RequiresQualifier. + @Override + protected @Nullable AnnotationMirror createRequiresOrEnsuresQualifier( + String expression, + AnnotationMirror qualifier, + AnnotatedTypeMirror declaredType, + Analysis.BeforeOrAfter preOrPost, + @Nullable List preconds) { + // TODO: This does not handle the possibility that the user set a different default + // annotation. + if (!(declaredType.hasAnnotation(NULLABLE) + || declaredType.hasAnnotation(POLYNULL) + || declaredType.hasAnnotation(MONOTONIC_NONNULL))) { + return null; + } + + if (preOrPost == BeforeOrAfter.AFTER + && declaredType.hasAnnotation(MONOTONIC_NONNULL) + && preconds.contains(requiresNonNullAnno(expression))) { + // The postcondition is implied by the precondition and the field being + // @MonotonicNonNull. + return null; + } + + if (AnnotationUtils.areSameByName( + qualifier, "org.checkerframework.checker.nullness.qual.NonNull")) { + if (preOrPost == BeforeOrAfter.BEFORE) { + return requiresNonNullAnno(expression); + } else { + return ensuresNonNullAnno(expression); + } + + if (preOrPost == BeforeOrAfter.AFTER + && declaredType.hasAnnotation(MONOTONIC_NONNULL) + && preconds.contains(requiresNonNullAnno(expression))) { + // The postcondition is implied by the precondition and the field being + // @MonotonicNonNull. + return null; + } + + if (AnnotationUtils.areSameByName( + qualifier, "org.checkerframework.checker.nullness.qual.NonNull")) { + if (preOrPost == BeforeOrAfter.BEFORE) { + return requiresNonNullAnno(expression); + } else { + return ensuresNonNullAnno(expression); + } + } + return super.createRequiresOrEnsuresQualifier( + expression, qualifier, declaredType, preOrPost, preconds); + } + */ + + /* NO-AFU + * Returns a {@code RequiresNonNull("...")} annotation for the given expression. + * + * @param expression an expression + * @return a {@code RequiresNonNull("...")} annotation for the given expression + */ + /* NO-AFU + private AnnotationMirror requiresNonNullAnno(String expression) { + AnnotationBuilder builder = new AnnotationBuilder(processingEnv, RequiresNonNull.class); + builder.setValue("value", new String[] {expression}); + AnnotationMirror am = builder.build(); + return am; + } + */ + + /* NO-AFU + * Returns a {@code EnsuresNonNull("...")} annotation for the given expression. + * + * @param expression an expression + * @return a {@code EnsuresNonNull("...")} annotation for the given expression + */ + /* NO-AFU + private AnnotationMirror ensuresNonNullAnno(String expression) { + AnnotationBuilder builder = new AnnotationBuilder(processingEnv, EnsuresNonNull.class); + builder.setValue("value", new String[] {expression}); + AnnotationMirror am = builder.build(); + return am; + } + */ + + /** + * Returns true if {@code node} is an invocation of Map.get. + * + * @param node a CFG node + * @return true if {@code node} is an invocation of Map.get + */ + public boolean isMapGet(Node node) { + return NodeUtils.isMethodInvocation(node, mapGet, getProcessingEnv()); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitAnnotatedTypeFormatter.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitAnnotatedTypeFormatter.java new file mode 100644 index 000000000000..ee85f8ada9e8 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitAnnotatedTypeFormatter.java @@ -0,0 +1,64 @@ +package org.checkerframework.checker.nullness; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedNullType; +import org.checkerframework.framework.type.DefaultAnnotatedTypeFormatter; +import org.checkerframework.framework.util.AnnotationFormatter; +import org.checkerframework.framework.util.DefaultAnnotationFormatter; + +import java.util.Set; + +/** A DefaultAnnotatedTypeFormatter that prints null literals without their annotations. */ +public class NullnessNoInitAnnotatedTypeFormatter extends DefaultAnnotatedTypeFormatter { + + /** + * Create a new NullnessNoInitAnnotatedTypeFormatter + * + * @param printVerboseGenerics whether to print type variables in a less ambiguous manner using + * {@code []} to delimit bounds + * @param printInvisibleQualifiers whether or not to print invisible qualifiers + */ + public NullnessNoInitAnnotatedTypeFormatter( + boolean printVerboseGenerics, boolean printInvisibleQualifiers) { + super( + new NullnessFormattingVisitor( + new DefaultAnnotationFormatter(), + printVerboseGenerics, + printInvisibleQualifiers)); + } + + /** The visitor used by the {@code NullnessNoInitAnnotatedTypeFormatter}. */ + protected static class NullnessFormattingVisitor extends FormattingVisitor { + + /** + * Create a new NullnessFormattingVisitor. + * + * @param annoFormatter the formatter to use + * @param printVerboseGenerics whether to print type variables in a less ambiguous manner + * using {@code []} to delimit bounds + * @param defaultInvisiblesSetting whether or not to print invisible qualifiers + */ + public NullnessFormattingVisitor( + AnnotationFormatter annoFormatter, + boolean printVerboseGenerics, + boolean defaultInvisiblesSetting) { + super(annoFormatter, printVerboseGenerics, defaultInvisiblesSetting); + } + + @Override + public String visitNull(AnnotatedNullType type, Set visiting) { + if (type.getAnnotation(Nullable.class) != null) { + // The null type will be understood as nullable by readers (I hope), therefore omit + // the annotations if they are @Nullable. + // Note: The visitTypeVariable will still print lower bounds with Null kind as + // "Void" + if (!currentPrintInvisibleSetting) { + return "null (NullType)"; + } + } + + return super.visitNull(type, visiting); + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitStore.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitStore.java new file mode 100644 index 000000000000..78942759eafe --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitStore.java @@ -0,0 +1,184 @@ +package org.checkerframework.checker.nullness; + +import org.checkerframework.checker.initialization.InitializationAnnotatedTypeFactory; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.PolyNull; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizer; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.framework.flow.CFAbstractAnalysis; +import org.checkerframework.framework.flow.CFAbstractStore; +import org.checkerframework.framework.qual.MonotonicQualifier; +import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; + +import java.util.HashMap; +import java.util.Map; + +/** + * In addition to the base class behavior, tracks whether {@link PolyNull} is known to be {@link + * NonNull} or {@link Nullable} (or not known to be either). + */ +public class NullnessNoInitStore extends CFAbstractStore { + + /** True if, at this point, {@link PolyNull} is known to be {@link NonNull}. */ + protected boolean isPolyNullNonNull; + + /** True if, at this point, {@link PolyNull} is known to be {@link Nullable}. */ + protected boolean isPolyNullNull; + + /** + * Initialized fields and their values. + * + *

    This is used by {@link #newFieldValueAfterMethodCall(FieldAccess, + * GenericAnnotatedTypeFactory, NullnessNoInitValue)} as cache to avoid performance issue in + * #1438. + * + * @see + * InitializationAnnotatedTypeFactory#isInitialized(org.checkerframework.framework.type.GenericAnnotatedTypeFactory, + * org.checkerframework.framework.flow.CFAbstractValue, + * javax.lang.model.element.VariableElement) + */ + protected Map initializedFields; + + /** + * Create a NullnessStore. + * + * @param analysis the analysis class this store belongs to + * @param sequentialSemantics should the analysis use sequential Java semantics (i.e., assume + * that only one thread is running at all times)? + */ + public NullnessNoInitStore( + CFAbstractAnalysis analysis, + boolean sequentialSemantics) { + super(analysis, sequentialSemantics); + isPolyNullNonNull = false; + isPolyNullNull = false; + } + + /** + * Create a NullnessStore (copy constructor). + * + * @param s a store to copy + */ + public NullnessNoInitStore(NullnessNoInitStore s) { + super(s); + isPolyNullNonNull = s.isPolyNullNonNull; + isPolyNullNull = s.isPolyNullNull; + if (s.initializedFields != null) { + initializedFields = s.initializedFields; + } + } + + @Override + protected NullnessNoInitValue newFieldValueAfterMethodCall( + FieldAccess fieldAccess, + GenericAnnotatedTypeFactory + atypeFactory, + NullnessNoInitValue value) { + // If the field is unassignable, it cannot change; thus we keep + // its current value. + // Unassignable fields must be handled before initialized fields + // because in the case of a field that is both unassignable and + // initialized, the initializedFields cache may contain an older, + // less refined value. + if (fieldAccess.isUnassignableByOtherCode()) { + return value; + } + + if (initializedFields == null) { + initializedFields = new HashMap<>(4); + } + + // If the field is initialized, it can change, but cannot be uninitialized. + // We thus keep a new value based on its declared type. + if (initializedFields.containsKey(fieldAccess)) { + return initializedFields.get(fieldAccess); + } else if (InitializationAnnotatedTypeFactory.isInitialized( + atypeFactory, value, fieldAccess.getField()) + && atypeFactory + .getAnnotationWithMetaAnnotation( + fieldAccess.getField(), MonotonicQualifier.class) + .isEmpty()) { + + NullnessNoInitValue newValue = + analysis.createAbstractValue( + atypeFactory.getAnnotatedType(fieldAccess.getField()).getAnnotations(), + value.getUnderlyingType()); + initializedFields.put(fieldAccess, newValue); + return newValue; + } + + // If the field has a monotonic annotation, we use the superclass's + // handling of monotonic annotations. + return super.newMonotonicFieldValueAfterMethodCall(fieldAccess, atypeFactory, value); + } + + @Override + public NullnessNoInitStore leastUpperBound(NullnessNoInitStore other) { + NullnessNoInitStore lub = super.leastUpperBound(other); + lub.isPolyNullNonNull = isPolyNullNonNull && other.isPolyNullNonNull; + lub.isPolyNullNull = isPolyNullNull && other.isPolyNullNull; + return lub; + } + + @Override + protected boolean supersetOf(CFAbstractStore o) { + if (!(o instanceof NullnessNoInitStore)) { + return false; + } + NullnessNoInitStore other = (NullnessNoInitStore) o; + if ((other.isPolyNullNonNull != isPolyNullNonNull) + || (other.isPolyNullNull != isPolyNullNull)) { + return false; + } + return super.supersetOf(other); + } + + @Override + protected String internalVisualize( + CFGVisualizer viz) { + return super.internalVisualize(viz) + + viz.getSeparator() + + viz.visualizeStoreKeyVal("isPolyNullNonNull", isPolyNullNonNull) + + viz.getSeparator() + + viz.visualizeStoreKeyVal("isPolyNullNull", isPolyNullNull); + } + + /** + * Returns true if, at this point, {@link PolyNull} is known to be {@link NonNull}. + * + * @return true if, at this point, {@link PolyNull} is known to be {@link NonNull} + */ + public boolean isPolyNullNonNull() { + return isPolyNullNonNull; + } + + /** + * Set the value of whether, at this point, {@link PolyNull} is known to be {@link NonNull}. + * + * @param isPolyNullNonNull whether, at this point, {@link PolyNull} is known to be {@link + * NonNull} + */ + public void setPolyNullNonNull(boolean isPolyNullNonNull) { + this.isPolyNullNonNull = isPolyNullNonNull; + } + + /** + * Returns true if, at this point, {@link PolyNull} is known to be {@link Nullable}. + * + * @return true if, at this point, {@link PolyNull} is known to be {@link Nullable} + */ + public boolean isPolyNullNull() { + return isPolyNullNull; + } + + /** + * Set the value of whether, at this point, {@link PolyNull} is known to be {@link Nullable}. + * + * @param isPolyNullNull whether, at this point, {@link PolyNull} is known to be {@link + * Nullable} + */ + public void setPolyNullNull(boolean isPolyNullNull) { + this.isPolyNullNull = isPolyNullNull; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitSubchecker.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitSubchecker.java new file mode 100644 index 000000000000..0536a3ccb7f0 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitSubchecker.java @@ -0,0 +1,72 @@ +package org.checkerframework.checker.nullness; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; + +import org.checkerframework.checker.initialization.InitializationChecker; +import org.checkerframework.checker.initialization.InitializationFieldAccessSubchecker; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.qual.StubFiles; + +import java.util.NavigableSet; +import java.util.Set; + +/** + * The subchecker of the {@link NullnessChecker} which actually checks {@link NonNull} and related + * qualifiers. + * + *

    The {@link NullnessChecker} uses this checker as the target (see {@link + * InitializationChecker#getTargetCheckerClass()}) for its initialization type system. + */ +@StubFiles({"junit-assertions.astub"}) +public class NullnessNoInitSubchecker extends BaseTypeChecker { + + /** Default constructor for NonNullChecker. */ + public NullnessNoInitSubchecker() {} + + @Override + public NullnessNoInitAnnotatedTypeFactory getTypeFactory() { + return (NullnessNoInitAnnotatedTypeFactory) super.getTypeFactory(); + } + + @Override + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); + if (!hasOption("assumeKeyFor")) { + checkers.add(KeyForSubchecker.class); + } + checkers.add(InitializationFieldAccessSubchecker.class); + return checkers; + } + + @Override + public NavigableSet getSuppressWarningsPrefixes() { + NavigableSet result = super.getSuppressWarningsPrefixes(); + result.add("nullness"); + return result; + } + + @Override + protected String getWarningMessagePrefix() { + return "nullness"; + } + + @Override + protected BaseTypeVisitor createSourceVisitor() { + return new NullnessNoInitVisitor(this); + } + + // The NullnessNoInitChecker should also skip defs skipped by the NullnessChecker + + @Override + public boolean shouldSkipDefs(ClassTree tree) { + return super.shouldSkipDefs(tree) || parentChecker.shouldSkipDefs(tree); + } + + @Override + public boolean shouldSkipDefs(ClassTree cls, MethodTree meth) { + return super.shouldSkipDefs(cls, meth) || parentChecker.shouldSkipDefs(cls, meth); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitTransfer.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitTransfer.java new file mode 100644 index 000000000000..7c951135d8c2 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitTransfer.java @@ -0,0 +1,513 @@ +package org.checkerframework.checker.nullness; + +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.PolyNull; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.dataflow.analysis.ConditionalTransferResult; +import org.checkerframework.dataflow.analysis.TransferInput; +import org.checkerframework.dataflow.analysis.TransferResult; +import org.checkerframework.dataflow.cfg.node.ArrayAccessNode; +import org.checkerframework.dataflow.cfg.node.FieldAccessNode; +import org.checkerframework.dataflow.cfg.node.InstanceOfNode; +import org.checkerframework.dataflow.cfg.node.MethodAccessNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.node.NullLiteralNode; +import org.checkerframework.dataflow.cfg.node.ReturnNode; +import org.checkerframework.dataflow.cfg.node.ThrowNode; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.LocalVariable; +import org.checkerframework.dataflow.util.PurityUtils; +import org.checkerframework.framework.flow.CFAbstractStore; +import org.checkerframework.framework.flow.CFAbstractTransfer; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.framework.type.visitor.SimpleAnnotatedTypeScanner; +import org.checkerframework.framework.util.AnnotatedTypes; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; +import org.checkerframework.javacutil.TypesUtils; + +import java.util.List; +import java.util.Map; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; + +/** + * Transfer function for the non-null type system. Performs the following refinements: + * + *

      + *
    1. After an expression is compared with the {@code null} literal, then that expression can + * safely be considered {@link NonNull} if the result of the comparison is false or {@link + * Nullable} if the result is true. + *
    2. If an expression is dereferenced, then it can safely be assumed to non-null in the future. + * If it would not be, then the dereference would have raised a {@link NullPointerException}. + *
    3. Tracks whether {@link PolyNull} is known to be {@link NonNull} or {@link Nullable} (or not + * known to be either). + *
    + */ +public class NullnessNoInitTransfer + extends CFAbstractTransfer< + NullnessNoInitValue, NullnessNoInitStore, NullnessNoInitTransfer> { + + /** The @{@link NonNull} annotation. */ + protected final AnnotationMirror NONNULL; + + /** The @{@link Nullable} annotation. */ + protected final AnnotationMirror NULLABLE; + + /** The @{@link PolyNull} annotation. */ + protected final AnnotationMirror POLYNULL; + + /** + * Java's Map interface. + * + *

    The qualifiers in this type don't matter -- it is not used as a fully-annotated + * AnnotatedDeclaredType, but just passed to asSuper(). + */ + protected final AnnotatedDeclaredType MAP_TYPE; + + /** The type factory for the nullness analysis that was passed to the constructor. */ + protected final NullnessNoInitAnnotatedTypeFactory nullnessTypeFactory; + + /** + * The type factory for the map key analysis, or null if the Map Key Checker should not be run. + */ + protected final @Nullable KeyForAnnotatedTypeFactory keyForTypeFactory; + + /** True if -AassumeKeyFor was provided on the command line. */ + private final boolean assumeKeyFor; + + /** + * True if conservativeArgumentNullnessAfterInvocation flag is turned off, meaning that after a + * method call or constructor invocation, arguments of the invocation (including the receiver) + * are assumed to be non-null. + */ + private final boolean nonNullAssumptionAfterInvocation; + + /** + * Create a new NullnessTransfer for the given analysis. + * + * @param analysis nullness analysis + */ + public NullnessNoInitTransfer(NullnessNoInitAnalysis analysis) { + super(analysis); + this.nullnessTypeFactory = (NullnessNoInitAnnotatedTypeFactory) analysis.getTypeFactory(); + Elements elements = nullnessTypeFactory.getElementUtils(); + BaseTypeChecker checker = nullnessTypeFactory.getChecker(); + assumeKeyFor = checker.hasOption("assumeKeyFor"); + if (assumeKeyFor) { + this.keyForTypeFactory = null; + } else { + // It is error-prone to put a type factory in a field. It is OK here because + // keyForTypeFactory is used only to call methods isMapGet() and isKeyForMap(). + this.keyForTypeFactory = + nullnessTypeFactory.getTypeFactoryOfSubchecker(KeyForSubchecker.class); + } + + NONNULL = AnnotationBuilder.fromClass(elements, NonNull.class); + NULLABLE = AnnotationBuilder.fromClass(elements, Nullable.class); + POLYNULL = AnnotationBuilder.fromClass(elements, PolyNull.class); + + MAP_TYPE = + (AnnotatedDeclaredType) + AnnotatedTypeMirror.createType( + TypesUtils.typeFromClass(Map.class, analysis.getTypes(), elements), + nullnessTypeFactory, + false); + + nonNullAssumptionAfterInvocation = + !analysis.getTypeFactory() + .getChecker() + .getUltimateParentChecker() + .getBooleanOption("conservativeArgumentNullnessAfterInvocation", false); + } + + /** + * Sets a given {@link Node} to non-null in the given {@code store}. Calls to this method + * implement case 2. + * + * @param store the store to update + * @param node the node that should be non-null + */ + protected void makeNonNull(NullnessNoInitStore store, Node node) { + JavaExpression internalRepr = JavaExpression.fromNode(node); + store.insertValue(internalRepr, NONNULL); + } + + /** + * Sets a given node to non-null in the given transfer result. + * + * @param result the transfer result + * @param node the node to make non-null + */ + protected void makeNonNull( + TransferResult result, Node node) { + if (result.containsTwoStores()) { + makeNonNull(result.getThenStore(), node); + makeNonNull(result.getElseStore(), node); + } else { + makeNonNull(result.getRegularStore(), node); + } + } + + /** + * Refine the given result to @NonNull. + * + * @param result the result to refine + */ + protected void refineToNonNull( + TransferResult result) { + NullnessNoInitValue oldResultValue = result.getResultValue(); + NullnessNoInitValue refinedResultValue = + analysis.createSingleAnnotationValue(NONNULL, oldResultValue.getUnderlyingType()); + NullnessNoInitValue newResultValue = refinedResultValue.mostSpecific(oldResultValue, null); + result.setResultValue(newResultValue); + } + + @Override + protected @Nullable NullnessNoInitValue finishValue( + @Nullable NullnessNoInitValue value, NullnessNoInitStore store) { + value = super.finishValue(value, store); + if (value != null) { + value.isPolyNullNonNull = store.isPolyNullNonNull(); + value.isPolyNullNull = store.isPolyNullNull(); + } + return value; + } + + @Override + protected @Nullable NullnessNoInitValue finishValue( + @Nullable NullnessNoInitValue value, + NullnessNoInitStore thenStore, + NullnessNoInitStore elseStore) { + value = super.finishValue(value, thenStore, elseStore); + if (value != null) { + value.isPolyNullNonNull = + thenStore.isPolyNullNonNull() && elseStore.isPolyNullNonNull(); + value.isPolyNullNull = thenStore.isPolyNullNull() && elseStore.isPolyNullNull(); + } + return value; + } + + /** + * {@inheritDoc} + * + *

    Furthermore, this method refines the type to {@code NonNull} for the appropriate branch if + * an expression is compared to the {@code null} literal (listed as case 1 in the class + * description). + */ + @Override + protected TransferResult + strengthenAnnotationOfEqualTo( + TransferResult res, + Node firstNode, + Node secondNode, + NullnessNoInitValue firstValue, + NullnessNoInitValue secondValue, + boolean notEqualTo) { + res = + super.strengthenAnnotationOfEqualTo( + res, firstNode, secondNode, firstValue, secondValue, notEqualTo); + if (firstNode instanceof NullLiteralNode) { + NullnessNoInitStore thenStore = res.getThenStore(); + NullnessNoInitStore elseStore = res.getElseStore(); + + List secondParts = splitAssignments(secondNode); + for (Node secondPart : secondParts) { + JavaExpression secondInternal = JavaExpression.fromNode(secondPart); + if (CFAbstractStore.canInsertJavaExpression(secondInternal)) { + thenStore = thenStore == null ? res.getThenStore() : thenStore; + elseStore = elseStore == null ? res.getElseStore() : elseStore; + if (notEqualTo) { + thenStore.insertValue(secondInternal, NONNULL); + } else { + elseStore.insertValue(secondInternal, NONNULL); + } + } + } + + AnnotationMirrorSet secondAnnos = + secondValue != null ? secondValue.getAnnotations() : new AnnotationMirrorSet(); + if (nullnessTypeFactory.containsSameByClass(secondAnnos, PolyNull.class)) { + thenStore = thenStore == null ? res.getThenStore() : thenStore; + elseStore = elseStore == null ? res.getElseStore() : elseStore; + // TODO: methodTree is null for lambdas. Handle that case. See Issue3850.java. + MethodTree methodTree = analysis.getContainingMethod(secondNode.getTree()); + ExecutableElement methodElem = + methodTree == null ? null : TreeUtils.elementFromDeclaration(methodTree); + if (notEqualTo) { + elseStore.setPolyNullNull(true); + if (methodElem != null && polyNullIsNonNull(methodElem, thenStore)) { + thenStore.setPolyNullNonNull(true); + } + } else { + thenStore.setPolyNullNull(true); + if (methodElem != null && polyNullIsNonNull(methodElem, elseStore)) { + elseStore.setPolyNullNonNull(true); + } + } + } + + if (thenStore != null) { + return new ConditionalTransferResult<>(res.getResultValue(), thenStore, elseStore); + } + } + return res; + } + + /** + * Returns true if every formal parameter that is declared as @PolyNull is currently known to be + * non-null. + * + * @param method a method + * @param s a store + * @return true if every formal parameter declared as @PolyNull is non-null + */ + private boolean polyNullIsNonNull(ExecutableElement method, NullnessNoInitStore s) { + // No need to check the receiver, which is always non-null. + for (VariableElement var : method.getParameters()) { + AnnotatedTypeMirror varType = nullnessTypeFactory.fromElement(var); + + if (containsPolyNullNotAtTopLevel(varType)) { + return false; + } + + if (varType.hasAnnotation(POLYNULL)) { + NullnessNoInitValue v = s.getValue(new LocalVariable(var)); + if (!AnnotationUtils.containsSameByName(v.getAnnotations(), NONNULL)) { + return false; + } + } + } + return true; + } + + /** + * A scanner that returns true if there is an occurrence of @PolyNull that is not at the top + * level. + */ + // Not static so it can access field POLYNULL. + private class ContainsPolyNullNotAtTopLevelScanner + extends SimpleAnnotatedTypeScanner { + /** + * True if the top-level type has not yet been processed (by the first call to + * defaultAction). + */ + private boolean isTopLevel = true; + + /** Create a ContainsPolyNullNotAtTopLevelScanner. */ + ContainsPolyNullNotAtTopLevelScanner() { + super(Boolean::logicalOr, false); + } + + @Override + protected Boolean defaultAction(AnnotatedTypeMirror type, Void p) { + if (isTopLevel) { + isTopLevel = false; + return false; + } else { + return type.hasAnnotation(POLYNULL); + } + } + } + + /** + * Returns true if there is an occurrence of @PolyNull that is not at the top level. + * + * @param t a type + * @return true if there is an occurrence of @PolyNull that is not at the top level + */ + private boolean containsPolyNullNotAtTopLevel(AnnotatedTypeMirror t) { + return new ContainsPolyNullNotAtTopLevelScanner().visit(t); + } + + @Override + public TransferResult visitArrayAccess( + ArrayAccessNode n, TransferInput p) { + TransferResult result = + super.visitArrayAccess(n, p); + makeNonNull(result, n.getArray()); + return result; + } + + @Override + public TransferResult visitInstanceOf( + InstanceOfNode n, TransferInput p) { + TransferResult result = + super.visitInstanceOf(n, p); + NullnessNoInitStore thenStore = result.getThenStore(); + NullnessNoInitStore elseStore = result.getElseStore(); + makeNonNull(thenStore, n.getOperand()); + return new ConditionalTransferResult<>(result.getResultValue(), thenStore, elseStore); + } + + @Override + public TransferResult visitMethodAccess( + MethodAccessNode n, TransferInput p) { + TransferResult result = + super.visitMethodAccess(n, p); + // The receiver of an instance method access is non-null. A static method access does not + // ensure that the receiver is non-null. + if (!n.isStatic()) { + makeNonNull(result, n.getReceiver()); + } + return result; + } + + @Override + public TransferResult visitFieldAccess( + FieldAccessNode n, TransferInput p) { + TransferResult result = + super.visitFieldAccess(n, p); + // The receiver of an instance field access is non-null. A static field access does not + // ensure that the receiver is non-null. + if (!n.isStatic()) { + makeNonNull(result, n.getReceiver()); + } + return result; + } + + @Override + public TransferResult visitThrow( + ThrowNode n, TransferInput p) { + TransferResult result = super.visitThrow(n, p); + makeNonNull(result, n.getExpression()); + return result; + } + + /** + * {@inheritDoc} + * + *

    When the conservativeArgumentNullnessAfterInvocation flag is turned off, the receiver and + * arguments that are passed to non-null parameters in a method call or constructor invocation + * are unsoundly assumed to be non-null after the invocation. + * + *

    When the flag is turned on, the analysis is more conservative by checking the method is + * SideEffectFree or the receiver is unassignable. Only if either one of the two is true, is the + * receiver made non-null. Similar logic is applied to the arguments of the invocation. + * + *

    Provided that m is of a type that implements interface java.util.Map: + * + *

      + *
    • Given a call m.get(k), if k is @KeyFor("m") and m's value type is @NonNull, then the + * result is @NonNull in the thenStore and elseStore of the transfer result. + *
    + */ + @Override + public TransferResult visitMethodInvocation( + MethodInvocationNode n, TransferInput in) { + TransferResult result = + super.visitMethodInvocation(n, in); + + MethodInvocationTree tree = n.getTree(); + ExecutableElement method = TreeUtils.elementFromUse(tree); + + boolean isMethodSideEffectFree = + nullnessTypeFactory.isSideEffectFree(method) + || PurityUtils.isSideEffectFree(nullnessTypeFactory, method); + Node receiver = n.getTarget().getReceiver(); + if (nonNullAssumptionAfterInvocation + || isMethodSideEffectFree + || JavaExpression.fromNode(receiver).isUnassignableByOtherCode()) { + // Make receiver non-null. + makeNonNull(result, receiver); + } + + // For all formal parameters with a non-null annotation, make the actual argument non-null. + // The point of this is to prevent cascaded errors -- the Nullness Checker will issue a + // warning for the method invocation, but not for subsequent uses of the argument. See test + // case FlowNullness.java. + AnnotatedExecutableType methodType = nullnessTypeFactory.getAnnotatedType(method); + List methodParams = methodType.getParameterTypes(); + List methodArgs = tree.getArguments(); + for (int i = 0; i < methodParams.size() && i < methodArgs.size(); ++i) { + if (methodParams.get(i).hasAnnotation(NONNULL) + && (nonNullAssumptionAfterInvocation + || isMethodSideEffectFree + || JavaExpression.fromTree(methodArgs.get(i)) + .isUnassignableByOtherCode())) { + makeNonNull(result, n.getArgument(i)); + } + } + + // Refine result to @NonNull if n is an invocation of Map.get, the argument is a key for + // the map, and the map's value type is not @Nullable. + if (nullnessTypeFactory.isMapGet(n)) { + boolean isKeyFor; + if (keyForTypeFactory != null) { + String mapName = JavaExpression.fromNode(receiver).toString(); + isKeyFor = keyForTypeFactory.isKeyForMap(mapName, methodArgs.get(0)); + } else { + isKeyFor = assumeKeyFor; + } + if (isKeyFor) { + AnnotatedTypeMirror receiverType = nullnessTypeFactory.getReceiverType(n.getTree()); + if (!hasNullableValueType(receiverType)) { + makeNonNull(result, n); + refineToNonNull(result); + } + } + } + + return result; + } + + /** + * Returns true if mapType's value type (the V type argument to Map) is @Nullable. + * + * @param mapOrSubtype the Map type, or a subtype + * @return true if mapType's value type is @Nullable + */ + private boolean hasNullableValueType(AnnotatedTypeMirror mapOrSubtype) { + AnnotatedDeclaredType mapType = + AnnotatedTypes.asSuper(nullnessTypeFactory, mapOrSubtype, MAP_TYPE); + int numTypeArguments = mapType.getTypeArguments().size(); + if (numTypeArguments != 2) { + throw new TypeSystemError( + "Wrong number %d of type arguments: %s", numTypeArguments, mapType); + } + AnnotatedTypeMirror valueType = mapType.getTypeArguments().get(1); + return valueType.hasAnnotation(NULLABLE); + } + + @Override + public TransferResult visitReturn( + ReturnNode n, TransferInput in) { + TransferResult result = super.visitReturn(n, in); + + if (result.getResultValue() == null) { + // Make sure there is a value for return statements, to record (at this return + // statement) the values of isPolyNullNotNull and isPolyNullNull. + return recreateTransferResult(createDummyValue(), result); + } else { + return result; + } + } + + /** + * Creates a dummy abstract value (whose type is not supposed to be looked at). + * + * @return a dummy abstract value + */ + private NullnessNoInitValue createDummyValue() { + TypeMirror dummy = analysis.getEnv().getTypeUtils().getPrimitiveType(TypeKind.BOOLEAN); + AnnotationMirrorSet annos = new AnnotationMirrorSet(); + annos.addAll(nullnessTypeFactory.getQualifierHierarchy().getBottomAnnotations()); + return new NullnessNoInitValue(analysis, annos, dummy); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitValue.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitValue.java new file mode 100644 index 000000000000..8a3bb327d80b --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitValue.java @@ -0,0 +1,106 @@ +package org.checkerframework.checker.nullness; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.PolyNull; +import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.framework.flow.CFAbstractAnalysis; +import org.checkerframework.framework.flow.CFAbstractValue; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TypesUtils; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.type.TypeMirror; + +/** + * Behaves just like {@link CFValue}, but additionally tracks whether at this point {@link PolyNull} + * is known to be {@link NonNull} or {@link Nullable} (or not known to be either) + */ +public class NullnessNoInitValue extends CFAbstractValue { + + /** True if, at this point, {@link PolyNull} is known to be {@link NonNull}. */ + protected boolean isPolyNullNonNull; + + /** True if, at this point, {@link PolyNull} is known to be {@link Nullable}. */ + protected boolean isPolyNullNull; + + /** + * Creates a new NullnessValue. + * + * @param analysis the analysis + * @param annotations the annotations + * @param underlyingType the underlying type + */ + public NullnessNoInitValue( + CFAbstractAnalysis analysis, + AnnotationMirrorSet annotations, + TypeMirror underlyingType) { + super(analysis, annotations, underlyingType); + } + + @Override + protected NullnessNoInitValue upperBound( + @Nullable NullnessNoInitValue other, + TypeMirror upperBoundTypeMirror, + boolean shouldWiden) { + NullnessNoInitValue result = super.upperBound(other, upperBoundTypeMirror, shouldWiden); + + AnnotationMirror resultNullableAnno = + analysis.getTypeFactory().getAnnotationByClass(result.annotations, Nullable.class); + + if (resultNullableAnno != null && other != null) { + if ((this.isPolyNullNonNull + && this.containsNonNullOrPolyNull() + && other.isPolyNullNull + && other.containsNullableOrPolyNull()) + || (other.isPolyNullNonNull + && other.containsNonNullOrPolyNull() + && this.isPolyNullNull + && this.containsNullableOrPolyNull())) { + result.annotations.remove(resultNullableAnno); + result.annotations.add( + ((NullnessNoInitAnnotatedTypeFactory) analysis.getTypeFactory()).POLYNULL); + } + } + return result; + } + + /** + * Returns true if this value contains {@code @NonNull} or {@code @PolyNull}. + * + * @return true if this value contains {@code @NonNull} or {@code @PolyNull} + */ + @Pure + private boolean containsNonNullOrPolyNull() { + return analysis.getTypeFactory().containsSameByClass(annotations, NonNull.class) + || analysis.getTypeFactory().containsSameByClass(annotations, PolyNull.class); + } + + /** + * Returns true if this value contans {@code @Nullable} or {@code @PolyNull}. + * + * @return true if this value contans {@code @Nullable} or {@code @PolyNull} + */ + @Pure + private boolean containsNullableOrPolyNull() { + return analysis.getTypeFactory().containsSameByClass(annotations, Nullable.class) + || analysis.getTypeFactory().containsSameByClass(annotations, PolyNull.class); + } + + @SideEffectFree + @Override + public String toStringSimple() { + return "NV{" + + AnnotationUtils.toStringSimple(annotations) + + ", " + + TypesUtils.simpleTypeName(underlyingType) + + ", poly nn/n=" + + (isPolyNullNonNull ? 't' : 'f') + + '/' + + (isPolyNullNull ? 't' : 'f') + + '}'; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitVisitor.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitVisitor.java new file mode 100644 index 000000000000..bdd2be147e51 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessNoInitVisitor.java @@ -0,0 +1,1002 @@ +package org.checkerframework.checker.nullness; + +import com.sun.source.tree.AnnotatedTypeTree; +import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.ArrayAccessTree; +import com.sun.source.tree.ArrayTypeTree; +import com.sun.source.tree.AssertTree; +import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.CaseTree; +import com.sun.source.tree.CatchTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompoundAssignmentTree; +import com.sun.source.tree.ConditionalExpressionTree; +import com.sun.source.tree.DoWhileLoopTree; +import com.sun.source.tree.EnhancedForLoopTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.ForLoopTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.IfTree; +import com.sun.source.tree.InstanceOfTree; +import com.sun.source.tree.LiteralTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewArrayTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.ParameterizedTypeTree; +import com.sun.source.tree.SwitchTree; +import com.sun.source.tree.SynchronizedTree; +import com.sun.source.tree.ThrowTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.tree.UnaryTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.tree.WhileLoopTree; +import com.sun.source.util.TreePath; + +import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; +import org.checkerframework.checker.formatter.qual.FormatMethod; +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; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedPrimitiveType; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreePathUtil; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.SwitchExpressionUtils; +import org.checkerframework.javacutil.TypesUtils; + +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Set; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeMirror; + +/** The visitor for the nullness type-system. */ +public class NullnessNoInitVisitor extends BaseTypeVisitor { + // Error message keys + // private static final @CompilerMessageKey String ASSIGNMENT_TYPE_INCOMPATIBLE = + // "assignment.type.incompatible"; + /** Error message key. */ + private static final @CompilerMessageKey String UNBOXING_OF_NULLABLE = "unboxing.of.nullable"; + + /** Error message key. */ + private static final @CompilerMessageKey String LOCKING_NULLABLE = "locking.nullable"; + + /** Error message key. */ + private static final @CompilerMessageKey String THROWING_NULLABLE = "throwing.nullable"; + + /** Error message key. */ + private static final @CompilerMessageKey String ACCESSING_NULLABLE = "accessing.nullable"; + + /** Error message key. */ + private static final @CompilerMessageKey String CONDITION_NULLABLE = "condition.nullable"; + + /** Error message key. */ + private static final @CompilerMessageKey String ITERATING_NULLABLE = "iterating.over.nullable"; + + /** Error message key. */ + private static final @CompilerMessageKey String SWITCHING_NULLABLE = "switching.nullable"; + + /** Error message key. */ + private static final @CompilerMessageKey String DEREFERENCE_OF_NULLABLE = + "dereference.of.nullable"; + + /** Annotation mirrors for nullness annotations. */ + private final AnnotationMirror NONNULL, NULLABLE, MONOTONIC_NONNULL, POLYNULL; + + /** TypeMirror for java.lang.String. */ + private final TypeMirror stringType; + + /** The element for java.util.Collection.size(). */ + private final ExecutableElement collectionSize; + + /** The element for java.util.Collection.toArray(T). */ + private final ExecutableElement collectionToArray; + + /** The System.clearProperty(String) method. */ + private final ExecutableElement systemClearProperty; + + /** The System.setProperties(String) method. */ + private final ExecutableElement systemSetProperties; + + /** True if checked code may clear system properties. */ + private final boolean permitClearProperty; + + /** True if -AassumeAssertionsAreEnabled was passed on the command line. */ + private final boolean assumeAssertionsAreEnabled; + + /** True if -AassumeAssertionsAreDisabled was passed on the command line. */ + private final boolean assumeAssertionsAreDisabled; + + /** True if -Alint=redundantNullComparison was passed on the command line. */ + private final boolean redundantNullComparison; + + /** + * Create a new NullnessVisitor. + * + * @param checker the checker to which this visitor belongs + */ + public NullnessNoInitVisitor(BaseTypeChecker checker) { + super(checker); + + NONNULL = atypeFactory.NONNULL; + NULLABLE = atypeFactory.NULLABLE; + MONOTONIC_NONNULL = atypeFactory.MONOTONIC_NONNULL; + POLYNULL = atypeFactory.POLYNULL; + stringType = elements.getTypeElement(String.class.getCanonicalName()).asType(); + + ProcessingEnvironment env = checker.getProcessingEnvironment(); + this.collectionSize = TreeUtils.getMethod("java.util.Collection", "size", 0, env); + this.collectionToArray = TreeUtils.getMethod("java.util.Collection", "toArray", env, "T[]"); + systemClearProperty = TreeUtils.getMethod("java.lang.System", "clearProperty", 1, env); + systemSetProperties = TreeUtils.getMethod("java.lang.System", "setProperties", 1, env); + + this.permitClearProperty = + checker.getLintOption( + NullnessChecker.LINT_PERMITCLEARPROPERTY, + NullnessChecker.LINT_DEFAULT_PERMITCLEARPROPERTY); + assumeAssertionsAreEnabled = checker.hasOption("assumeAssertionsAreEnabled"); + assumeAssertionsAreDisabled = checker.hasOption("assumeAssertionsAreDisabled"); + redundantNullComparison = + checker.getLintOption( + NullnessChecker.LINT_REDUNDANTNULLCOMPARISON, + NullnessChecker.LINT_DEFAULT_REDUNDANTNULLCOMPARISON); + } + + @Override + public NullnessNoInitAnnotatedTypeFactory createTypeFactory() { + return new NullnessNoInitAnnotatedTypeFactory(checker); + } + + @Override + public boolean isValidUse(AnnotatedPrimitiveType type, Tree tree) { + // The Nullness Checker issues a more comprehensible "nullness.on.primitive" error rather + // than the "type.invalid.annotations.on.use" error this method would issue. + return true; + } + + private boolean containsSameByName( + Set> quals, AnnotationMirror anno) { + for (Class q : quals) { + if (atypeFactory.areSameByClass(anno, q)) { + return true; + } + } + return false; + } + + @Override + protected void checkConstructorResult( + AnnotatedExecutableType constructorType, ExecutableElement constructorElement) { + // Constructor results are always @NonNull. Other annotations are forbidden by + // #visitMethod. + // Nothing to check. + } + + @Override + protected void checkThisOrSuperConstructorCall( + MethodInvocationTree superCall, @CompilerMessageKey String errorKey) { + // Constructor results are always @NonNull, so the result type of a this/super call is + // always equal to the result type of the current constructor. + // Nothing to check. + } + + @Override + protected boolean commonAssignmentCheck( + Tree varTree, + ExpressionTree valueExp, + @CompilerMessageKey String errorKey, + Object... extraArgs) { + // Allow a MonotonicNonNull field to be initialized to null at its declaration, in a + // constructor, or in an initializer block. (The latter two are, strictly speaking, unsound + // because the constructor or initializer block might have previously set the field to a + // non-null value. Maybe add an option to disable that behavior.) + Element elem = initializedElement(varTree); + if (elem != null + && atypeFactory.fromElement(elem).hasEffectiveAnnotation(MONOTONIC_NONNULL) + && !checker.getLintOption( + NullnessChecker.LINT_NOINITFORMONOTONICNONNULL, + NullnessChecker.LINT_DEFAULT_NOINITFORMONOTONICNONNULL)) { + return true; + } + return super.commonAssignmentCheck(varTree, valueExp, errorKey, extraArgs); + } + + /** + * Returns the variable element, if the argument is an initialization; otherwise returns null. + * + * @param varTree an assignment LHS + * @return the initialized element, or null + */ + @SuppressWarnings("UnusedMethod") + private @Nullable Element initializedElement(Tree varTree) { + switch (varTree.getKind()) { + case VARIABLE: + // It's a variable declaration. + return TreeUtils.elementFromDeclaration((VariableTree) varTree); + + case MEMBER_SELECT: + MemberSelectTree mst = (MemberSelectTree) varTree; + ExpressionTree receiver = mst.getExpression(); + // This recognizes "this.fieldname = ..." but not "MyClass.fieldname = ..." or + // "MyClass.this.fieldname = ...". The latter forms are probably rare in a + // constructor. + // Note that this method should return non-null only for fields of this class, not + // fields of any other class, including outer classes. + if (receiver.getKind() != Tree.Kind.IDENTIFIER + || !((IdentifierTree) receiver).getName().contentEquals("this")) { + return null; + } + // fallthrough + case IDENTIFIER: + TreePath path = getCurrentPath(); + if (TreePathUtil.inConstructor(path)) { + return TreeUtils.elementFromUse((ExpressionTree) varTree); + } else { + return null; + } + + default: + return null; + } + } + + @Override + protected boolean commonAssignmentCheck( + AnnotatedTypeMirror varType, + ExpressionTree valueExp, + @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. + // TODO: A visitor should not change types, so this call to replacePolyQualifier is a hack. + // To work around this hack here, NullnessVisitor#visitConditionalExpression needs a further + // hack. Both should be undone, by properly resolving polymorphic qualifiers in the + // ATF/transfer. + atypeFactory.replacePolyQualifier(varType, valueExp); + return super.commonAssignmentCheck(varType, valueExp, errorKey, extraArgs); + } + + @Override + @FormatMethod + protected boolean commonAssignmentCheck( + AnnotatedTypeMirror varType, + AnnotatedTypeMirror valueType, + Tree valueTree, + @CompilerMessageKey String errorKey, + Object... extraArgs) { + if (TypesUtils.isPrimitive(varType.getUnderlyingType()) + && !TypesUtils.isPrimitive(valueType.getUnderlyingType())) { + boolean succeed = checkForNullability(valueType, valueTree, UNBOXING_OF_NULLABLE); + if (!succeed) { + // Only issue the unboxing of nullable error. + return false; + } + } + return super.commonAssignmentCheck(varType, valueType, valueTree, errorKey, extraArgs); + } + + /** Case 1: Check for null dereferencing. */ + @Override + public Void visitMemberSelect(MemberSelectTree tree, Void p) { + // if (atypeFactory.isUnreachable(tree)) { + // return super.visitMemberSelect(tree, p); + // } + Element e = TreeUtils.elementFromUse(tree); + if (e.getKind() == ElementKind.CLASS) { + if (atypeFactory.containsNullnessAnnotation(null, tree.getExpression())) { + checker.reportError(tree, "nullness.on.outer"); + } + } else if (!(TreeUtils.isSelfAccess(tree) + || tree.getExpression().getKind() == Tree.Kind.PARAMETERIZED_TYPE + // case 8. static member access + || ElementUtils.isStatic(e))) { + checkForNullability(tree.getExpression(), DEREFERENCE_OF_NULLABLE); + } + + return super.visitMemberSelect(tree, p); + } + + /** Case 2: Check for implicit {@code .iterator} call. */ + @Override + public Void visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) { + checkForNullability(tree.getExpression(), ITERATING_NULLABLE); + return super.visitEnhancedForLoop(tree, p); + } + + /** Case 3: Check for array dereferencing. */ + @Override + public Void visitArrayAccess(ArrayAccessTree tree, Void p) { + checkForNullability(tree.getExpression(), ACCESSING_NULLABLE); + return super.visitArrayAccess(tree, p); + } + + @Override + public Void visitNewArray(NewArrayTree tree, Void p) { + AnnotatedArrayType type = atypeFactory.getAnnotatedType(tree); + AnnotatedTypeMirror componentType = type.getComponentType(); + if (componentType.hasEffectiveAnnotation(NONNULL) + && !isNewArrayAllZeroDims(tree) + && !isNewArrayInToArray(tree) + && !TypesUtils.isPrimitive(componentType.getUnderlyingType()) + && (checker.getLintOption("soundArrayCreationNullness", false) + // temporary, for backward compatibility + || checker.getLintOption("forbidnonnullarraycomponents", false))) { + checker.reportError( + tree, + "new.array.type.invalid", + componentType.getAnnotations(), + type.toString()); + } + + return super.visitNewArray(tree, p); + } + + /** + * Determine whether all dimensions given in a new array expression have zero as length. For + * example "new Object[0][0];". Also true for empty dimensions, as in "new Object[] {...}". + * + * @param tree the constructor invocation to check + * @return true if every array dimension has a size of zero + */ + private static boolean isNewArrayAllZeroDims(NewArrayTree tree) { + boolean isAllZeros = true; + for (ExpressionTree dim : tree.getDimensions()) { + if (dim instanceof LiteralTree) { + Object val = ((LiteralTree) dim).getValue(); + if (!(val instanceof Number) || !Integer.valueOf(0).equals(val)) { + isAllZeros = false; + break; + } + } else { + isAllZeros = false; + break; + } + } + return isAllZeros; + } + + /** + * Return true if the given tree is "new X[]", in the context "toArray(new X[])". + * + * @param tree a tree to test + * @return true if the tree is a new array within a call to toArray() + */ + private boolean isNewArrayInToArray(NewArrayTree tree) { + if (tree.getDimensions().size() != 1) { + return false; + } + + ExpressionTree dim = tree.getDimensions().get(0); + ProcessingEnvironment env = checker.getProcessingEnvironment(); + + if (!TreeUtils.isMethodInvocation(dim, collectionSize, env)) { + return false; + } + + ExpressionTree rcvsize = ((MethodInvocationTree) dim).getMethodSelect(); + if (!(rcvsize instanceof MemberSelectTree)) { + return false; + } + rcvsize = ((MemberSelectTree) rcvsize).getExpression(); + if (!(rcvsize instanceof IdentifierTree)) { + return false; + } + + Tree encl = getCurrentPath().getParentPath().getLeaf(); + + if (!TreeUtils.isMethodInvocation(encl, collectionToArray, env)) { + return false; + } + + ExpressionTree rcvtoarray = ((MethodInvocationTree) encl).getMethodSelect(); + if (!(rcvtoarray instanceof MemberSelectTree)) { + return false; + } + rcvtoarray = ((MemberSelectTree) rcvtoarray).getExpression(); + if (!(rcvtoarray instanceof IdentifierTree)) { + return false; + } + + return ((IdentifierTree) rcvsize).getName() == ((IdentifierTree) rcvtoarray).getName(); + } + + /** Case 4: Check for thrown exception nullness. */ + @Override + protected void checkThrownExpression(ThrowTree tree) { + checkForNullability(tree.getExpression(), THROWING_NULLABLE); + } + + /** Case 5: Check for synchronizing locks. */ + @Override + public Void visitSynchronized(SynchronizedTree tree, Void p) { + checkForNullability(tree.getExpression(), LOCKING_NULLABLE); + return super.visitSynchronized(tree, p); + } + + @Override + public Void visitAssert(AssertTree tree, Void p) { + // See also + // org.checkerframework.dataflow.cfg.builder.CFGBuilder.CFGTranslationPhaseOne.visitAssert + + // In cases where neither assumeAssertionsAreEnabled nor assumeAssertionsAreDisabled are + // turned on and @AssumeAssertions is not used, checkForNullability is still called since + // the CFGBuilder will have generated one branch for which asserts are assumed to be + // enabled. + + boolean doVisitAssert; + if (assumeAssertionsAreEnabled + || CFCFGBuilder.assumeAssertionsActivatedForAssertTree(checker, tree)) { + doVisitAssert = true; + } else if (assumeAssertionsAreDisabled) { + doVisitAssert = false; + } else { + // no option given -> visit + doVisitAssert = true; + } + + if (doVisitAssert) { + checkForNullability(tree.getCondition(), CONDITION_NULLABLE); + return super.visitAssert(tree, p); + } + + return null; + } + + @Override + public Void visitIf(IfTree tree, Void p) { + checkForNullability(tree.getCondition(), CONDITION_NULLABLE); + return super.visitIf(tree, p); + } + + @Override + public Void visitInstanceOf(InstanceOfTree tree, Void p) { + // The "reference type" is the type after "instanceof". + Tree refTypeTree = tree.getType(); + if (refTypeTree == null) { + // TODO: the type is null for deconstructor patterns. + // Handle them properly. + return null; + } + if (refTypeTree.getKind() == Tree.Kind.ANNOTATED_TYPE) { + List annotations = + TreeUtils.annotationsFromTree((AnnotatedTypeTree) refTypeTree); + if (AnnotationUtils.containsSame(annotations, NULLABLE)) { + checker.reportError(tree, "instanceof.nullable"); + } + if (AnnotationUtils.containsSame(annotations, NONNULL)) { + checker.reportWarning(tree, "instanceof.nonnull.redundant"); + } + } + // Don't call super because it will issue an incorrect instanceof.unsafe warning. + return null; + } + + /** + * Reports an error if a comparison of a @NonNull expression with the null literal is performed. + * Does nothing unless {@code -Alint=redundantNullComparison} is passed on the command line. + * + * @param tree a tree that might be a comparison of a @NonNull expression with the null literal + */ + protected void checkForRedundantTests(BinaryTree tree) { + ExpressionTree leftOp = tree.getLeftOperand(); + ExpressionTree rightOp = tree.getRightOperand(); + + // respect command-line option + if (!redundantNullComparison) { + return; + } + + // equality tests + if ((tree.getKind() == Tree.Kind.EQUAL_TO || tree.getKind() == Tree.Kind.NOT_EQUAL_TO)) { + AnnotatedTypeMirror left = atypeFactory.getAnnotatedType(leftOp); + AnnotatedTypeMirror right = atypeFactory.getAnnotatedType(rightOp); + if (leftOp.getKind() == Tree.Kind.NULL_LITERAL + && right.hasEffectiveAnnotation(NONNULL)) { + checker.reportWarning(tree, "nulltest.redundant", rightOp.toString()); + } else if (rightOp.getKind() == Tree.Kind.NULL_LITERAL + && left.hasEffectiveAnnotation(NONNULL)) { + checker.reportWarning(tree, "nulltest.redundant", leftOp.toString()); + } + } + } + + /** Case 6: Check for redundant nullness tests Case 7: unboxing case: primitive operations. */ + @Override + public Void visitBinary(BinaryTree tree, Void p) { + ExpressionTree leftOp = tree.getLeftOperand(); + ExpressionTree rightOp = tree.getRightOperand(); + + if (isUnboxingOperation(tree)) { + checkForNullability(leftOp, UNBOXING_OF_NULLABLE); + checkForNullability(rightOp, UNBOXING_OF_NULLABLE); + } + + checkForRedundantTests(tree); + + return super.visitBinary(tree, p); + } + + /** Case 7: unboxing case: primitive operation. */ + @Override + public Void visitUnary(UnaryTree tree, Void p) { + checkForNullability(tree.getExpression(), UNBOXING_OF_NULLABLE); + return super.visitUnary(tree, p); + } + + /** Case 7: unboxing case: primitive operation. */ + @Override + public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { + // ignore String concatenation + if (!isString(tree)) { + checkForNullability(tree.getVariable(), UNBOXING_OF_NULLABLE); + checkForNullability(tree.getExpression(), UNBOXING_OF_NULLABLE); + } + return super.visitCompoundAssignment(tree, p); + } + + /** Case 7: unboxing case: casting to a primitive. */ + @Override + public Void visitTypeCast(TypeCastTree tree, Void p) { + if (isPrimitive(tree) && !isPrimitive(tree.getExpression())) { + if (!checkForNullability(tree.getExpression(), UNBOXING_OF_NULLABLE)) { + // If unboxing of nullable is issued, don't issue any other errors. + return null; + } + } + return super.visitTypeCast(tree, p); + } + + @Override + public Void visitMethod(MethodTree tree, Void p) { + VariableTree receiver = tree.getReceiverParameter(); + + if (TreeUtils.isConstructor(tree)) { + // Constructor results are always @NonNull. Any annotations are forbidden. + List annoTrees = tree.getModifiers().getAnnotations(); + if (atypeFactory.containsNullnessAnnotation(annoTrees)) { + checker.reportError(tree, "nullness.on.constructor"); + } + } + + if (receiver != null) { + List annoTrees = receiver.getModifiers().getAnnotations(); + Tree type = receiver.getType(); + if (atypeFactory.containsNullnessAnnotation(annoTrees, type)) { + checker.reportError(tree, "nullness.on.receiver"); + } + } + + return super.visitMethod(tree, p); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { + if (!permitClearProperty) { + ProcessingEnvironment env = checker.getProcessingEnvironment(); + if (TreeUtils.isMethodInvocation(tree, systemClearProperty, env)) { + String literal = literalFirstArgument(tree); + if (literal == null + || SystemGetPropertyHandler.predefinedSystemProperties.contains(literal)) { + checker.reportError(tree, "clear.system.property"); + } + } + if (TreeUtils.isMethodInvocation(tree, systemSetProperties, env)) { + checker.reportError(tree, "clear.system.property"); + } + } + return super.visitMethodInvocation(tree, p); + } + + /** + * If the first argument of a method call is a literal, return it; otherwise return null. + * + * @param tree a method invocation whose first formal parameter is of String type + * @return the first argument if it is a literal, otherwise null + */ + /*package-private*/ static @Nullable String literalFirstArgument(MethodInvocationTree tree) { + List args = tree.getArguments(); + assert args.size() > 0; + ExpressionTree arg = args.get(0); + if (arg.getKind() == Tree.Kind.STRING_LITERAL) { + String literal = (String) ((LiteralTree) arg).getValue(); + return literal; + } + return null; + } + + @Override + public void processClassTree(ClassTree classTree) { + Tree extendsClause = classTree.getExtendsClause(); + if (extendsClause != null) { + reportErrorIfSupertypeContainsNullnessAnnotation(extendsClause); + } + for (Tree implementsClause : classTree.getImplementsClause()) { + reportErrorIfSupertypeContainsNullnessAnnotation(implementsClause); + } + + if (classTree.getKind() == Tree.Kind.ENUM) { + for (Tree member : classTree.getMembers()) { + if (member.getKind() == Tree.Kind.VARIABLE + && TreeUtils.elementFromDeclaration((VariableTree) member).getKind() + == ElementKind.ENUM_CONSTANT) { + VariableTree varDecl = (VariableTree) member; + List annoTrees = + varDecl.getModifiers().getAnnotations(); + Tree type = varDecl.getType(); + if (atypeFactory.containsNullnessAnnotation(annoTrees, type)) { + checker.reportError(member, "nullness.on.enum"); + } + } + } + } + + super.processClassTree(classTree); + } + + /** + * Report "nullness.on.supertype" error if a supertype has a nullness annotation. + * + * @param typeTree a supertype tree, from an {@code extends} or {@code implements} clause + */ + private void reportErrorIfSupertypeContainsNullnessAnnotation(Tree typeTree) { + if (typeTree.getKind() == Tree.Kind.ANNOTATED_TYPE) { + List annoTrees = + ((AnnotatedTypeTree) typeTree).getAnnotations(); + if (atypeFactory.containsNullnessAnnotation(annoTrees)) { + checker.reportError(typeTree, "nullness.on.supertype"); + } + } + } + + // ///////////// Utility methods ////////////////////////////// + + /** + * Issues the error message if the type of the tree is not of a {@link NonNull} type. + * + * @param tree the tree where the error is to reported + * @param errMsg the error message (must be {@link CompilerMessageKey}) + * @return whether or not the check succeeded + */ + private boolean checkForNullability(ExpressionTree tree, @CompilerMessageKey String errMsg) { + AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(tree); + return checkForNullability(type, tree, errMsg); + } + + /** + * Issues the error message if an expression with this type may be null. + * + * @param type annotated type + * @param tree the tree where the error is to reported + * @param errMsg the error message (must be {@link CompilerMessageKey}) + * @return whether or not the check succeeded + */ + private boolean checkForNullability( + AnnotatedTypeMirror type, Tree tree, @CompilerMessageKey String errMsg) { + if (!type.hasEffectiveAnnotation(NONNULL)) { + checker.reportError(tree, errMsg, tree); + return false; + } + return true; + } + + @Override + protected void checkMethodInvocability( + AnnotatedExecutableType method, MethodInvocationTree tree) { + if (method.getReceiverType() == null) { + // Static methods don't have a receiver to check. + return; + } + + if (!TreeUtils.isSelfAccess(tree) + && + // Static methods don't have a receiver + method.getReceiverType() != null) { + // TODO: should all or some constructors be excluded? + // method.getElement().getKind() != ElementKind.CONSTRUCTOR) { + AnnotationMirrorSet receiverAnnos = atypeFactory.getReceiverType(tree).getAnnotations(); + AnnotatedTypeMirror methodReceiver = method.getReceiverType().getErased(); + AnnotatedTypeMirror treeReceiver = methodReceiver.shallowCopy(false); + AnnotatedTypeMirror rcv = atypeFactory.getReceiverType(tree); + treeReceiver.addAnnotations(rcv.getEffectiveAnnotations()); + // If receiver is Nullable, then we don't want to issue a warning about method + // invocability (we'd rather have only the "dereference.of.nullable" message). + if (treeReceiver.hasAnnotation(NULLABLE) + || receiverAnnos.contains(MONOTONIC_NONNULL) + || treeReceiver.hasAnnotation(POLYNULL)) { + return; + } + } + super.checkMethodInvocability(method, tree); + } + + /** + * Returns true if the binary operation could cause an unboxing operation. + * + * @param tree a binary operation + * @return true if the binary operation could cause an unboxing operation + */ + private boolean isUnboxingOperation(BinaryTree tree) { + if (tree.getKind() == Tree.Kind.EQUAL_TO || tree.getKind() == Tree.Kind.NOT_EQUAL_TO) { + // it is valid to check equality between two reference types, even + // if one (or both) of them is null + return isPrimitive(tree.getLeftOperand()) != isPrimitive(tree.getRightOperand()); + } else { + // All BinaryTree's are of type String, a primitive type or the reference type + // equivalent of a primitive type. Furthermore, Strings don't have a primitive type, and + // therefore only BinaryTrees that aren't String can cause unboxing. + return !isString(tree); + } + } + + /** + * Returns true if the type of the tree is a super of String. + * + * @param tree a tree + * @return true if the type of the tree is a super of String + */ + private boolean isString(ExpressionTree tree) { + TypeMirror type = TreeUtils.typeOf(tree); + return types.isAssignable(stringType, type); + } + + /** + * Returns true if the type of the tree is a primitive. + * + * @param tree a tree + * @return true if the type of the tree is a primitive + */ + private static boolean isPrimitive(ExpressionTree tree) { + return TreeUtils.typeOf(tree).getKind().isPrimitive(); + } + + @Override + public Void visitSwitch(SwitchTree tree, Void p) { + if (!TreeUtils.hasNullCaseLabel(tree)) { + checkForNullability(tree.getExpression(), SWITCHING_NULLABLE); + } + if (redundantNullComparison && TreeUtils.isEnhancedSwitchStatement(tree)) { + ExpressionTree expression = tree.getExpression(); + List cases = tree.getCases(); + checkSwitchNullRedundant(expression, cases); + } + return super.visitSwitch(tree, p); + } + + @Override + public void visitSwitchExpression17(Tree switchExprTree) { + if (!TreeUtils.hasNullCaseLabel(switchExprTree)) { + checkForNullability( + SwitchExpressionUtils.getExpression(switchExprTree), SWITCHING_NULLABLE); + } + if (redundantNullComparison) { + ExpressionTree expression = SwitchExpressionUtils.getExpression(switchExprTree); + List cases = SwitchExpressionUtils.getCases(switchExprTree); + checkSwitchNullRedundant(expression, cases); + } + super.visitSwitchExpression17(switchExprTree); + } + + /** + * Reports a warning if the expression of the switch statement or expression is {@code @NonNull} + * and one of the case labels in the case trees is the null literal. + * + * @param expression the expression of the switch statement or expression + * @param cases the cases of the switch statement or expression + */ + private void checkSwitchNullRedundant( + ExpressionTree expression, List cases) { + AnnotatedTypeMirror switchType = atypeFactory.getAnnotatedType(expression); + if (!switchType.hasEffectiveAnnotation(NONNULL)) { + return; + } + outer: + for (CaseTree caseTree : cases) { + List caseLabels = TreeUtilsAfterJava11.CaseUtils.getLabels(caseTree); + for (Tree caseLabel : caseLabels) { + if (caseLabel.getKind() == Tree.Kind.NULL_LITERAL) { + checker.reportWarning(caseLabel, "nulltest.redundant", expression.toString()); + break outer; + } + } + } + } + + @Override + public Void visitForLoop(ForLoopTree tree, Void p) { + if (tree.getCondition() != null) { + // Condition is null e.g. in "for (;;) {...}" + checkForNullability(tree.getCondition(), CONDITION_NULLABLE); + } + return super.visitForLoop(tree, p); + } + + @Override + public Void visitNewClass(NewClassTree tree, Void p) { + ExpressionTree enclosingExpr = tree.getEnclosingExpression(); + if (enclosingExpr != null) { + checkForNullability(enclosingExpr, DEREFERENCE_OF_NULLABLE); + } + AnnotatedDeclaredType type = atypeFactory.getAnnotatedType(tree); + ExpressionTree identifier = tree.getIdentifier(); + if (identifier instanceof AnnotatedTypeTree) { + AnnotatedTypeTree t = (AnnotatedTypeTree) identifier; + for (AnnotationMirror a : atypeFactory.getAnnotatedType(t).getAnnotations()) { + // is this an annotation of the nullness checker? + boolean nullnessCheckerAnno = + containsSameByName(atypeFactory.getNullnessAnnotations(), a); + if (nullnessCheckerAnno && !AnnotationUtils.areSame(NONNULL, a)) { + // The type is not non-null => warning + checker.reportWarning(tree, "new.class.type.invalid", type.getAnnotations()); + // Note that other consistency checks are made by isValid. + } + } + if (t.toString().contains("@PolyNull")) { + // TODO: this is a hack, but PolyNull gets substituted + // afterwards + checker.reportWarning(tree, "new.class.type.invalid", type.getAnnotations()); + } + } + // TODO: It might be nicer to introduce a framework-level + // isValidNewClassType or some such. + return super.visitNewClass(tree, p); + } + + @Override + public Void visitWhileLoop(WhileLoopTree tree, Void p) { + checkForNullability(tree.getCondition(), CONDITION_NULLABLE); + return super.visitWhileLoop(tree, p); + } + + @Override + public Void visitDoWhileLoop(DoWhileLoopTree tree, Void p) { + checkForNullability(tree.getCondition(), CONDITION_NULLABLE); + return super.visitDoWhileLoop(tree, p); + } + + @Override + public Void visitConditionalExpression(ConditionalExpressionTree tree, Void p) { + checkForNullability(tree.getCondition(), CONDITION_NULLABLE); + // Note: Because of the hack in NullnessVisitor#commonAssignmentCheck, this code needs to + // use a copy of the types for the two invocations of #commonAssignmentCheck. These hacks + // should be undone, by properly resolving polymorphic qualifiers in the ATF/transfer. As + // this code is mostly duplicated from the super method, this code needs to be kept in sync. + AnnotatedTypeMirror condThen = atypeFactory.getAnnotatedType(tree); + AnnotatedTypeMirror condElse = condThen.deepCopy(); + this.commonAssignmentCheck( + condThen, tree.getTrueExpression(), "conditional.type.incompatible"); + this.commonAssignmentCheck( + condElse, tree.getFalseExpression(), "conditional.type.incompatible"); + // Avoid calling super, as the super method does not handle conditional branches correctly. + // Instead, manually implement the logic from TreeScanner#visitConditionalExpression to + // traverse the subtree. + Void r = scan(tree.getCondition(), p); + r = reduce(scan(tree.getTrueExpression(), p), r); + r = reduce(scan(tree.getFalseExpression(), p), r); + return r; + } + + @Override + protected void checkExceptionParameter(CatchTree tree) { + VariableTree param = tree.getParameter(); + List annoTrees = param.getModifiers().getAnnotations(); + Tree paramType = param.getType(); + if (atypeFactory.containsNullnessAnnotation(annoTrees, paramType)) { + // This is a warning rather than an error because writing `@Nullable` could make sense + // if the catch block re-assigns the variable to null. (That would be bad style.) + checker.reportWarning(param, "nullness.on.exception.parameter"); + } + + // Don't call super. + // BasetypeVisitor forces annotations on exception parameters to be top, but because + // exceptions can never be null, the Nullness Checker does not require this check. + } + + @Override + public Void visitAnnotation(AnnotationTree tree, Void p) { + // All annotation arguments are non-null and initialized, so no need to check them. + return null; + } + + @Override + public void visitAnnotatedType( + @Nullable List annoTrees, Tree typeTree) { + // Look for a MEMBER_SELECT or PRIMITIVE within the type. + Tree t = typeTree; + while (t != null) { + switch (t.getKind()) { + case MEMBER_SELECT: + Tree expr = ((MemberSelectTree) t).getExpression(); + if (atypeFactory.containsNullnessAnnotation(annoTrees, expr)) { + checker.reportError(expr, "nullness.on.outer"); + } + t = null; + break; + case PRIMITIVE_TYPE: + if (atypeFactory.containsNullnessAnnotation(annoTrees, t)) { + checker.reportError(t, "nullness.on.primitive"); + } + t = null; + break; + case ANNOTATED_TYPE: + AnnotatedTypeTree at = ((AnnotatedTypeTree) t); + Tree underlying = at.getUnderlyingType(); + if (underlying.getKind() == Tree.Kind.PRIMITIVE_TYPE) { + if (atypeFactory.containsNullnessAnnotation(null, at)) { + checker.reportError(t, "nullness.on.primitive"); + } + t = null; + } else { + t = underlying; + } + break; + case ARRAY_TYPE: + t = ((ArrayTypeTree) t).getType(); + break; + case PARAMETERIZED_TYPE: + t = ((ParameterizedTypeTree) t).getType(); + break; + default: + t = null; + break; + } + } + + super.visitAnnotatedType(annoTrees, typeTree); + } + + @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/NullnessStore.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessStore.java deleted file mode 100644 index c4595b2310f6..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessStore.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.checkerframework.checker.nullness; - -import org.checkerframework.checker.initialization.InitializationStore; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.nullness.qual.PolyNull; -import org.checkerframework.dataflow.cfg.CFGVisualizer; -import org.checkerframework.framework.flow.CFAbstractAnalysis; -import org.checkerframework.framework.flow.CFAbstractStore; - -/** - * Behaves like {@link InitializationStore}, but additionally tracks whether {@link PolyNull} is - * known to be {@link Nullable}. - */ -public class NullnessStore extends InitializationStore { - - protected boolean isPolyNullNull; - - public NullnessStore( - CFAbstractAnalysis analysis, - boolean sequentialSemantics) { - super(analysis, sequentialSemantics); - isPolyNullNull = false; - } - - public NullnessStore(NullnessStore s) { - super(s); - isPolyNullNull = s.isPolyNullNull; - } - - @Override - public NullnessStore leastUpperBound(NullnessStore other) { - NullnessStore lub = super.leastUpperBound(other); - if (isPolyNullNull == other.isPolyNullNull) { - lub.isPolyNullNull = isPolyNullNull; - } else { - lub.isPolyNullNull = false; - } - return lub; - } - - @Override - protected boolean supersetOf(CFAbstractStore o) { - if (!(o instanceof InitializationStore)) { - return false; - } - NullnessStore other = (NullnessStore) o; - if (other.isPolyNullNull != isPolyNullNull) { - return false; - } - return super.supersetOf(other); - } - - @Override - protected String internalVisualize(CFGVisualizer viz) { - return super.internalVisualize(viz) - + viz.visualizeStoreKeyVal("isPolyNonNull", isPolyNullNull); - } - - public boolean isPolyNullNull() { - return isPolyNullNull; - } - - public void setPolyNullNull(boolean isPolyNullNull) { - this.isPolyNullNull = isPolyNullNull; - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessTransfer.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessTransfer.java deleted file mode 100644 index 9eb346e08852..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessTransfer.java +++ /dev/null @@ -1,326 +0,0 @@ -package org.checkerframework.checker.nullness; - -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.MethodInvocationTree; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.Elements; -import org.checkerframework.checker.initialization.InitializationTransfer; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.nullness.qual.PolyNull; -import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.dataflow.analysis.ConditionalTransferResult; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; -import org.checkerframework.dataflow.analysis.RegularTransferResult; -import org.checkerframework.dataflow.analysis.TransferInput; -import org.checkerframework.dataflow.analysis.TransferResult; -import org.checkerframework.dataflow.cfg.node.ArrayAccessNode; -import org.checkerframework.dataflow.cfg.node.FieldAccessNode; -import org.checkerframework.dataflow.cfg.node.InstanceOfNode; -import org.checkerframework.dataflow.cfg.node.MethodAccessNode; -import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; -import org.checkerframework.dataflow.cfg.node.Node; -import org.checkerframework.dataflow.cfg.node.NullLiteralNode; -import org.checkerframework.dataflow.cfg.node.ReturnNode; -import org.checkerframework.dataflow.cfg.node.ThrowNode; -import org.checkerframework.framework.flow.CFAbstractAnalysis; -import org.checkerframework.framework.flow.CFAbstractStore; -import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; -import org.checkerframework.framework.util.AnnotatedTypes; -import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationUtils; -import org.checkerframework.javacutil.BugInCF; -import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypesUtils; - -/** - * Transfer function for the non-null type system. Performs the following refinements: - * - *
      - *
    1. After an expression is compared with the {@code null} literal, then that expression can - * safely be considered {@link NonNull} if the result of the comparison is false. - *
    2. If an expression is dereferenced, then it can safely be assumed to non-null in the future. - * If it would not be, then the dereference would have raised a {@link NullPointerException}. - *
    3. Tracks whether {@link PolyNull} is known to be {@link Nullable}. - *
    - */ -public class NullnessTransfer - extends InitializationTransfer { - - /** The @{@link NonNull} annotation. */ - protected final AnnotationMirror NONNULL; - /** The @{@link Nullable} annotation. */ - protected final AnnotationMirror NULLABLE; - - /** - * Java's Map interface. - * - *

    The qualifiers in this type don't matter -- it is not used as a fully-annotated - * AnnotatedDeclaredType, but just passed to asSuper(). - */ - protected final AnnotatedDeclaredType MAP_TYPE; - - /** The type factory for the nullness analysis that was passed to the constructor. */ - protected final GenericAnnotatedTypeFactory< - NullnessValue, - NullnessStore, - NullnessTransfer, - ? extends CFAbstractAnalysis> - nullnessTypeFactory; - - /** The type factory for the map key analysis. */ - protected final KeyForAnnotatedTypeFactory keyForTypeFactory; - - /** Create a new NullnessTransfer for the given analysis. */ - public NullnessTransfer(NullnessAnalysis analysis) { - super(analysis); - this.nullnessTypeFactory = analysis.getTypeFactory(); - Elements elements = nullnessTypeFactory.getElementUtils(); - this.keyForTypeFactory = - ((BaseTypeChecker) nullnessTypeFactory.getContext().getChecker()) - .getTypeFactoryOfSubchecker(KeyForSubchecker.class); - NONNULL = AnnotationBuilder.fromClass(elements, NonNull.class); - NULLABLE = AnnotationBuilder.fromClass(elements, Nullable.class); - - MAP_TYPE = - (AnnotatedDeclaredType) - AnnotatedTypeMirror.createType( - TypesUtils.typeFromClass(Map.class, analysis.getTypes(), elements), - nullnessTypeFactory, - false); - } - - /** - * Sets a given {@link Node} to non-null in the given {@code store}. Calls to this method - * implement case 2. - */ - protected void makeNonNull(NullnessStore store, Node node) { - Receiver internalRepr = FlowExpressions.internalReprOf(nullnessTypeFactory, node); - store.insertValue(internalRepr, NONNULL); - } - - /** Sets a given {@link Node} {@code node} to non-null in the given {@link TransferResult}. */ - protected void makeNonNull(TransferResult result, Node node) { - if (result.containsTwoStores()) { - makeNonNull(result.getThenStore(), node); - makeNonNull(result.getElseStore(), node); - } else { - makeNonNull(result.getRegularStore(), node); - } - } - - /** Refine the given result to @NonNull. */ - protected void refineToNonNull(TransferResult result) { - NullnessValue oldResultValue = result.getResultValue(); - NullnessValue refinedResultValue = - analysis.createSingleAnnotationValue(NONNULL, oldResultValue.getUnderlyingType()); - NullnessValue newResultValue = refinedResultValue.mostSpecific(oldResultValue, null); - result.setResultValue(newResultValue); - } - - @Override - protected NullnessValue finishValue(NullnessValue value, NullnessStore store) { - value = super.finishValue(value, store); - if (value != null) { - value.isPolyNullNull = store.isPolyNullNull(); - } - return value; - } - - /** - * {@inheritDoc} - * - *

    Furthermore, this method refines the type to {@code NonNull} for the appropriate branch if - * an expression is compared to the {@code null} literal (listed as case 1 in the class - * description). - */ - @Override - protected TransferResult strengthenAnnotationOfEqualTo( - TransferResult res, - Node firstNode, - Node secondNode, - NullnessValue firstValue, - NullnessValue secondValue, - boolean notEqualTo) { - res = - super.strengthenAnnotationOfEqualTo( - res, firstNode, secondNode, firstValue, secondValue, notEqualTo); - if (firstNode instanceof NullLiteralNode) { - NullnessStore thenStore = res.getThenStore(); - NullnessStore elseStore = res.getElseStore(); - - List secondParts = splitAssignments(secondNode); - for (Node secondPart : secondParts) { - Receiver secondInternal = - FlowExpressions.internalReprOf(nullnessTypeFactory, secondPart); - if (CFAbstractStore.canInsertReceiver(secondInternal)) { - thenStore = thenStore == null ? res.getThenStore() : thenStore; - elseStore = elseStore == null ? res.getElseStore() : elseStore; - if (notEqualTo) { - thenStore.insertValue(secondInternal, NONNULL); - } else { - elseStore.insertValue(secondInternal, NONNULL); - } - } - } - - Set secondAnnos = - secondValue != null - ? secondValue.getAnnotations() - : AnnotationUtils.createAnnotationSet(); - if (nullnessTypeFactory.containsSameByClass(secondAnnos, PolyNull.class)) { - thenStore = thenStore == null ? res.getThenStore() : thenStore; - elseStore = elseStore == null ? res.getElseStore() : elseStore; - thenStore.setPolyNullNull(true); - } - - if (thenStore != null) { - return new ConditionalTransferResult<>(res.getResultValue(), thenStore, elseStore); - } - } - return res; - } - - @Override - public TransferResult visitArrayAccess( - ArrayAccessNode n, TransferInput p) { - TransferResult result = super.visitArrayAccess(n, p); - makeNonNull(result, n.getArray()); - return result; - } - - @Override - public TransferResult visitInstanceOf( - InstanceOfNode n, TransferInput p) { - TransferResult result = super.visitInstanceOf(n, p); - NullnessStore thenStore = result.getThenStore(); - NullnessStore elseStore = result.getElseStore(); - makeNonNull(thenStore, n.getOperand()); - return new ConditionalTransferResult<>(result.getResultValue(), thenStore, elseStore); - } - - @Override - public TransferResult visitMethodAccess( - MethodAccessNode n, TransferInput p) { - TransferResult result = super.visitMethodAccess(n, p); - makeNonNull(result, n.getReceiver()); - return result; - } - - @Override - public TransferResult visitFieldAccess( - FieldAccessNode n, TransferInput p) { - TransferResult result = super.visitFieldAccess(n, p); - makeNonNull(result, n.getReceiver()); - return result; - } - - @Override - public TransferResult visitThrow( - ThrowNode n, TransferInput p) { - TransferResult result = super.visitThrow(n, p); - makeNonNull(result, n.getExpression()); - return result; - } - - /* - * Provided that m is of a type that implements interface java.util.Map: - *

      - *
    • Given a call m.get(k), if k is @KeyFor("m") and m's value type is @NonNull, - * then the result is @NonNull in the thenStore and elseStore of the transfer result. - *
    - */ - @Override - public TransferResult visitMethodInvocation( - MethodInvocationNode n, TransferInput in) { - TransferResult result = super.visitMethodInvocation(n, in); - - // Make receiver non-null. - Node receiver = n.getTarget().getReceiver(); - makeNonNull(result, receiver); - - // For all formal parameters with a non-null annotation, make the actual argument non-null. - // The point of this is to prevent cascaded errors -- the Nullness Checker will issue a - // warning for the method invocation, but not for subsequent uses of the argument. See test - // case FlowNullness.java. - MethodInvocationTree tree = n.getTree(); - ExecutableElement method = TreeUtils.elementFromUse(tree); - AnnotatedExecutableType methodType = nullnessTypeFactory.getAnnotatedType(method); - List methodParams = methodType.getParameterTypes(); - List methodArgs = tree.getArguments(); - for (int i = 0; i < methodParams.size() && i < methodArgs.size(); ++i) { - if (methodParams.get(i).hasAnnotation(NONNULL)) { - makeNonNull(result, n.getArgument(i)); - } - } - - // Refine result to @NonNull if n is an invocation of Map.get, the argument is a key for - // the map, and the map's value type is not @Nullable. - if (keyForTypeFactory != null && keyForTypeFactory.isMapGet(n)) { - String mapName = - FlowExpressions.internalReprOf(nullnessTypeFactory, receiver).toString(); - AnnotatedTypeMirror receiverType = nullnessTypeFactory.getReceiverType(n.getTree()); - - if (keyForTypeFactory.isKeyForMap(mapName, methodArgs.get(0)) - && !hasNullableValueType((AnnotatedDeclaredType) receiverType)) { - makeNonNull(result, n); - refineToNonNull(result); - } - } - - return result; - } - - /** - * Returns true if mapType's value type (the V type argument to Map) is @Nullable. - * - * @param mapOrSubtype the Map type, or a subtype - * @return true if mapType's value type is @Nullable - */ - private boolean hasNullableValueType(AnnotatedDeclaredType mapOrSubtype) { - AnnotatedDeclaredType mapType = - AnnotatedTypes.asSuper(nullnessTypeFactory, mapOrSubtype, MAP_TYPE); - int numTypeArguments = mapType.getTypeArguments().size(); - if (numTypeArguments != 2) { - throw new BugInCF("Wrong number %d of type arguments: %s", numTypeArguments, mapType); - } - AnnotatedTypeMirror valueType = mapType.getTypeArguments().get(1); - return valueType.hasAnnotation(NULLABLE); - } - - @Override - public TransferResult visitReturn( - ReturnNode n, TransferInput in) { - // HACK: make sure we have a value for return statements, because we - // need to record whether (at this return statement) isPolyNullNull is - // set or not. - NullnessValue value = createDummyValue(); - if (in.containsTwoStores()) { - NullnessStore thenStore = in.getThenStore(); - NullnessStore elseStore = in.getElseStore(); - return new ConditionalTransferResult<>( - finishValue(value, thenStore, elseStore), thenStore, elseStore); - } else { - NullnessStore info = in.getRegularStore(); - return new RegularTransferResult<>(finishValue(value, info), info); - } - } - - /** Creates a dummy abstract value (whose type is not supposed to be looked at). */ - private NullnessValue createDummyValue() { - TypeMirror dummy = analysis.getEnv().getTypeUtils().getPrimitiveType(TypeKind.BOOLEAN); - Set annos = AnnotationUtils.createAnnotationSet(); - annos.addAll(nullnessTypeFactory.getQualifierHierarchy().getBottomAnnotations()); - return new NullnessValue(analysis, annos, dummy); - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessUtil.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessUtil.java deleted file mode 100644 index 1eee7405a79c..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessUtil.java +++ /dev/null @@ -1,160 +0,0 @@ -package org.checkerframework.checker.nullness; - -import org.checkerframework.checker.nullness.qual.EnsuresNonNull; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** - * Utility class for the Nullness Checker. - * - *

    To avoid the need to write the NullnessUtil class name, do: - * - *

    import static org.checkerframework.checker.nullness.NullnessUtil.castNonNull;
    - * - * or - * - *
    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. - */ -@SuppressWarnings({ - "nullness", // Nullness utilities are trusted regarding nullness. - "cast" // Casts look redundant if Nullness Checker is not run. -}) -public final class NullnessUtil { - - private NullnessUtil() { - throw new AssertionError("shouldn't be instantiated"); - } - - /** - * A method that suppresses warnings from the Nullness Checker. - * - *

    The method takes a possibly-null reference, unsafely casts it to have the @NonNull type - * qualifier, and returns it. The Nullness Checker considers both the return value, and also the - * argument, to be non-null after the method call. Therefore, the {@code castNonNull} method can - * be used either as a cast expression or as a statement. The Nullness Checker issues no - * warnings in any of the following code: - * - *

    
    -     *   // one way to use as a cast:
    -     *  {@literal @}NonNull String s = castNonNull(possiblyNull1);
    -     *
    -     *   // another way to use as a cast:
    -     *   castNonNull(possiblyNull2).toString();
    -     *
    -     *   // one way to use as a statement:
    -     *   castNonNull(possiblyNull3);
    -     *   possiblyNull3.toString();`
    -     * }
    - * - * The {@code castNonNull} method is intended to be used in situations where the programmer - * definitively knows that a given reference is not null, but the type system is unable to make - * this deduction. It is not intended for defensive programming, in which a programmer cannot - * prove that the value is not null but wishes to have an earlier indication if it is. See the - * Checker Framework Manual for further discussion. - * - *

    The method throws {@link AssertionError} if Java assertions are enabled and the argument - * 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 - * @return the argument, casted to have the type qualifier @NonNull - */ - public static @EnsuresNonNull("#1") @NonNull T castNonNull( - @Nullable T ref) { - assert ref != null : "Misuse of castNonNull: called with a null argument"; - 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. - * - * @see #castNonNull(Object) - */ - public static @EnsuresNonNull("#1") - @NonNull T @NonNull [] castNonNullDeep(T @Nullable [] arr) { - return (@NonNull T[]) castNonNullArray(arr); - } - - /** - * 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. - * - * @see #castNonNull(Object) - */ - public static @EnsuresNonNull("#1") - @NonNull T @NonNull [][] castNonNullDeep(T @Nullable [] @Nullable [] arr) { - return (@NonNull T[][]) castNonNullArray(arr); - } - - /** - * 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. - * - * @see #castNonNull(Object) - */ - public static @EnsuresNonNull("#1") - @NonNull T @NonNull [][][] castNonNullDeep(T @Nullable [] @Nullable [] @Nullable [] arr) { - return (@NonNull T[][][]) castNonNullArray(arr); - } - - /** - * 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. - * - * @see #castNonNull(Object) - */ - public static @EnsuresNonNull("#1") - @NonNull T @NonNull [][][][] castNonNullDeep( - T @Nullable [] @Nullable [] @Nullable [] @Nullable [] arr) { - return (@NonNull T[][][][]) castNonNullArray(arr); - } - - /** - * 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. - * - * @see #castNonNull(Object) - */ - public static @EnsuresNonNull("#1") - @NonNull T @NonNull [][][][][] castNonNullDeep( - T @Nullable [] @Nullable [] @Nullable [] @Nullable [] @Nullable [] arr) { - return (@NonNull T[][][][][]) castNonNullArray(arr); - } - - private static @NonNull T @NonNull [] castNonNullArray( - T @Nullable [] arr) { - assert arr != null : "Misuse of castNonNullArray: called with a null array argument"; - for (int i = 0; i < arr.length; ++i) { - assert arr[i] != null : "Misuse of castNonNull: called with a null array element"; - checkIfArray(arr[i]); - } - return (@NonNull T[]) arr; - } - - private static void checkIfArray(@NonNull Object ref) { - assert ref != null : "Misuse of checkIfArray: called with a null argument"; - Class comp = ref.getClass().getComponentType(); - if (comp != null) { - // comp is non-null for arrays, otherwise null. - if (comp.isPrimitive()) { - // Nothing to do for arrays of primitive type: primitives are - // never null. - } else { - castNonNullArray((Object[]) ref); - } - } - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessValue.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessValue.java deleted file mode 100644 index 7432459047bb..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessValue.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.checkerframework.checker.nullness; - -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.nullness.qual.PolyNull; -import org.checkerframework.framework.flow.CFAbstractAnalysis; -import org.checkerframework.framework.flow.CFAbstractValue; -import org.checkerframework.framework.flow.CFValue; - -/** - * Behaves just like {@link CFValue}, but additionally tracks whether at this point {@link PolyNull} - * is known to be {@link Nullable}. - */ -public class NullnessValue extends CFAbstractValue { - - protected boolean isPolyNullNull; - - public NullnessValue( - CFAbstractAnalysis analysis, - Set annotations, - TypeMirror underlyingType) { - super(analysis, annotations, underlyingType); - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java b/checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java deleted file mode 100644 index e72a5392f39a..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/nullness/NullnessVisitor.java +++ /dev/null @@ -1,639 +0,0 @@ -package org.checkerframework.checker.nullness; - -import com.sun.source.tree.AnnotatedTypeTree; -import com.sun.source.tree.AnnotationTree; -import com.sun.source.tree.ArrayAccessTree; -import com.sun.source.tree.AssertTree; -import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.CatchTree; -import com.sun.source.tree.CompoundAssignmentTree; -import com.sun.source.tree.ConditionalExpressionTree; -import com.sun.source.tree.DoWhileLoopTree; -import com.sun.source.tree.EnhancedForLoopTree; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.ForLoopTree; -import com.sun.source.tree.IdentifierTree; -import com.sun.source.tree.IfTree; -import com.sun.source.tree.LiteralTree; -import com.sun.source.tree.MemberSelectTree; -import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.NewArrayTree; -import com.sun.source.tree.NewClassTree; -import com.sun.source.tree.SwitchTree; -import com.sun.source.tree.SynchronizedTree; -import com.sun.source.tree.ThrowTree; -import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; -import com.sun.source.tree.TypeCastTree; -import com.sun.source.tree.UnaryTree; -import com.sun.source.tree.VariableTree; -import com.sun.source.tree.WhileLoopTree; -import java.lang.annotation.Annotation; -import java.util.List; -import java.util.Set; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; -import org.checkerframework.checker.initialization.InitializationVisitor; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.flow.CFCFGBuilder; -import org.checkerframework.framework.type.AnnotatedTypeMirror; -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.ElementUtils; -import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypesUtils; - -/** The visitor for the nullness type-system. */ -public class NullnessVisitor - extends InitializationVisitor { - // Error message keys - // private static final @CompilerMessageKey String ASSIGNMENT_TYPE_INCOMPATIBLE = - // "assignment.type.incompatible"; - private static final @CompilerMessageKey String UNBOXING_OF_NULLABLE = "unboxing.of.nullable"; - private static final @CompilerMessageKey String KNOWN_NONNULL = "known.nonnull"; - private static final @CompilerMessageKey String LOCKING_NULLABLE = "locking.nullable"; - private static final @CompilerMessageKey String THROWING_NULLABLE = "throwing.nullable"; - private static final @CompilerMessageKey String ACCESSING_NULLABLE = "accessing.nullable"; - private static final @CompilerMessageKey String CONDITION_NULLABLE = "condition.nullable"; - private static final @CompilerMessageKey String ITERATING_NULLABLE = "iterating.over.nullable"; - private static final @CompilerMessageKey String SWITCHING_NULLABLE = "switching.nullable"; - private static final @CompilerMessageKey String DEREFERENCE_OF_NULLABLE = - "dereference.of.nullable"; - - // Annotation and type constants - private final AnnotationMirror NONNULL, NULLABLE, MONOTONIC_NONNULL; - private final TypeMirror stringType; - - /** The element for java.util.Collection.size(). */ - private final ExecutableElement collectionSize; - - /** The element for java.util.Collection.toArray(T). */ - private final ExecutableElement collectionToArray; - - /** The System.clearProperty(String) method. */ - private final ExecutableElement systemClearProperty; - - /** The System.setProperties(String) method. */ - private final ExecutableElement systemSetProperties; - - /** True if checked code may clear system properties. */ - private final boolean permitClearProperty; - - /** - * Create a new NullnessVisitor. - * - * @param checker the checker to which this visitor belongs - */ - public NullnessVisitor(BaseTypeChecker checker) { - super(checker); - - NONNULL = atypeFactory.NONNULL; - NULLABLE = atypeFactory.NULLABLE; - MONOTONIC_NONNULL = atypeFactory.MONOTONIC_NONNULL; - stringType = elements.getTypeElement("java.lang.String").asType(); - - ProcessingEnvironment env = checker.getProcessingEnvironment(); - this.collectionSize = - TreeUtils.getMethod(java.util.Collection.class.getName(), "size", 0, env); - this.collectionToArray = - TreeUtils.getMethod(java.util.Collection.class.getName(), "toArray", env, "T[]"); - systemClearProperty = - TreeUtils.getMethod(java.lang.System.class.getName(), "clearProperty", 1, env); - systemSetProperties = - TreeUtils.getMethod(java.lang.System.class.getName(), "setProperties", 1, env); - - this.permitClearProperty = - checker.getLintOption( - NullnessChecker.LINT_PERMITCLEARPROPERTY, - NullnessChecker.LINT_DEFAULT_PERMITCLEARPROPERTY); - } - - @Override - public NullnessAnnotatedTypeFactory createTypeFactory() { - return new NullnessAnnotatedTypeFactory(checker); - } - - @Override - public boolean isValidUse( - AnnotatedDeclaredType declarationType, AnnotatedDeclaredType useType, Tree tree) { - // At most, a single qualifier on a type. - boolean foundInit = false; - boolean foundNonNull = false; - Set> initQuals = atypeFactory.getInitializationAnnotations(); - Set> nonNullQuals = atypeFactory.getNullnessAnnotations(); - - for (AnnotationMirror anno : useType.getAnnotations()) { - if (containsSameByName(initQuals, anno)) { - if (foundInit) { - return false; - } - foundInit = true; - } else if (containsSameByName(nonNullQuals, anno)) { - if (foundNonNull) { - return false; - } - foundNonNull = true; - } - } - - if (tree.getKind() == Tree.Kind.VARIABLE) { - Element vs = TreeUtils.elementFromTree(tree); - switch (vs.getKind()) { - case EXCEPTION_PARAMETER: - if (useType.hasAnnotation(NULLABLE)) { - // Exception parameters cannot use Nullable - // annotations. They default to NonNull. - return false; - } - break; - default: - // nothing to do - break; - } - } - - return super.isValidUse(declarationType, useType, tree); - } - - private boolean containsSameByName( - Set> quals, AnnotationMirror anno) { - for (Class q : quals) { - if (atypeFactory.areSameByClass(anno, q)) { - return true; - } - } - return false; - } - - @Override - protected void commonAssignmentCheck( - Tree varTree, ExpressionTree valueExp, @CompilerMessageKey String errorKey) { - - // allow MonotonicNonNull to be initialized to null at declaration - if (varTree.getKind() == Tree.Kind.VARIABLE) { - Element elem = TreeUtils.elementFromDeclaration((VariableTree) varTree); - if (atypeFactory.fromElement(elem).hasEffectiveAnnotation(MONOTONIC_NONNULL) - && !checker.getLintOption( - NullnessChecker.LINT_NOINITFORMONOTONICNONNULL, - NullnessChecker.LINT_DEFAULT_NOINITFORMONOTONICNONNULL)) { - return; - } - } - super.commonAssignmentCheck(varTree, valueExp, errorKey); - } - - @Override - protected void commonAssignmentCheck( - AnnotatedTypeMirror varType, - ExpressionTree valueExp, - @CompilerMessageKey String errorKey) { - // 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); - } - - @Override - protected void commonAssignmentCheck( - AnnotatedTypeMirror varType, - AnnotatedTypeMirror valueType, - Tree valueTree, - @CompilerMessageKey String errorKey) { - if (TypesUtils.isPrimitive(varType.getUnderlyingType()) - && !TypesUtils.isPrimitive(valueType.getUnderlyingType())) { - boolean succeed = checkForNullability(valueType, valueTree, UNBOXING_OF_NULLABLE); - if (!succeed) { - // Only issue the unboxing of nullable error. - return; - } - } - super.commonAssignmentCheck(varType, valueType, valueTree, errorKey); - } - - /** Case 1: Check for null dereferencing. */ - @Override - public Void visitMemberSelect(MemberSelectTree node, Void p) { - Element e = TreeUtils.elementFromTree(node); - if (!(TreeUtils.isSelfAccess(node) - || node.getExpression().getKind() == Kind.PARAMETERIZED_TYPE - // case 8. static member access - || ElementUtils.isStatic(e))) { - checkForNullability(node.getExpression(), DEREFERENCE_OF_NULLABLE); - } - return super.visitMemberSelect(node, p); - } - - /** Case 2: Check for implicit {@code .iterator} call. */ - @Override - public Void visitEnhancedForLoop(EnhancedForLoopTree node, Void p) { - checkForNullability(node.getExpression(), ITERATING_NULLABLE); - return super.visitEnhancedForLoop(node, p); - } - - /** Case 3: Check for array dereferencing. */ - @Override - public Void visitArrayAccess(ArrayAccessTree node, Void p) { - checkForNullability(node.getExpression(), ACCESSING_NULLABLE); - return super.visitArrayAccess(node, p); - } - - @Override - public Void visitNewArray(NewArrayTree node, Void p) { - AnnotatedArrayType type = atypeFactory.getAnnotatedType(node); - AnnotatedTypeMirror componentType = type.getComponentType(); - if (componentType.hasEffectiveAnnotation(NONNULL) - && !isNewArrayAllZeroDims(node) - && !isNewArrayInToArray(node) - && !TypesUtils.isPrimitive(componentType.getUnderlyingType()) - && (checker.getLintOption("soundArrayCreationNullness", false) - // temporary, for backward compatibility - || checker.getLintOption("forbidnonnullarraycomponents", false))) { - checker.reportError( - node, - "new.array.type.invalid", - componentType.getAnnotations(), - type.toString()); - } - - return super.visitNewArray(node, p); - } - - /** - * Determine whether all dimensions given in a new array expression have zero as length. For - * example "new Object[0][0];". Also true for empty dimensions, as in "new Object[] {...}". - */ - private static boolean isNewArrayAllZeroDims(NewArrayTree node) { - boolean isAllZeros = true; - for (ExpressionTree dim : node.getDimensions()) { - if (dim instanceof LiteralTree) { - Object val = ((LiteralTree) dim).getValue(); - if (!(val instanceof Number) || !Integer.valueOf(0).equals(val)) { - isAllZeros = false; - break; - } - } else { - isAllZeros = false; - break; - } - } - return isAllZeros; - } - - private boolean isNewArrayInToArray(NewArrayTree node) { - if (node.getDimensions().size() != 1) { - return false; - } - - ExpressionTree dim = node.getDimensions().get(0); - ProcessingEnvironment env = checker.getProcessingEnvironment(); - - if (!TreeUtils.isMethodInvocation(dim, collectionSize, env)) { - return false; - } - - ExpressionTree rcvsize = ((MethodInvocationTree) dim).getMethodSelect(); - if (!(rcvsize instanceof MemberSelectTree)) { - return false; - } - rcvsize = ((MemberSelectTree) rcvsize).getExpression(); - if (!(rcvsize instanceof IdentifierTree)) { - return false; - } - - Tree encl = getCurrentPath().getParentPath().getLeaf(); - - if (!TreeUtils.isMethodInvocation(encl, collectionToArray, env)) { - return false; - } - - ExpressionTree rcvtoarray = ((MethodInvocationTree) encl).getMethodSelect(); - if (!(rcvtoarray instanceof MemberSelectTree)) { - return false; - } - rcvtoarray = ((MemberSelectTree) rcvtoarray).getExpression(); - if (!(rcvtoarray instanceof IdentifierTree)) { - return false; - } - - return ((IdentifierTree) rcvsize).getName() == ((IdentifierTree) rcvtoarray).getName(); - } - - /** Case 4: Check for thrown exception nullness. */ - @Override - protected void checkThrownExpression(ThrowTree node) { - checkForNullability(node.getExpression(), THROWING_NULLABLE); - } - - /** Case 5: Check for synchronizing locks. */ - @Override - public Void visitSynchronized(SynchronizedTree node, Void p) { - checkForNullability(node.getExpression(), LOCKING_NULLABLE); - return super.visitSynchronized(node, p); - } - - @Override - public Void visitAssert(AssertTree node, Void p) { - // See also org.checkerframework.dataflow.cfg.CFGBuilder.CFGTranslationPhaseOne.visitAssert - - // In cases where neither assumeAssertionsAreEnabled nor assumeAssertionsAreDisabled are - // turned on and @AssumeAssertions is not used, checkForNullability is still called since - // the CFGBuilder will have generated one branch for which asserts are assumed to be - // enabled. - - boolean doVisitAssert = true; - - if (checker.hasOption("assumeAssertionsAreEnabled") - || CFCFGBuilder.assumeAssertionsActivatedForAssertTree(checker, node)) { - doVisitAssert = true; - } else if (checker.hasOption("assumeAssertionsAreDisabled")) { - doVisitAssert = false; - } - - if (doVisitAssert) { - checkForNullability(node.getCondition(), CONDITION_NULLABLE); - return super.visitAssert(node, p); - } - - return null; - } - - @Override - public Void visitIf(IfTree node, Void p) { - checkForNullability(node.getCondition(), CONDITION_NULLABLE); - return super.visitIf(node, p); - } - - /** - * Reports an error if a comparison of a @NonNull expression with the null literal is performed. - */ - protected void checkForRedundantTests(BinaryTree node) { - - final ExpressionTree leftOp = node.getLeftOperand(); - final ExpressionTree rightOp = node.getRightOperand(); - - // respect command-line option - if (!checker.getLintOption( - NullnessChecker.LINT_REDUNDANTNULLCOMPARISON, - NullnessChecker.LINT_DEFAULT_REDUNDANTNULLCOMPARISON)) { - return; - } - - // equality tests - if ((node.getKind() == Tree.Kind.EQUAL_TO || node.getKind() == Tree.Kind.NOT_EQUAL_TO)) { - AnnotatedTypeMirror left = atypeFactory.getAnnotatedType(leftOp); - AnnotatedTypeMirror right = atypeFactory.getAnnotatedType(rightOp); - if (leftOp.getKind() == Tree.Kind.NULL_LITERAL - && right.hasEffectiveAnnotation(NONNULL)) { - checker.reportWarning(node, KNOWN_NONNULL, rightOp.toString()); - } else if (rightOp.getKind() == Tree.Kind.NULL_LITERAL - && left.hasEffectiveAnnotation(NONNULL)) { - checker.reportWarning(node, KNOWN_NONNULL, leftOp.toString()); - } - } - } - - /** Case 6: Check for redundant nullness tests Case 7: unboxing case: primitive operations. */ - @Override - public Void visitBinary(BinaryTree node, Void p) { - final ExpressionTree leftOp = node.getLeftOperand(); - final ExpressionTree rightOp = node.getRightOperand(); - - if (isUnboxingOperation(node)) { - checkForNullability(leftOp, UNBOXING_OF_NULLABLE); - checkForNullability(rightOp, UNBOXING_OF_NULLABLE); - } - - checkForRedundantTests(node); - - return super.visitBinary(node, p); - } - - /** Case 7: unboxing case: primitive operation. */ - @Override - public Void visitUnary(UnaryTree node, Void p) { - checkForNullability(node.getExpression(), UNBOXING_OF_NULLABLE); - return super.visitUnary(node, p); - } - - /** Case 7: unboxing case: primitive operation. */ - @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { - // ignore String concatenation - if (!isString(node)) { - checkForNullability(node.getVariable(), UNBOXING_OF_NULLABLE); - checkForNullability(node.getExpression(), UNBOXING_OF_NULLABLE); - } - return super.visitCompoundAssignment(node, p); - } - - /** Case 7: unboxing case: casting to a primitive. */ - @Override - public Void visitTypeCast(TypeCastTree node, Void p) { - if (isPrimitive(node) && !isPrimitive(node.getExpression())) { - if (!checkForNullability(node.getExpression(), UNBOXING_OF_NULLABLE)) { - // If unboxing of nullable is issued, don't issue any other errors. - return null; - } - } - return super.visitTypeCast(node, p); - } - - @Override - public Void visitMethodInvocation(MethodInvocationTree node, Void p) { - if (!permitClearProperty) { - ProcessingEnvironment env = checker.getProcessingEnvironment(); - if (TreeUtils.isMethodInvocation(node, systemClearProperty, env)) { - String literal = literalFirstArgument(node); - if (literal == null - || SystemGetPropertyHandler.predefinedSystemProperties.contains(literal)) { - checker.reportError(node, "clear.system.property"); - } - } - if (TreeUtils.isMethodInvocation(node, systemSetProperties, env)) { - checker.reportError(node, "clear.system.property"); - } - } - return super.visitMethodInvocation(node, p); - } - - /** - * If the first argument of a method call is a literal, return it; otherwise return null. - * - * @param tree a method invocation whose first formal parameter is of String type - * @return the first argument if it is a literal, otherwise null - */ - /*package-private*/ static @Nullable String literalFirstArgument(MethodInvocationTree tree) { - List args = tree.getArguments(); - assert args.size() > 0; - ExpressionTree arg = args.get(0); - if (arg.getKind() == Tree.Kind.STRING_LITERAL) { - String literal = (String) ((LiteralTree) arg).getValue(); - return literal; - } - return null; - } - - // ///////////// Utility methods ////////////////////////////// - - /** - * Issues the error message if the type of the tree is not of a {@link NonNull} type. - * - * @param tree the tree where the error is to reported - * @param errMsg the error message (must be {@link CompilerMessageKey}) - * @return whether or not the check succeeded - */ - private boolean checkForNullability(ExpressionTree tree, @CompilerMessageKey String errMsg) { - AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(tree); - return checkForNullability(type, tree, errMsg); - } - - /** - * Issues the error message if an expression with this type may be null. - * - * @param type annotated type - * @param tree the tree where the error is to reported - * @param errMsg the error message (must be {@link CompilerMessageKey}) - * @return whether or not the check succeeded - */ - private boolean checkForNullability( - AnnotatedTypeMirror type, Tree tree, @CompilerMessageKey String errMsg) { - if (!type.hasEffectiveAnnotation(NONNULL)) { - checker.reportError(tree, errMsg, tree); - return false; - } - return true; - } - - @Override - protected void checkMethodInvocability( - AnnotatedExecutableType method, MethodInvocationTree node) { - if (!TreeUtils.isSelfAccess(node) - && - // Static methods don't have a receiver - method.getReceiverType() != null) { - // TODO: should all or some constructors be excluded? - // method.getElement().getKind() != ElementKind.CONSTRUCTOR) { - Set recvAnnos = atypeFactory.getReceiverType(node).getAnnotations(); - AnnotatedTypeMirror methodReceiver = method.getReceiverType().getErased(); - AnnotatedTypeMirror treeReceiver = methodReceiver.shallowCopy(false); - AnnotatedTypeMirror rcv = atypeFactory.getReceiverType(node); - treeReceiver.addAnnotations(rcv.getEffectiveAnnotations()); - // If receiver is Nullable, then we don't want to issue a warning - // about method invocability (we'd rather have only the - // "dereference.of.nullable" message). - if (treeReceiver.hasAnnotation(NULLABLE) || recvAnnos.contains(MONOTONIC_NONNULL)) { - return; - } - } - super.checkMethodInvocability(method, node); - } - - /** @return true if binary operation could cause an unboxing operation */ - private final boolean isUnboxingOperation(BinaryTree tree) { - if (tree.getKind() == Tree.Kind.EQUAL_TO || tree.getKind() == Tree.Kind.NOT_EQUAL_TO) { - // it is valid to check equality between two reference types, even - // if one (or both) of them is null - return isPrimitive(tree.getLeftOperand()) != isPrimitive(tree.getRightOperand()); - } else { - // All BinaryTree's are of type String, a primitive type or the - // reference type equivalent of a primitive type. Furthermore, - // Strings don't have a primitive type, and therefore only - // BinaryTrees that aren't String can cause unboxing. - return !isString(tree); - } - } - - /** @return true if the type of the tree is a super of String */ - private final boolean isString(ExpressionTree tree) { - TypeMirror type = TreeUtils.typeOf(tree); - return types.isAssignable(stringType, type); - } - - /** @return true if the type of the tree is a primitive */ - private static final boolean isPrimitive(ExpressionTree tree) { - return TreeUtils.typeOf(tree).getKind().isPrimitive(); - } - - @Override - public Void visitSwitch(SwitchTree node, Void p) { - checkForNullability(node.getExpression(), SWITCHING_NULLABLE); - return super.visitSwitch(node, p); - } - - @Override - public Void visitForLoop(ForLoopTree node, Void p) { - if (node.getCondition() != null) { - // Condition is null e.g. in "for (;;) {...}" - checkForNullability(node.getCondition(), CONDITION_NULLABLE); - } - return super.visitForLoop(node, p); - } - - @Override - public Void visitNewClass(NewClassTree node, Void p) { - AnnotatedDeclaredType type = atypeFactory.getAnnotatedType(node); - ExpressionTree identifier = node.getIdentifier(); - if (identifier instanceof AnnotatedTypeTree) { - AnnotatedTypeTree t = (AnnotatedTypeTree) identifier; - for (AnnotationMirror a : atypeFactory.getAnnotatedType(t).getAnnotations()) { - // is this an annotation of the nullness checker? - boolean nullnessCheckerAnno = - containsSameByName(atypeFactory.getNullnessAnnotations(), a); - if (nullnessCheckerAnno && !AnnotationUtils.areSame(NONNULL, a)) { - // The type is not non-null => warning - checker.reportWarning(node, "new.class.type.invalid", type.getAnnotations()); - // Note that other consistency checks are made by isValid. - } - } - if (t.toString().contains("@PolyNull")) { - // TODO: this is a hack, but PolyNull gets substituted - // afterwards - checker.reportWarning(node, "new.class.type.invalid", type.getAnnotations()); - } - } - // TODO: It might be nicer to introduce a framework-level - // isValidNewClassType or some such. - return super.visitNewClass(node, p); - } - - @Override - public Void visitWhileLoop(WhileLoopTree node, Void p) { - checkForNullability(node.getCondition(), CONDITION_NULLABLE); - return super.visitWhileLoop(node, p); - } - - @Override - public Void visitDoWhileLoop(DoWhileLoopTree node, Void p) { - checkForNullability(node.getCondition(), CONDITION_NULLABLE); - return super.visitDoWhileLoop(node, p); - } - - @Override - public Void visitConditionalExpression(ConditionalExpressionTree node, Void p) { - checkForNullability(node.getCondition(), CONDITION_NULLABLE); - return super.visitConditionalExpression(node, p); - } - - @Override - protected void checkExceptionParameter(CatchTree node) { - // BasetypeVisitor forces annotations on exception parameters to be top, - // but because exceptions can never be null, the Nullness Checker - // does not require this check. - } - - @Override - public Void visitAnnotation(AnnotationTree node, Void p) { - // All annotation arguments are non-null and initialized, so no need to check them. - return null; - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/SystemGetPropertyHandler.java b/checker/src/main/java/org/checkerframework/checker/nullness/SystemGetPropertyHandler.java index dd104f95ec83..346e008e8965 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/SystemGetPropertyHandler.java +++ b/checker/src/main/java/org/checkerframework/checker/nullness/SystemGetPropertyHandler.java @@ -1,22 +1,24 @@ package org.checkerframework.checker.nullness; import com.sun.source.tree.MethodInvocationTree; + +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Arrays; import java.util.Collection; import java.util.HashSet; + import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ExecutableElement; -import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; -import org.checkerframework.javacutil.TreeUtils; /** * Utility class for handling {@link java.lang.System#getProperty(String)} and related invocations. * - *

    The result of the method call is is assumed to be non-null if the argument is a literal key - * that is guaranteed to be present in the system properties (according to the documentation of - * {@link java.lang.System#getProperty(String)}), as in {@code - * System.getProperties("line.separator")}. + *

    The result of the method call is assumed to be non-null if the argument is a literal key that + * is guaranteed to be present in the system properties (according to the documentation of {@link + * java.lang.System#getProperty(String)}), as in {@code System.getProperties("line.separator")}. */ public class SystemGetPropertyHandler { @@ -30,7 +32,7 @@ public class SystemGetPropertyHandler { private final ProcessingEnvironment env; /** The factory for constructing and looking up types. */ - private final NullnessAnnotatedTypeFactory factory; + private final NullnessNoInitAnnotatedTypeFactory factory; /** The System.getProperty(String) method. */ private final ExecutableElement systemGetProperty; @@ -41,7 +43,7 @@ public class SystemGetPropertyHandler { /** * System properties that are defined at startup on every JVM. * - *

    This list is from the Javadoc of System.getProperties, for Java 11. + *

    This list is from the Javadoc of System.getProperties, for Java 17. */ public static final Collection predefinedSystemProperties = new HashSet<>( @@ -59,6 +61,7 @@ public class SystemGetPropertyHandler { "java.vm.vendor", "java.vm.name", "java.specification.version", + "java.specification.maintenance.version", "java.specification.vendor", "java.specification.name", "java.class.version", @@ -74,7 +77,15 @@ public class SystemGetPropertyHandler { "line.separator", "user.name", "user.home", - "user.dir")); + "user.dir", + "native.encoding", + "stdout.encoding", + "stderr.encoding", + "jdk.module.path", + "jdk.module.upgrade.path", + "jdk.module.main", + "jdk.module.main.class", + "file.encoding")); /** * Creates a SystemGetPropertyHandler. @@ -86,16 +97,14 @@ public class SystemGetPropertyHandler { */ public SystemGetPropertyHandler( ProcessingEnvironment env, - NullnessAnnotatedTypeFactory factory, + NullnessNoInitAnnotatedTypeFactory factory, boolean permitClearProperty) { this.env = env; this.factory = factory; this.permitClearProperty = permitClearProperty; - systemGetProperty = - TreeUtils.getMethod(java.lang.System.class.getName(), "getProperty", 1, env); - systemSetProperty = - TreeUtils.getMethod(java.lang.System.class.getName(), "setProperty", 2, env); + systemGetProperty = TreeUtils.getMethod("java.lang.System", "getProperty", 1, env); + systemSetProperty = TreeUtils.getMethod("java.lang.System", "setProperty", 2, env); } /** @@ -110,7 +119,7 @@ public void handle(MethodInvocationTree tree, AnnotatedExecutableType method) { } if (TreeUtils.isMethodInvocation(tree, systemGetProperty, env) || TreeUtils.isMethodInvocation(tree, systemSetProperty, env)) { - String literal = NullnessVisitor.literalFirstArgument(tree); + String literal = NullnessNoInitVisitor.literalFirstArgument(tree); if (literal != null && predefinedSystemProperties.contains(literal)) { AnnotatedTypeMirror type = method.getReturnType(); type.replaceAnnotation(factory.NONNULL); diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/collection-object-parameters-may-be-null.astub b/checker/src/main/java/org/checkerframework/checker/nullness/collection-object-parameters-may-be-null.astub index b2cb372f4f07..c11a7d4a345f 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/collection-object-parameters-may-be-null.astub +++ b/checker/src/main/java/org/checkerframework/checker/nullness/collection-object-parameters-may-be-null.astub @@ -1,6 +1,6 @@ // This file is a workaround for https://tinyurl.com/cfissue/1326 . // For documentation, see -// https://checkerframework.org/manual/#collection-object-parameters-may-be-null . +// https://eisop.github.io/cf/manual/#collection-object-parameters-may-be-null . // This file relaxes the specifications for the following methods when they are documented to // possibly throw NPE. It does not change the specifications when they are documented to definitely @@ -129,7 +129,8 @@ public interface Deque extends Queue { boolean contains(@Nullable Object o); } -public interface List extends Collection { +// No `extends Collection` to be compatible with both Java above and below 21 +public interface List { boolean contains(@Nullable Object o); boolean remove(@Nullable Object o); boolean containsAll(Collection c); diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/collection-object-parameters-may-be-null.readme b/checker/src/main/java/org/checkerframework/checker/nullness/collection-object-parameters-may-be-null.readme index 6a5413fcaa9d..4f4812832866 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/collection-object-parameters-may-be-null.readme +++ b/checker/src/main/java/org/checkerframework/checker/nullness/collection-object-parameters-may-be-null.readme @@ -1,6 +1,6 @@ -# Run the following in both copies of the annotated JDK. For example, in: -cd $t/libraries/jdk-fork-mernst-branch-collection-object-parameters-may-be-null/src/java.base/share/classes -cd $t/checker-framework-fork-mernst-branch-collection-object-parameters-may-be-null/checker/jdk/nullness/src +# This file contains notes about creating file collection-object-parameters-may-be-null.astub . + +# Run the following in src/java.base/share/classes/ in the annotated JDK. rm -f *.txt diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/jdk11.astub b/checker/src/main/java/org/checkerframework/checker/nullness/jdk11.astub new file mode 100644 index 000000000000..9b042e84389e --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nullness/jdk11.astub @@ -0,0 +1,17 @@ +// This file is for classes that appear in JDK 11 but not in JDK 17. + +package com.sun.javadoc; + +import org.checkerframework.checker.nullness.qual.Nullable; + +public interface FieldDoc extends MemberDoc { + + @Nullable Object constantValue(); + + @Nullable String constantValueExpression(); +} + +public interface ProgramElementDoc extends Doc { + + @Nullable ClassDoc containingClass(); +} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/junit-assertions.astub b/checker/src/main/java/org/checkerframework/checker/nullness/junit-assertions.astub new file mode 100644 index 000000000000..ec1a83b9a989 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nullness/junit-assertions.astub @@ -0,0 +1,597 @@ +package org.junit.jupiter.api; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class Assertions { + + public static V fail(); + + public static V fail(String message); + + public static V fail(String message, Throwable cause); + + public static V fail(Throwable cause); + + public static V fail(Supplier messageSupplier); + + public static void assertTrue(boolean condition); + + public static void assertTrue(boolean condition, Supplier messageSupplier); + + public static void assertTrue(BooleanSupplier booleanSupplier); + + public static void assertTrue(BooleanSupplier booleanSupplier, String message); + + public static void assertTrue(boolean condition, String message); + + public static void assertTrue(BooleanSupplier booleanSupplier, Supplier messageSupplier); + + public static void assertFalse(boolean condition); + + public static void assertFalse(boolean condition, String message); + + public static void assertFalse(boolean condition, Supplier messageSupplier); + + public static void assertFalse(BooleanSupplier booleanSupplier); + + public static void assertFalse(BooleanSupplier booleanSupplier, String message); + + public static void assertFalse(BooleanSupplier booleanSupplier, Supplier messageSupplier); + + public static void assertNull(@Nullable Object actual); + + public static void assertNull(@Nullable Object actual, String message); + + public static void assertNull(@Nullable Object actual, Supplier messageSupplier); + + @EnsuresNonNull("#1") + public static void assertNotNull(Object actual); + + @EnsuresNonNull("#1") + public static void assertNotNull(Object actual, String message); + + @EnsuresNonNull("#1") + public static void assertNotNull(Object actual, Supplier messageSupplier); + + public static void assertEquals(short expected, short actual); + + public static void assertEquals(short expected, Short actual); + + public static void assertEquals(Short expected, short actual); + + public static void assertEquals(@Nullable Short expected, @Nullable Short actual); + + public static void assertEquals(short expected, short actual, String message); + + public static void assertEquals(short expected, Short actual, String message); + + public static void assertEquals(Short expected, short actual, String message); + + public static void assertEquals(@Nullable Short expected, @Nullable Short actual, String message); + + public static void assertEquals(short expected, short actual, Supplier messageSupplier); + + public static void assertEquals(short expected, Short actual, Supplier messageSupplier); + + public static void assertEquals(Short expected, short actual, Supplier messageSupplier); + + public static void assertEquals(@Nullable Short expected, @Nullable Short actual, Supplier messageSupplier); + + public static void assertEquals(byte expected, byte actual); + + public static void assertEquals(byte expected, Byte actual); + + public static void assertEquals(Byte expected, byte actual); + + public static void assertEquals(@Nullable Byte expected, @Nullable Byte actual); + + public static void assertEquals(byte expected, byte actual, String message); + + public static void assertEquals(byte expected, Byte actual, String message); + + public static void assertEquals(Byte expected, byte actual, String message); + + public static void assertEquals(@Nullable Byte expected, @Nullable Byte actual, String message); + + public static void assertEquals(byte expected, byte actual, Supplier messageSupplier); + + public static void assertEquals(byte expected, Byte actual, Supplier messageSupplier); + + public static void assertEquals(Byte expected, byte actual, Supplier messageSupplier); + + public static void assertEquals(@Nullable Byte expected, @Nullable Byte actual, Supplier messageSupplier); + + public static void assertEquals(int expected, int actual); + + public static void assertEquals(int expected, Integer actual); + + public static void assertEquals(Integer expected, int actual); + + public static void assertEquals(@Nullable Integer expected, @Nullable Integer actual); + + public static void assertEquals(int expected, int actual, String message); + + public static void assertEquals(int expected, Integer actual, String message); + + public static void assertEquals(Integer expected, int actual, String message); + + public static void assertEquals(@Nullable Integer expected, @Nullable Integer actual, String message); + + public static void assertEquals(int expected, int actual, Supplier messageSupplier); + + public static void assertEquals(int expected, Integer actual, Supplier messageSupplier); + + public static void assertEquals(Integer expected, int actual, Supplier messageSupplier); + + public static void assertEquals(@Nullable Integer expected, @Nullable Integer actual, Supplier messageSupplier); + + public static void assertEquals(long expected, long actual); + + public static void assertEquals(long expected, Long actual); + + public static void assertEquals(Long expected, long actual); + + public static void assertEquals(@Nullable Long expected, @Nullable Long actual); + + public static void assertEquals(long expected, long actual, String message); + + public static void assertEquals(long expected, Long actual, String message); + + public static void assertEquals(Long expected, long actual, String message); + + public static void assertEquals(@Nullable Long expected, @Nullable Long actual, String message); + + public static void assertEquals(long expected, long actual, Supplier messageSupplier); + + public static void assertEquals(long expected, Long actual, Supplier messageSupplier); + + public static void assertEquals(Long expected, long actual, Supplier messageSupplier); + + public static void assertEquals(@Nullable Long expected, @Nullable Long actual, Supplier messageSupplier); + + public static void assertEquals(float expected, float actual); + + public static void assertEquals(float expected, Float actual); + + public static void assertEquals(Float expected, float actual); + + public static void assertEquals(@Nullable Float expected, @Nullable Float actual); + + public static void assertEquals(float expected, float actual, String message); + + public static void assertEquals(float expected, Float actual, String message); + + public static void assertEquals(Float expected, float actual, String message); + + public static void assertEquals(@Nullable Float expected, @Nullable Float actual, String message); + + public static void assertEquals(float expected, float actual, Supplier messageSupplier); + + public static void assertEquals(float expected, Float actual, Supplier messageSupplier); + + public static void assertEquals(Float expected, float actual, Supplier messageSupplier); + + public static void assertEquals(@Nullable Float expected, @Nullable Float actual, Supplier messageSupplier); + + public static void assertEquals(float expected, float actual, float delta); + + public static void assertEquals(float expected, float actual, float delta, String message); + + public static void assertEquals(float expected, float actual, float delta, Supplier messageSupplier); + + public static void assertEquals(double expected, double actual); + + public static void assertEquals(double expected, Double actual); + + public static void assertEquals(Double expected, double actual); + + public static void assertEquals(@Nullable Double expected, @Nullable Double actual); + + public static void assertEquals(double expected, double actual, String message); + + public static void assertEquals(double expected, Double actual, String message); + + public static void assertEquals(Double expected, double actual, String message); + + public static void assertEquals(@Nullable Double expected, @Nullable Double actual, String message); + + public static void assertEquals(double expected, double actual, Supplier messageSupplier); + + public static void assertEquals(double expected, Double actual, Supplier messageSupplier); + + public static void assertEquals(Double expected, double actual, Supplier messageSupplier); + + public static void assertEquals(@Nullable Double expected, @Nullable Double actual, Supplier messageSupplier); + + public static void assertEquals(double expected, double actual, double delta); + + public static void assertEquals(double expected, double actual, double delta, String message); + + public static void assertEquals(double expected, double actual, double delta, Supplier messageSupplier); + + public static void assertEquals(char expected, char actual); + + public static void assertEquals(char expected, Character actual); + + public static void assertEquals(Character expected, char actual); + + public static void assertEquals(@Nullable Character expected, @Nullable Character actual); + + public static void assertEquals(char expected, char actual, String message); + + public static void assertEquals(char expected, Character actual, String message); + + public static void assertEquals(Character expected, char actual, String message); + + public static void assertEquals(@Nullable Character expected, @Nullable Character actual, String message); + + public static void assertEquals(char expected, char actual, Supplier messageSupplier); + + public static void assertEquals(char expected, Character actual, Supplier messageSupplier); + + public static void assertEquals(Character expected, char actual, Supplier messageSupplier); + + public static void assertEquals(@Nullable Character expected, @Nullable Character actual, Supplier messageSupplier); + + public static void assertEquals(@Nullable Object expected, @Nullable Object actual); + + public static void assertEquals(@Nullable Object expected, @Nullable Object actual, String message); + + public static void assertEquals(@Nullable Object expected, @Nullable Object actual, Supplier messageSupplier); + + public static void assertArrayEquals(boolean @Nullable [] expected, boolean @Nullable [] actual); + + public static void assertArrayEquals(boolean @Nullable [] expected, boolean @Nullable [] actual, String message); + + public static void assertArrayEquals(boolean @Nullable [] expected, boolean @Nullable [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(char @Nullable [] expected, char @Nullable [] actual); + + public static void assertArrayEquals(char @Nullable [] expected, char @Nullable [] actual, String message); + + public static void assertArrayEquals(char @Nullable [] expected, char @Nullable [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(byte @Nullable [] expected, byte @Nullable [] actual); + + public static void assertArrayEquals(byte @Nullable [] expected, byte @Nullable [] actual, String message); + + public static void assertArrayEquals(byte @Nullable [] expected, byte @Nullable [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(short @Nullable [] expected, short @Nullable [] actual); + + public static void assertArrayEquals(short @Nullable [] expected, short @Nullable [] actual, String message); + + public static void assertArrayEquals(short @Nullable [] expected, short @Nullable [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(int @Nullable [] expected, int @Nullable [] actual); + + public static void assertArrayEquals(int @Nullable [] expected, int @Nullable [] actual, String message); + + public static void assertArrayEquals(int @Nullable [] expected, int @Nullable [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(long @Nullable [] expected, long @Nullable [] actual); + + public static void assertArrayEquals(long @Nullable [] expected, long @Nullable [] actual, String message); + + public static void assertArrayEquals(long @Nullable [] expected, long @Nullable [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(float @Nullable [] expected, float @Nullable [] actual); + + public static void assertArrayEquals(float @Nullable [] expected, float @Nullable [] actual, String message); + + public static void assertArrayEquals(float @Nullable [] expected, float @Nullable [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(float[] expected, float[] actual, float delta); + + public static void assertArrayEquals(float[] expected, float[] actual, float delta, String message); + + public static void assertArrayEquals(float[] expected, float[] actual, float delta, + Supplier messageSupplier); + + public static void assertArrayEquals(double @Nullable [] expected, double @Nullable [] actual); + + public static void assertArrayEquals(double @Nullable [] expected, double @Nullable [] actual, String message); + + public static void assertArrayEquals(double @Nullable [] expected, double @Nullable [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(double[] expected, double[] actual, double delta); + + public static void assertArrayEquals(double[] expected, double[] actual, double delta, String message); + + public static void assertArrayEquals(double[] expected, double[] actual, double delta, + Supplier messageSupplier); + + public static void assertArrayEquals(Object @Nullable [] expected, Object @Nullable [] actual); + + public static void assertArrayEquals(Object @Nullable [] expected, Object @Nullable [] actual, String message); + + public static void assertArrayEquals(Object @Nullable [] expected, Object @Nullable [] actual, Supplier messageSupplier); + + public static void assertIterableEquals(@Nullable Iterable expected, @Nullable Iterable actual); + + public static void assertIterableEquals(@Nullable Iterable expected, @Nullable Iterable actual, String message); + + public static void assertIterableEquals(@Nullable Iterable expected, @Nullable Iterable actual, + Supplier messageSupplier); + + public static void assertLinesMatch(List expectedLines, List actualLines); + + public static void assertLinesMatch(List expectedLines, List actualLines, String message); + + public static void assertLinesMatch(List expectedLines, List actualLines, + Supplier messageSupplier); + + public static void assertLinesMatch(Stream expectedLines, Stream actualLines); + + public static void assertLinesMatch(Stream expectedLines, Stream actualLines, String message); + + public static void assertLinesMatch(Stream expectedLines, Stream actualLines, + Supplier messageSupplier); + + public static void assertNotEquals(byte unexpected, byte actual); + + public static void assertNotEquals(byte unexpected, Byte actual); + + public static void assertNotEquals(Byte unexpected, byte actual); + + public static void assertNotEquals(Byte unexpected, Byte actual); + + public static void assertNotEquals(byte unexpected, byte actual, String message); + + public static void assertNotEquals(byte unexpected, Byte actual, String message); + + public static void assertNotEquals(Byte unexpected, byte actual, String message); + + public static void assertNotEquals(Byte unexpected, Byte actual, String message); + + public static void assertNotEquals(byte unexpected, byte actual, Supplier messageSupplier); + + public static void assertNotEquals(byte unexpected, Byte actual, Supplier messageSupplier); + + public static void assertNotEquals(Byte unexpected, byte actual, Supplier messageSupplier); + + public static void assertNotEquals(Byte unexpected, Byte actual, Supplier messageSupplier); + + public static void assertNotEquals(short unexpected, short actual); + + public static void assertNotEquals(short unexpected, Short actual); + + public static void assertNotEquals(Short unexpected, short actual); + + public static void assertNotEquals(Short unexpected, Short actual); + + public static void assertNotEquals(short unexpected, short actual, String message); + + public static void assertNotEquals(short unexpected, Short actual, String message); + + public static void assertNotEquals(Short unexpected, short actual, String message); + + public static void assertNotEquals(Short unexpected, Short actual, String message); + + public static void assertNotEquals(short unexpected, short actual, Supplier messageSupplier); + + public static void assertNotEquals(short unexpected, Short actual, Supplier messageSupplier); + + public static void assertNotEquals(Short unexpected, short actual, Supplier messageSupplier); + + public static void assertNotEquals(Short unexpected, Short actual, Supplier messageSupplier); + + public static void assertNotEquals(int unexpected, int actual); + + public static void assertNotEquals(int unexpected, Integer actual); + + public static void assertNotEquals(Integer unexpected, int actual); + + public static void assertNotEquals(Integer unexpected, Integer actual); + + public static void assertNotEquals(int unexpected, int actual, String message); + + public static void assertNotEquals(int unexpected, Integer actual, String message); + + public static void assertNotEquals(Integer unexpected, int actual, String message); + + public static void assertNotEquals(Integer unexpected, Integer actual, String message); + + public static void assertNotEquals(int unexpected, int actual, Supplier messageSupplier); + + public static void assertNotEquals(int unexpected, Integer actual, Supplier messageSupplier); + + public static void assertNotEquals(Integer unexpected, int actual, Supplier messageSupplier); + + public static void assertNotEquals(Integer unexpected, Integer actual, Supplier messageSupplier); + + public static void assertNotEquals(long unexpected, long actual); + + public static void assertNotEquals(long unexpected, Long actual); + + public static void assertNotEquals(Long unexpected, long actual); + + public static void assertNotEquals(Long unexpected, Long actual); + + public static void assertNotEquals(long unexpected, long actual, String message); + + public static void assertNotEquals(long unexpected, Long actual, String message); + + public static void assertNotEquals(Long unexpected, long actual, String message); + + public static void assertNotEquals(Long unexpected, Long actual, String message); + + public static void assertNotEquals(long unexpected, long actual, Supplier messageSupplier); + + public static void assertNotEquals(long unexpected, Long actual, Supplier messageSupplier); + + public static void assertNotEquals(Long unexpected, long actual, Supplier messageSupplier); + + public static void assertNotEquals(Long unexpected, Long actual, Supplier messageSupplier); + + public static void assertNotEquals(float unexpected, float actual); + + public static void assertNotEquals(float unexpected, Float actual); + + public static void assertNotEquals(Float unexpected, float actual); + + public static void assertNotEquals(Float unexpected, Float actual); + + public static void assertNotEquals(float unexpected, float actual, String message); + + public static void assertNotEquals(float unexpected, Float actual, String message); + + public static void assertNotEquals(Float unexpected, float actual, String message); + + public static void assertNotEquals(Float unexpected, Float actual, String message); + + public static void assertNotEquals(float unexpected, float actual, Supplier messageSupplier); + + public static void assertNotEquals(float unexpected, Float actual, Supplier messageSupplier); + + public static void assertNotEquals(Float unexpected, float actual, Supplier messageSupplier); + + public static void assertNotEquals(Float unexpected, Float actual, Supplier messageSupplier); + + public static void assertNotEquals(float unexpected, float actual, float delta); + + public static void assertNotEquals(float unexpected, float actual, float delta, String message); + + public static void assertNotEquals(float unexpected, float actual, float delta, Supplier messageSupplier); + + public static void assertNotEquals(double unexpected, double actual); + + public static void assertNotEquals(double unexpected, Double actual); + + public static void assertNotEquals(Double unexpected, double actual); + + public static void assertNotEquals(Double unexpected, Double actual); + + public static void assertNotEquals(double unexpected, double actual, String message); + + public static void assertNotEquals(double unexpected, Double actual, String message); + + public static void assertNotEquals(Double unexpected, double actual, String message); + + public static void assertNotEquals(Double unexpected, Double actual, String message); + + public static void assertNotEquals(double unexpected, double actual, Supplier messageSupplier); + + public static void assertNotEquals(double unexpected, Double actual, Supplier messageSupplier); + + public static void assertNotEquals(Double unexpected, double actual, Supplier messageSupplier); + + public static void assertNotEquals(Double unexpected, Double actual, Supplier messageSupplier); + + public static void assertNotEquals(double unexpected, double actual, double delta); + + public static void assertNotEquals(double unexpected, double actual, double delta, String message); + + public static void assertNotEquals(double unexpected, double actual, double delta, + Supplier messageSupplier); + + public static void assertNotEquals(char unexpected, char actual); + + public static void assertNotEquals(char unexpected, Character actual); + + public static void assertNotEquals(Character unexpected, char actual); + + public static void assertNotEquals(Character unexpected, Character actual); + + public static void assertNotEquals(char unexpected, char actual, String message); + + public static void assertNotEquals(char unexpected, Character actual, String message); + + public static void assertNotEquals(Character unexpected, char actual, String message); + + public static void assertNotEquals(Character unexpected, Character actual, String message); + + public static void assertNotEquals(char unexpected, char actual, Supplier messageSupplier); + + public static void assertNotEquals(char unexpected, Character actual, Supplier messageSupplier); + + public static void assertNotEquals(Character unexpected, char actual, Supplier messageSupplier); + + public static void assertNotEquals(Character unexpected, Character actual, Supplier messageSupplier); + + public static void assertNotEquals(@Nullable Object unexpected, @Nullable Object actual); + + public static void assertNotEquals(@Nullable Object unexpected, @Nullable Object actual, String message); + + public static void assertNotEquals(@Nullable Object unexpected, @Nullable Object actual, Supplier messageSupplier); + + public static void assertSame(Object expected, Object actual); + + public static void assertSame(Object expected, Object actual, String message); + + public static void assertSame(Object expected, Object actual, Supplier messageSupplier); + + public static void assertNotSame(Object unexpected, Object actual); + + public static void assertNotSame(Object unexpected, Object actual, String message); + + public static void assertNotSame(Object unexpected, Object actual, Supplier messageSupplier); + + public static void assertAll(Executable... executables) throws MultipleFailuresError; + + public static void assertAll(String heading, Executable... executables) throws MultipleFailuresError; + + public static void assertAll(Collection executables) throws MultipleFailuresError; + + public static void assertAll(String heading, Collection executables) throws MultipleFailuresError; + + public static void assertAll(Stream executables) throws MultipleFailuresError; + + public static void assertAll(String heading, Stream executables) throws MultipleFailuresError; + + public static T assertThrows(Class expectedType, Executable executable); + + public static T assertThrows(Class expectedType, Executable executable, String message); + + public static T assertThrows(Class expectedType, Executable executable, + Supplier messageSupplier); + + public static void assertDoesNotThrow(Executable executable); + + public static void assertDoesNotThrow(Executable executable, String message); + + public static void assertDoesNotThrow(Executable executable, Supplier messageSupplier); + + public static T assertDoesNotThrow(ThrowingSupplier supplier); + + public static T assertDoesNotThrow(ThrowingSupplier supplier, String message); + + public static T assertDoesNotThrow(ThrowingSupplier supplier, Supplier messageSupplier); + + public static void assertTimeout(Duration timeout, Executable executable); + + public static void assertTimeout(Duration timeout, Executable executable, String message); + + public static void assertTimeout(Duration timeout, Executable executable, Supplier messageSupplier); + + public static T assertTimeout(Duration timeout, ThrowingSupplier supplier); + + public static T assertTimeout(Duration timeout, ThrowingSupplier supplier, String message); + + public static T assertTimeout(Duration timeout, ThrowingSupplier supplier, + Supplier messageSupplier); + + public static void assertTimeoutPreemptively(Duration timeout, Executable executable); + + public static void assertTimeoutPreemptively(Duration timeout, Executable executable, String message); + + public static void assertTimeoutPreemptively(Duration timeout, Executable executable, + Supplier messageSupplier); + + public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier); + + public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, String message); + + public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, + Supplier messageSupplier); +} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/messages.properties b/checker/src/main/java/org/checkerframework/checker/nullness/messages.properties index d6230466b1b7..f1ccf838bf44 100644 --- a/checker/src/main/java/org/checkerframework/checker/nullness/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/nullness/messages.properties @@ -1,4 +1,6 @@ -### Error messages for the NonNullChecker +### Error messages for the Nullness Checker + +# Dereferencing a possibly-null reference dereference.of.nullable=dereference of possibly-null reference %s iterating.over.nullable=iterating over possibly-null reference %s unboxing.of.nullable=unboxing a possibly-null reference %s @@ -7,12 +9,24 @@ locking.nullable=synchronizing over a possibly-null lock %s accessing.nullable=accessing a possibly-null array %s condition.nullable=condition on a possibly-null value %s switching.nullable=switching on a possibly-null value %s -known.nonnull=redundant check; "%s" is non-null -nonnull.nonstatic.with.class=field "%s" is not static but uses a class name -class.not.found.nullness.parse.error=class not found for field "%s" -field.not.found.nullness.parse.error=Field "%s" not found + +# Messages for special-cased methods +toarray.nullable.elements.not.newarray=call of toArray on collection of non-null elements yields an array of possibly-null elements; omit the argument to toArray or make it an explicit array constructor +toarray.nullable.elements.mismatched.size=call of toArray on collection of non-null elements yields an array of possibly-null elements; cannot determine that the argument array has the same size as the receiver collection +clear.system.property=call might clear a predefined system property; pass -Alint=permitClearProperty to permit it + +# Unnecessary operations +nulltest.redundant=redundant test against null; "%s" is non-null + +# Annotations that are invalid or unnecessary +instanceof.nullable=instanceof is only true for a non-null expression +instanceof.nonnull.redundant=redundant @NonNull annotation on instanceof new.array.type.invalid=annotations %s may not be applied as component type for array "%s" new.class.type.invalid=the annotations %s do not need be applied in object creations -toArray.nullable.elements.not.newarray=call of toArray on collection of non-null elements yields an array of possibly-null elements; omit the argument to toArray or make it an explicit array constructor -toArray.nullable.elements.mismatched.size=call of toArray on collection of non-null elements yields an array of possibly-null elements; cannot determine that the argument array has the same size as the receiver collection -clear.system.property=call might clear a predefined system property; pass -Alint=permitClearProperty to permit it +nullness.on.constructor=do not write nullness annotations on a constructor, whose result is always non-null +nullness.on.enum=do not write nullness annotations on an enum constant, which is always non-null +nullness.on.exception.parameter=do not write nullness annotations on an exception parameter, which is always non-null +nullness.on.outer=nullness annotations are not applicable to outer types +nullness.on.primitive=nullness annotations are not applicable to primitive types +nullness.on.receiver=do not write nullness annotations on the receiver formal parameter `this`, which is always non-null +nullness.on.supertype=do not write nullness annotations on supertypes in a class declaration diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/permit-nullness-assertion-exception.astub b/checker/src/main/java/org/checkerframework/checker/nullness/permit-nullness-assertion-exception.astub new file mode 100644 index 000000000000..c101d3d4992d --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nullness/permit-nullness-assertion-exception.astub @@ -0,0 +1,46 @@ +// This stub file makes the Nullness Checker not warn about null pointer +// exceptions thrown by nullness assertion methods. There is no longer any +// guarantee that your program will not throw a NullPointerException. + +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.function.Supplier; + + +package java.util; + +public class Objects { + @EnsuresNonNull("#1") + public static T requireNonNull(@Nullable T obj); + @EnsuresNonNull("#1") + public static T requireNonNull(@Nullable T obj, String message); +} + + +// No type annotations for com.google.common.base.Preconditions or +// com.google.common.base.Verify are needed because they are annotated with +// @CheckForNull and the Checker Framework treats @CheckForNull as @Nullable. + + +package org.junit; + +public class Assertions { + @EnsuresNonNull("#1") + public static void assertNotNull(@Nullable Object actual); + @EnsuresNonNull("#1") + public static void assertNotNull(@Nullable Object actual, String message); + @EnsuresNonNull("#1") + public static void assertNotNull(@Nullable Object actual, Supplier messageSupplier); +} + + +package org.junit.jupiter.api; + +public class Assertions { + @EnsuresNonNull("#1") + public static void assertNotNull(@Nullable Object actual); + @EnsuresNonNull("#1") + public static void assertNotNull(@Nullable Object actual, String message); + @EnsuresNonNull("#1") + public static void assertNotNull(@Nullable Object actual, Supplier messageSupplier); +} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/qual/NonNull.java b/checker/src/main/java/org/checkerframework/checker/nullness/qual/NonNull.java deleted file mode 100644 index ae3b403665f2..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/nullness/qual/NonNull.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.checkerframework.checker.nullness.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.DefaultFor; -import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; -import org.checkerframework.framework.qual.LiteralKind; -import org.checkerframework.framework.qual.QualifierForLiterals; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TypeKind; -import org.checkerframework.framework.qual.TypeUseLocation; -import org.checkerframework.framework.qual.UpperBoundFor; - -/** - * If an expression's type is qualified by {@code @NonNull}, then the expression never evaluates to - * {@code null}. - * - *

    For fields of a class, the {@link NonNull} annotation indicates that this field is never - * {@code null} after the class has been fully initialized. For static fields, the {@link - * NonNull} annotation indicates that this field is never {@code null} after the containing - * class is initialized. See {@link - * org.checkerframework.checker.initialization.InitializationChecker} for more details. - * - *

    This annotation is rarely written in source code, because it is the default. - * - * @see Nullable - * @see MonotonicNonNull - * @see org.checkerframework.checker.nullness.NullnessChecker - * @checker_framework.manual #nullness-checker Nullness Checker - * @checker_framework.manual #bottom-type the bottom type - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(MonotonicNonNull.class) -@QualifierForLiterals(LiteralKind.STRING) -@DefaultQualifierInHierarchy -@DefaultFor(TypeUseLocation.EXCEPTION_PARAMETER) -@UpperBoundFor( - typeKinds = { - TypeKind.PACKAGE, - TypeKind.INT, - TypeKind.BOOLEAN, - TypeKind.CHAR, - TypeKind.DOUBLE, - TypeKind.FLOAT, - TypeKind.LONG, - TypeKind.SHORT, - TypeKind.BYTE - }) -public @interface NonNull {} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/qual/Nullable.java b/checker/src/main/java/org/checkerframework/checker/nullness/qual/Nullable.java deleted file mode 100644 index 650d7b0e8f37..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/nullness/qual/Nullable.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.checkerframework.checker.nullness.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.DefaultFor; -import org.checkerframework.framework.qual.LiteralKind; -import org.checkerframework.framework.qual.QualifierForLiterals; -import org.checkerframework.framework.qual.SubtypeOf; - -/** - * {@link Nullable} is a type annotation that indicates that the value is not known to be non-null - * (see {@link NonNull}). Only if an expression has a {@link Nullable} type may it be assigned - * {@code null}. - * - *

    This annotation is associated with the {@link - * org.checkerframework.checker.nullness.NullnessChecker}. - * - * @see NonNull - * @see MonotonicNonNull - * @see org.checkerframework.checker.nullness.NullnessChecker - * @checker_framework.manual #nullness-checker Nullness Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf({}) -@QualifierForLiterals(LiteralKind.NULL) -@DefaultFor(types = Void.class) -public @interface Nullable {} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/qual/RequiresNonNull.java b/checker/src/main/java/org/checkerframework/checker/nullness/qual/RequiresNonNull.java deleted file mode 100644 index cf60d43f3ea3..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/nullness/qual/RequiresNonNull.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.checkerframework.checker.nullness.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.PreconditionAnnotation; - -// TODO: In a fix for https://tinyurl.com/cfissue/1917, add the text: -// Every prefix expression must also be non-null; for example, {@code -// @RequiresNonNull(expression="a.b.c")} implies that both {@code a.b} and {@code a.b.c} must be -// non-null. -/** - * Indicates a method precondition: the method expects the specified expressions to be non-null when - * the annotated method is invoked. - * - *

    For example: - * - * - *

    - * class MyClass {
    - *   @Nullable Object field1;
    - *   @Nullable Object field2;
    - *
    - *   @RequiresNonNull({"field1", "other.field1"})
    - *   void method1(@NonNull MyClass other) {
    - *     field1.toString();           // OK, this.field1 is known to be non-null
    - *     field2.toString();           // error, might throw NullPointerException
    - *     other.field1.toString();     // OK, other.field1 is known to be non-null
    - *     other.field2.toString();     // error, might throw NullPointerException
    - *   }
    - *
    - *   void method2() {
    - *     MyClass other = new MyClass();
    - *
    - *     field1 = new Object();
    - *     other.field1 = new Object();
    - *     method1();                   // OK, satisfies method precondition
    - *
    - *     field1 = null;
    - *     other.field1 = new Object();
    - *     method1();                   // error, does not satisfy this.field1 method precondition
    - *
    - *     field1 = new Object();
    - *     other.field1 = null;
    - *     method1();                   // error, does not satisfy other.field1 method precondition
    - *   }
    - * 
    - * - * Do not use this annotation for formal parameters (instead, give them a {@code @NonNull} type, - * which is the default and need not be written). The {@code @RequiresNonNull} annotation is - * intended for other expressions, such as field accesses or method calls. - * - * @checker_framework.manual #nullness-checker Nullness Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) -@PreconditionAnnotation(qualifier = NonNull.class) -public @interface RequiresNonNull { - /** - * The Java expressions that need to be {@link - * org.checkerframework.checker.nullness.qual.NonNull}. - * - * @checker_framework.manual #java-expressions-as-arguments Syntax of Java expressions - */ - String[] value(); -} diff --git a/checker/src/main/java/org/checkerframework/checker/nullness/sometimes-nullable.astub b/checker/src/main/java/org/checkerframework/checker/nullness/sometimes-nullable.astub new file mode 100644 index 000000000000..df269e6957c5 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/nullness/sometimes-nullable.astub @@ -0,0 +1,13 @@ +// This file uses `@Nullable` for methods in the JDK that sometimes, but not always, permit null as +// an argument. For more discussion, see section "Conservative nullness annotations on the JDK" in +// the Checker Framework manual (https://eisop.github.io/cf/manual/#nullness-jdk-conservative). + +// This file is very incomplete and should be expanded. + +package java.lang.ref; + +import org.checkerframework.checker.nullness.qual.Nullable; + +public class WeakReference extends Reference { + public WeakReference(@Nullable T referent, @Nullable ReferenceQueue q); +} diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java new file mode 100644 index 000000000000..52142d042dd3 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalAnnotatedTypeFactory.java @@ -0,0 +1,138 @@ +package org.checkerframework.checker.optional; + +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MemberReferenceTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.Tree.Kind; + +import org.checkerframework.checker.optional.qual.Present; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.flow.CFAbstractAnalysis; +import org.checkerframework.framework.flow.CFStore; +import org.checkerframework.framework.flow.CFTransfer; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.TreeUtils; + +import java.util.Collection; +import java.util.function.Function; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; + +/** OptionalAnnotatedTypeFactory for the Optional Checker. */ +public class OptionalAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + + /** The element for java.util.Optional.map(). */ + private final ExecutableElement optionalMap; + + /** The @{@link Present} annotation. */ + protected final AnnotationMirror PRESENT = AnnotationBuilder.fromClass(elements, Present.class); + + /** + * Creates an OptionalAnnotatedTypeFactory. + * + * @param checker the Optional Checker associated with this type factory + */ + public OptionalAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + optionalMap = TreeUtils.getMethodOrNull("java.util.Optional", "map", 1, getProcessingEnv()); + postInit(); + } + + @Override + public AnnotatedTypeMirror getAnnotatedType(Tree tree) { + AnnotatedTypeMirror result = super.getAnnotatedType(tree); + optionalMapNonNull(tree, result); + return result; + } + + /** + * If {@code tree} is a call to {@link java.util.Optional#map(Function)} whose argument is a + * method reference, then this method adds {@code @Present} to {@code type} if the following is + * true: + * + *
      + *
    • The type of the receiver to {@link java.util.Optional#map(Function)} is + * {@code @Present}, and + *
    • {@link #returnHasNullable(MemberReferenceTree)} returns false. + *
    + * + * @param tree a tree + * @param type the type of the tree, which may be side-effected by this method + */ + private void optionalMapNonNull(Tree tree, AnnotatedTypeMirror type) { + if (!TreeUtils.isMethodInvocation(tree, optionalMap, processingEnv)) { + return; + } + MethodInvocationTree mapTree = (MethodInvocationTree) tree; + ExpressionTree argTree = mapTree.getArguments().get(0); + if (argTree.getKind() == Kind.MEMBER_REFERENCE) { + MemberReferenceTree memberReferenceTree = (MemberReferenceTree) argTree; + AnnotatedTypeMirror optType = getReceiverType(mapTree); + if (optType == null || !optType.hasEffectiveAnnotation(Present.class)) { + return; + } + if (!returnHasNullable(memberReferenceTree)) { + // The method still could have a @PolyNull on the return and might return null. + // If @PolyNull is the primary annotation on the parameter and not on any type + // arguments or array elements, then it is still safe to mark the optional type as + // present. + // TODO: Add the check for poly null on arguments. + type.replaceAnnotation(PRESENT); + } + } + } + + /** + * Returns true if the return type of the function type of {@code memberReferenceTree} is + * annotated with {@code @Nullable}. + * + * @param memberReferenceTree a member reference + * @return true if the return type of the function type of {@code memberReferenceTree} is + * annotated with {@code @Nullable} + */ + private boolean returnHasNullable(MemberReferenceTree memberReferenceTree) { + if (TreeUtils.MemberReferenceKind.getMemberReferenceKind(memberReferenceTree) + .isConstructorReference()) { + return false; + } + ExecutableElement memberReferenceFuncType = TreeUtils.elementFromUse(memberReferenceTree); + if (memberReferenceFuncType.getEnclosingElement().getKind() + == ElementKind.ANNOTATION_TYPE) { + // Annotation element accessor are always non-null; + return false; + } + + if (!checker.hasOption("optionalMapAssumeNonNull")) { + return true; + } + return containsNullable(memberReferenceFuncType.getAnnotationMirrors()) + || containsNullable(memberReferenceFuncType.getReturnType().getAnnotationMirrors()); + } + + /** + * Returns true if {@code annos} contains a nullable annotation. + * + * @param annos a collection of annotations + * @return true if {@code annos} contains a nullable annotation + */ + private boolean containsNullable(Collection annos) { + for (AnnotationMirror anno : annos) { + if (anno.getAnnotationType().asElement().getSimpleName().contentEquals("Nullable")) { + return true; + } + } + return false; + } + + @Override + public CFTransfer createFlowTransferFunction( + CFAbstractAnalysis analysis) { + return new OptionalTransfer(analysis); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalChecker.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalChecker.java index da622fd2a2c0..4ef8be6e4fb2 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalChecker.java @@ -1,15 +1,23 @@ package org.checkerframework.checker.optional; -import java.util.Optional; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; +import org.checkerframework.framework.qual.StubFiles; +import org.checkerframework.framework.source.SupportedOptions; + +import java.util.Optional; /** * A type-checker that prevents misuse of the {@link java.util.Optional} class. * * @checker_framework.manual #optional-checker Optional Checker */ -// TODO: For a call to ofNullable, if the argument has type @NonNull, make the return type have type -// @Present. Make Optional Checker a subchecker of the Nullness Checker. +// TODO: For a call to ofNullable, if the argument has type +// @NonNull, make the return type have type @Present. @RelevantJavaTypes(Optional.class) -public class OptionalChecker extends BaseTypeChecker {} +@StubFiles({"javaparser.astub"}) +@SupportedOptions("optionalMapAssumeNonNull") +public class OptionalChecker extends BaseTypeChecker { + /** Create an OptionalChecker. */ + public OptionalChecker() {} +} diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java new file mode 100644 index 000000000000..ceb8574c7ffc --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalTransfer.java @@ -0,0 +1,108 @@ +package org.checkerframework.checker.optional; + +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePath; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.optional.qual.Present; +import org.checkerframework.dataflow.cfg.UnderlyingAST; +import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGLambda; +import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.framework.flow.CFAbstractAnalysis; +import org.checkerframework.framework.flow.CFStore; +import org.checkerframework.framework.flow.CFTransfer; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.TreeUtils; + +import java.util.List; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.util.Elements; + +/** The transfer function for the Optional Checker. */ +public class OptionalTransfer extends CFTransfer { + + /** The @{@link Present} annotation. */ + private final AnnotationMirror PRESENT; + + /** The element for java.util.Optional.ifPresent(). */ + private final ExecutableElement optionalIfPresent; + + /** The element for java.util.Optional.ifPresentOrElse(), or null. */ + private final @Nullable ExecutableElement optionalIfPresentOrElse; + + /** The type factory associated with this transfer function. */ + private final AnnotatedTypeFactory atypeFactory; + + /** + * Create an OptionalTransfer. + * + * @param analysis the Optional Checker instance + */ + public OptionalTransfer(CFAbstractAnalysis analysis) { + super(analysis); + atypeFactory = analysis.getTypeFactory(); + Elements elements = atypeFactory.getElementUtils(); + PRESENT = AnnotationBuilder.fromClass(elements, Present.class); + ProcessingEnvironment env = atypeFactory.getProcessingEnv(); + optionalIfPresent = TreeUtils.getMethod("java.util.Optional", "ifPresent", 1, env); + optionalIfPresentOrElse = + TreeUtils.getMethodOrNull("java.util.Optional", "ifPresentOrElse", 2, env); + } + + @Override + public CFStore initialStore(UnderlyingAST underlyingAST, List parameters) { + + CFStore result = super.initialStore(underlyingAST, parameters); + + if (underlyingAST.getKind() == UnderlyingAST.Kind.LAMBDA) { + // Check whether this lambda is an argument to `Optional.ifPresent()` or + // `Optional.ifPresentOrElse()`. If so, then within the lambda, the receiver of the + // `ifPresent*` method is @Present. + CFGLambda cfgLambda = (CFGLambda) underlyingAST; + LambdaExpressionTree lambdaTree = cfgLambda.getLambdaTree(); + List lambdaParams = lambdaTree.getParameters(); + if (lambdaParams.size() == 1) { + TreePath lambdaPath = atypeFactory.getPath(lambdaTree); + Tree lambdaParent = lambdaPath.getParentPath().getLeaf(); + if (lambdaParent.getKind() == Tree.Kind.METHOD_INVOCATION) { + MethodInvocationTree invok = (MethodInvocationTree) lambdaParent; + ExecutableElement methodElt = TreeUtils.elementFromUse(invok); + if (methodElt.equals(optionalIfPresent) + || methodElt.equals(optionalIfPresentOrElse)) { + // `underlyingAST` is an invocation of `Optional.ifPresent()` or + // `Optional.ifPresentOrElse()`. In the lambda, the receiver is @Present. + ExpressionTree methodSelectTree = + TreeUtils.withoutParens(invok.getMethodSelect()); + ExpressionTree receiverTree = + ((MemberSelectTree) methodSelectTree).getExpression(); + JavaExpression receiverJe = JavaExpression.fromTree(receiverTree); + result.insertValue(receiverJe, PRESENT); + } + } + } + } + + // TODO: Similar logic to the above can be applied in the Nullness Checker. + // Some methods take a function as an argument, guaranteeing that, if the function is + // called: + // * the value passed to the function is non-null + // * some other argument to the method is non-null + // Examples: + // * Jodd's `StringUtil.ifNotNull()` + // * `Opt.ifPresent()` + // * `Opt.map()` + + return result; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java b/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java index 70730b415341..eb21564c5f97 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/optional/OptionalVisitor.java @@ -1,17 +1,44 @@ package org.checkerframework.checker.optional; +import com.sun.source.tree.BinaryTree; import com.sun.source.tree.BlockTree; import com.sun.source.tree.ConditionalExpressionTree; import com.sun.source.tree.ExpressionStatementTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IfTree; +import com.sun.source.tree.MemberReferenceTree; import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.ParenthesizedTree; import com.sun.source.tree.StatementTree; import com.sun.source.tree.Tree; import com.sun.source.tree.Tree.Kind; +import com.sun.source.tree.UnaryTree; import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePath; + +import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.optional.qual.OptionalCreator; +import org.checkerframework.checker.optional.qual.OptionalEliminator; +import org.checkerframework.checker.optional.qual.OptionalPropagator; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeValidator; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; +import org.plumelib.util.IPair; + +import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; + import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; @@ -19,15 +46,6 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; -import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.common.basetype.BaseTypeValidator; -import org.checkerframework.common.basetype.BaseTypeVisitor; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.framework.type.AnnotatedTypeFactory; -import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypesUtils; /** * The OptionalVisitor enforces the Optional Checker rules. These rules are described in the Checker @@ -38,24 +56,29 @@ public class OptionalVisitor extends BaseTypeVisitor { + /** The Collection type. */ private final TypeMirror collectionType; /** The element for java.util.Optional.get(). */ private final ExecutableElement optionalGet; + /** The element for java.util.Optional.isPresent(). */ private final ExecutableElement optionalIsPresent; - /** The element for java.util.Optional.of(). */ - private final ExecutableElement optionalOf; - /** The element for java.util.Optional.ofNullable(). */ - private final ExecutableElement optionalOfNullable; - /** The element for java.util.Optional.orElse(). */ - private final ExecutableElement optionalOrElse; - /** The element for java.util.Optional.orElseGet(). */ - private final ExecutableElement optionalOrElseGet; - /** The element for java.util.Optional.orElseThrow(). */ - private final ExecutableElement optionalOrElseThrow; - - /** Create an OptionalVisitor. */ + + /** The element for java.util.Optional.isEmpty(), or null if running under JDK 8. */ + private final @Nullable ExecutableElement optionalIsEmpty; + + /** The element for java.util.stream.Stream.filter(). */ + private final ExecutableElement streamFilter; + + /** The element for java.util.stream.Stream.map(). */ + private final ExecutableElement streamMap; + + /** + * Create an OptionalVisitor. + * + * @param checker the associated OptionalChecker + */ public OptionalVisitor(BaseTypeChecker checker) { super(checker); collectionType = types.erasure(TypesUtils.typeFromClass(Collection.class, types, elements)); @@ -63,11 +86,10 @@ public OptionalVisitor(BaseTypeChecker checker) { ProcessingEnvironment env = checker.getProcessingEnvironment(); optionalGet = TreeUtils.getMethod("java.util.Optional", "get", 0, env); optionalIsPresent = TreeUtils.getMethod("java.util.Optional", "isPresent", 0, env); - optionalOf = TreeUtils.getMethod("java.util.Optional", "of", 1, env); - optionalOfNullable = TreeUtils.getMethod("java.util.Optional", "ofNullable", 1, env); - optionalOrElse = TreeUtils.getMethod("java.util.Optional", "orElse", 1, env); - optionalOrElseGet = TreeUtils.getMethod("java.util.Optional", "orElseGet", 1, env); - optionalOrElseThrow = TreeUtils.getMethod("java.util.Optional", "orElseThrow", 1, env); + optionalIsEmpty = TreeUtils.getMethodOrNull("java.util.Optional", "isEmpty", 0, env); + + streamFilter = TreeUtils.getMethod("java.util.stream.Stream", "filter", 1, env); + streamMap = TreeUtils.getMethod("java.util.stream.Stream", "map", 1, env); } @Override @@ -75,41 +97,96 @@ protected BaseTypeValidator createTypeValidator() { return new OptionalTypeValidator(checker, this, atypeFactory); } - /** @return true iff expression is a call to java.util.Optional.get */ + /** + * Returns true iff {@code expression} is a call to java.util.Optional.get. + * + * @param expression an expression + * @return true iff {@code expression} is a call to java.util.Optional.get + */ private boolean isCallToGet(ExpressionTree expression) { ProcessingEnvironment env = checker.getProcessingEnvironment(); return TreeUtils.isMethodInvocation(expression, optionalGet, env); } - /** @return true iff expression is a call to java.util.Optional.isPresent */ - private boolean isCallToIsPresent(ExpressionTree expression) { + /** + * Is the expression a call to {@code isPresent} or {@code isEmpty}? If not, returns null. If + * so, returns a pair of (boolean, receiver expression). The boolean is true if the given + * expression is a call to {@code isPresent} and is false if the given expression is a call to + * {@code isEmpty}. + * + * @param expression an expression + * @return a pair of a boolean (indicating whether the expression is a call to {@code + * Optional.isPresent} or to {@code Optional.isEmpty}) and its receiver; or null if not a + * call to either of the methods + */ + private @Nullable IPair isCallToIsPresent( + ExpressionTree expression) { ProcessingEnvironment env = checker.getProcessingEnvironment(); - return TreeUtils.isMethodInvocation(expression, optionalIsPresent, env); + boolean negate = false; + while (true) { + switch (expression.getKind()) { + case PARENTHESIZED: + expression = ((ParenthesizedTree) expression).getExpression(); + break; + case LOGICAL_COMPLEMENT: + expression = ((UnaryTree) expression).getExpression(); + negate = !negate; + break; + case METHOD_INVOCATION: + if (TreeUtils.isMethodInvocation(expression, optionalIsPresent, env)) { + return IPair.of(!negate, TreeUtils.getReceiverTree(expression)); + } else if (optionalIsEmpty != null + && TreeUtils.isMethodInvocation(expression, optionalIsEmpty, env)) { + return IPair.of(negate, TreeUtils.getReceiverTree(expression)); + } else { + return null; + } + default: + return null; + } + } } - /** @return true iff expression is a call to Optional creation: of, ofNullable. */ + /** + * Returns true iff the method being called is Optional creation: empty, of, ofNullable. + * + * @param methInvok a method invocation + * @return true iff the method being called is Optional creation: empty, of, ofNullable + */ private boolean isOptionalCreation(MethodInvocationTree methInvok) { - ProcessingEnvironment env = checker.getProcessingEnvironment(); - return TreeUtils.isMethodInvocation(methInvok, optionalOf, env) - || TreeUtils.isMethodInvocation(methInvok, optionalOfNullable, env); + ExecutableElement method = TreeUtils.elementFromUse(methInvok); + return atypeFactory.getDeclAnnotation(method, OptionalCreator.class) != null; } /** - * @return true iff expression is a call to Optional elimination: get, orElse, orElseGet, + * Returns true iff the method being called is Optional propagation: filter, flatMap, map, or. + * + * @param methInvok a method invocation + * @return true true iff the method being called is Optional propagation: filter, flatMap, map, + * or + */ + private boolean isOptionalPropagation(MethodInvocationTree methInvok) { + ExecutableElement method = TreeUtils.elementFromUse(methInvok); + return atypeFactory.getDeclAnnotation(method, OptionalPropagator.class) != null; + } + + /** + * Returns true iff the method being called is Optional elimination: get, orElse, orElseGet, + * orElseThrow. + * + * @param methInvok a method invocation + * @return true iff the method being called is Optional elimination: get, orElse, orElseGet, * orElseThrow */ - private boolean isOptionalElimation(MethodInvocationTree methInvok) { - ProcessingEnvironment env = checker.getProcessingEnvironment(); - return TreeUtils.isMethodInvocation(methInvok, optionalGet, env) - || TreeUtils.isMethodInvocation(methInvok, optionalOrElse, env) - || TreeUtils.isMethodInvocation(methInvok, optionalOrElseGet, env) - || TreeUtils.isMethodInvocation(methInvok, optionalOrElseThrow, env); + private boolean isOptionalElimination(MethodInvocationTree methInvok) { + ExecutableElement method = TreeUtils.elementFromUse(methInvok); + return atypeFactory.getDeclAnnotation(method, OptionalEliminator.class) != null; } @Override - public Void visitConditionalExpression(ConditionalExpressionTree node, Void p) { - handleTernaryIsPresentGet(node); - return super.visitConditionalExpression(node, p); + public Void visitConditionalExpression(ConditionalExpressionTree tree, Void p) { + handleTernaryIsPresentGet(tree); + return super.visitConditionalExpression(tree, p); } /** @@ -118,20 +195,26 @@ public Void visitConditionalExpression(ConditionalExpressionTree node, Void p) { *

    Pattern match for: {@code VAR.isPresent() ? VAR.get().METHOD() : VALUE} * *

    Prefer: {@code VAR.map(METHOD).orElse(VALUE);} + * + * @param tree a conditional expression that can perhaps be simplified */ // TODO: Should handle this via a transfer function, instead of pattern-matching. - public void handleTernaryIsPresentGet(ConditionalExpressionTree node) { - - ExpressionTree condExpr = TreeUtils.withoutParens(node.getCondition()); - ExpressionTree trueExpr = TreeUtils.withoutParens(node.getTrueExpression()); - ExpressionTree falseExpr = TreeUtils.withoutParens(node.getFalseExpression()); + public void handleTernaryIsPresentGet(ConditionalExpressionTree tree) { - if (!isCallToIsPresent(condExpr)) { + ExpressionTree condExpr = TreeUtils.withoutParens(tree.getCondition()); + IPair isPresentCall = isCallToIsPresent(condExpr); + if (isPresentCall == null) { return; } - ExpressionTree receiver = TreeUtils.getReceiverTree(condExpr); + ExpressionTree trueExpr = TreeUtils.withoutParens(tree.getTrueExpression()); + ExpressionTree falseExpr = TreeUtils.withoutParens(tree.getFalseExpression()); + if (!isPresentCall.first) { + ExpressionTree tmp = trueExpr; + trueExpr = falseExpr; + falseExpr = tmp; + } - if (trueExpr.getKind() != Kind.METHOD_INVOCATION) { + if (trueExpr.getKind() != Tree.Kind.METHOD_INVOCATION) { return; } ExpressionTree trueReceiver = TreeUtils.getReceiverTree(trueExpr); @@ -142,11 +225,12 @@ public void handleTernaryIsPresentGet(ConditionalExpressionTree node) { // What is a better way to do this than string comparison? // Use transfer functions and Store entries. + ExpressionTree receiver = isPresentCall.second; if (sameExpression(receiver, getReceiver)) { ExecutableElement ele = TreeUtils.elementFromUse((MethodInvocationTree) trueExpr); checker.reportWarning( - node, + tree, "prefer.map.and.orelse", receiver, // The literal "CONTAININGCLASS::" is gross. @@ -157,10 +241,16 @@ public void handleTernaryIsPresentGet(ConditionalExpressionTree node) { } } - /** Return true if the two trees represent the same expression. */ + /** + * Returns true if the two trees represent the same expression. + * + * @param tree1 the first tree + * @param tree2 the second tree + * @return true if the two trees represent the same expression + */ private boolean sameExpression(ExpressionTree tree1, ExpressionTree tree2) { - FlowExpressions.Receiver r1 = FlowExpressions.internalReprOf(atypeFactory, tree1); - FlowExpressions.Receiver r2 = FlowExpressions.internalReprOf(atypeFactory, tree1); + JavaExpression r1 = JavaExpression.fromTree(tree1); + JavaExpression r2 = JavaExpression.fromTree(tree2); if (r1 != null && !r1.containsUnknown() && r2 != null && !r2.containsUnknown()) { return r1.equals(r2); } else { @@ -169,9 +259,9 @@ private boolean sameExpression(ExpressionTree tree1, ExpressionTree tree2) { } @Override - public Void visitIf(IfTree node, Void p) { - handleConditionalStatementIsPresentGet(node); - return super.visitIf(node, p); + public Void visitIf(IfTree tree, Void p) { + handleConditionalStatementIsPresentGet(tree); + return super.visitIf(tree, p); } /** @@ -180,12 +270,24 @@ public Void visitIf(IfTree node, Void p) { *

    Pattern match for: {@code if (VAR.isPresent()) { METHOD(VAR.get()); }} * *

    Prefer: {@code VAR.ifPresent(METHOD);} + * + * @param tree an if statement that can perhaps be simplified */ - public void handleConditionalStatementIsPresentGet(IfTree node) { + public void handleConditionalStatementIsPresentGet(IfTree tree) { - ExpressionTree condExpr = TreeUtils.withoutParens(node.getCondition()); - StatementTree thenStmt = skipBlocks(node.getThenStatement()); - StatementTree elseStmt = skipBlocks(node.getElseStatement()); + ExpressionTree condExpr = TreeUtils.withoutParens(tree.getCondition()); + IPair isPresentCall = isCallToIsPresent(condExpr); + if (isPresentCall == null) { + return; + } + + StatementTree thenStmt = skipBlocks(tree.getThenStatement()); + StatementTree elseStmt = skipBlocks(tree.getElseStatement()); + if (!isPresentCall.first) { + StatementTree tmp = thenStmt; + thenStmt = elseStmt; + elseStmt = tmp; + } if (!(elseStmt == null || (elseStmt.getKind() == Tree.Kind.BLOCK @@ -193,15 +295,12 @@ public void handleConditionalStatementIsPresentGet(IfTree node) { // else block is missing or is an empty block: "{}" return; } - if (!isCallToIsPresent(condExpr)) { - return; - } - ExpressionTree receiver = TreeUtils.getReceiverTree(condExpr); - if (thenStmt.getKind() != Kind.EXPRESSION_STATEMENT) { + + if (thenStmt.getKind() != Tree.Kind.EXPRESSION_STATEMENT) { return; } ExpressionTree thenExpr = ((ExpressionStatementTree) thenStmt).getExpression(); - if (thenExpr.getKind() != Kind.METHOD_INVOCATION) { + if (thenExpr.getKind() != Tree.Kind.METHOD_INVOCATION) { return; } MethodInvocationTree invok = (MethodInvocationTree) thenExpr; @@ -213,6 +312,7 @@ public void handleConditionalStatementIsPresentGet(IfTree node) { if (!isCallToGet(arg)) { return; } + ExpressionTree receiver = isPresentCall.second; ExpressionTree getReceiver = TreeUtils.getReceiverTree(arg); if (!receiver.toString().equals(getReceiver.toString())) { return; @@ -226,33 +326,148 @@ public void handleConditionalStatementIsPresentGet(IfTree node) { methodString.substring(0, dotPos) + "::" + methodString.substring(dotPos + 1); } - checker.reportWarning(node, "prefer.ifpresent", receiver, methodString); + checker.reportWarning(tree, "prefer.ifpresent", receiver, methodString); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { + handleCreationElimination(tree); + handleNestedOptionalCreation(tree); + return super.visitMethodInvocation(tree, p); } @Override - public Void visitMethodInvocation(MethodInvocationTree node, Void p) { - handleCreationElimination(node); - return super.visitMethodInvocation(node, p); + public Void visitBinary(BinaryTree tree, Void p) { + handleCompareToNull(tree); + return super.visitBinary(tree, p); + } + + /** + * Partially enforces Rule #1. + * + *

    If an Optional value is compared with the null literal, it indicates that the programmer + * expects it might have been assigned a null value (or no value at all) somewhere in the code. + * + * @param tree a binary tree representing a binary operation. + */ + private void handleCompareToNull(BinaryTree tree) { + if (!isEqualityOperation(tree)) { + return; + } + ExpressionTree leftOp = TreeUtils.withoutParens(tree.getLeftOperand()); + ExpressionTree rightOp = TreeUtils.withoutParens(tree.getRightOperand()); + TypeMirror leftOpType = TreeUtils.typeOf(leftOp); + TypeMirror rightOpType = TreeUtils.typeOf(rightOp); + + if (leftOp.getKind() == Tree.Kind.NULL_LITERAL && isOptionalType(rightOpType)) { + checker.reportWarning(tree, "optional.null.comparison"); + } + if (rightOp.getKind() == Tree.Kind.NULL_LITERAL && isOptionalType(leftOpType)) { + checker.reportWarning(tree, "optional.null.comparison"); + } + } + + /** + * Returns true if the binary operation is {@code ==} or {@code !=}. + * + * @param tree a binary operation + * @return true if the binary operation is {@code ==} or {@code !=} + */ + private boolean isEqualityOperation(BinaryTree tree) { + return tree.getKind() == Tree.Kind.EQUAL_TO || tree.getKind() == Tree.Kind.NOT_EQUAL_TO; + } + + // Partially enforces Rule #1. (Only handles the literal `null`, not all nullable expressions.) + @Override + protected boolean commonAssignmentCheck( + AnnotatedTypeMirror varType, + ExpressionTree valueExpTree, + @CompilerMessageKey String errorKey, + Object... extraArgs) { + boolean result = super.commonAssignmentCheck(varType, valueExpTree, errorKey, extraArgs); + ExpressionTree valueWithoutParens = TreeUtils.withoutParens(valueExpTree); + if (valueWithoutParens.getKind() == Kind.NULL_LITERAL + && isOptionalType(varType.getUnderlyingType())) { + checker.reportWarning(valueWithoutParens, "optional.null.assignment"); + return false; + } + return result; } /** * Rule #4. * - *

    Pattern match for: {@code CREATION().ELIMINATION()} + *

    Pattern match for: {@code CREATION().PROPAGATION()*.ELIMINATION()} * *

    Prefer: {@code VAR.ifPresent(METHOD);} + * + * @param tree a method invocation that can perhaps be simplified */ - public void handleCreationElimination(MethodInvocationTree node) { - if (!isOptionalElimation(node)) { + public void handleCreationElimination(MethodInvocationTree tree) { + if (!isOptionalElimination(tree)) { return; } - ExpressionTree receiver = TreeUtils.getReceiverTree(node); - if (!(receiver.getKind() == Kind.METHOD_INVOCATION - && isOptionalCreation((MethodInvocationTree) receiver))) { - return; + ExpressionTree receiver = TreeUtils.getReceiverTree(tree); + while (true) { + if (receiver == null) { + // The receiver can be null if the receiver is the implicit "this.". + return; + } + if (receiver.getKind() != Tree.Kind.METHOD_INVOCATION) { + return; + } + MethodInvocationTree methodCall = (MethodInvocationTree) receiver; + if (isOptionalPropagation(methodCall)) { + receiver = TreeUtils.getReceiverTree(methodCall); + continue; + } else if (isOptionalCreation(methodCall)) { + checker.reportWarning(tree, "introduce.eliminate"); + return; + } else { + return; + } } + } - checker.reportWarning(node, "introduce.eliminate"); + /** + * Partial support for Rule #5 and Rule #7. + * + *

    Rule #5: Avoid nested Optional chains, or operations that have an intermediate Optional + * value. + * + *

    Rule #7: Don't use Optional to wrap any collection type. + * + *

    Certain types are illegal, such as {@code Optional}. The type validator may see + * a supertype of the most precise run-time type; for example, it may see the type as {@code + * Optional}, and it would not flag any problem with such a type. This method + * checks at {@code Optional} creation sites. + * + *

    TODO: This finds only some {@code Optional}: those that consist of {@code + * Optional.of(optionalExpr)} or {@code Optional.ofNullable(optionalExpr)}, where {@code + * optionalExpr} has type {@code Optional}. There are other ways that {@code Optional} + * can be created, such as {@code optionalExpr.map(Optional::of)}. + * + *

    TODO: Also check at collection creation sites, but there are so many of them, and there + * often are not values of the element type at the collection creation site. + * + * @param tree a method invocation that might create an Optional of an illegal type + */ + public void handleNestedOptionalCreation(MethodInvocationTree tree) { + if (!isOptionalCreation(tree)) { + return; + } + if (tree.getArguments().isEmpty()) { + // This is a call to Optional.empty(), which takes no argument. + return; + } + ExpressionTree arg = tree.getArguments().get(0); + AnnotatedTypeMirror argAtm = atypeFactory.getAnnotatedType(arg); + TypeMirror argType = argAtm.getUnderlyingType(); + if (isOptionalType(argType)) { + checker.reportWarning(tree, "optional.nesting"); + } else if (isCollectionType(argType)) { + checker.reportWarning(tree, "optional.collection"); + } } /** @@ -261,23 +476,41 @@ && isOptionalCreation((MethodInvocationTree) receiver))) { *

    Don't use Optional in fields and method parameters. */ @Override - public Void visitVariable(VariableTree node, Void p) { - VariableElement ve = TreeUtils.elementFromDeclaration(node); + public Void visitVariable(VariableTree tree, Void p) { + VariableElement ve = TreeUtils.elementFromDeclaration(tree); TypeMirror tm = ve.asType(); if (isOptionalType(tm)) { - ElementKind ekind = TreeUtils.elementFromDeclaration(node).getKind(); + ElementKind ekind = TreeUtils.elementFromDeclaration(tree).getKind(); if (ekind.isField()) { - checker.reportWarning(node, "optional.field"); + checker.reportWarning(tree, "optional.field"); } else if (ekind == ElementKind.PARAMETER) { - checker.reportWarning(node, "optional.parameter"); + TreePath paramPath = getCurrentPath(); + Tree parent = paramPath.getParentPath().getLeaf(); + if (parent.getKind() == Tree.Kind.LAMBDA_EXPRESSION) { + // Exception to rule: lambda parameters can have type Optional. + } else { + checker.reportWarning(tree, "optional.parameter"); + } } } - return super.visitVariable(node, p); + return super.visitVariable(tree, p); } /** - * Handles part of Rule #6, and also Rule #7: Don't permit {@code Collection>} or - * {@code Optional>}. + * Handles Rule #5, part of Rule #6, and also Rule #7. + * + *

    Rule #5: Avoid nested Optional chains, or operations that have an intermediate Optional + * value. + * + *

    Rule #6: Don't use Optional in fields, parameters, and collections. + * + *

    Rule #7: Don't use Optional to wrap any collection type. + * + *

    The validator is called on the type of every expression, such as on the right-hand side of + * {@code x = Optional.of(Optional.of("baz"));}. However, the type of the right-hand side is + * {@code Optional}, not {@code Optional>}. Therefore, to + * fully check for improper types, it is necessary to examine, in the type checker, the argument + * to construction of an Optional. Method {@link handleNestedOptionalCreation} does so. */ private final class OptionalTypeValidator extends BaseTypeValidator { @@ -288,14 +521,18 @@ public OptionalTypeValidator( super(checker, visitor, atypeFactory); } - // TODO: Why is "isValid" called twice on the right-hand-side of a variable initializer? - // It leads to the error being issued twice. /** - * Rules 6 (partial) and 7: Don't permit {@code Collection>} or {@code - * Optional>}. + * Handles Rule #5, part of Rule #6, and also Rule #7. + * + *

    Rule #5: Avoid nested Optional chains, or operations that have an intermediate + * Optional value. + * + *

    Rule #6: Don't use Optional in fields, parameters, and collections. + * + *

    Rule #7: Don't use Optional to wrap any collection type. */ @Override - public boolean isValid(AnnotatedTypeMirror type, Tree tree) { + public Void visitDeclared(AnnotatedDeclaredType type, Tree tree) { TypeMirror tm = type.getUnderlyingType(); if (isCollectionType(tm)) { List typeArgs = ((DeclaredType) tm).getTypeArguments(); @@ -308,24 +545,49 @@ public boolean isValid(AnnotatedTypeMirror type, Tree tree) { } } else if (isOptionalType(tm)) { List typeArgs = ((DeclaredType) tm).getTypeArguments(); - assert typeArgs.size() == 1; - TypeMirror typeArg = typeArgs.get(0); - if (isCollectionType(typeArg)) { - checker.reportError(tree, "optional.collection"); + // If typeArgs.size()==0, then the user wrote a raw type `Optional`. + if (typeArgs.size() == 1) { + TypeMirror typeArg = typeArgs.get(0); + if (isCollectionType(typeArg)) { + checker.reportWarning(tree, "optional.collection"); + } + if (isOptionalType(typeArg)) { + checker.reportWarning(tree, "optional.nesting"); + } } } - return super.isValid(type, tree); + return super.visitDeclared(type, tree); } } - /** Return true if tm represents a subtype of Collection (other than the Null type). */ + /** + * Return true if tm is a subtype of Collection (other than the Null type). + * + * @param tm a type + * @return true if the given type is a subtype of Collection + */ private boolean isCollectionType(TypeMirror tm) { return tm.getKind() == TypeKind.DECLARED && types.isSubtype(tm, collectionType); } - /** Return true if tm represents java.util.Optional. */ + /** The fully-qualified names of the 4 optional classes in java.util. */ + private static final Set fqOptionalTypes = + new HashSet<>( + Arrays.asList( + "java.util.Optional", + "java.util.OptionalDouble", + "java.util.OptionalInt", + "java.util.OptionalLong")); + + /** + * Return true if tm is class Optional, OptionalDouble, OptionalInt, or OptionalLong in + * java.util. + * + * @param tm a type + * @return true if the given type is Optional, OptionalDouble, OptionalInt, or OptionalLong + */ private boolean isOptionalType(TypeMirror tm) { - return TypesUtils.isDeclaredOfName(tm, "java.util.Optional"); + return TypesUtils.isDeclaredOfName(tm, fqOptionalTypes); } /** @@ -336,7 +598,7 @@ private boolean isOptionalType(TypeMirror tm) { * @return the single enclosed statement, if it exists; otherwise, the same tree */ // TODO: The Optional Checker should work over the CFG, then it would not need this any longer. - public static StatementTree skipBlocks(final StatementTree tree) { + public static StatementTree skipBlocks(StatementTree tree) { if (tree == null) { return tree; } @@ -351,4 +613,64 @@ public static StatementTree skipBlocks(final StatementTree tree) { } return s; } + + @Override + public Void visitMemberReference(MemberReferenceTree tree, Void p) { + if (isFilterIsPresentMapGet(tree)) { + // TODO: This is a (sound) workaround until + // https://github.com/typetools/checker-framework/issues/1345 + // is fixed. + return null; + } + return super.visitMemberReference(tree, p); + } + + /** + * Returns true if {@code memberRefTree} is the {@code Optional::get} in {@code + * Stream.filter(Optional::isPresent).map(Optional::get)}. + * + * @param memberRefTree a member reference tree + * @return true if {@code memberRefTree} the {@code Optional::get} in {@code + * Stream.filter(Optional::isPresent).map(Optional::get)} + */ + private boolean isFilterIsPresentMapGet(MemberReferenceTree memberRefTree) { + if (!TreeUtils.elementFromUse(memberRefTree).equals(optionalGet)) { + // The method reference is not Optional::get + return false; + } + // "getPath" means "the path to the node `Optional::get`". + TreePath getPath = getCurrentPath(); + TreePath getParentPath = getPath.getParentPath(); + // "getParent" means "the parent of the node `Optional::get`". + Tree getParent = getParentPath.getLeaf(); + if (getParent.getKind() == Tree.Kind.METHOD_INVOCATION) { + MethodInvocationTree hasGetAsArgumentTree = (MethodInvocationTree) getParent; + ExecutableElement hasGetAsArgumentElement = + TreeUtils.elementFromUse(hasGetAsArgumentTree); + if (!hasGetAsArgumentElement.equals(streamMap)) { + // Optional::get is not an argument to stream#map + return false; + } + // hasGetAsArgumentTree is an invocation of Stream#map(...). + Tree mapReceiverTree = TreeUtils.getReceiverTree(hasGetAsArgumentTree); + // Will check whether mapParent is the call `Stream.filter(Optional::isPresent)`. + if (mapReceiverTree != null + && mapReceiverTree.getKind() == Tree.Kind.METHOD_INVOCATION) { + MethodInvocationTree fluentToMapTree = (MethodInvocationTree) mapReceiverTree; + ExecutableElement fluentToMapElement = TreeUtils.elementFromUse(fluentToMapTree); + if (!fluentToMapElement.equals(streamFilter)) { + // The receiver of map(Optional::get) is not Stream#filter + return false; + } + MethodInvocationTree filterInvocationTree = fluentToMapTree; + ExpressionTree filterArgTree = filterInvocationTree.getArguments().get(0); + if (filterArgTree.getKind() == Tree.Kind.MEMBER_REFERENCE) { + ExecutableElement filterArgElement = + TreeUtils.elementFromUse((MemberReferenceTree) filterArgTree); + return filterArgElement.equals(optionalIsPresent); + } + } + } + return false; + } } diff --git a/checker/src/main/java/org/checkerframework/checker/optional/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/optional/javaparser.astub new file mode 100644 index 000000000000..593a04b0cabe --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/optional/javaparser.astub @@ -0,0 +1,412 @@ +// Every `getParentNode()` method except that of `CompilationUnit`, `Node`, and `StubUnit` returns a @Present Optional. + +import org.checkerframework.checker.optional.qual.Present; + + +package com.github.javaparser; + +public class JavaToken { + @Present Optional getRange(); +} +public class TokenRange implements Iterable { + @Present Optional toRange(); +} + + +package com.github.javaparser.ast; + +class ArrayCreationLevel { + @Present Optional getParentNode(); +} +class ImportDeclaration { + @Present Optional getParentNode(); +} +class Modifier { + @Present Optional getParentNode(); +} +class Node { + // This might be unsound. + @Present Optional getRange(); + // This might be unsound. + @Present Optional getTokenRange(); +} +class NodeList { + @Present Optional getParentNode(); +} +class PackageDeclaration { + @Present Optional getParentNode(); +} + + +package com.github.javaparser.ast.body; + +class AnnotationDeclaration { + @Present Optional getParentNode(); +} +class AnnotationMemberDeclaration { + @Present Optional getParentNode(); +} +class BodyDeclaration> { + @Present Optional getParentNode(); +} +class CallableDeclaration> { + @Present Optional getParentNode(); +} +class ClassOrInterfaceDeclaration { + @Present Optional getParentNode(); +} +class CompactConstructorDeclaration { + @Present Optional getParentNode(); +} +class ConstructorDeclaration { + @Present Optional getParentNode(); +} +class EnumConstantDeclaration { + @Present Optional getParentNode(); +} +class EnumDeclaration { + @Present Optional getParentNode(); +} +class FieldDeclaration { + @Present Optional getParentNode(); +} +class InitializerDeclaration { + @Present Optional getParentNode(); +} +class MethodDeclaration { + @Present Optional getParentNode(); +} +class Parameter { + @Present Optional getParentNode(); +} +class ReceiverParameter { + @Present Optional getParentNode(); +} +class RecordDeclaration { + @Present Optional getParentNode(); + @Present Optional getFullyQualifiedName(); +} +class TypeDeclaration> { + @Present Optional getParentNode(); +} +class VariableDeclarator { + @Present Optional getParentNode(); +} + + +package com.github.javaparser.ast.comments; + +class BlockComment { + @Present Optional getParentNode(); +} +class Comment { + @Present Optional getParentNode(); +} +class JavadocComment { + @Present Optional getParentNode(); +} +class LineComment { + @Present Optional getParentNode(); +} + + +package com.github.javaparser.ast.expr; + +class AnnotationExpr { + @Present Optional getParentNode(); +} +class ArrayAccessExpr { + @Present Optional getParentNode(); +} +class ArrayCreationExpr { + @Present Optional getParentNode(); +} +class ArrayInitializerExpr { + @Present Optional getParentNode(); +} +class AssignExpr { + @Present Optional getParentNode(); +} +class BinaryExpr { + @Present Optional getParentNode(); +} +class BooleanLiteralExpr { + @Present Optional getParentNode(); +} +class CastExpr { + @Present Optional getParentNode(); +} +class CharLiteralExpr { + @Present Optional getParentNode(); +} +class ClassExpr { + @Present Optional getParentNode(); +} +class ConditionalExpr { + @Present Optional getParentNode(); +} +class DoubleLiteralExpr { + @Present Optional getParentNode(); +} +class EnclosedExpr { + @Present Optional getParentNode(); +} +class Expression { + @Present Optional getParentNode(); +} +class FieldAccessExpr { + @Present Optional getParentNode(); +} +class InstanceOfExpr { + @Present Optional getParentNode(); +} +class IntegerLiteralExpr { + @Present Optional getParentNode(); +} +class LambdaExpr { + @Present Optional getParentNode(); +} +class LiteralExpr { + @Present Optional getParentNode(); +} +class LiteralStringValueExpr { + @Present Optional getParentNode(); +} +class LongLiteralExpr { + @Present Optional getParentNode(); +} +class MarkerAnnotationExpr { + @Present Optional getParentNode(); +} +class MemberValuePair { + @Present Optional getParentNode(); +} +class MethodCallExpr { + @Present Optional getParentNode(); +} +class MethodReferenceExpr { + @Present Optional getParentNode(); +} +class Name { + @Present Optional getParentNode(); +} +class NameExpr { + @Present Optional getParentNode(); +} +class NormalAnnotationExpr { + @Present Optional getParentNode(); +} +class NullLiteralExpr { + @Present Optional getParentNode(); +} +class ObjectCreationExpr { + @Present Optional getParentNode(); +} +class PatternExpr { + @Present Optional getParentNode(); +} +class SimpleName { + @Present Optional getParentNode(); +} +class SingleMemberAnnotationExpr { + @Present Optional getParentNode(); +} +class StringLiteralExpr { + @Present Optional getParentNode(); +} +class SuperExpr { + @Present Optional getParentNode(); +} +class SwitchExpr { + @Present Optional getParentNode(); +} +class TextBlockLiteralExpr { + @Present Optional getParentNode(); +} +class ThisExpr { + @Present Optional getParentNode(); +} +class TypeExpr { + @Present Optional getParentNode(); +} +class UnaryExpr { + @Present Optional getParentNode(); +} +class VariableDeclarationExpr { + @Present Optional getParentNode(); +} + + +package com.github.javaparser.ast.modules; + +class ModuleDeclaration { + @Present Optional getParentNode(); +} +class ModuleDirective { + @Present Optional getParentNode(); +} +class ModuleExportsDirective { + @Present Optional getParentNode(); +} +class ModuleOpensDirective { + @Present Optional getParentNode(); +} +class ModuleProvidesDirective { + @Present Optional getParentNode(); +} +class ModuleRequiresDirective { + @Present Optional getParentNode(); +} +class ModuleUsesDirective { + @Present Optional getParentNode(); +} + + +package com.github.javaparser.ast.nodeTypes; + +public interface NodeWithRange { + // This might be unsound. + @Present Optional getRange(); + + // This might be unsound. + @Present Optional getBegin(); + + // This might be unsound. + @Present Optional getEnd(); +} + + +package com.github.javaparser.ast.stmt; + +class AssertStmt { + @Present Optional getParentNode(); +} +class BlockStmt { + @Present Optional getParentNode(); +} +class BreakStmt { + @Present Optional getParentNode(); +} +class CatchClause { + @Present Optional getParentNode(); +} +class ContinueStmt { + @Present Optional getParentNode(); +} +class DoStmt { + @Present Optional getParentNode(); +} +class EmptyStmt { + @Present Optional getParentNode(); +} +class ExplicitConstructorInvocationStmt { + @Present Optional getParentNode(); +} +class ExpressionStmt { + @Present Optional getParentNode(); +} +class ForEachStmt { + @Present Optional getParentNode(); +} +class ForStmt { + @Present Optional getParentNode(); +} +class IfStmt { + @Present Optional getParentNode(); +} +class LabeledStmt { + @Present Optional getParentNode(); +} +class LocalClassDeclarationStmt { + @Present Optional getParentNode(); +} +class LocalRecordDeclarationStmt { + @Present Optional getParentNode(); +} +class ReturnStmt { + @Present Optional getParentNode(); +} +class Statement { + @Present Optional getParentNode(); +} +class SwitchEntry { + @Present Optional getParentNode(); +} +class SwitchStmt { + @Present Optional getParentNode(); +} +class SynchronizedStmt { + @Present Optional getParentNode(); +} +class ThrowStmt { + @Present Optional getParentNode(); +} +class TryStmt { + @Present Optional getParentNode(); +} +class UnparsableStmt { + @Present Optional getParentNode(); +} +class WhileStmt { + @Present Optional getParentNode(); +} +class YieldStmt { + @Present Optional getParentNode(); +} + + +package com.github.javaparser.ast.type; + +class ArrayType { + @Present Optional getParentNode(); + // This might be unsound. + @Present Optional getTokenRange(); + class ArrayBracketPair { + // This might be unsound. + @Present Optional getTokenRange(); + } +} +class ClassOrInterfaceType { + @Present Optional getParentNode(); +} +class IntersectionType { + @Present Optional getParentNode(); +} +class PrimitiveType { + @Present Optional getParentNode(); +} +class ReferenceType { + @Present Optional getParentNode(); +} +class Type { + @Present Optional getParentNode(); +} +class TypeParameter { + @Present Optional getParentNode(); +} +class UnionType { + @Present Optional getParentNode(); +} +class UnknownType { + @Present Optional getParentNode(); +} +class VarType { + @Present Optional getParentNode(); +} +class VoidType { + @Present Optional getParentNode(); +} +class WildcardType { + @Present Optional getParentNode(); + @Present Optional getSuperType(); +} + + +package com.github.javaparser.printer.lexicalpreservation; + +public class TokenTextElement extends TextElement { + @Present Optional getRange(); +} +public class ChildTextElement extends TextElement { + @Present Optional getRange(); +} diff --git a/checker/src/main/java/org/checkerframework/checker/optional/messages.properties b/checker/src/main/java/org/checkerframework/checker/optional/messages.properties index c9c1c51d7fd0..d252b53cf92c 100644 --- a/checker/src/main/java/org/checkerframework/checker/optional/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/optional/messages.properties @@ -1,8 +1,11 @@ ### Error messages for the Optional Checker prefer.map.and.orelse=It is better style to use map and orElse.%nConsider changing to:%n%s.map(CONTAININGCLASS::%s).orElse(%s) prefer.ifpresent=It is better style to use ifPresent.%nConsider changing to:%n%s.ifPresent(%s) -introduce.eliminate=It is bad style to create an Optional just to chain methods to get a value. +introduce.eliminate=It is bad style to create an Optional just to chain methods to get a non-optional value. optional.as.element.type=Don't use Optional as the element type in a collection. +optional.null.assignment=Don't assign null to an Optional. +optional.null.comparison=Don't compare Optional to null. optional.collection=Don't use Optional to wrap a collection type.%nUse an empty collection to represent the absence of values. +optional.nesting=Don't use Optional to wrap another Optional value. optional.field=Don't use Optional as the type of a field. optional.parameter=Don't use Optional as the type of a formal parameter. diff --git a/checker/src/main/java/org/checkerframework/checker/propkey/PropertyKeyAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/propkey/PropertyKeyAnnotatedTypeFactory.java index 5c7a6971330b..142f51f98295 100644 --- a/checker/src/main/java/org/checkerframework/checker/propkey/PropertyKeyAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/propkey/PropertyKeyAnnotatedTypeFactory.java @@ -4,26 +4,32 @@ import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.LiteralTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.propkey.qual.PropertyKey; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; +import org.checkerframework.framework.type.treeannotator.TreeAnnotator; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.plumelib.reflection.Signatures; +import org.plumelib.util.CollectionsPlume; + +import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.lang.annotation.Annotation; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Properties; import java.util.ResourceBundle; import java.util.Set; + import javax.lang.model.element.AnnotationMirror; -import javax.tools.Diagnostic.Kind; -import org.checkerframework.checker.propkey.qual.PropertyKey; -import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; -import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; -import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.javacutil.AnnotationBuilder; -import org.plumelib.reflection.Signatures; +import javax.tools.Diagnostic; /** * This AnnotatedTypeFactory adds PropertyKey annotations to String literals that contain values @@ -68,7 +74,7 @@ public KeyLookupTreeAnnotator( @Override public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) { - if (!type.isAnnotatedInHierarchy(theAnnot) + if (!type.hasAnnotationInHierarchy(theAnnot) && tree.getKind() == Tree.Kind.STRING_LITERAL && strContains(lookupKeys, tree.getValue().toString())) { type.addAnnotation(theAnnot); @@ -82,16 +88,16 @@ && strContains(lookupKeys, tree.getValue().toString())) { // Result of binary op might not be a property key. @Override - public Void visitBinary(BinaryTree node, AnnotatedTypeMirror type) { + public Void visitBinary(BinaryTree tree, AnnotatedTypeMirror type) { type.removeAnnotation(theAnnot); - return null; // super.visitBinary(node, type); + return null; // super.visitBinary(tree, type); } // Result of unary op might not be a property key. @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMirror type) { + public Void visitCompoundAssignment(CompoundAssignmentTree tree, AnnotatedTypeMirror type) { type.removeAnnotation(theAnnot); - return null; // super.visitCompoundAssignment(node, type); + return null; // super.visitCompoundAssignment(tree, type); } } @@ -131,63 +137,66 @@ private Set buildLookupKeys() { Set result = new HashSet<>(); if (checker.hasOption("propfiles")) { - result.addAll(keysOfPropertyFiles(checker.getOption("propfiles"))); + result.addAll( + keysOfPropertyFiles(checker.getStringsOption("propfiles", File.pathSeparator))); } if (checker.hasOption("bundlenames")) { - result.addAll(keysOfResourceBundle(checker.getOption("bundlenames"))); + result.addAll(keysOfResourceBundle(checker.getStringsOption("bundlenames", ':'))); } return result; } - private Set keysOfPropertyFiles(String names) { - String[] namesArr = names.split(":"); - - if (namesArr == null) { - checker.message(Kind.WARNING, "Couldn't parse the properties files: <" + names + ">"); + /** + * Obtains the keys from all the property files. + * + * @param propfiles a list of property file names + * @return a set of all the keys found in all the property files + */ + private Set keysOfPropertyFiles(List propfiles) { + if (propfiles.isEmpty()) { return Collections.emptySet(); } - Set result = new HashSet<>(); + Set result = new HashSet<>(CollectionsPlume.mapCapacity(propfiles)); - for (String name : namesArr) { + for (String propfile : propfiles) { try { Properties prop = new Properties(); ClassLoader cl = this.getClass().getClassLoader(); if (cl == null) { - // the class loader is null if the system class loader was - // used + // The class loader is null if the system class loader was used. cl = ClassLoader.getSystemClassLoader(); } - InputStream in = cl.getResourceAsStream(name); - - if (in == null) { - // if the classloader didn't manage to load the file, try - // whether a FileInputStream works. For absolute paths this - // might help. - try { - in = new FileInputStream(name); - } catch (FileNotFoundException e) { - // ignore - } - } - if (in == null) { - checker.message(Kind.WARNING, "Couldn't find the properties file: " + name); - // report(null, "propertykeychecker.filenotfound", name); - // return Collections.emptySet(); - continue; + try (InputStream in = cl.getResourceAsStream(propfile)) { + if (in != null) { + prop.load(in); + } else { + // If the classloader didn't manage to load the file, try whether a + // FileInputStream works. For absolute paths this might help. + try (InputStream fis = new FileInputStream(propfile)) { + prop.load(fis); + } catch (FileNotFoundException e) { + checker.message( + Diagnostic.Kind.WARNING, + "Couldn't find the properties file: " + propfile); + // report(null, "propertykeychecker.filenotfound", propfile); + // return Collections.emptySet(); + continue; + } + } } - prop.load(in); result.addAll(prop.stringPropertyNames()); } catch (Exception e) { - // TODO: is there a nicer way to report messages, that are not - // connected to an AST node? - // One cannot use report, because it needs a node. + // TODO: is there a nicer way to report messages, that are not connected to an AST + // node? + // One cannot use `report`, because it needs a node. checker.message( - Kind.WARNING, "Exception in PropertyKeyChecker.keysOfPropertyFile: " + e); + Diagnostic.Kind.WARNING, + "Exception in PropertyKeyChecker.keysOfPropertyFile: " + e); e.printStackTrace(); } } @@ -195,18 +204,20 @@ private Set keysOfPropertyFiles(String names) { return result; } - private Set keysOfResourceBundle(String bundleNames) { - String[] namesArr = bundleNames.split(":"); - - if (namesArr == null) { - checker.message( - Kind.WARNING, "Couldn't parse the resource bundles: <" + bundleNames + ">"); + /** + * Returns the keys for the given resource bundles. + * + * @param bundleNames names of resource bundles + * @return the keys for the given resource bundles + */ + private Set keysOfResourceBundle(List bundleNames) { + if (bundleNames.isEmpty()) { return Collections.emptySet(); } - Set result = new HashSet<>(); + Set result = new HashSet<>(CollectionsPlume.mapCapacity(bundleNames)); - for (String bundleName : namesArr) { + for (String bundleName : bundleNames) { if (!Signatures.isBinaryName(bundleName)) { System.err.println( "Malformed resource bundle: <" + bundleName + "> should be a binary name."); @@ -215,7 +226,7 @@ private Set keysOfResourceBundle(String bundleNames) { ResourceBundle bundle = ResourceBundle.getBundle(bundleName); if (bundle == null) { checker.message( - Kind.WARNING, + Diagnostic.Kind.WARNING, "Couldn't find the resource bundle: <" + bundleName + "> for locale <" diff --git a/checker/src/main/java/org/checkerframework/checker/propkey/PropertyKeyChecker.java b/checker/src/main/java/org/checkerframework/checker/propkey/PropertyKeyChecker.java index f0a893c19b4e..0b251add1b0c 100644 --- a/checker/src/main/java/org/checkerframework/checker/propkey/PropertyKeyChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/propkey/PropertyKeyChecker.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.propkey; -import java.util.Locale; -import java.util.ResourceBundle; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; import org.checkerframework.framework.source.SupportedOptions; +import java.util.Locale; +import java.util.ResourceBundle; + /** * A type-checker that checks that only valid keys are used to access property files and resource * bundles. Subclasses can specialize this class for the different uses of property files, for @@ -17,7 +18,7 @@ *

  • Property files: A common method for localization using a property file, mapping * the keys to values. Programmers pass the property file locations via {@code propfiles} * option (e.g. {@code -Apropfiles=/path/to/messages.properties}), separating multiple files - * by a colon ":". + * by {@link java.io.File#pathSeparator}. *
  • {@link ResourceBundle}: Programmers pass the {@code baseName} name of the bundle * via {@code bundlename} (e.g. {@code -Abundlename=MyResource}. The checker uses the resource * associated with the default {@link Locale} in the compilation system. diff --git a/checker/src/main/java/org/checkerframework/checker/regex/RegexAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/regex/RegexAnnotatedTypeFactory.java index 796d06142303..b500fc36e4d7 100644 --- a/checker/src/main/java/org/checkerframework/checker/regex/RegexAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/regex/RegexAnnotatedTypeFactory.java @@ -6,21 +6,16 @@ import com.sun.source.tree.LiteralTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; -import java.lang.annotation.Annotation; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.AnnotationValue; -import javax.lang.model.element.ExecutableElement; + +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.regex.qual.PartialRegex; import org.checkerframework.checker.regex.qual.PolyRegex; import org.checkerframework.checker.regex.qual.Regex; import org.checkerframework.checker.regex.qual.RegexBottom; import org.checkerframework.checker.regex.qual.UnknownRegex; +import org.checkerframework.checker.regex.util.RegexUtil; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFAnalysis; import org.checkerframework.framework.flow.CFStore; @@ -31,16 +26,28 @@ import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedIntersectionType; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedWildcardType; +import org.checkerframework.framework.type.MostlyNoElementQualifierHierarchy; import org.checkerframework.framework.type.QualifierHierarchy; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.LiteralTreeAnnotator; import org.checkerframework.framework.type.treeannotator.PropagationTreeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.framework.util.GraphQualifierHierarchy; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory; +import org.checkerframework.framework.util.QualifierKind; import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Set; +import java.util.regex.Pattern; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.util.Elements; /** * Adds {@link Regex} to the type of tree, in the following cases: @@ -75,35 +82,39 @@ public class RegexAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The @{@link Regex} annotation. */ protected final AnnotationMirror REGEX = AnnotationBuilder.fromClass(elements, Regex.class); + /** The @{@link RegexBottom} annotation. */ protected final AnnotationMirror REGEXBOTTOM = AnnotationBuilder.fromClass(elements, RegexBottom.class); + /** The @{@link PartialRegex} annotation. */ protected final AnnotationMirror PARTIALREGEX = AnnotationBuilder.fromClass(elements, PartialRegex.class); + /** The @{@link PolyRegex} annotation. */ protected final AnnotationMirror POLYREGEX = AnnotationBuilder.fromClass(elements, PolyRegex.class); + /** The @{@link UnknownRegex} annotation. */ + protected final AnnotationMirror UNKNOWNREGEX = + AnnotationBuilder.fromClass(elements, UnknownRegex.class); + + /** A set containing just {@link #UNKNOWNREGEX}. */ + protected final AnnotationMirrorSet UNKNOWNREGEX_SET = + AnnotationMirrorSet.singleton(UNKNOWNREGEX); + /** The method that returns the value element of a {@code @Regex} annotation. */ protected final ExecutableElement regexValueElement = TreeUtils.getMethod( - org.checkerframework.checker.regex.qual.Regex.class.getName(), - "value", - 0, - processingEnv); + "org.checkerframework.checker.regex.qual.Regex", "value", 0, processingEnv); /** * The value method of the PartialRegex qualifier. * * @see org.checkerframework.checker.regex.qual.PartialRegex */ - private final ExecutableElement partialRegexValue = - TreeUtils.getMethod( - org.checkerframework.checker.regex.qual.PartialRegex.class.getName(), - "value", - 0, - processingEnv); + private final ExecutableElement partialRegexValueElement = + TreeUtils.getMethod(PartialRegex.class, "value", 0, processingEnv); /** * The Pattern.compile method. @@ -111,22 +122,24 @@ public class RegexAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { * @see java.util.regex.Pattern#compile(String) */ private final ExecutableElement patternCompile = - TreeUtils.getMethod( - java.util.regex.Pattern.class.getName(), "compile", 1, processingEnv); + TreeUtils.getMethod("java.util.regex.Pattern", "compile", 1, processingEnv); - // TODO use? private TypeMirror[] legalReferenceTypes; + /** + * The Pattern.compile method that takes two formal parameters (second one is flags). + * + * @see java.util.regex.Pattern#compile(String, int) + */ + private final ExecutableElement patternCompile2 = + TreeUtils.getMethod("java.util.regex.Pattern", "compile", 2, processingEnv); + /** + * Create a new RegexAnnotatedTypeFactory. + * + * @param checker the checker + */ public RegexAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - /* - legalReferenceTypes = new TypeMirror[] { - getTypeMirror("java.lang.CharSequence"), - getTypeMirror("java.lang.Character"), - getTypeMirror("java.util.regex.Pattern"), - getTypeMirror("java.util.regex.MatchResult") }; - */ - this.postInit(); } @@ -144,7 +157,7 @@ public CFTransfer createFlowTransferFunction( } /** Returns a new Regex annotation with the given group count. */ - /*package-scope*/ AnnotationMirror createRegexAnnotation(int groupCount) { + /*package-private*/ AnnotationMirror createRegexAnnotation(int groupCount) { AnnotationBuilder builder = new AnnotationBuilder(processingEnv, Regex.class); if (groupCount > 0) { builder.setValue("value", groupCount); @@ -153,8 +166,8 @@ public CFTransfer createFlowTransferFunction( } @Override - public QualifierHierarchy createQualifierHierarchy(MultiGraphFactory factory) { - return new RegexQualifierHierarchy(factory, REGEXBOTTOM); + protected QualifierHierarchy createQualifierHierarchy() { + return new RegexQualifierHierarchy(this.getSupportedTypeQualifiers(), elements); } /** @@ -163,58 +176,124 @@ public QualifierHierarchy createQualifierHierarchy(MultiGraphFactory factory) { * subtype of {@code @Regex(1)}. All regex annotations are subtypes of {@code @Regex}, which has * a default value of 0. */ - private final class RegexQualifierHierarchy extends GraphQualifierHierarchy { + private final class RegexQualifierHierarchy extends MostlyNoElementQualifierHierarchy { - public RegexQualifierHierarchy(MultiGraphFactory f, AnnotationMirror bottom) { - super(f, bottom); + /** Qualifier kind for the @{@link Regex} annotation. */ + private final QualifierKind REGEX_KIND; + + /** Qualifier kind for the @{@link PartialRegex} annotation. */ + private final QualifierKind PARTIALREGEX_KIND; + + /** + * Creates a RegexQualifierHierarchy from the given classes. + * + * @param qualifierClasses classes of annotations that are the qualifiers for this hierarchy + * @param elements element utils + */ + private RegexQualifierHierarchy( + Collection> qualifierClasses, Elements elements) { + super(qualifierClasses, elements, RegexAnnotatedTypeFactory.this); + REGEX_KIND = getQualifierKind(REGEX); + PARTIALREGEX_KIND = getQualifierKind(PARTIALREGEX); } @Override - public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { - if (AnnotationUtils.areSameByName(subAnno, REGEX) - && AnnotationUtils.areSameByName(superAnno, REGEX)) { + protected boolean isSubtypeWithElements( + AnnotationMirror subAnno, + QualifierKind subKind, + AnnotationMirror superAnno, + QualifierKind superKind) { + if (subKind == REGEX_KIND && superKind == REGEX_KIND) { int rhsValue = getRegexValue(subAnno); int lhsValue = getRegexValue(superAnno); return lhsValue <= rhsValue; + } else if (subKind == PARTIALREGEX_KIND && superKind == PARTIALREGEX_KIND) { + return AnnotationUtils.areSame(subAnno, superAnno); } - // TODO: subtyping between PartialRegex? - // Ignore annotation values to ensure that annotation is in supertype map. - if (AnnotationUtils.areSameByName(superAnno, REGEX)) { - superAnno = REGEX; - } - if (AnnotationUtils.areSameByName(subAnno, REGEX)) { - subAnno = REGEX; - } - if (AnnotationUtils.areSameByName(superAnno, PARTIALREGEX)) { - superAnno = PARTIALREGEX; + throw new TypeSystemError("Unexpected qualifiers: %s %s", subAnno, superAnno); + } + + @Override + protected AnnotationMirror leastUpperBoundWithElements( + AnnotationMirror a1, + QualifierKind qualifierKind1, + AnnotationMirror a2, + QualifierKind qualifierKind2, + QualifierKind lubKind) { + if (qualifierKind1 == REGEX_KIND && qualifierKind2 == REGEX_KIND) { + int value1 = getRegexValue(a1); + int value2 = getRegexValue(a2); + if (value1 < value2) { + return a1; + } else { + return a2; + } + } else if (qualifierKind1 == PARTIALREGEX_KIND && qualifierKind2 == PARTIALREGEX_KIND) { + if (AnnotationUtils.areSame(a1, a2)) { + return a1; + } else { + return UNKNOWNREGEX; + } + } else if (qualifierKind1 == PARTIALREGEX_KIND || qualifierKind1 == REGEX_KIND) { + return a1; + } else if (qualifierKind2 == PARTIALREGEX_KIND || qualifierKind2 == REGEX_KIND) { + return a2; } - if (AnnotationUtils.areSameByName(subAnno, PARTIALREGEX)) { - subAnno = PARTIALREGEX; + throw new TypeSystemError("Unexpected qualifiers: %s %s", a1, a2); + } + + @Override + protected AnnotationMirror greatestLowerBoundWithElements( + AnnotationMirror a1, + QualifierKind qualifierKind1, + AnnotationMirror a2, + QualifierKind qualifierKind2, + QualifierKind glbKind) { + if (qualifierKind1 == REGEX_KIND && qualifierKind2 == REGEX_KIND) { + int value1 = getRegexValue(a1); + int value2 = getRegexValue(a2); + if (value1 > value2) { + return a1; + } else { + return a2; + } + } else if (qualifierKind1 == PARTIALREGEX_KIND && qualifierKind2 == PARTIALREGEX_KIND) { + if (AnnotationUtils.areSame(a1, a2)) { + return a1; + } else { + return REGEXBOTTOM; + } + } else if (qualifierKind1 == PARTIALREGEX_KIND || qualifierKind1 == REGEX_KIND) { + return a1; + } else if (qualifierKind2 == PARTIALREGEX_KIND || qualifierKind2 == REGEX_KIND) { + return a2; } - return super.isSubtype(subAnno, superAnno); + throw new TypeSystemError("Unexpected qualifiers: %s %s", a1, a2); } - /** Gets the value out of a regex annotation. */ + /** + * Gets the value out of a regex annotation. + * + * @param anno a @Regex annotation + * @return the {@code value} element of the annotation + */ private int getRegexValue(AnnotationMirror anno) { - return (Integer) - AnnotationUtils.getElementValuesWithDefaults(anno) - .get(regexValueElement) - .getValue(); + return AnnotationUtils.getElementValue(anno, regexValueElement, Integer.class, 0); } } /** * Returns the group count value of the given annotation or 0 if there's a problem getting the * group count value. + * + * @param anno a @Regex annotation + * @return the {@code value} element of the annotation */ public int getGroupCount(AnnotationMirror anno) { - AnnotationValue groupCountValue = - AnnotationUtils.getElementValuesWithDefaults(anno).get(regexValueElement); - // If group count value is null then there's no Regex annotation - // on the parameter so set the group count to 0. This would happen - // if a non-regex string is passed to Pattern.compile but warnings - // are suppressed. - return (groupCountValue == null) ? 0 : (Integer) groupCountValue.getValue(); + if (anno == null) { + return 0; + } + return AnnotationUtils.getElementValue(anno, regexValueElement, Integer.class, 0); } /** Returns the number of groups in the given regex String. */ @@ -222,19 +301,10 @@ public static int getGroupCount(@Regex String regexp) { return Pattern.compile(regexp).matcher("").groupCount(); } - /** - * This method is a copy of RegexUtil.isRegex. We cannot directly use RegexUtil, because it uses - * type annotations which cannot be used in IDEs (yet). - */ - @SuppressWarnings("purity") // the checker cannot prove that the method is pure, but it is - @Pure - private static boolean isRegex(String s) { - try { - Pattern.compile(s); - } catch (PatternSyntaxException e) { - return false; - } - return true; + @Override + public AnnotationMirrorSet getWidenedAnnotations( + AnnotationMirrorSet annos, TypeKind typeKind, TypeKind widenedTypeKind) { + return UNKNOWNREGEX_SET; } @Override @@ -254,7 +324,7 @@ public RegexPropagationTreeAnnotator(AnnotatedTypeFactory atypeFactory) { } @Override - public Void visitBinary(BinaryTree node, AnnotatedTypeMirror type) { + public Void visitBinary(BinaryTree tree, AnnotatedTypeMirror type) { // Don't call super method which will try to create a LUB // Even when it is not yet valid: i.e. between a @PolyRegex and a @Regex return null; @@ -273,7 +343,7 @@ public RegexTreeAnnotator(AnnotatedTypeFactory atypeFactory) { */ @Override public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) { - if (!type.isAnnotatedInHierarchy(REGEX)) { + if (!type.hasAnnotationInHierarchy(REGEX)) { String regex = null; if (tree.getKind() == Tree.Kind.STRING_LITERAL) { regex = (String) tree.getValue(); @@ -281,7 +351,7 @@ public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) { regex = Character.toString((Character) tree.getValue()); } if (regex != null) { - if (isRegex(regex)) { + if (RegexUtil.isRegex(regex)) { int groupCount = getGroupCount(regex); type.addAnnotation(createRegexAnnotation(groupCount)); } else { @@ -298,7 +368,7 @@ public Void visitLiteral(LiteralTree tree, AnnotatedTypeMirror type) { */ @Override public Void visitBinary(BinaryTree tree, AnnotatedTypeMirror type) { - if (!type.isAnnotatedInHierarchy(REGEX) && TreeUtils.isStringConcatenation(tree)) { + if (!type.hasAnnotationInHierarchy(REGEX) && TreeUtils.isStringConcatenation(tree)) { AnnotatedTypeMirror lExpr = getAnnotatedType(tree.getLeftOperand()); AnnotatedTypeMirror rExpr = getAnnotatedType(tree.getRightOperand()); @@ -312,19 +382,18 @@ public Void visitBinary(BinaryTree tree, AnnotatedTypeMirror type) { boolean rExprPoly = rExpr.hasAnnotation(PolyRegex.class); if (lExprRE && rExprRE) { - // Remove current @Regex annotation... - type.removeAnnotationInHierarchy(REGEX); - // ...and add a new one with the correct group count value. - type.addAnnotation(createRegexAnnotation(lGroupCount + rGroupCount)); + // Remove current @Regex annotation and add a new one with the correct group + // count value. + type.replaceAnnotation(createRegexAnnotation(lGroupCount + rGroupCount)); } else if ((lExprPoly && rExprPoly) || (lExprPoly && rExprRE) || (lExprRE && rExprPoly)) { - type.addAnnotation(PolyRegex.class); + type.addAnnotation(POLYREGEX); } else if (lExprPart && rExprPart) { String lRegex = getPartialRegexValue(lExpr); String rRegex = getPartialRegexValue(rExpr); String concat = lRegex + rRegex; - if (isRegex(concat)) { + if (RegexUtil.isRegex(concat)) { int groupCount = getGroupCount(concat); type.addAnnotation(createRegexAnnotation(groupCount)); } else { @@ -345,13 +414,13 @@ public Void visitBinary(BinaryTree tree, AnnotatedTypeMirror type) { /** Case 2: Also handle compound String concatenation. */ @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMirror type) { - if (TreeUtils.isStringCompoundConcatenation(node)) { - AnnotatedTypeMirror rhs = getAnnotatedType(node.getExpression()); - AnnotatedTypeMirror lhs = getAnnotatedType(node.getVariable()); + public Void visitCompoundAssignment(CompoundAssignmentTree tree, AnnotatedTypeMirror type) { + if (TreeUtils.isStringCompoundConcatenation(tree)) { + AnnotatedTypeMirror rhs = getAnnotatedType(tree.getExpression()); + AnnotatedTypeMirror lhs = getAnnotatedType(tree.getVariable()); - final Integer lhsRegexCount = getMinimumRegexCount(lhs); - final Integer rhsRegexCount = getMinimumRegexCount(rhs); + Integer lhsRegexCount = getMinimumRegexCount(lhs); + Integer rhsRegexCount = getMinimumRegexCount(rhs); if (lhsRegexCount != null && rhsRegexCount != null) { int lCount = getGroupCount(lhs.getAnnotation(Regex.class)); @@ -360,7 +429,7 @@ public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMi type.addAnnotation(createRegexAnnotation(lCount + rCount)); } } - return null; // super.visitCompoundAssignment(node, type); + return null; // super.visitCompoundAssignment(tree, type); } /** @@ -370,11 +439,11 @@ public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMi */ @Override public Void visitMethodInvocation(MethodInvocationTree tree, AnnotatedTypeMirror type) { - // TODO: Also get this to work with 2 argument Pattern.compile. - if (TreeUtils.isMethodInvocation(tree, patternCompile, processingEnv)) { + if (TreeUtils.isMethodInvocation(tree, patternCompile, processingEnv) + || TreeUtils.isMethodInvocation(tree, patternCompile2, processingEnv)) { ExpressionTree arg0 = tree.getArguments().get(0); - final AnnotatedTypeMirror argType = getAnnotatedType(arg0); + AnnotatedTypeMirror argType = getAnnotatedType(arg0); Integer regexCount = getMinimumRegexCount(argType); AnnotationMirror bottomAnno = getAnnotatedType(arg0).getAnnotation(RegexBottom.class); @@ -398,13 +467,20 @@ private AnnotationMirror createPartialRegexAnnotation(String partial) { return builder.build(); } - /** Returns the value of a PartialRegex annotation. */ + /** + * Returns the {@code value} element of a {@code @PartialRegex} annotation, if there is one + * in {@code type}. + * + * @param type a type + * @return the {@code value} element of a {@code @PartialRegex} annotation, or "" if none + */ private String getPartialRegexValue(AnnotatedTypeMirror type) { - return (String) - AnnotationUtils.getElementValuesWithDefaults( - type.getAnnotation(PartialRegex.class)) - .get(partialRegexValue) - .getValue(); + AnnotationMirror partialRegexAnno = type.getAnnotation(PartialRegex.class); + if (partialRegexAnno == null) { + return ""; + } + return AnnotationUtils.getElementValue( + partialRegexAnno, partialRegexValueElement, String.class, ""); } /** @@ -417,8 +493,8 @@ private String getPartialRegexValue(AnnotatedTypeMirror type) { * @param type type that may carry a Regex annotation * @return the Integer value of the Regex annotation (0 if no value exists) */ - private Integer getMinimumRegexCount(final AnnotatedTypeMirror type) { - final AnnotationMirror primaryRegexAnno = type.getAnnotation(Regex.class); + private @Nullable Integer getMinimumRegexCount(AnnotatedTypeMirror type) { + AnnotationMirror primaryRegexAnno = type.getAnnotation(Regex.class); if (primaryRegexAnno == null) { switch (type.getKind()) { case TYPEVAR: @@ -430,8 +506,8 @@ private Integer getMinimumRegexCount(final AnnotatedTypeMirror type) { case INTERSECTION: Integer maxBound = null; - for (final AnnotatedTypeMirror bound : - ((AnnotatedIntersectionType) type).directSuperTypes()) { + for (AnnotatedTypeMirror bound : + ((AnnotatedIntersectionType) type).getBounds()) { Integer boundRegexNum = getMinimumRegexCount(bound); if (boundRegexNum != null) { if (maxBound == null || boundRegexNum > maxBound) { @@ -450,42 +526,42 @@ private Integer getMinimumRegexCount(final AnnotatedTypeMirror type) { return getGroupCount(primaryRegexAnno); } - // This won't work correctly until flow sensitivity is supported by the - // the Regex Checker. For example: + // This won't work correctly until flow sensitivity is supported by the + // the Regex Checker. For example: // - // char @Regex [] arr = {'r', 'e'}; - // arr[0] = '('; // type is still "char @Regex []", but this is no longer correct + // char @Regex [] arr = {'r', 'e'}; + // arr[0] = '('; // type is still "char @Regex []", but this is no longer correct // - // There are associated tests in tests/regex/Simple.java:testCharArrays - // that can be uncommented when this is uncommented. - // /** - // * Case 4: a char array that as a String is a valid regular expression. - // */ - // @Override - // public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { - // boolean isCharArray = ((ArrayType) type.getUnderlyingType()) - // .getComponentType().getKind() == TypeKind.CHAR; - // if (isCharArray && tree.getInitializers() != null) { - // List initializers = tree.getInitializers(); - // StringBuilder charArray = new StringBuilder(); - // boolean allLiterals = true; - // for (int i = 0; allLiterals && i < initializers.size(); i++) { - // ExpressionTree e = initializers.get(i); - // if (e.getKind() == Tree.Kind.CHAR_LITERAL) { - // charArray.append(((LiteralTree) e).getValue()); - // } else if (getAnnotatedType(e).hasAnnotation(Regex.class)) { - // // if there's an @Regex char in the array then substitute - // // it with a . - // charArray.append('.'); - // } else { - // allLiterals = false; - // } - // } - // if (allLiterals && RegexUtil.isRegex(charArray.toString())) { - // type.addAnnotation(Regex.class); - // } - // } - // return super.visitNewArray(tree, type); - // } + // There are associated tests in tests/regex/Simple.java:testCharArrays + // that can be uncommented when this is uncommented. + // /** + // * Case 4: a char array that as a String is a valid regular expression. + // */ + // @Override + // public Void visitNewArray(NewArrayTree tree, AnnotatedTypeMirror type) { + // boolean isCharArray = ((ArrayType) type.getUnderlyingType()) + // .getComponentType().getKind() == TypeKind.CHAR; + // if (isCharArray && tree.getInitializers() != null) { + // List initializers = tree.getInitializers(); + // StringBuilder charArray = new StringBuilder(); + // boolean allLiterals = true; + // for (int i = 0; allLiterals && i < initializers.size(); i++) { + // ExpressionTree e = initializers.get(i); + // if (e.getKind() == Tree.Kind.CHAR_LITERAL) { + // charArray.append(((LiteralTree) e).getValue()); + // } else if (getAnnotatedType(e).hasAnnotation(Regex.class)) { + // // if there's an @Regex char in the array then substitute + // // it with a . + // charArray.append('.'); + // } else { + // allLiterals = false; + // } + // } + // if (allLiterals && RegexUtil.isRegex(charArray.toString())) { + // type.addAnnotation(Regex.class); + // } + // } + // return super.visitNewArray(tree, type); + // } } } diff --git a/checker/src/main/java/org/checkerframework/checker/regex/RegexChecker.java b/checker/src/main/java/org/checkerframework/checker/regex/RegexChecker.java index 4b6e27d49bd5..822b687e5feb 100644 --- a/checker/src/main/java/org/checkerframework/checker/regex/RegexChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/regex/RegexChecker.java @@ -5,6 +5,10 @@ import org.checkerframework.framework.qual.RelevantJavaTypes; import org.checkerframework.framework.qual.StubFiles; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * A type-checker plug-in for the {@link Regex} qualifier that finds syntactically invalid regular * expressions. @@ -12,5 +16,13 @@ * @checker_framework.manual #regex-checker Regex Checker */ @StubFiles("apache-xerces.astub") -@RelevantJavaTypes(CharSequence.class) +@RelevantJavaTypes({ + CharSequence.class, + // javax.swing.text.Segment.class + char.class, + Character.class, + Pattern.class, + Matcher.class, + MatchResult.class +}) public class RegexChecker extends BaseTypeChecker {} diff --git a/checker/src/main/java/org/checkerframework/checker/regex/RegexTransfer.java b/checker/src/main/java/org/checkerframework/checker/regex/RegexTransfer.java index 66d8ade11fc2..4cf6e3cd40ec 100644 --- a/checker/src/main/java/org/checkerframework/checker/regex/RegexTransfer.java +++ b/checker/src/main/java/org/checkerframework/checker/regex/RegexTransfer.java @@ -1,10 +1,6 @@ package org.checkerframework.checker.regex; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; import org.checkerframework.dataflow.analysis.ConditionalTransferResult; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; import org.checkerframework.dataflow.analysis.RegularTransferResult; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; @@ -17,6 +13,7 @@ import org.checkerframework.dataflow.cfg.node.MethodAccessNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.dataflow.util.NodeUtils; import org.checkerframework.framework.flow.CFAbstractAnalysis; import org.checkerframework.framework.flow.CFStore; @@ -25,12 +22,15 @@ import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; + /** The transfer function for the Regex Checker. */ public class RegexTransfer extends CFTransfer { // isRegex and asRegex are tested as signatures (string name plus formal parameters), not // ExecutableElement, because they exist in two packages: - // org.checkerframework.checker.regex.RegexUtil.isRegex(String,int) + // org.checkerframework.checker.regex.util.RegexUtil.isRegex(String,int) // org.plumelib.util.RegexUtil.isRegex(String,int) // and org.plumelib.util might not be on the classpath. private static final String IS_REGEX_METHOD_NAME = "isRegex"; @@ -50,9 +50,8 @@ public RegexTransfer(CFAbstractAnalysis analysis) analysis.getTypeFactory().getProcessingEnv()); } - // TODO: These are special cases for isRegex(String, int) and asRegex(String, int). - // They should be replaced by adding an @EnsuresQualifierIf annotation that supports - // specifying attributes. + // TODO: These are special cases for isRegex(String, int) and asRegex(String, int). They should + // be replaced by adding an @EnsuresQualifierIf annotation that supports specifying attributes. @Override public TransferResult visitMethodInvocation( MethodInvocationNode n, TransferInput in) { @@ -86,9 +85,7 @@ private TransferResult handleRegexUtil( CFStore elseStore = thenStore.copy(); ConditionalTransferResult newResult = new ConditionalTransferResult<>(result.getResultValue(), thenStore, elseStore); - Receiver firstParam = - FlowExpressions.internalReprOf( - factory.getContext().getAnnotationProvider(), n.getArgument(0)); + JavaExpression firstParam = JavaExpression.fromNode(n.getArgument(0)); // add annotation with correct group count (if possible, // regex annotation without count otherwise) @@ -172,7 +169,7 @@ public TransferResult visitGreaterThanOrEqual( * @param possibleMatcher the Node that might be a call of Matcher.groupCount() * @param possibleConstant the Node that might be a constant * @param isAlsoEqual whether the comparison operation is strict or reflexive - * @param resultIn TransferResult + * @param resultIn the TransferResult * @return the possibly refined output TransferResult */ private TransferResult handleMatcherGroupCount( @@ -197,8 +194,7 @@ private TransferResult handleMatcherGroupCount( MethodAccessNode methodAccessNode = ((MethodInvocationNode) possibleMatcher).getTarget(); Node receiver = methodAccessNode.getReceiver(); - Receiver matcherReceiver = - FlowExpressions.internalReprOf(analysis.getTypeFactory(), receiver); + JavaExpression matcherReceiver = JavaExpression.fromNode(receiver); IntegerLiteralNode iln = (IntegerLiteralNode) possibleConstant; int groupCount; @@ -222,8 +218,11 @@ private TransferResult handleMatcherGroupCount( /** * Returns true if the given receiver is a class named "RegexUtil". Examples of such classes are - * org.checkerframework.checker.regex.RegexUtil and org.plumelib.util.RegexUtil, and the user - * might copy one into their own project. + * org.checkerframework.checker.regex.util.RegexUtil and org.plumelib.util.RegexUtil, and the + * user might copy one into their own project. + * + * @param receiver some string + * @return true if the given receiver is a class named "RegexUtil" */ private boolean isRegexUtil(String receiver) { return receiver.equals("RegexUtil") || receiver.endsWith(".RegexUtil"); diff --git a/checker/src/main/java/org/checkerframework/checker/regex/RegexUtil.java b/checker/src/main/java/org/checkerframework/checker/regex/RegexUtil.java deleted file mode 100644 index 2269bfa4b9b7..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/regex/RegexUtil.java +++ /dev/null @@ -1,336 +0,0 @@ -// This class should be kept in sync with org.plumelib.util.RegexUtil in the plume-util project. - -package org.checkerframework.checker.regex; - -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; -import org.checkerframework.checker.index.qual.GTENegativeOne; -import org.checkerframework.checker.lock.qual.GuardSatisfied; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.regex.qual.Regex; -import org.checkerframework.dataflow.qual.Pure; -import org.checkerframework.dataflow.qual.SideEffectFree; -import org.checkerframework.framework.qual.EnsuresQualifierIf; - -/** - * Utility methods for regular expressions, most notably for testing whether a string is a regular - * expression. - * - *

    For an example of intended use, see section 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 - * project. - */ -// The Purity Checker cannot show for most methods in this class that -// they are pure, even though they are. -@SuppressWarnings("all:purity") -public final class RegexUtil { - - /** This class is a collection of methods; it does not represent anything. */ - private RegexUtil() { - throw new Error("do not instantiate"); - } - - /** - * A checked version of {@link PatternSyntaxException}. - * - *

    This exception is useful when an illegal regex is detected but the contextual information - * to report a helpful error message is not available at the current depth in the call stack. By - * using a checked PatternSyntaxException the error must be handled up the call stack where a - * better error message can be reported. - * - *

    Typical usage is: - * - *

    -     * void myMethod(...) throws CheckedPatternSyntaxException {
    -     *   ...
    -     *   if (! isRegex(myString)) {
    -     *     throw new CheckedPatternSyntaxException(...);
    -     *   }
    -     *   ... Pattern.compile(myString) ...
    -     * 
    - * - * Simply calling {@code Pattern.compile} would have a similar effect, in that {@code - * PatternSyntaxException} would be thrown at run time if {@code myString} is not a regular - * expression. There are two problems with such an approach. First, a client of {@code myMethod} - * might forget to handle the exception, since {@code PatternSyntaxException} is not checked. - * Also, the Regex Checker would issue a warning about the call to {@code Pattern.compile} that - * might throw an exception. The above usage pattern avoids both problems. - * - * @see PatternSyntaxException - */ - public static class CheckedPatternSyntaxException extends Exception { - - private static final long serialVersionUID = 6266881831979001480L; - - /** The PatternSyntaxException that this is a wrapper around. */ - private final PatternSyntaxException pse; - - /** - * Constructs a new CheckedPatternSyntaxException equivalent to the given {@link - * PatternSyntaxException}. - * - *

    Consider calling this constructor with the result of {@link RegexUtil#regexError}. - * - * @param pse the PatternSyntaxException to be wrapped - */ - public CheckedPatternSyntaxException(PatternSyntaxException pse) { - this.pse = pse; - } - - /** - * Constructs a new CheckedPatternSyntaxException. - * - * @param desc a description of the error - * @param regex the erroneous pattern - * @param index the approximate index in the pattern of the error, or {@code -1} if the - * index is not known - */ - public CheckedPatternSyntaxException(String desc, String regex, @GTENegativeOne int index) { - this(new PatternSyntaxException(desc, regex, index)); - } - - /** - * Retrieves the description of the error. - * - * @return the description of the error - */ - public String getDescription() { - return pse.getDescription(); - } - - /** - * Retrieves the error index. - * - * @return the approximate index in the pattern of the error, or {@code -1} if the index is - * not known - */ - public int getIndex() { - return pse.getIndex(); - } - - /** - * Returns a multi-line string containing the description of the syntax error and its index, - * the erroneous regular-expression pattern, and a visual indication of the error index - * within the pattern. - * - * @return the full detail message - */ - @Override - @Pure - public String getMessage(@GuardSatisfied CheckedPatternSyntaxException this) { - return pse.getMessage(); - } - - /** - * Retrieves the erroneous regular-expression pattern. - * - * @return the erroneous pattern - */ - public String getPattern() { - return pse.getPattern(); - } - } - - /** - * Returns true if the argument is a syntactically valid regular expression. - * - * @param s string to check for being a regular expression - * @return true iff s is a regular expression - */ - @Pure - @EnsuresQualifierIf(result = true, expression = "#1", qualifier = Regex.class) - public static boolean isRegex(String s) { - return isRegex(s, 0); - } - - /** - * Returns true if the argument is a syntactically valid regular expression with at least the - * given number of groups. - * - * @param s string to check for being a regular expression - * @param groups number of groups expected - * @return true iff s is a regular expression with {@code groups} groups - */ - @SuppressWarnings({"regex", "all:deterministic"}) // RegexUtil; for purity, catches an exception - @Pure - // @EnsuresQualifierIf annotation is extraneous because this method is special-cased - // in RegexTransfer. - @EnsuresQualifierIf(result = true, expression = "#1", qualifier = Regex.class) - public static boolean isRegex(String s, int groups) { - Pattern p; - try { - p = Pattern.compile(s); - } catch (PatternSyntaxException e) { - return false; - } - return getGroupCount(p) >= groups; - } - - /** - * Returns true if the argument is a syntactically valid regular expression. - * - * @param c char to check for being a regular expression - * @return true iff c is a regular expression - */ - @SuppressWarnings({ - "regex", - "all:purity.not.deterministic.call", - "lock" - }) // RegexUtil; temp value used in pure method is equal up to equals but not up to == - @Pure - @EnsuresQualifierIf(result = true, expression = "#1", qualifier = Regex.class) - public static boolean isRegex(final char c) { - return isRegex(Character.toString(c)); - } - - /** - * Returns null if the argument is a syntactically valid regular expression. Otherwise returns a - * string describing why the argument is not a regex. - * - * @param s string to check for being a regular expression - * @return null, or a string describing why the argument is not a regex - */ - @SideEffectFree - public static @Nullable String regexError(String s) { - return regexError(s, 0); - } - - /** - * Returns null if the argument is a syntactically valid regular expression with at least the - * given number of groups. Otherwise returns a string describing why the argument is not a - * regex. - * - * @param s string to check for being a regular expression - * @param groups number of groups expected - * @return null, or a string describing why the argument is not a regex - */ - @SuppressWarnings({"regex", "not.sef"}) // RegexUtil; - @SideEffectFree - public static @Nullable String regexError(String s, int groups) { - try { - Pattern p = Pattern.compile(s); - int actualGroups = getGroupCount(p); - if (actualGroups < groups) { - return regexErrorMessage(s, groups, actualGroups); - } - } catch (PatternSyntaxException e) { - return e.getMessage(); - } - return null; - } - - /** - * Returns null if the argument is a syntactically valid regular expression. Otherwise returns a - * PatternSyntaxException describing why the argument is not a regex. - * - * @param s string to check for being a regular expression - * @return null, or a PatternSyntaxException describing why the argument is not a regex - */ - @SideEffectFree - public static @Nullable PatternSyntaxException regexException(String s) { - return regexException(s, 0); - } - - /** - * Returns null if the argument is a syntactically valid regular expression with at least the - * given number of groups. Otherwise returns a PatternSyntaxException describing why the - * argument is not a regex. - * - * @param s string to check for being a regular expression - * @param groups number of groups expected - * @return null, or a PatternSyntaxException describing why the argument is not a regex - */ - @SuppressWarnings("regex") // RegexUtil - @SideEffectFree - public static @Nullable PatternSyntaxException regexException(String s, int groups) { - try { - Pattern p = Pattern.compile(s); - int actualGroups = getGroupCount(p); - if (actualGroups < groups) { - return new PatternSyntaxException( - regexErrorMessage(s, groups, actualGroups), s, -1); - } - } catch (PatternSyntaxException pse) { - return pse; - } - return null; - } - - /** - * Returns the argument as a {@code @Regex String} if it is a regex, otherwise throws an error. - * The purpose of this method is to suppress Regex Checker warnings. It should be very rarely - * needed. - * - * @param s string to check for being a regular expression - * @return its argument - * @throws Error if argument is not a regex - */ - @SideEffectFree - // The return type annotation is a conservative bound. - public static @Regex String asRegex(String s) { - return asRegex(s, 0); - } - - /** - * Returns the argument as a {@code @Regex(groups) String} if it is a regex with at least the - * given number of groups, otherwise throws an error. The purpose of this method is to suppress - * Regex Checker warnings. It should be very rarely needed. - * - * @param s string to check for being a regular expression - * @param groups number of groups expected - * @return its argument - * @throws Error if argument is not a regex - */ - @SuppressWarnings("regex") // RegexUtil - @SideEffectFree - // The return type annotation is irrelevant; it is special-cased by - // RegexAnnotatedTypeFactory. - public static @Regex String asRegex(String s, int groups) { - try { - Pattern p = Pattern.compile(s); - int actualGroups = getGroupCount(p); - if (actualGroups < groups) { - throw new Error(regexErrorMessage(s, groups, actualGroups)); - } - return s; - } catch (PatternSyntaxException e) { - throw new Error(e); - } - } - - /** - * Generates an error message for s when expectedGroups are needed, but s only has actualGroups. - * - * @param s string to check for being a regular expression - * @param expectedGroups the number of needed capturing groups - * @param actualGroups the number of groups that {@code s} has - * @return an error message for s when expectedGroups groups are needed, but s only has - * actualGroups groups - */ - @SideEffectFree - private static String regexErrorMessage(String s, int expectedGroups, int actualGroups) { - return "regex \"" - + s - + "\" has " - + actualGroups - + " groups, but " - + expectedGroups - + " groups are needed."; - } - - /** - * Return the count of groups in the argument. - * - * @param p pattern whose groups to count - * @return the count of groups in the argument - */ - @SuppressWarnings({"all:purity", "lock"}) // does not depend on object identity - @Pure - private static int getGroupCount(Pattern p) { - return p.matcher("").groupCount(); - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/regex/RegexVisitor.java b/checker/src/main/java/org/checkerframework/checker/regex/RegexVisitor.java index 1e5ce341b9f1..cbe60cddeca3 100644 --- a/checker/src/main/java/org/checkerframework/checker/regex/RegexVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/regex/RegexVisitor.java @@ -5,18 +5,17 @@ import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.VariableElement; + import org.checkerframework.checker.regex.qual.Regex; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType; -import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedPrimitiveType; import org.checkerframework.javacutil.TreeUtils; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.VariableElement; + /** * A type-checking visitor for the Regex type system. * @@ -25,8 +24,7 @@ *

      *
    1. Allows any String to be passed to Pattern.compile if the Pattern.LITERAL flag is * passed. - *
    2. Checks compound String concatenation to ensure correct usage of Regex Strings. - *
    3. Checks calls to {@code MatchResult.start}, {@code MatchResult.end} and {@code + *
    4. Checks calls to {@code MatchResult.start}, {@code MatchResult.end} and {@code * MatchResult.group} to ensure that a valid group number is passed. *
    * @@ -34,68 +32,80 @@ */ public class RegexVisitor extends BaseTypeVisitor { - private final ExecutableElement matchResultEnd; - private final ExecutableElement matchResultGroup; - private final ExecutableElement matchResultStart; + /** The method java.util.regex.MatchResult.end(int). */ + private final ExecutableElement matchResultEndInt; + + /** The method java.util.regex.MatchResult.group(int). */ + private final ExecutableElement matchResultGroupInt; + + /** The method java.util.regex.MatchResult.start(int). */ + private final ExecutableElement matchResultStartInt; + + /** The method java.util.regex.Pattern.compile. */ private final ExecutableElement patternCompile; + + /** The field java.util.regex.Pattern.LITERAL. */ private final VariableElement patternLiteral; + /** + * Create a RegexVisitor. + * + * @param checker the associated RegexChecker + */ public RegexVisitor(BaseTypeChecker checker) { super(checker); ProcessingEnvironment env = checker.getProcessingEnvironment(); - this.matchResultEnd = - TreeUtils.getMethod(java.util.regex.MatchResult.class.getName(), "end", 1, env); - this.matchResultGroup = - TreeUtils.getMethod(java.util.regex.MatchResult.class.getName(), "group", 1, env); - this.matchResultStart = - TreeUtils.getMethod(java.util.regex.MatchResult.class.getName(), "start", 1, env); + this.matchResultEndInt = + TreeUtils.getMethod("java.util.regex.MatchResult", "end", env, "int"); + this.matchResultGroupInt = + TreeUtils.getMethod("java.util.regex.MatchResult", "group", env, "int"); + this.matchResultStartInt = + TreeUtils.getMethod("java.util.regex.MatchResult", "start", env, "int"); this.patternCompile = - TreeUtils.getMethod(java.util.regex.Pattern.class.getName(), "compile", 2, env); - this.patternLiteral = - TreeUtils.getField(java.util.regex.Pattern.class.getName(), "LITERAL", env); + TreeUtils.getMethod( + "java.util.regex.Pattern", "compile", env, "java.lang.String", "int"); + this.patternLiteral = TreeUtils.getField("java.util.regex.Pattern", "LITERAL", env); } - /** - * Case 1: Don't require a Regex annotation on the String argument to Pattern.compile if the - * Pattern.LITERAL flag is passed. - */ @Override - public Void visitMethodInvocation(MethodInvocationTree node, Void p) { + public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { ProcessingEnvironment env = checker.getProcessingEnvironment(); - if (TreeUtils.isMethodInvocation(node, patternCompile, env)) { - ExpressionTree flagParam = node.getArguments().get(1); - if (flagParam.getKind() == Kind.MEMBER_SELECT) { + if (TreeUtils.isMethodInvocation(tree, patternCompile, env)) { + /* + * Case 1: Don't require a Regex annotation on the String argument to Pattern.compile if the + * Pattern.LITERAL flag is passed. + */ + ExpressionTree flagParam = tree.getArguments().get(1); + if (flagParam.getKind() == Tree.Kind.MEMBER_SELECT) { MemberSelectTree memSelect = (MemberSelectTree) flagParam; if (TreeUtils.isSpecificFieldAccess(memSelect, patternLiteral)) { - // This is a call to Pattern.compile with the Pattern.LITERAL - // flag so the first parameter doesn't need to be a - // @Regex String. Don't call the super method to skip checking - // if the first parameter is a @Regex String, but make sure to + // This is a call to Pattern.compile with the Pattern.LITERAL flag so the first + // parameter doesn't need to be a @Regex String. Don't call the super method to + // skip checking if the first parameter is a @Regex String, but make sure to // still recurse on all of the different parts of the method call. - Void r = scan(node.getTypeArguments(), p); - r = reduce(scan(node.getMethodSelect(), p), r); - r = reduce(scan(node.getArguments(), p), r); + Void r = scan(tree.getTypeArguments(), p); + r = reduce(scan(tree.getMethodSelect(), p), r); + r = reduce(scan(tree.getArguments(), p), r); return r; } } - } else if (TreeUtils.isMethodInvocation(node, matchResultEnd, env) - || TreeUtils.isMethodInvocation(node, matchResultGroup, env) - || TreeUtils.isMethodInvocation(node, matchResultStart, env)) { - /** - * Case 3: Checks calls to {@code MatchResult.start}, {@code MatchResult.end} and {@code - * MatchResult.group} to ensure that a valid group number is passed. - */ - ExpressionTree group = node.getArguments().get(0); - if (group.getKind() == Kind.INT_LITERAL) { + } else if (TreeUtils.isMethodInvocation(tree, matchResultEndInt, env) + || TreeUtils.isMethodInvocation(tree, matchResultGroupInt, env) + || TreeUtils.isMethodInvocation(tree, matchResultStartInt, env)) { + // Case 2: Checks calls to {@code MatchResult.start}, {@code MatchResult.end} and {@code + // MatchResult.group} to ensure that a valid group number is passed. + ExpressionTree group = tree.getArguments().get(0); + if (group.getKind() == Tree.Kind.INT_LITERAL) { LiteralTree literal = (LiteralTree) group; int paramGroups = (Integer) literal.getValue(); - ExpressionTree receiver = TreeUtils.getReceiverTree(node); + ExpressionTree receiver = TreeUtils.getReceiverTree(tree); if (receiver == null) { // When checking implementations of java.util.regex.MatcherResult, calls to // group (and other methods) don't have a receiver tree. So, just do the - // regular checking. Verifying an implemenation of a subclass of MatcherResult - // is out of the scope of this checker. - return super.visitMethodInvocation(node, p); + // regular checking. + // Verifying an implementation of a subclass of MatcherResult is out of the + // scope of this checker. + return super.visitMethodInvocation(tree, p); } int annoGroups = 0; AnnotatedTypeMirror receiverType = atypeFactory.getAnnotatedType(receiver); @@ -112,46 +122,6 @@ public Void visitMethodInvocation(MethodInvocationTree node, Void p) { checker.reportWarning(group, "group.count.unknown"); } } - return super.visitMethodInvocation(node, p); - } - - /** Case 2: Check String compound concatenation for valid Regex use. */ - // TODO: Remove this. This should be handled by flow. - /* - @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { - // Default behavior from superclass - } - */ - - @Override - public boolean isValidUse( - AnnotatedDeclaredType declarationType, AnnotatedDeclaredType useType, Tree tree) { - // TODO: only allow Regex and PolyRegex annotations on types in legalReferenceTypes. - // This is pending an implementation of AnnotatedTypeMirror.getExplicitAnnotations - // that supports local variables, array types and parameterized types. - /*// Only allow annotations on subtypes of the types in legalReferenceTypes. - if (!useType.getExplicitAnnotations().isEmpty()) { - Types typeUtils = env.getTypeUtils(); - for (TypeMirror type : legalReferenceTypes) { - if (typeUtils.isSubtype(declarationType.getUnderlyingType(), type)) { - return true; - } - } - return false; - }*/ - return super.isValidUse(declarationType, useType, tree); - } - - @Override - public boolean isValidUse(AnnotatedPrimitiveType type, Tree tree) { - // TODO: only allow Regex and PolyRegex annotations on chars. - // This is pending an implementation of AnnotatedTypeMirror.getExplicitAnnotations - // that supports local variables and array types. - /*// Only allow annotations on char. - if (!type.getExplicitAnnotations().isEmpty()) { - return type.getKind() == TypeKind.CHAR; - }*/ - return super.isValidUse(type, tree); + return super.visitMethodInvocation(tree, p); } } diff --git a/checker/src/main/java/org/checkerframework/checker/regex/qual/PolyRegex.java b/checker/src/main/java/org/checkerframework/checker/regex/qual/PolyRegex.java deleted file mode 100644 index 80e5bffdc569..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/regex/qual/PolyRegex.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.checkerframework.checker.regex.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.PolymorphicQualifier; - -/** - * A polymorphic qualifier for the Regex type system. - * - *

    Any method written using {@code @PolyRegex} conceptually has two versions: one in which every - * instance of {@code @PolyRegex String} has been replaced by {@code @Regex String}, and one in - * which every instance of {@code @PolyRegex String} has been replaced by {@code String}. - * - * @checker_framework.manual #regex-checker Regex Checker - * @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@PolymorphicQualifier(UnknownRegex.class) -public @interface PolyRegex {} diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/IOUtils.astub b/checker/src/main/java/org/checkerframework/checker/resourceleak/IOUtils.astub new file mode 100644 index 000000000000..a65a98686dd8 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/IOUtils.astub @@ -0,0 +1,41 @@ +package org.apache.commons.io; + +import org.checkerframework.checker.calledmethods.qual.*; + +class IOUtils { + @EnsuresCalledMethods(value = "#1", methods = "close") + @EnsuresCalledMethodsOnException(value = "#1", methods = "close") + static void closeQuietly(Reader arg0); + + @EnsuresCalledMethods(value = "#1", methods = "close") + @EnsuresCalledMethodsOnException(value = "#1", methods = "close") + static void closeQuietly(Writer arg0); + + @EnsuresCalledMethods(value = "#1", methods = "close") + @EnsuresCalledMethodsOnException(value = "#1", methods = "close") + static void closeQuietly(InputStream arg0); + + @EnsuresCalledMethods(value = "#1", methods = "close") + @EnsuresCalledMethodsOnException(value = "#1", methods = "close") + static void closeQuietly(OutputStream arg0); + + @EnsuresCalledMethods(value = "#1", methods = "close") + @EnsuresCalledMethodsOnException(value = "#1", methods = "close") + static void closeQuietly(Closeable arg0); + + @SuppressWarnings("ensuresvarargs.unverified") + @EnsuresCalledMethodsVarArgs("close") + static void closeQuietly(Closeable... arg0); + + @EnsuresCalledMethods(value = "#1", methods = "close") + @EnsuresCalledMethodsOnException(value = "#1", methods = "close") + static void closeQuietly(Socket arg0); + + @EnsuresCalledMethods(value = "#1", methods = "close") + @EnsuresCalledMethodsOnException(value = "#1", methods = "close") + static void closeQuietly(Selector arg0); + + @EnsuresCalledMethods(value = "#1", methods = "close") + @EnsuresCalledMethodsOnException(value = "#1", methods = "close") + static void closeQuietly(ServerSocket arg0); +} diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java new file mode 100644 index 000000000000..02830312a733 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java @@ -0,0 +1,2651 @@ +package org.checkerframework.checker.resourceleak; + +import com.google.common.base.Preconditions; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePath; + +import org.checkerframework.checker.calledmethods.qual.CalledMethods; +import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression; +import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; +import org.checkerframework.checker.mustcall.MustCallChecker; +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.MustCallAlias; +import org.checkerframework.checker.mustcall.qual.NotOwning; +import org.checkerframework.checker.mustcall.qual.Owning; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.accumulation.AccumulationStore; +import org.checkerframework.common.accumulation.AccumulationValue; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.UnderlyingAST; +import org.checkerframework.dataflow.cfg.UnderlyingAST.Kind; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.Block.BlockType; +import org.checkerframework.dataflow.cfg.block.ExceptionBlock; +import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; +import org.checkerframework.dataflow.cfg.node.AssignmentNode; +import org.checkerframework.dataflow.cfg.node.ClassNameNode; +import org.checkerframework.dataflow.cfg.node.FieldAccessNode; +import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.node.NullLiteralNode; +import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; +import org.checkerframework.dataflow.cfg.node.ReturnNode; +import org.checkerframework.dataflow.cfg.node.SuperNode; +import org.checkerframework.dataflow.cfg.node.ThisNode; +import org.checkerframework.dataflow.cfg.node.TypeCastNode; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.LocalVariable; +import org.checkerframework.dataflow.expression.ThisReference; +import org.checkerframework.dataflow.util.NodeUtils; +import org.checkerframework.framework.flow.CFStore; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.framework.util.JavaExpressionParseUtil.JavaExpressionParseException; +import org.checkerframework.framework.util.StringToJavaExpression; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreePathUtil; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; +import org.checkerframework.javacutil.TypesUtils; +import org.plumelib.util.IPair; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.StringJoiner; +import java.util.stream.Collectors; + +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** + * An analyzer that checks consistency of {@link MustCall} and {@link CalledMethods} types, thereby + * detecting resource leaks. For any expression e the analyzer ensures that when e + * goes out of scope, there exists a resource alias r of e (which might be + * e itself) such that the must-call methods of r (i.e. the values of r's + * MustCall type) are contained in the value of r's CalledMethods type. For any e + * for which this property does not hold, the analyzer reports a {@code + * "required.method.not.called"} error, indicating a possible resource leak. + * + *

    Mechanically, the analysis does two tasks. + * + *

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

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

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

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

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

    Throughout, this class uses the temporary-variable facilities provided by the Must Call and + * Resource Leak type factories both to emulate a three-address-form IR (simplifying some analysis + * logic) and to permit expressions to have their types refined in their respective checkers' + * stores. These temporary variables can be members of resource-alias sets. Without temporary + * variables, the checker wouldn't be able to verify code such as {@code new Socket(host, + * port).close()}, which would cause false positives. Temporaries are created for {@code new} + * expressions, method calls (for the return value), and ternary expressions. Other types of + * expressions may be supported in the future. + */ +/*package-private*/ +class MustCallConsistencyAnalyzer { + + /** True if errors related to static owning fields should be suppressed. */ + private final boolean permitStaticOwning; + + /** True if errors related to field initialization should be suppressed. */ + private final boolean permitInitializationLeak; + + /** + * Aliases about which the checker has already reported about a resource leak, to avoid + * duplicate reports. + */ + private final Set reportedErrorAliases = new HashSet<>(); + + /** + * The type factory for the Resource Leak Checker, which is used to get called methods types and + * to access the Must Call Checker. + */ + private final ResourceLeakAnnotatedTypeFactory typeFactory; + + /** + * A cache for the result of calling {@code ResourceLeakAnnotatedTypeFactory.getStoreAfter()} on + * a node. The cache prevents repeatedly computing least upper bounds on stores + */ + private final IdentityHashMap cmStoreAfter = new IdentityHashMap<>(); + + /** + * A cache for the result of calling {@code MustCallAnnotatedTypeFactory.getStoreAfter()} on a + * node. The cache prevents repeatedly computing least upper bounds on stores + */ + private final IdentityHashMap mcStoreAfter = new IdentityHashMap<>(); + + /** The Resource Leak Checker, used to issue errors. */ + private final ResourceLeakChecker checker; + + /** + * The analysis from the Resource Leak Checker, used to get input stores based on CFG blocks. + */ + private final ResourceLeakAnalysis analysis; + + /** True if -AnoLightweightOwnership was passed on the command line. */ + private final boolean noLightweightOwnership; + + /** True if -AcountMustCall was passed on the command line. */ + private final boolean countMustCall; + + /** A description for how a method might exit. */ + /*package-private*/ enum MethodExitKind { + + /** The method exits normally by returning. */ + NORMAL_RETURN, + + /** The method exits by throwing an exception. */ + EXCEPTIONAL_EXIT; + + /** An immutable set containing all possible ways for a method to exit. */ + public static final Set ALL = + ImmutableSet.copyOf(EnumSet.allOf(MethodExitKind.class)); + } + + /** + * An Obligation is a dataflow fact: a set of resource aliases and when those resources need to + * be cleaned up. Abstractly, each Obligation represents a resource for which the analyzed + * program might have a must-call obligation. Each Obligation is a pair of a set of resource + * aliases and their must-call obligation. Must-call obligations are tracked by the {@link + * MustCallChecker} and are accessed by looking up the type(s) in its type system of the + * resource aliases contained in each {@code Obligation} using {@link + * #getMustCallMethods(ResourceLeakAnnotatedTypeFactory, CFStore)}. + * + *

    An Obligation might not matter on all paths out of a method. For instance, after a + * constructor assigns a resource to an {@link Owning} field, the resource only needs to be + * closed if the constructor throws an exception. If the constructor exits normally then the + * obligation is satisfied because the field is now responsible for its must-call obligations. + * See {@link #whenToEnforce}, which defines when the Obligation needs to be enforced. + * + *

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

    {@code Obligation} is deeply immutable. If some code were to accidentally mutate a + * {@code resourceAliases} set it could be really nasty to debug, so this set is always + * immutable. + */ + public final ImmutableSet resourceAliases; + + /** + * The ways a method can exit along which this Obligation has to be enforced. For example, + * this will usually be {@link MethodExitKind#ALL}, indicating that this Obligation has to + * be enforced no matter how the method exits. It may also be a smaller set indicating that + * the Obligation only has to be enforced on certain exit conditions. + * + *

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

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

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

      + *
    • it is passed to another method or constructor in an @MustCallAlias position, and + * then the containing method returns that method’s result, or the call is a super() + * constructor call annotated with {@link MustCallAlias}, or + *
    • it is stored in an owning field of the class under analysis + *
    + */ + public final boolean derivedFromMustCallAliasParam; + + /** + * Create a new resource alias. This constructor should only be used if the resource alias + * was not derived from a method parameter annotated as {@link MustCallAlias}. + * + * @param reference the local variable + * @param tree the tree + */ + public ResourceAlias(LocalVariable reference, Tree tree) { + this(reference, reference.getElement(), tree); + } + + /** + * Create a new resource alias. This constructor should only be used if the resource alias + * was not derived from a method parameter annotated as {@link MustCallAlias}. + * + * @param reference the reference + * @param element the element for the given reference + * @param tree the tree + */ + public ResourceAlias(JavaExpression reference, Element element, Tree tree) { + this(reference, element, tree, false); + } + + /** + * Create a new resource alias. + * + * @param reference the local variable + * @param element the element for the reference + * @param tree the tree + * @param derivedFromMustCallAliasParam true iff this resource alias was created because of + * an {@link MustCallAlias} parameter + */ + public ResourceAlias( + JavaExpression reference, + Element element, + Tree tree, + boolean derivedFromMustCallAliasParam) { + this.reference = reference; + this.element = element; + this.tree = tree; + this.derivedFromMustCallAliasParam = derivedFromMustCallAliasParam; + } + + @Override + public String toString() { + return "(ResourceAlias: reference: " + reference + " |||| tree: " + tree + ")"; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ResourceAlias that = (ResourceAlias) o; + return reference.equals(that.reference) && tree.equals(that.tree); + } + + @Override + public int hashCode() { + return Objects.hash(reference, tree); + } + + /** + * Returns an appropriate String for representing this in an error message. In particular, + * if {@link #reference} is a temporary variable, we return the String representation of + * {@link #tree}, to avoid exposing the temporary name (which has no meaning for the user) + * in the error message + * + * @return an appropriate String for representing this in an error message + */ + public String stringForErrorMessage() { + String referenceStr = reference.toString(); + // We assume that any temporary variable name will not be a syntactically-valid + // identifier or keyword. + return !SourceVersion.isIdentifier(referenceStr) ? tree.toString() : referenceStr; + } + } + + /** + * Creates a consistency analyzer. Typically, the type factory's postAnalyze method would + * instantiate a new consistency analyzer using this constructor and then call {@link + * #analyze(ControlFlowGraph)}. + * + * @param typeFactory the type factory + * @param analysis the analysis from the type factory. Usually this would have protected access, + * so this constructor cannot get it directly. + */ + /*package-private*/ MustCallConsistencyAnalyzer( + ResourceLeakAnnotatedTypeFactory typeFactory, ResourceLeakAnalysis analysis) { + this.typeFactory = typeFactory; + this.checker = (ResourceLeakChecker) typeFactory.getChecker(); + this.analysis = analysis; + this.permitStaticOwning = checker.hasOption("permitStaticOwning"); + this.permitInitializationLeak = checker.hasOption("permitInitializationLeak"); + this.noLightweightOwnership = checker.hasOption(MustCallChecker.NO_LIGHTWEIGHT_OWNERSHIP); + this.countMustCall = checker.hasOption(ResourceLeakChecker.COUNT_MUST_CALL); + } + + /** + * The main function of the consistency dataflow analysis. The analysis tracks dataflow facts + * ("Obligations") of type {@link Obligation}, each representing a set of owning resource + * aliases for some value with a non-empty {@code @MustCall} obligation. The set of tracked + * Obligations is guaranteed to include at least one Obligation for each actual resource in the + * program, but might include other, spurious Obligations, too (that is, it is a conservative + * over-approximation of the true Obligation set). + * + *

    The analysis improves its precision by removing Obligations from tracking when it can + * prove that they do not represent real resources. For example, it is not necessary to track + * expressions with empty {@code @MustCall} obligations, because they are trivially fulfilled. + * Nor is tracking non-owning aliases necessary, because by definition they cannot be used to + * fulfill must-call obligations. + * + * @param cfg the control flow graph of the method to check + */ + // TODO: This analysis is currently implemented directly using a worklist; in the future, it + // should be rewritten to use the dataflow framework of the Checker Framework. + /*package-private*/ void analyze(ControlFlowGraph cfg) { + // The `visited` set contains everything that has been added to the worklist, even if it has + // not yet been removed and analyzed. + Set visited = new HashSet<>(); + Deque worklist = new ArrayDeque<>(); + + // Add any owning parameters to the initial set of variables to track. + BlockWithObligations entry = + new BlockWithObligations(cfg.getEntryBlock(), computeOwningParameters(cfg)); + worklist.add(entry); + visited.add(entry); + + while (!worklist.isEmpty()) { + BlockWithObligations current = worklist.remove(); + propagateObligationsToSuccessorBlocks( + cfg, current.obligations, current.block, visited, worklist); + } + } + + /** + * Update a set of Obligations to account for a method or constructor invocation. + * + * @param obligations the Obligations to update + * @param node the method or constructor invocation + * @param exceptionType a description of the outgoing CFG edge from the node: null + * to indicate normal return, or a {@link TypeMirror} to indicate a subclass of the given + * throwable class was thrown + */ + private void updateObligationsForInvocation( + Set obligations, Node node, @Nullable TypeMirror exceptionType) { + removeObligationsAtOwnershipTransferToParameters(obligations, node, exceptionType); + if (node instanceof MethodInvocationNode + && typeFactory.canCreateObligations() + && typeFactory.hasCreatesMustCallFor((MethodInvocationNode) node)) { + checkCreatesMustCallForInvocation(obligations, (MethodInvocationNode) node); + // Count calls to @CreatesMustCallFor methods as creating new resources. Doing so could + // result in slightly over-counting, because @CreatesMustCallFor doesn't guarantee that + // a new resource is created: it just means that a new resource might have been created. + incrementNumMustCall(node); + } + + if (!shouldTrackInvocationResult(obligations, node)) { + return; + } + + if (typeFactory.declaredTypeHasMustCall(node.getTree())) { + // The incrementNumMustCall call above increments the count for the target of the + // @CreatesMustCallFor annotation. By contrast, this call increments the count for the + // return value of the method (which can't be the target of the annotation, because our + // syntax doesn't support that). + incrementNumMustCall(node); + } + updateObligationsWithInvocationResult(obligations, node); + } + + /** + * Checks that an invocation of a CreatesMustCallFor method is valid. + * + *

    Such an invocation is valid if any of the conditions in {@link + * #isValidCreatesMustCallForExpression(Set, JavaExpression, TreePath)} is true for each + * expression in the argument to the CreatesMustCallFor annotation. As a special case, the + * invocation of a CreatesMustCallFor method with "this" as its expression is permitted in the + * constructor of the relevant class (invoking a constructor already creates an obligation). If + * none of these conditions are true for any of the expressions, this method issues a + * reset.not.owning error. + * + *

    For soundness, this method also guarantees that if any of the expressions in the + * CreatesMustCallFor annotation has a tracked Obligation, any tracked resource aliases of it + * will be removed (lest the analysis conclude that it is already closed because one of these + * aliases was closed before the method was invoked). Aliases created after the + * CreatesMustCallFor method is invoked are still permitted. + * + * @param obligations the currently-tracked Obligations; this value is side-effected if there is + * an Obligation in it which tracks any expression from the CreatesMustCallFor annotation as + * one of its resource aliases + * @param node a method invocation node, invoking a method with a CreatesMustCallFor annotation + */ + private void checkCreatesMustCallForInvocation( + Set obligations, MethodInvocationNode node) { + + TreePath currentPath = typeFactory.getPath(node.getTree()); + List cmcfExpressions = + CreatesMustCallForToJavaExpression.getCreatesMustCallForExpressionsAtInvocation( + node, typeFactory, typeFactory); + List missing = new ArrayList<>(0); + for (JavaExpression expression : cmcfExpressions) { + if (!isValidCreatesMustCallForExpression(obligations, expression, currentPath)) { + missing.add(expression); + } + } + + if (missing.isEmpty()) { + // All expressions matched one of the rules, so the invocation is valid. + return; + } + + // Special case for invocations of CreatesMustCallFor("this") methods in the constructor. + if (missing.size() == 1) { + JavaExpression expression = missing.get(0); + if (expression instanceof ThisReference && TreePathUtil.inConstructor(currentPath)) { + return; + } + } + + StringJoiner missingStrs = new StringJoiner(","); + for (JavaExpression m : missing) { + String s = m.toString(); + missingStrs.add(s.equals("this") ? s + " of type " + m.getType() : s); + } + checker.reportError( + node.getTree(), + "reset.not.owning", + node.getTarget().getMethod().getSimpleName().toString(), + missingStrs.toString()); + } + + /** + * Checks the validity of the given expression from an invoked method's {@link + * org.checkerframework.checker.mustcall.qual.CreatesMustCallFor} annotation. Helper method for + * {@link #checkCreatesMustCallForInvocation(Set, MethodInvocationNode)}. + * + *

    An expression is valid if one of the following conditions is true: + * + *

      + *
    • 1) the expression is an owning pointer, + *
    • 2) the expression already has a tracked Obligation (i.e. there is already a resource + * alias in some Obligation's resource alias set that refers to the expression), or + *
    • 3) the method in which the invocation occurs also has an @CreatesMustCallFor + * annotation, with the same expression. + *
    + * + * @param obligations the currently-tracked Obligations; this value is side-effected if there is + * an Obligation in it which tracks {@code expression} as one of its resource aliases + * @param expression an element of a method's @CreatesMustCallFor annotation + * @param invocationPath the path to the invocation of the method from whose @CreateMustCallFor + * annotation {@code expression} came + * @return true iff the expression is valid, as defined above + */ + private boolean isValidCreatesMustCallForExpression( + Set obligations, JavaExpression expression, TreePath invocationPath) { + if (expression instanceof FieldAccess) { + Element elt = ((FieldAccess) expression).getField(); + if (!noLightweightOwnership && typeFactory.hasOwning(elt)) { + // The expression is an Owning field. This satisfies case 1. + return true; + } + } else if (expression instanceof LocalVariable) { + Element elt = ((LocalVariable) expression).getElement(); + if (!noLightweightOwnership && typeFactory.hasOwning(elt)) { + // The expression is an Owning formal parameter. Note that this cannot actually + // be a local variable (despite expressions's type being LocalVariable) because + // the @Owning annotation can only be written on methods, parameters, and fields; + // formal parameters are also represented by LocalVariable in the bodies of methods. + // This satisfies case 1. + return true; + } else { + Obligation toRemove = null; + Obligation toAdd = null; + for (Obligation obligation : obligations) { + ResourceAlias alias = obligation.getResourceAlias(expression); + if (alias != null) { + // This satisfies case 2 above. Remove all its aliases, then return below. + if (toRemove != null) { + throw new TypeSystemError( + "tried to remove multiple sets containing a reset expression at" + + " once"); + } + toRemove = obligation; + toAdd = new Obligation(ImmutableSet.of(alias), obligation.whenToEnforce); + } + } + + if (toRemove != null) { + obligations.remove(toRemove); + obligations.add(toAdd); + // This satisfies case 2. + return true; + } + } + } + + // TODO: Getting this every time is inefficient if a method has many @CreatesMustCallFor + // annotations, but that should be rare. + MethodTree callerMethodTree = TreePathUtil.enclosingMethod(invocationPath); + if (callerMethodTree == null) { + return false; + } + ExecutableElement callerMethodElt = TreeUtils.elementFromDeclaration(callerMethodTree); + MustCallAnnotatedTypeFactory mcAtf = + typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + List callerCmcfValues = + ResourceLeakVisitor.getCreatesMustCallForValues( + callerMethodElt, mcAtf, typeFactory); + if (callerCmcfValues.isEmpty()) { + return false; + } + for (String callerCmcfValue : callerCmcfValues) { + JavaExpression callerTarget; + try { + callerTarget = + StringToJavaExpression.atMethodBody( + callerCmcfValue, callerMethodTree, checker); + } catch (JavaExpressionParseException e) { + // Do not issue an error here, because it would be a duplicate. + // The error will be issued by the Transfer class of the checker, + // via the CreatesMustCallForElementSupplier interface. + callerTarget = null; + } + + if (areSame(expression, callerTarget)) { + // This satisfies case 3. + return true; + } + } + return false; + } + + /** + * Checks whether the two JavaExpressions are the same. This is identical to calling equals() on + * one of them, with two exceptions: the second expression can be null, and {@code this} + * references are compared using their underlying type. (ThisReference#equals always returns + * true, which is probably a bug and isn't accurate in the case of nested classes.) + * + * @param target a JavaExpression + * @param enclosingTarget another, possibly null, JavaExpression + * @return true iff they represent the same program element + */ + private boolean areSame(JavaExpression target, @Nullable JavaExpression enclosingTarget) { + if (enclosingTarget == null) { + return false; + } + if (enclosingTarget instanceof ThisReference && target instanceof ThisReference) { + return enclosingTarget.getType().toString().equals(target.getType().toString()); + } else { + return enclosingTarget.equals(target); + } + } + + /** + * Given a node representing a method or constructor call, updates the set of Obligations to + * account for the result, which is treated as a new resource alias. Adds the new resource alias + * to the set of an Obligation in {@code obligations}: either an existing Obligation if the + * result is definitely resource-aliased with it, or a new Obligation if not. + * + * @param obligations the currently-tracked Obligations. This is always side-effected: either a + * new resource alias is added to the resource alias set of an existing Obligation, or a new + * Obligation with a single-element resource alias set is created and added. + * @param node the invocation node whose result is to be tracked; must be {@link + * MethodInvocationNode} or {@link ObjectCreationNode} + */ + /*package-private*/ void updateObligationsWithInvocationResult( + Set obligations, Node node) { + Tree tree = node.getTree(); + // Only track the result of the call if there is a temporary variable for the call node + // (because if there is no temporary, then the invocation must produce an untrackable value, + // such as a primitive type). + LocalVariableNode tmpVar = typeFactory.getTempVarForNode(node); + if (tmpVar == null) { + return; + } + + // `mustCallAliases` is a (possibly-empty) list of arguments passed in a MustCallAlias + // position. + List mustCallAliases = getMustCallAliasArgumentNodes(node); + // If call returns @This, add the receiver to mustCallAliases. + if (node instanceof MethodInvocationNode + && typeFactory.returnsThis((MethodInvocationTree) tree)) { + mustCallAliases.add( + removeCastsAndGetTmpVarIfPresent( + ((MethodInvocationNode) node).getTarget().getReceiver())); + } + + if (mustCallAliases.isEmpty()) { + // If mustCallAliases is an empty List, add tmpVarAsResourceAlias to a new set. + ResourceAlias tmpVarAsResourceAlias = + new ResourceAlias(new LocalVariable(tmpVar), tree); + obligations.add( + new Obligation(ImmutableSet.of(tmpVarAsResourceAlias), MethodExitKind.ALL)); + } else { + for (Node mustCallAlias : mustCallAliases) { + if (mustCallAlias instanceof FieldAccessNode) { + // Do not track the call result if the MustCallAlias argument is a field. + // Handling of @Owning fields is a completely separate check, and there is never + // a need to track an alias of a non-@Owning field, as by definition such a + // field does not have must-call obligations! + } else if (mustCallAlias instanceof LocalVariableNode) { + // If mustCallAlias is a local variable already being tracked, add + // tmpVarAsResourceAlias to the set containing mustCallAlias. + Obligation obligationContainingMustCallAlias = + getObligationForVar(obligations, (LocalVariableNode) mustCallAlias); + if (obligationContainingMustCallAlias != null) { + ResourceAlias tmpVarAsResourceAlias = + new ResourceAlias( + new LocalVariable(tmpVar), + tmpVar.getElement(), + tree, + obligationContainingMustCallAlias + .derivedFromMustCallAlias()); + Set newResourceAliasSet = + FluentIterable.from( + obligationContainingMustCallAlias.resourceAliases) + .append(tmpVarAsResourceAlias) + .toSet(); + obligations.remove(obligationContainingMustCallAlias); + obligations.add( + new Obligation( + newResourceAliasSet, + obligationContainingMustCallAlias.whenToEnforce)); + // It is not an error if there is no Obligation containing the must-call + // alias. In that case, what has usually happened is that no Obligation was + // created in the first place. + // For example, when checking the invocation of a "wrapper stream" + // constructor, if the argument in the must-call alias position is some + // stream with no must-call obligations like a ByteArrayInputStream, then no + // Obligation object will have been created for it and therefore + // obligationContainingMustCallAlias will be null. + } + } + } + } + } + + /** + * Returns true if the result of the given method or constructor invocation node should be + * tracked in {@code obligations}. In some cases, there is no need to track the result because + * the must-call obligations are already satisfied in some other way or there cannot possibly be + * must-call obligations because of the structure of the code. + * + *

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

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

    This method can also side-effect {@code obligations}, if node is a super or this + * constructor call with MustCallAlias annotations, by removing that Obligation. + * + * @param obligations the current set of Obligations, which may be side-effected + * @param node the invocation node to check; must be {@link MethodInvocationNode} or {@link + * ObjectCreationNode} + * @return true iff the result of {@code node} should be tracked in {@code obligations} + */ + private boolean shouldTrackInvocationResult(Set obligations, Node node) { + Tree callTree = node.getTree(); + if (callTree.getKind() == Tree.Kind.NEW_CLASS) { + // Constructor results from new expressions are tracked as long as the declared type has + // a non-empty @MustCall annotation. + NewClassTree newClassTree = (NewClassTree) callTree; + ExecutableElement executableElement = TreeUtils.elementFromUse(newClassTree); + TypeElement typeElt = + TypesUtils.getTypeElement(ElementUtils.getType(executableElement)); + return typeElt == null + || !typeFactory.hasEmptyMustCallValue(typeElt) + || !typeFactory.hasEmptyMustCallValue(newClassTree); + } + + // Now callTree.getKind() == Tree.Kind.METHOD_INVOCATION. + MethodInvocationTree methodInvokeTree = (MethodInvocationTree) callTree; + + if (TreeUtils.isSuperConstructorCall(methodInvokeTree) + || TreeUtils.isThisConstructorCall(methodInvokeTree)) { + List mustCallAliasArguments = getMustCallAliasArgumentNodes(node); + // If there is a MustCallAlias argument that is also in the set of Obligations, then + // remove it; its must-call obligation has been fulfilled by being passed on to the + // MustCallAlias constructor (because a this/super constructor call can only occur in + // the body of another constructor). + for (Node mustCallAliasArgument : mustCallAliasArguments) { + if (mustCallAliasArgument instanceof LocalVariableNode) { + removeObligationsContainingVar( + obligations, (LocalVariableNode) mustCallAliasArgument); + } + } + return false; + } + return !returnTypeIsMustCallAliasWithUntrackable((MethodInvocationNode) node) + && shouldTrackReturnType((MethodInvocationNode) node); + } + + /** + * Returns true if this node represents a method invocation of a must-call-alias method, where + * the argument in the must-call-alias position is untrackable: an owning field or a pointer + * that is guaranteed to be non-owning, such as {@code "this"} or a non-owning field. Owning + * fields are handled by the rest of the checker, not by this algorithm, so they are + * "untrackable". Non-owning fields and this nodes are guaranteed to be non-owning, and are + * therefore also "untrackable". Because both owning and non-owning fields are untrackable (and + * there are no other kinds of fields), this method returns true for all field accesses. + * + * @param node a method invocation node + * @return true if this is the invocation of a method whose return type is MCA with an owning + * field or a definitely non-owning pointer + */ + private boolean returnTypeIsMustCallAliasWithUntrackable(MethodInvocationNode node) { + List mustCallAliasArguments = getMustCallAliasArgumentNodes(node); + for (Node mustCallAliasArg : mustCallAliasArguments) { + if (!(mustCallAliasArg instanceof FieldAccessNode + || mustCallAliasArg instanceof ThisNode)) { + return false; + } + } + return !mustCallAliasArguments.isEmpty(); + } + + /** + * Checks if {@code node} is either directly enclosed by a {@link TypeCastNode}, by looking at + * the successor block in the CFG. In this case the enclosing operator is a "no-op" that + * evaluates to the same value as {@code node}. This method is only used within {@link + * #propagateObligationsToSuccessorBlocks(ControlFlowGraph, Set, Block, Set, Deque)} to ensure + * Obligations are propagated to cast nodes properly. It relies on the assumption that a {@link + * TypeCastNode} will only appear in a CFG as the first node in a block. + * + * @param node the CFG node + * @return {@code true} if {@code node} is in a {@link SingleSuccessorBlock} {@code b}, the + * first {@link Node} in {@code b}'s successor block is a {@link TypeCastNode}, and {@code + * node} is an operand of the successor node; {@code false} otherwise + */ + private boolean inCast(Node node) { + if (!(node.getBlock() instanceof SingleSuccessorBlock)) { + return false; + } + Block successorBlock = ((SingleSuccessorBlock) node.getBlock()).getSuccessor(); + if (successorBlock != null) { + List succNodes = successorBlock.getNodes(); + if (succNodes.size() > 0) { + Node succNode = succNodes.get(0); + if (succNode instanceof TypeCastNode) { + return ((TypeCastNode) succNode).getOperand().equals(node); + } + } + } + return false; + } + + /** + * Transfer ownership of any locals passed as arguments to {@code @Owning} parameters at a + * method or constructor call by removing the Obligations corresponding to those locals. + * + * @param obligations the current set of Obligations, which is side-effected to remove + * Obligations for locals that are passed as owning parameters to the method or constructor + * @param node a method or constructor invocation node + * @param exceptionType a description of the outgoing CFG edge from the node: null + * to indicate normal return, or a {@link TypeMirror} to indicate a subclass of the given + * throwable class was thrown + */ + private void removeObligationsAtOwnershipTransferToParameters( + Set obligations, Node node, @Nullable TypeMirror exceptionType) { + + if (exceptionType != null) { + // Do not transfer ownership if the called method throws an exception. + return; + } + + if (noLightweightOwnership) { + // Never transfer ownership to parameters, matching the default in the analysis built + // into Eclipse. + return; + } + + List arguments = getArgumentsOfInvocation(node); + List parameters = getParametersOfInvocation(node); + + if (arguments.size() != parameters.size()) { + // This could happen, e.g., with varargs, or with strange cases like generated Enum + // constructors. In the varargs case (i.e. if the varargs parameter is owning), + // only the first of the varargs arguments will actually get transferred: the second + // and later varargs arguments will continue to be tracked at the call-site. + // For now, just skip this case - the worst that will happen is a false positive in + // cases like the varargs one described above. + // TODO allow for ownership transfer here if needed in future + return; + } + for (int i = 0; i < arguments.size(); i++) { + Node n = removeCastsAndGetTmpVarIfPresent(arguments.get(i)); + if (n instanceof LocalVariableNode) { + LocalVariableNode local = (LocalVariableNode) n; + if (varTrackedInObligations(obligations, local)) { + + // check if parameter has an @Owning annotation + VariableElement parameter = parameters.get(i); + if (typeFactory.hasOwning(parameter)) { + Obligation localObligation = getObligationForVar(obligations, local); + // Passing to an owning parameter is not sufficient to resolve the + // obligation created from a MustCallAlias parameter, because the + // containing method must actually return the value. + if (!localObligation.derivedFromMustCallAlias()) { + // Transfer ownership! + obligations.remove(localObligation); + } + } + } + } + } + } + + /** + * If the return type of the enclosing method is {@code @Owning}, treat the must-call + * obligations of the return expression as satisfied by removing all references to them from + * {@code obligations}. + * + * @param obligations the current set of tracked Obligations. If ownership is transferred, it is + * side-effected to remove any Obligations that are resource-aliased to the return node. + * @param cfg the CFG of the enclosing method + * @param node a return node + */ + private void updateObligationsForOwningReturn( + Set obligations, ControlFlowGraph cfg, ReturnNode node) { + if (isTransferOwnershipAtReturn(cfg)) { + Node returnExpr = node.getResult(); + returnExpr = getTempVarOrNode(returnExpr); + if (returnExpr instanceof LocalVariableNode) { + removeObligationsContainingVar(obligations, (LocalVariableNode) returnExpr); + } + } + } + + /** + * Helper method that gets the temporary node corresponding to {@code node}, if one exists. If + * not, this method returns its input. + * + * @param node a node + * @return the temporary for node, or node if no temporary exists + */ + /*package-private*/ Node getTempVarOrNode(Node node) { + Node temp = typeFactory.getTempVarForNode(node); + if (temp != null) { + return temp; + } + return node; + } + + /** + * Should ownership be transferred to the return type of the method corresponding to a CFG? + * Returns true when there is no {@link NotOwning} annotation on the return type. + * + * @param cfg the CFG of the method + * @return true iff ownership should be transferred to the return type of the method + * corresponding to a CFG + */ + private boolean isTransferOwnershipAtReturn(ControlFlowGraph cfg) { + if (noLightweightOwnership) { + // If not using LO, default to always transfer at return, just like Eclipse does. + return true; + } + + UnderlyingAST underlyingAST = cfg.getUnderlyingAST(); + if (underlyingAST instanceof UnderlyingAST.CFGMethod) { + // TODO: lambdas? In that case false is returned below, which means that ownership will + // not be transferred. + MethodTree method = ((UnderlyingAST.CFGMethod) underlyingAST).getMethod(); + ExecutableElement executableElement = TreeUtils.elementFromDeclaration(method); + return !typeFactory.hasNotOwning(executableElement); + } + return false; + } + + /** + * Updates a set of Obligations to account for an assignment. Assigning to an owning field might + * remove Obligations, assigning to a resource variable might remove obligations, assigning to a + * new local variable might modify an Obligation (by increasing the size of its resource alias + * set), etc. + * + * @param obligations the set of Obligations to update + * @param cfg the control flow graph that contains {@code assignmentNode} + * @param assignmentNode the assignment + */ + private void updateObligationsForAssignment( + Set obligations, ControlFlowGraph cfg, AssignmentNode assignmentNode) { + Node lhs = assignmentNode.getTarget(); + Element lhsElement = TreeUtils.elementFromTree(lhs.getTree()); + if (lhsElement == null) { + return; + } + // Use the temporary variable for the rhs if it exists. + Node rhs = NodeUtils.removeCasts(assignmentNode.getExpression()); + rhs = getTempVarOrNode(rhs); + + // Ownership transfer to @Owning field. + if (lhsElement.getKind() == ElementKind.FIELD) { + boolean isOwningField = !noLightweightOwnership && typeFactory.hasOwning(lhsElement); + // Check that the must-call obligations of the lhs have been satisfied, if the field is + // non-final and owning. + if (isOwningField + && typeFactory.canCreateObligations() + && !ElementUtils.isFinal(lhsElement)) { + checkReassignmentToField(obligations, assignmentNode); + } + + // Remove Obligations from local variables, now that the owning field is responsible. + // (When obligation creation is turned off, non-final fields cannot take ownership.) + if (isOwningField + && rhs instanceof LocalVariableNode + && (typeFactory.canCreateObligations() || ElementUtils.isFinal(lhsElement))) { + + LocalVariableNode rhsVar = (LocalVariableNode) rhs; + + MethodTree containingMethod = cfg.getContainingMethod(assignmentNode.getTree()); + boolean inConstructor = + containingMethod != null && TreeUtils.isConstructor(containingMethod); + + // Determine which obligations this field assignment can clear. In a constructor, + // assignments to `this.field` only clears obligations on normal return, since + // on exception `this` becomes inaccessible. + Set toClear; + if (inConstructor + && lhs instanceof FieldAccessNode + && ((FieldAccessNode) lhs).getReceiver() instanceof ThisNode) { + toClear = Collections.singleton(MethodExitKind.NORMAL_RETURN); + } else { + toClear = MethodExitKind.ALL; + } + + @Nullable Element enclosingElem = lhsElement.getEnclosingElement(); + @Nullable TypeElement enclosingType = + enclosingElem != null + ? ElementUtils.enclosingTypeElement(enclosingElem) + : null; + + // Assigning to an owning field is sufficient to clear a must-call alias obligation + // in a constructor, if the enclosing class has at most one @Owning field. If the + // class had multiple owning fields, then a soundness bug would occur: the must call + // alias relationship would allow the whole class' obligation to be fulfilled by + // closing only one of the parameters passed to the constructor (but the other + // owning fields might not actually have had their obligations fulfilled). See test + // case checker/tests/resourceleak/TwoOwningMCATest.java for an example. + if (hasAtMostOneOwningField(enclosingType)) { + removeObligationsContainingVar( + obligations, + rhsVar, + MustCallAliasHandling.NO_SPECIAL_HANDLING, + toClear); + } else { + removeObligationsContainingVar( + obligations, + rhsVar, + MustCallAliasHandling + .RETAIN_OBLIGATIONS_DERIVED_FROM_A_MUST_CALL_ALIAS_PARAMETER, + toClear); + } + + // Finally, if any obligations containing this var remain, then closing the field + // will satisfy them. Here we are overly cautious and only track final fields. In + // the future we could perhaps relax this guard with careful handling for field + // reassignments. + if (ElementUtils.isFinal(lhsElement)) { + addAliasToObligationsContainingVar( + obligations, + rhsVar, + new ResourceAlias( + JavaExpression.fromNode(lhs), lhsElement, lhs.getTree())); + } + } + } else if (lhs instanceof LocalVariableNode) { + LocalVariableNode lhsVar = (LocalVariableNode) lhs; + updateObligationsForPseudoAssignment(obligations, assignmentNode, lhsVar, rhs); + } + } + + /** + * Returns true iff the given type element has 0 or 1 @Owning fields. + * + * @param element an element for a class + * @return true iff element has no more than 1 owning field + */ + private boolean hasAtMostOneOwningField(TypeElement element) { + List fields = + ElementUtils.getAllFieldsIn(element, typeFactory.getElementUtils()); + // Has an owning field already been encountered? + boolean hasOwningField = false; + for (VariableElement field : fields) { + if (typeFactory.hasOwning(field)) { + if (hasOwningField) { + return false; + } else { + hasOwningField = true; + } + } + } + // We haven't seen two owning fields, so there must be 1 or 0. + return true; + } + + /** + * Add a new alias to all Obligations that have {@code var} in their resource-alias set. This + * method should be used when {@code var} and {@code newAlias} definitively point to the same + * object in memory. + * + * @param obligations the set of Obligations to modify + * @param var a variable + * @param newAlias a new {@link ResourceAlias} to add + */ + private void addAliasToObligationsContainingVar( + Set obligations, LocalVariableNode var, ResourceAlias newAlias) { + Iterator it = obligations.iterator(); + List newObligations = new ArrayList<>(); + + while (it.hasNext()) { + Obligation obligation = it.next(); + if (obligation.canBeSatisfiedThrough(var)) { + it.remove(); + + Set newAliases = new LinkedHashSet<>(obligation.resourceAliases); + newAliases.add(newAlias); + + newObligations.add(new Obligation(newAliases, obligation.whenToEnforce)); + } + } + + obligations.addAll(newObligations); + } + + /** + * Remove any Obligations that contain {@code var} in their resource-alias set. + * + * @param obligations the set of Obligations to modify + * @param var a variable + */ + /*package-private*/ void removeObligationsContainingVar( + Set obligations, LocalVariableNode var) { + removeObligationsContainingVar( + obligations, var, MustCallAliasHandling.NO_SPECIAL_HANDLING, MethodExitKind.ALL); + } + + /** + * Helper type for {@link #removeObligationsContainingVar(Set, LocalVariableNode, + * MustCallAliasHandling, Set)} + */ + private enum MustCallAliasHandling { + /** + * Obligations derived from {@link MustCallAlias} parameters do not require special + * handling, and they should be removed like any other obligation. + */ + NO_SPECIAL_HANDLING, + + /** + * Obligations derived from {@link MustCallAlias} parameters are not satisfied and should be + * retained. + */ + RETAIN_OBLIGATIONS_DERIVED_FROM_A_MUST_CALL_ALIAS_PARAMETER, + } + + /** + * Remove Obligations that contain {@code var} in their resource-alias set. + * + *

    Some operations do not satisfy all Obligations. For instance, assigning to a field in a + * constructor only satisfies Obligations when the constructor exits normally (i.e. without + * throwing an exception). The last two arguments to this method can be used to retain some + * Obligations in special circumstances. + * + * @param obligations the set of Obligations to modify + * @param var a variable + * @param mustCallAliasHandling how to treat Obligations derived from {@link MustCallAlias} + * parameters + * @param whatToClear the kind of Obligations to remove + */ + private void removeObligationsContainingVar( + Set obligations, + LocalVariableNode var, + MustCallAliasHandling mustCallAliasHandling, + Set whatToClear) { + List newObligations = new ArrayList<>(); + + Iterator it = obligations.iterator(); + while (it.hasNext()) { + Obligation obligation = it.next(); + + if (obligation.canBeSatisfiedThrough(var) + && (mustCallAliasHandling == MustCallAliasHandling.NO_SPECIAL_HANDLING + || !obligation.derivedFromMustCallAlias())) { + it.remove(); + + Set whenToEnforce = new HashSet<>(obligation.whenToEnforce); + whenToEnforce.removeAll(whatToClear); + + if (!whenToEnforce.isEmpty()) { + newObligations.add(new Obligation(obligation.resourceAliases, whenToEnforce)); + } + } + } + + obligations.addAll(newObligations); + } + + /** + * Update a set of tracked Obligations to account for a (pseudo-)assignment to some variable, as + * in a gen-kill dataflow analysis problem. That is, add ("gen") and remove ("kill") resource + * aliases from Obligations in the {@code obligations} set as appropriate based on the + * (pseudo-)assignment performed by {@code node}. This method may also remove an Obligation + * entirely if the analysis concludes that its resource alias set is empty because the last + * tracked alias to it has been overwritten (including checking that the must-call obligations + * were satisfied before the assignment). + * + *

    Pseudo-assignments may include operations that "assign" to a temporary variable, exposing + * the possible value flow into the variable. E.g., for a ternary expression {@code b ? x : y} + * whose temporary variable is {@code t}, this method may process "assignments" {@code t = x} + * and {@code t = y}, thereby capturing the two possible values of {@code t}. + * + * @param obligations the tracked Obligations, which will be side-effected + * @param node the node performing the pseudo-assignment; it is not necessarily an assignment + * node + * @param lhsVar the left-hand side variable for the pseudo-assignment + * @param rhs the right-hand side for the pseudo-assignment, which must have been converted to a + * temporary variable (via a call to {@link + * ResourceLeakAnnotatedTypeFactory#getTempVarForNode}) + */ + /*package-private*/ void updateObligationsForPseudoAssignment( + Set obligations, Node node, LocalVariableNode lhsVar, Node rhs) { + // Replacements to eventually perform in Obligations. This map is kept to avoid a + // ConcurrentModificationException in the loop below. + Map replacements = new LinkedHashMap<>(); + // Cache to re-use on subsequent iterations. + ResourceAlias aliasForAssignment = null; + for (Obligation obligation : obligations) { + // This is a non-null value iff the resource alias set for obligation needs to + // change because of the pseudo-assignment. The value of this variable is the new + // alias set for `obligation` if it is non-null. + Set newResourceAliasesForObligation = null; + + // Always kill the lhs var if it is present in the resource alias set for this + // Obligation by removing it from the resource alias set. + ResourceAlias aliasForLhs = obligation.getResourceAlias(lhsVar); + if (aliasForLhs != null) { + newResourceAliasesForObligation = new LinkedHashSet<>(obligation.resourceAliases); + newResourceAliasesForObligation.remove(aliasForLhs); + } + // If rhs is a variable tracked in the Obligation's resource alias set, gen the lhs + // by adding it to the resource alias set. + if (rhs instanceof LocalVariableNode + && obligation.canBeSatisfiedThrough((LocalVariableNode) rhs)) { + LocalVariableNode rhsVar = (LocalVariableNode) rhs; + if (newResourceAliasesForObligation == null) { + newResourceAliasesForObligation = + new LinkedHashSet<>(obligation.resourceAliases); + } + if (aliasForAssignment == null) { + // It is possible to observe assignments to temporary variables, e.g., + // synthetic assignments to ternary expression variables in the CFG. For such + // cases, use the tree associated with the temp var for the resource alias, + // as that is the tree where errors should be reported. + Tree treeForAlias = + typeFactory.isTempVar(lhsVar) + ? typeFactory.getTreeForTempVar(lhsVar) + : node.getTree(); + aliasForAssignment = new ResourceAlias(new LocalVariable(lhsVar), treeForAlias); + } + newResourceAliasesForObligation.add(aliasForAssignment); + // Remove temp vars from tracking once they are assigned to another location. + if (typeFactory.isTempVar(rhsVar)) { + ResourceAlias aliasForRhs = obligation.getResourceAlias(rhsVar); + if (aliasForRhs != null) { + newResourceAliasesForObligation.remove(aliasForRhs); + } + } + } + + // If no changes were made to the resource alias set, there is no need to update the + // Obligation. + if (newResourceAliasesForObligation == null) { + continue; + } + + if (newResourceAliasesForObligation.isEmpty()) { + // Because the last reference to the resource has been overwritten, check the + // must-call obligation. + MustCallAnnotatedTypeFactory mcAtf = + typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + checkMustCall( + obligation, + typeFactory.getStoreBefore(node), + mcAtf.getStoreBefore(node), + "variable overwritten by assignment " + node.getTree()); + replacements.put(obligation, null); + } else { + replacements.put( + obligation, + new Obligation(newResourceAliasesForObligation, obligation.whenToEnforce)); + } + } + + // Finally, update the set of Obligations according to the replacements. + for (Map.Entry entry : replacements.entrySet()) { + obligations.remove(entry.getKey()); + if (entry.getValue() != null && !entry.getValue().resourceAliases.isEmpty()) { + obligations.add(entry.getValue()); + } + } + } + + /** + * Issues an error if the given re-assignment to a non-final, owning field is not valid. A + * re-assignment is valid if the called methods type of the lhs before the assignment satisfies + * the must-call obligations of the field. + * + *

    Despite the name of this method, the argument {@code node} might be the first and only + * assignment to a field. + * + * @param obligations current tracked Obligations + * @param node an assignment to a non-final, owning field + */ + private void checkReassignmentToField(Set obligations, AssignmentNode node) { + + Node lhsNode = node.getTarget(); + + if (!(lhsNode instanceof FieldAccessNode)) { + throw new TypeSystemError( + "checkReassignmentToField: non-field node " + + node + + " of class " + + node.getClass()); + } + + FieldAccessNode lhs = (FieldAccessNode) lhsNode; + Node receiver = lhs.getReceiver(); + + if (permitStaticOwning && receiver instanceof ClassNameNode) { + return; + } + + // TODO: it would be better to defer getting the path until after checking + // for a CreatesMustCallFor annotation, because getting the path can be expensive. + // It might be possible to exploit the CFG structure to find the containing + // method (rather than using the path, as below), because if a method is being + // analyzed then it should be the root of the CFG (I think). + TreePath currentPath = typeFactory.getPath(node.getTree()); + MethodTree enclosingMethodTree = TreePathUtil.enclosingMethod(currentPath); + + if (enclosingMethodTree == null) { + // The assignment is taking place outside of a method: in a variable declaration's + // initializer or in an initializer block. + // The Resource Leak Checker issues no error if the assignment is a field initializer. + if (node.getTree().getKind() == Tree.Kind.VARIABLE) { + // An assignment to a field that is also a declaration must be a field initializer + // (VARIABLE Trees are only used for declarations). Assignment in a field + // initializer is always permitted. + return; + } else if (permitInitializationLeak + && TreePathUtil.isTopLevelAssignmentInInitializerBlock(currentPath)) { + // This is likely not reassignment; if reassignment, the number of assignments that + // were not warned about is limited to other initializations (is not unbounded). + // This behavior is unsound; see InstanceInitializer.java test case. + return; + } else { + // Issue an error if the field has a non-empty must-call type. + MustCallAnnotatedTypeFactory mcTypeFactory = + typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + AnnotationMirror mcAnno = + mcTypeFactory + .getAnnotatedType(lhs.getElement()) + .getAnnotation(MustCall.class); + List mcValues = + AnnotationUtils.getElementValueArray( + mcAnno, mcTypeFactory.getMustCallValueElement(), String.class); + if (mcValues.isEmpty()) { + return; + } + VariableElement lhsElement = TreeUtils.variableElementFromTree(lhs.getTree()); + checker.reportError( + node.getTree(), + "required.method.not.called", + formatMissingMustCallMethods(mcValues), + "field " + lhsElement.getSimpleName().toString(), + lhsElement.asType().toString(), + "Field assignment outside method or declaration might overwrite field's" + + " current value"); + return; + } + } else if (permitInitializationLeak && TreeUtils.isConstructor(enclosingMethodTree)) { + Element enclosingClassElement = + TreeUtils.elementFromDeclaration(enclosingMethodTree).getEnclosingElement(); + if (ElementUtils.isTypeElement(enclosingClassElement)) { + Element receiverElement = TypesUtils.getTypeElement(receiver.getType()); + if (Objects.equals(enclosingClassElement, receiverElement)) { + return; + } + } + } + + // Check that there is a corresponding CreatesMustCallFor annotation, unless this is + // 1) an assignment to a field of a newly-declared local variable whose scope does not + // extend beyond the method's body (and which therefore could not be targeted by an + // annotation on the method declaration), or 2) the rhs is a null literal (so there's + // nothing to reset). + if (!(receiver instanceof LocalVariableNode + && varTrackedInObligations(obligations, (LocalVariableNode) receiver)) + && !(node.getExpression() instanceof NullLiteralNode)) { + checkEnclosingMethodIsCreatesMustCallFor(node, enclosingMethodTree); + } + + // The following code handles a special case where the field being assigned is itself + // getting passed in an owning position to another method on the RHS of the assignment. + // For example, if the field's type is a class whose constructor takes another instance + // of itself (such as a node in a linked list) in an owning position, re-assigning the field + // to a new instance that takes the field's value as an owning parameter is safe (the new + // value has taken responsibility for closing the old value). In such a case, it is not + // required that the must-call obligation of the field be satisfied via method calls before + // the assignment, since the invoked method will take ownership of the object previously + // referenced by the field and handle the obligation. This fixes the false positive in + // https://github.com/typetools/checker-framework/issues/5971. + Node rhs = node.getExpression(); + if (!noLightweightOwnership + && (rhs instanceof ObjectCreationNode || rhs instanceof MethodInvocationNode)) { + + List arguments = getArgumentsOfInvocation(rhs); + List parameters = getParametersOfInvocation(rhs); + + if (arguments.size() == parameters.size()) { + for (int i = 0; i < arguments.size(); i++) { + VariableElement param = parameters.get(i); + if (typeFactory.hasOwning(param)) { + Node argument = arguments.get(i); + if (argument.equals(lhs)) { + return; + } + } + } + } else { + // This could happen, e.g., with varargs, or with strange cases like generated Enum + // constructors. In the varargs case (i.e. if the varargs parameter is owning), + // only the first of the varargs arguments will actually get transferred: the second + // and later varargs arguments will continue to be tracked at the call-site. + // For now, just skip this case - the worst that will happen is a false positive in + // cases like the varargs one described above. + // TODO allow for ownership transfer here if needed in future, but for now do + // nothing + } + } + + MustCallAnnotatedTypeFactory mcTypeFactory = + typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + + // Get the Must Call type for the field. If there's info about this field in the store, use + // that. Otherwise, use the declared type of the field + CFStore mcStore = mcTypeFactory.getStoreBefore(lhs); + CFValue mcValue = mcStore.getValue(lhs); + AnnotationMirror mcAnno = null; + if (mcValue != null) { + mcAnno = AnnotationUtils.getAnnotationByClass(mcValue.getAnnotations(), MustCall.class); + } + if (mcAnno == null) { + // No stored value (or the stored value is Poly/top), so use the declared type. + mcAnno = mcTypeFactory.getAnnotatedType(lhs.getElement()).getAnnotation(MustCall.class); + } + // if mcAnno is still null, then the declared type must be something other than + // @MustCall (probably @MustCallUnknown). Do nothing in this case: a warning + // about the field will be issued elsewhere (it will be impossible to satisfy its + // obligations!). + if (mcAnno == null) { + return; + } + List mcValues = + AnnotationUtils.getElementValueArray( + mcAnno, mcTypeFactory.getMustCallValueElement(), String.class); + + if (mcValues.isEmpty()) { + return; + } + + // Get the store before the RHS rather than the assignment node, because the CFG always has + // the RHS first. If the RHS has side-effects, then the assignment node's store will have + // had its inferred types erased. + AccumulationStore cmStoreBefore = typeFactory.getStoreBefore(rhs); + AccumulationValue cmValue = cmStoreBefore == null ? null : cmStoreBefore.getValue(lhs); + AnnotationMirror cmAnno = null; + if (cmValue != null) { // When store contains the lhs + Set accumulatedValues = cmValue.getAccumulatedValues(); + if (accumulatedValues != null) { // type variable or wildcard type + cmAnno = typeFactory.createCalledMethods(accumulatedValues.toArray(new String[0])); + } else { + for (AnnotationMirror anno : cmValue.getAnnotations()) { + if (AnnotationUtils.areSameByName( + anno, + "org.checkerframework.checker.calledmethods.qual.CalledMethods")) { + cmAnno = anno; + } + } + } + } + if (cmAnno == null) { + cmAnno = typeFactory.top; + } + if (!calledMethodsSatisfyMustCall(mcValues, cmAnno)) { + VariableElement lhsElement = TreeUtils.variableElementFromTree(lhs.getTree()); + if (!checker.shouldSkipUses(lhsElement)) { + checker.reportError( + node.getTree(), + "required.method.not.called", + formatMissingMustCallMethods(mcValues), + "field " + lhsElement.getSimpleName().toString(), + lhsElement.asType().toString(), + " Non-final owning field might be overwritten"); + } + } + } + + /** + * Checks that the method that encloses an assignment is marked with @CreatesMustCallFor + * annotation whose target is the object whose field is being re-assigned. + * + * @param node an assignment node whose lhs is a non-final, owning field + * @param enclosingMethod the MethodTree in which the re-assignment takes place + */ + private void checkEnclosingMethodIsCreatesMustCallFor( + AssignmentNode node, MethodTree enclosingMethod) { + Node lhs = node.getTarget(); + if (!(lhs instanceof FieldAccessNode)) { + return; + } + if (permitStaticOwning && ((FieldAccessNode) lhs).getReceiver() instanceof ClassNameNode) { + return; + } + + String receiverString = receiverAsString((FieldAccessNode) lhs); + if ("this".equals(receiverString) && TreeUtils.isConstructor(enclosingMethod)) { + // Constructors always create must-call obligations, so there is no need for them to + // be annotated. + return; + } + ExecutableElement enclosingMethodElt = TreeUtils.elementFromDeclaration(enclosingMethod); + MustCallAnnotatedTypeFactory mcAtf = + typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + + List cmcfValues = + ResourceLeakVisitor.getCreatesMustCallForValues( + enclosingMethodElt, mcAtf, typeFactory); + + if (cmcfValues.isEmpty()) { + checker.reportError( + enclosingMethod, + "missing.creates.mustcall.for", + enclosingMethodElt.getSimpleName().toString(), + receiverString, + ((FieldAccessNode) lhs).getFieldName()); + return; + } + + List checked = new ArrayList<>(); + for (String targetStrWithoutAdaptation : cmcfValues) { + String targetStr; + try { + targetStr = + StringToJavaExpression.atMethodBody( + targetStrWithoutAdaptation, enclosingMethod, checker) + .toString(); + } catch (JavaExpressionParseException e) { + targetStr = targetStrWithoutAdaptation; + } + if (targetStr.equals(receiverString)) { + // This @CreatesMustCallFor annotation matches. + return; + } + checked.add(targetStr); + } + checker.reportError( + enclosingMethod, + "incompatible.creates.mustcall.for", + enclosingMethodElt.getSimpleName().toString(), + receiverString, + ((FieldAccessNode) lhs).getFieldName(), + String.join(", ", checked)); + } + + /** + * Gets a standardized name for an object whose field is being re-assigned. + * + * @param fieldAccessNode a field access node + * @return the name of the object whose field is being accessed (the receiver), as a string + */ + private String receiverAsString(FieldAccessNode fieldAccessNode) { + Node receiver = fieldAccessNode.getReceiver(); + if (receiver instanceof ThisNode) { + return "this"; + } + if (receiver instanceof LocalVariableNode) { + return ((LocalVariableNode) receiver).getName(); + } + if (receiver instanceof ClassNameNode) { + return ((ClassNameNode) receiver).getElement().toString(); + } + if (receiver instanceof SuperNode) { + return "super"; + } + throw new TypeSystemError( + "unexpected receiver of field assignment: " + + receiver + + " of type " + + receiver.getClass()); + } + + /** + * Finds the arguments passed in the {@code @MustCallAlias} positions for a call. + * + * @param callNode callNode representing the call; must be {@link MethodInvocationNode} or + * {@link ObjectCreationNode} + * @return if {@code callNode} invokes a method with a {@code @MustCallAlias} annotation on some + * formal parameter(s) (or the receiver), returns the result of calling {@link + * #removeCastsAndGetTmpVarIfPresent(Node)} on the argument(s) passed in corresponding + * position(s). Otherwise, returns an empty List. + */ + private List getMustCallAliasArgumentNodes(Node callNode) { + Preconditions.checkArgument( + callNode instanceof MethodInvocationNode || callNode instanceof ObjectCreationNode); + List result = new ArrayList<>(); + if (!typeFactory.hasMustCallAlias(callNode.getTree())) { + return result; + } + + List args = getArgumentsOfInvocation(callNode); + List parameters = getParametersOfInvocation(callNode); + for (int i = 0; i < args.size(); i++) { + if (typeFactory.hasMustCallAlias(parameters.get(i))) { + result.add(removeCastsAndGetTmpVarIfPresent(args.get(i))); + } + } + + // If none of the parameters were @MustCallAlias, it must be the receiver + if (result.isEmpty() && callNode instanceof MethodInvocationNode) { + result.add( + removeCastsAndGetTmpVarIfPresent( + ((MethodInvocationNode) callNode).getTarget().getReceiver())); + } + + return result; + } + + /** + * If a temporary variable exists for node after typecasts have been removed, return it. + * Otherwise, return node. + * + * @param node a node + * @return either a tempvar for node's content sans typecasts, or node + */ + /*package-private*/ Node removeCastsAndGetTmpVarIfPresent(Node node) { + // TODO: Create temp vars for TypeCastNodes as well, so there is no need to explicitly + // remove casts here. + node = NodeUtils.removeCasts(node); + return getTempVarOrNode(node); + } + + /** + * Get the nodes representing the arguments of a method or constructor invocation from the + * invocation node. + * + * @param node a MethodInvocation or ObjectCreation node + * @return the arguments, in order + */ + /*package-private*/ List getArgumentsOfInvocation(Node node) { + if (node instanceof MethodInvocationNode) { + MethodInvocationNode invocationNode = (MethodInvocationNode) node; + return invocationNode.getArguments(); + } else if (node instanceof ObjectCreationNode) { + return ((ObjectCreationNode) node).getArguments(); + } else { + throw new TypeSystemError("unexpected node type " + node.getClass()); + } + } + + /** + * Get the elements representing the formal parameters of a method or constructor, from an + * invocation of that method or constructor. + * + * @param node a method invocation or object creation node + * @return a list of the declarations of the formal parameters of the method or constructor + * being invoked + */ + /*package-private*/ List getParametersOfInvocation(Node node) { + ExecutableElement executableElement; + if (node instanceof MethodInvocationNode) { + MethodInvocationNode invocationNode = (MethodInvocationNode) node; + executableElement = TreeUtils.elementFromUse(invocationNode.getTree()); + } else if (node instanceof ObjectCreationNode) { + executableElement = TreeUtils.elementFromUse(((ObjectCreationNode) node).getTree()); + } else { + throw new TypeSystemError("unexpected node type " + node.getClass()); + } + + return executableElement.getParameters(); + } + + /** + * Is the return type of the invoked method one that should be tracked? + * + * @param node a method invocation + * @return true iff the checker is in no-lightweight-ownership mode, or the method has a + * {@code @MustCallAlias} annotation, or (1) the method has a return type that needs to be + * tracked (i.e., it has a non-empty {@code @MustCall} obligation and (2) the method + * declaration does not have a {@code @NotOwning} annotation + */ + private boolean shouldTrackReturnType(MethodInvocationNode node) { + if (noLightweightOwnership) { + // Default to always transferring at return if not using LO, just like Eclipse does. + return true; + } + MethodInvocationTree methodInvocationTree = node.getTree(); + ExecutableElement executableElement = TreeUtils.elementFromUse(methodInvocationTree); + if (typeFactory.hasMustCallAlias(executableElement)) { + // assume tracking is required + return true; + } + TypeMirror type = ElementUtils.getType(executableElement); + // void or primitive-returning methods are "not owning" by construction + if (type.getKind() == TypeKind.VOID || type.getKind().isPrimitive()) { + return false; + } + TypeElement typeElt = TypesUtils.getTypeElement(type); + // no need to track if type has no possible @MustCall obligation + if (typeElt != null + && typeFactory.hasEmptyMustCallValue(typeElt) + && typeFactory.hasEmptyMustCallValue(methodInvocationTree)) { + return false; + } + // check for absence of @NotOwning annotation + return !typeFactory.hasNotOwning(executableElement); + } + + /** + * Get all successor blocks for some block, except for those corresponding to ignored exception + * types. See {@link ResourceLeakAnalysis#isIgnoredExceptionType(TypeMirror)}. Each exceptional + * successor is paired with the type of exception that leads to it, for use in error messages. + * + * @param block input block + * @return set of pairs (b, t), where b is a successor block, and t is the type of exception for + * the CFG edge from block to b, or {@code null} if b is a non-exceptional successor + */ + private Set> getSuccessorsExceptIgnoredExceptions( + Block block) { + if (block.getType() == Block.BlockType.EXCEPTION_BLOCK) { + ExceptionBlock excBlock = (ExceptionBlock) block; + Set> result = new LinkedHashSet<>(); + // regular successor + Block regularSucc = excBlock.getSuccessor(); + if (regularSucc != null) { + result.add(IPair.of(regularSucc, null)); + } + // non-ignored exception successors + Map> exceptionalSuccessors = excBlock.getExceptionalSuccessors(); + for (Map.Entry> entry : exceptionalSuccessors.entrySet()) { + TypeMirror exceptionType = entry.getKey(); + if (!analysis.isIgnoredExceptionType(exceptionType)) { + for (Block exSucc : entry.getValue()) { + result.add(IPair.of(exSucc, exceptionType)); + } + } + } + return result; + } else { + Set> result = new LinkedHashSet<>(); + for (Block b : block.getSuccessors()) { + result.add(IPair.of(b, null)); + } + return result; + } + } + + /** + * Propagates a set of Obligations to successors, and performs consistency checks when variables + * are going out of scope. + * + *

    The basic algorithm loops over the successor blocks of the current block. For each + * successor, two things happen: + * + *

    First, it constructs an updated set of Obligations using {@code incomingObligations}, the + * nodes in {@code currentBlock}, and the nature of the edge from {@code currentBlock} to the + * successor. The edge can either be normal control flow or an exception. See + * + *

      + *
    • {@link #updateObligationsForAssignment(Set, ControlFlowGraph, AssignmentNode)} + *
    • {@link #updateObligationsForOwningReturn(Set, ControlFlowGraph, ReturnNode)} + *
    • {@link #updateObligationsForInvocation(Set, Node, TypeMirror)} + *
    + * + *

    Second, it checks every Obligation in obligations. If the successor is an exit block or + * all of an Obligation's resource aliases might be going out of scope, then a consistency check + * occurs (with two exceptions, both related to temporary variables that don't actually get + * assigned; see code comments for details) and an error is issued if it fails. If the successor + * is any other kind of block and there is information about at least one of the Obligation's + * aliases in the successor store (i.e. the resource itself definitely does not go out of + * scope), then the Obligation is passed forward to the successor ("propagated") with any + * definitely out-of-scope aliases removed from its resource alias set. + * + * @param cfg the control flow graph + * @param incomingObligations the Obligations for the current block + * @param currentBlock the current block + * @param visited block-Obligations pairs already analyzed or already on the worklist + * @param worklist current worklist + */ + private void propagateObligationsToSuccessorBlocks( + ControlFlowGraph cfg, + Set incomingObligations, + Block currentBlock, + Set visited, + Deque worklist) { + // For each successor block that isn't caused by an ignored exception type, this loop + // computes the set of Obligations that should be propagated to it and then adds it to the + // worklist if any of its resource aliases are still in scope in the successor block. If + // none are, then the loop performs a consistency check for that Obligation. + for (IPair successorAndExceptionType : + getSuccessorsExceptIgnoredExceptions(currentBlock)) { + + // A *mutable* set that eventually holds the set of dataflow facts to be propagated to + // successor blocks. The set is initialized to the current dataflow facts and updated by + // the methods invoked in the for loop below. + Set obligations = new LinkedHashSet<>(incomingObligations); + + // PERFORMANCE NOTE: The computed changes to `obligations` are mostly the same for each + // successor block, but can vary slightly depending on the exception type. There might + // be some opportunities for optimization in this mostly-redundant work. + for (Node node : currentBlock.getNodes()) { + if (node instanceof AssignmentNode) { + updateObligationsForAssignment(obligations, cfg, (AssignmentNode) node); + } else if (node instanceof ReturnNode) { + updateObligationsForOwningReturn(obligations, cfg, (ReturnNode) node); + } else if (node instanceof MethodInvocationNode + || node instanceof ObjectCreationNode) { + updateObligationsForInvocation( + obligations, node, successorAndExceptionType.second); + } + // All other types of nodes are ignored. This is safe, because other kinds of + // nodes cannot create or modify the resource-alias sets that the algorithm is + // tracking. + } + + propagateObligationsToSuccessorBlock( + obligations, + currentBlock, + successorAndExceptionType.first, + successorAndExceptionType.second, + visited, + worklist); + } + } + + /** + * Helper for {@link #propagateObligationsToSuccessorBlocks(ControlFlowGraph, Set, Block, Set, + * Deque)} that propagates obligations along a single edge. + * + * @param obligations the Obligations for the current block + * @param currentBlock the current block + * @param successor a successor of the current block + * @param exceptionType the type of edge from currentBlock to successor + * : null for normal control flow, or a throwable type for exceptional + * control flow + * @param visited block-Obligations pairs already analyzed or already on the worklist + * @param worklist current worklist + */ + private void propagateObligationsToSuccessorBlock( + Set obligations, + Block currentBlock, + Block successor, + @Nullable TypeMirror exceptionType, + Set visited, + Deque worklist) { + List currentBlockNodes = currentBlock.getNodes(); + // successorObligations eventually contains the Obligations to propagate to successor. + // The loop below mutates it. + Set successorObligations = new LinkedHashSet<>(); + // A detailed reason to give in the case that the last resource alias of an Obligation + // goes out of scope without a called-methods type that satisfies the corresponding + // must-call obligation along the current control-flow edge. Computed here for + // efficiency; used in the loop over the Obligations, below. + String exitReasonForErrorMessage = + exceptionType == null + ? + // Technically the variable may be going out of scope before the method + // exit, but that doesn't seem to provide additional helpful + // information. + "regular method exit" + : "possible exceptional exit due to " + + ((ExceptionBlock) currentBlock).getNode().getTree() + + " with exception type " + + exceptionType; + // Computed outside the Obligation loop for efficiency. + AccumulationStore regularStoreOfSuccessor = analysis.getInput(successor).getRegularStore(); + for (Obligation obligation : obligations) { + // This boolean is true if there is no evidence that the Obligation does not go out + // of scope - that is, if there is definitely a resource alias that is in scope in + // the successor. + boolean obligationGoesOutOfScopeBeforeSuccessor = true; + for (ResourceAlias resourceAlias : obligation.resourceAliases) { + if (aliasInScopeInSuccessor(regularStoreOfSuccessor, resourceAlias)) { + obligationGoesOutOfScopeBeforeSuccessor = false; + break; + } + } + // This check is to determine if this Obligation's resource aliases are definitely + // going out of scope: if this is an exit block or there is no information about any + // of them in the successor store, all aliases must be going out of scope and a + // consistency check should occur. + if (successor.getType() == BlockType.SPECIAL_BLOCK /* special blocks are exit blocks */ + || obligationGoesOutOfScopeBeforeSuccessor) { + MustCallAnnotatedTypeFactory mcAtf = + typeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + + // If successor is an exceptional successor, and Obligation represents the + // temporary variable for currentBlock's node, do not propagate or do a + // consistency check, as in the exceptional case the "assignment" to the + // temporary variable does not succeed. + // + // Note that this test cannot be "successor.getType() == + // BlockType.EXCEPTIONAL_BLOCK", because not every exceptional successor is an + // exceptional block. For example, the successor might be a regular block + // (containing a catch clause, for example), or a special block indicating an + // exceptional exit. Nor can this test be "currentBlock.getType() == + // BlockType.EXCEPTIONAL_BLOCK", because some exception types are ignored. + // Whether exceptionType is null captures the logic of both of these cases. + if (exceptionType != null) { + Node exceptionalNode = + NodeUtils.removeCasts(((ExceptionBlock) currentBlock).getNode()); + LocalVariableNode tmpVarForExcNode = + typeFactory.getTempVarForNode(exceptionalNode); + if (tmpVarForExcNode != null + && obligation.resourceAliases.size() == 1 + && obligation.canBeSatisfiedThrough(tmpVarForExcNode)) { + continue; + } + } + + // Always propagate the Obligation to the successor if current block represents + // code nested in a cast. Without this logic, the analysis may report a false + // positive when the Obligation represents a temporary variable for a nested + // expression, as the temporary may not appear in the successor store and hence + // seems to be going out of scope. The temporary will be handled with special + // logic; casts are unwrapped at various points in the analysis. + if (currentBlockNodes.size() == 1 && inCast(currentBlockNodes.get(0))) { + successorObligations.add(obligation); + continue; + } + + // At this point, a consistency check will definitely occur, unless the + // obligation was derived from a MustCallAlias parameter. If it was, an error is + // immediately issued, because such a parameter should not go out of scope + // without its obligation being resolved some other way. + if (obligation.derivedFromMustCallAlias()) { + // MustCallAlias annotations only have meaning if the method returns + // normally, so issue an error if and only if this exit is happening on a + // normal exit path. + if (exceptionType == null + && obligation.whenToEnforce.contains(MethodExitKind.NORMAL_RETURN)) { + checker.reportError( + obligation.resourceAliases.asList().get(0).tree, + "mustcallalias.out.of.scope", + exitReasonForErrorMessage); + } + // Whether or not an error is issued, the check is now complete - there is + // no further checking to do on a must-call-alias-derived obligation along + // an exceptional path. + continue; + } + + // Which stores from the called-methods and must-call checkers are used in + // the consistency check varies depending on the context. The rules are: + // 1. if the current block has no nodes (and therefore the store must come from + // a block + // rather than a node): + // 1a. if there is information about any alias in the resource alias set + // in the successor store, use the successor's CM and MC stores, which + // contain whatever information is true after this block finishes. + // 1b. if there is not any information about any alias in the resource alias + // set in the successor store, use the current blocks' CM and MC stores, + // which contain whatever information is true before this (empty) block. + // 2. if the current block has one or more nodes, always use the CM store after + // the last node. To decide which MC store to use: + // 2a. if the last node in the block is the invocation of an + // @CreatesMustCallFor method that might throw an exception, and the + // consistency check is for an exceptional path, use the MC store + // immediately before the method invocation, because the method threw an + // exception rather than finishing and therefore did not actually create + // any must-call obligation, so the MC store after might contain + // must-call obligations that do not need to be fulfilled along this + // path. + // 2b. in all other cases, use the MC store from after the last node in + // the block. + CFStore mcStore; + AccumulationStore cmStore; + if (currentBlockNodes.size() == 0 /* currentBlock is special or conditional */) { + cmStore = + obligationGoesOutOfScopeBeforeSuccessor + ? analysis.getInput(currentBlock).getRegularStore() // 1a. (CM) + : regularStoreOfSuccessor; // 1b. (CM) + mcStore = + mcAtf.getStoreForBlock( + obligationGoesOutOfScopeBeforeSuccessor, + currentBlock, // 1a. (MC) + successor); // 1b. (MC) + } else { + // In this case, current block has at least one node. + // Use the called-methods store immediately after the last node in + // currentBlock. + Node last = currentBlockNodes.get(currentBlockNodes.size() - 1); // 2. (CM) + + if (cmStoreAfter.containsKey(last)) { + cmStore = cmStoreAfter.get(last); + } else { + cmStore = typeFactory.getStoreAfter(last); + cmStoreAfter.put(last, cmStore); + } + // If this is an exceptional block, check the MC store beforehand to avoid + // issuing an error about a call to a CreatesMustCallFor method that might + // throw an exception. Otherwise, use the store after. + if (exceptionType != null && isInvocationOfCreatesMustCallForMethod(last)) { + mcStore = mcAtf.getStoreBefore(last); // 2a. (MC) + } else { + if (mcStoreAfter.containsKey(last)) { + mcStore = mcStoreAfter.get(last); + } else { + mcStore = mcAtf.getStoreAfter(last); // 2b. (MC) + mcStoreAfter.put(last, mcStore); + } + } + } + + MethodExitKind exitKind = + exceptionType == null + ? MethodExitKind.NORMAL_RETURN + : MethodExitKind.EXCEPTIONAL_EXIT; + if (obligation.whenToEnforce.contains(exitKind)) { + checkMustCall(obligation, cmStore, mcStore, exitReasonForErrorMessage); + } + } else { + // In this case, there is info in the successor store about some alias in the + // Obligation. + // Handles the possibility that some resource in the Obligation may go out of + // scope. + Set copyOfResourceAliases = + new LinkedHashSet<>(obligation.resourceAliases); + copyOfResourceAliases.removeIf( + alias -> !aliasInScopeInSuccessor(regularStoreOfSuccessor, alias)); + successorObligations.add( + new Obligation(copyOfResourceAliases, obligation.whenToEnforce)); + } + } + + propagate(new BlockWithObligations(successor, successorObligations), visited, worklist); + } + + /** + * Returns true if {@code alias.reference} is definitely in-scope in the successor store: that + * is, there is a value for it in {@code successorStore}. + * + * @param successorStore the regular store of the successor block + * @param alias the resource alias to check + * @return true if the variable is definitely in scope for the purposes of the consistency + * checking algorithm in the successor block from which the store came + */ + private boolean aliasInScopeInSuccessor(AccumulationStore successorStore, ResourceAlias alias) { + return successorStore.getValue(alias.reference) != null; + } + + /** + * Returns true if node is a MethodInvocationNode of a method with a CreatesMustCallFor + * annotation. + * + * @param node a node + * @return true if node is a MethodInvocationNode of a method with a CreatesMustCallFor + * annotation + */ + private boolean isInvocationOfCreatesMustCallForMethod(Node node) { + if (!(node instanceof MethodInvocationNode)) { + return false; + } + MethodInvocationNode miNode = (MethodInvocationNode) node; + return typeFactory.hasCreatesMustCallFor(miNode); + } + + /** + * Finds {@link Owning} formal parameters for the method corresponding to a CFG. + * + * @param cfg the CFG + * @return the owning formal parameters of the method that corresponds to the given cfg, or an + * empty set if the given CFG doesn't correspond to a method body + */ + private Set computeOwningParameters(ControlFlowGraph cfg) { + // TODO what about lambdas? + if (cfg.getUnderlyingAST().getKind() == Kind.METHOD) { + MethodTree method = ((UnderlyingAST.CFGMethod) cfg.getUnderlyingAST()).getMethod(); + Set result = new LinkedHashSet<>(1); + for (VariableTree param : method.getParameters()) { + VariableElement paramElement = TreeUtils.elementFromDeclaration(param); + boolean hasMustCallAlias = typeFactory.hasMustCallAlias(paramElement); + if (hasMustCallAlias + || (typeFactory.declaredTypeHasMustCall(param) + && !noLightweightOwnership + && paramElement.getAnnotation(Owning.class) != null)) { + result.add( + new Obligation( + ImmutableSet.of( + new ResourceAlias( + new LocalVariable(paramElement), + paramElement, + param, + hasMustCallAlias)), + Collections.singleton(MethodExitKind.NORMAL_RETURN))); + // Increment numMustCall for each @Owning parameter tracked by the enclosing + // method. + incrementNumMustCall(paramElement); + } + } + return result; + } + return Collections.emptySet(); + } + + /** + * Checks whether there is some resource alias set R in {@code obligations} such that + * R contains a {@link ResourceAlias} whose local variable is {@code node}. + * + * @param obligations the set of Obligations to search + * @param var the local variable to look for + * @return true iff there is a resource alias set in {@code obligations} that contains node + */ + private static boolean varTrackedInObligations( + Set obligations, LocalVariableNode var) { + for (Obligation obligation : obligations) { + if (obligation.canBeSatisfiedThrough(var)) { + return true; + } + } + return false; + } + + /** + * Gets the Obligation whose resource aliase set contains the given local variable, if one + * exists in {@code obligations}. + * + * @param obligations set of Obligations + * @param node variable of interest + * @return the Obligation in {@code obligations} whose resource alias set contains {@code node}, + * or {@code null} if there is no such Obligation + */ + /*package-private*/ static @Nullable Obligation getObligationForVar( + Set obligations, LocalVariableNode node) { + for (Obligation obligation : obligations) { + if (obligation.canBeSatisfiedThrough(node)) { + return obligation; + } + } + return null; + } + + /** + * For the given Obligation, checks that at least one of its variables has its {@code @MustCall} + * obligation satisfied, based on {@code @CalledMethods} and {@code @MustCall} types in the + * given stores. + * + * @param obligation the Obligation + * @param cmStore the called-methods store + * @param mcStore the must-call store + * @param outOfScopeReason if the {@code @MustCall} obligation is not satisfied, a useful + * explanation to include in the error message + */ + private void checkMustCall( + Obligation obligation, + AccumulationStore cmStore, + CFStore mcStore, + String outOfScopeReason) { + + Map> mustCallValues = + obligation.getMustCallMethods(typeFactory, mcStore); + + // optimization: if mustCallValues is null, always issue a warning (there is no way to + // satisfy the check). A null mustCallValue occurs when the type is top (@MustCallUnknown). + if (mustCallValues == null) { + // Report the error at the first alias' definition. This choice is arbitrary but + // consistent. + ResourceAlias firstAlias = obligation.resourceAliases.iterator().next(); + if (!reportedErrorAliases.contains(firstAlias)) { + if (!checker.shouldSkipUses(TreeUtils.elementFromTree(firstAlias.tree))) { + reportedErrorAliases.add(firstAlias); + checker.reportError( + firstAlias.tree, + "required.method.not.known", + firstAlias.stringForErrorMessage(), + firstAlias.reference.getType().toString(), + outOfScopeReason); + } + } + return; + } + if (mustCallValues.isEmpty()) { + throw new TypeSystemError( + "unexpected empty must-call values for obligation " + obligation); + } + + boolean mustCallSatisfied = false; + for (ResourceAlias alias : obligation.resourceAliases) { + + List mustCallValuesForAlias = mustCallValues.get(alias); + // optimization when there are no methods to call + if (mustCallValuesForAlias.isEmpty()) { + mustCallSatisfied = true; + break; + } + + // sometimes the store is null! this looks like a bug in checker dataflow. + // TODO track down and report the root-cause bug + AccumulationValue cmValue = cmStore != null ? cmStore.getValue(alias.reference) : null; + AnnotationMirror cmAnno = null; + + if (cmValue != null) { // When store contains the lhs + Set accumulatedValues = cmValue.getAccumulatedValues(); + if (accumulatedValues != null) { // type variable or wildcard type + cmAnno = + typeFactory.createCalledMethods( + accumulatedValues.toArray(new String[0])); + } else { + for (AnnotationMirror anno : cmValue.getAnnotations()) { + if (AnnotationUtils.areSameByName( + anno, + "org.checkerframework.checker.calledmethods.qual.CalledMethods")) { + cmAnno = anno; + } + } + } + } + if (cmAnno == null) { + cmAnno = + typeFactory + .getAnnotatedType(alias.element) + .getEffectiveAnnotationInHierarchy(typeFactory.top); + } + + if (calledMethodsSatisfyMustCall(mustCallValuesForAlias, cmAnno)) { + mustCallSatisfied = true; + break; + } + } + + if (!mustCallSatisfied) { + // Report the error at the first alias' definition. This choice is arbitrary but + // consistent. + ResourceAlias firstAlias = obligation.resourceAliases.iterator().next(); + if (!reportedErrorAliases.contains(firstAlias)) { + if (!checker.shouldSkipUses(TreeUtils.elementFromTree(firstAlias.tree))) { + reportedErrorAliases.add(firstAlias); + checker.reportError( + firstAlias.tree, + "required.method.not.called", + formatMissingMustCallMethods(mustCallValues.get(firstAlias)), + firstAlias.stringForErrorMessage(), + firstAlias.reference.getType().toString(), + outOfScopeReason); + } + } + } + } + + /** + * Increment the -AcountMustCall counter. + * + * @param node the node being counted, to extract the type + */ + private void incrementNumMustCall(Node node) { + if (countMustCall) { + TypeMirror type = node.getType(); + incrementMustCallImpl(type); + } + } + + /** + * Increment the -AcountMustCall counter. + * + * @param elt the elt being counted, to extract the type + */ + private void incrementNumMustCall(Element elt) { + if (countMustCall) { + TypeMirror type = elt.asType(); + incrementMustCallImpl(type); + } + } + + /** + * Shared implementation for the two version of countMustCall. Don't call this directly. + * + * @param type the type of the object that has a must call obligation + */ + private void incrementMustCallImpl(TypeMirror type) { + // only count uses of JDK classes, since that's what the paper reported + if (!isJdkClass(TypesUtils.getTypeElement(type).getQualifiedName().toString())) { + return; + } + checker.numMustCall++; + } + + /** + * Is the given class a java* class? This is a heuristic for whether the class was defined in + * the JDK. + * + * @param qualifiedName a fully qualified name of a class + * @return true iff the type's fully-qualified name starts with "java", indicating that it is + * from a java.* or javax.* package (probably) + */ + /*package-private*/ static boolean isJdkClass(String qualifiedName) { + return qualifiedName.startsWith("java"); + } + + /** + * Do the called methods represented by the {@link CalledMethods} type {@code cmAnno} include + * all the methods in {@code mustCallValues}? + * + * @param mustCallValues the strings representing the must-call obligations + * @param cmAnno an annotation from the called-methods type hierarchy + * @return true iff cmAnno is a subtype of a called-methods annotation with the same values as + * mustCallValues + */ + /*package-private*/ boolean calledMethodsSatisfyMustCall( + List mustCallValues, AnnotationMirror cmAnno) { + // Create this annotation and use a subtype test because there's no guarantee that + // cmAnno is actually an instance of CalledMethods: it could be CMBottom or CMPredicate. + AnnotationMirror cmAnnoForMustCallMethods = + typeFactory.createCalledMethods( + mustCallValues.toArray(new String[mustCallValues.size()])); + return typeFactory + .getQualifierHierarchy() + .isSubtypeQualifiersOnly(cmAnno, cmAnnoForMustCallMethods); + } + + /** + * If the input {@code state} has not been visited yet, add it to {@code visited} and {@code + * worklist}. + * + * @param state the current state + * @param visited the states that have been analyzed or are already on the worklist + * @param worklist the states that will be analyzed + */ + private static void propagate( + BlockWithObligations state, + Set visited, + Deque worklist) { + + if (visited.add(state)) { + worklist.add(state); + } + } + + /** + * Formats a list of must-call method names to be printed in an error message. + * + * @param mustCallVal the list of must-call strings + * @return a formatted string + */ + /*package-private*/ static String formatMissingMustCallMethods(List mustCallVal) { + int size = mustCallVal.size(); + if (size == 0) { + throw new TypeSystemError("empty mustCallVal " + mustCallVal); + } else if (size == 1) { + return "method " + mustCallVal.get(0); + } else { + return "methods " + String.join(", ", mustCallVal); + } + } + + /** + * A pair of a {@link Block} and a set of dataflow facts on entry to the block. Each dataflow + * fact represents a set of resource aliases for some tracked resource. The analyzer's worklist + * consists of BlockWithObligations objects, each representing the need to handle the set of + * dataflow facts reaching the block during analysis. + */ + /*package-private*/ static class BlockWithObligations { + + /** The block. */ + public final Block block; + + /** The dataflow facts. */ + public final ImmutableSet obligations; + + /** + * Create a new BlockWithObligations from a block and a set of dataflow facts. + * + * @param b the block + * @param obligations the set of incoming Obligations at the start of the block (may be the + * empty set) + */ + public BlockWithObligations(Block b, Set obligations) { + this.block = b; + this.obligations = ImmutableSet.copyOf(obligations); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BlockWithObligations that = (BlockWithObligations) o; + return block.equals(that.block) && obligations.equals(that.obligations); + } + + @Override + public int hashCode() { + return Objects.hash(block, obligations); + } + + @Override + public String toString() { + return String.format( + "BWO{%s %d, %d obligations %d}", + block.getType(), block.getUid(), obligations.size(), obligations.hashCode()); + } + + /** + * Returns a printed representation of a collection of BlockWithObligations. If a + * BlockWithObligations appears multiple times in the collection, it is printed more + * succinctly after the first time. + * + * @param bwos a collection of BlockWithObligations, to format + * @return a printed representation of a collection of BlockWithObligations + */ + public static String collectionToString(Collection bwos) { + List blocksWithDuplicates = new ArrayList<>(); + for (BlockWithObligations bwo : bwos) { + blocksWithDuplicates.add(bwo.block); + } + List duplicateBlocks = duplicates(blocksWithDuplicates); + StringJoiner result = new StringJoiner(", ", "BWOs[", "]"); + for (BlockWithObligations bwo : bwos) { + ImmutableSet obligations = bwo.obligations; + if (duplicateBlocks.contains(bwo.block)) { + result.add( + String.format( + "BWO{%s %d, %d obligations %s}", + bwo.block.getType(), + bwo.block.getUid(), + obligations.size(), + obligations)); + } else { + result.add( + String.format( + "BWO{%s %d, %d obligations}", + bwo.block.getType(), bwo.block.getUid(), obligations.size())); + } + } + return result.toString(); + } + } + + // TODO: Use from plume-lib's CollectionsPlume once version 1.9.0 is released. + /** + * Returns the elements (once each) that appear more than once in the given collection. + * + * @param the type of elements + * @param c a collection + * @return the elements (once each) that appear more than once in the given collection + */ + public static List duplicates(Collection c) { + // Inefficient (because of streams) but simple implementation. + Set withoutDuplicates = new HashSet<>(); + return c.stream().filter(n -> !withoutDuplicates.add(n)).collect(Collectors.toList()); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallInference.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallInference.java new file mode 100644 index 000000000000..af7c31ca27ba --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallInference.java @@ -0,0 +1,978 @@ +package org.checkerframework.checker.resourceleak; + +import com.google.common.collect.ImmutableSet; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.VariableTree; + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.mustcall.qual.MustCallAlias; +import org.checkerframework.checker.mustcall.qual.NotOwning; +import org.checkerframework.checker.mustcall.qual.Owning; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.BlockWithObligations; +import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.MethodExitKind; +import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation; +import org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.ResourceAlias; +import org.checkerframework.common.accumulation.AccumulationStore; +import org.checkerframework.common.accumulation.AccumulationValue; +import org.checkerframework.common.wholeprograminference.WholeProgramInference; +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.ConditionalBlock; +import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock; +import org.checkerframework.dataflow.cfg.node.ArrayCreationNode; +import org.checkerframework.dataflow.cfg.node.AssignmentNode; +import org.checkerframework.dataflow.cfg.node.FieldAccessNode; +import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; +import org.checkerframework.dataflow.cfg.node.ReturnNode; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.expression.LocalVariable; +import org.checkerframework.dataflow.util.NodeUtils; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.TreePathUtil; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; +import org.plumelib.util.CollectionsPlume; + +import java.util.ArrayDeque; +import java.util.Arrays; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; + +/** + * This class implements the annotation inference algorithm for the Resource Leak Checker. It infers + * annotations such as {@code @}{@link Owning} on owning fields and parameters, {@code @}{@link + * NotOwning} on return types, {@code @}{@link EnsuresCalledMethods} on methods, {@code @}{@link + * MustCallAlias} on parameters and return types, and {@code @}{@link InheritableMustCall} on class + * declarations. + * + *

    Each instance of this class corresponds to a single control flow graph (CFG), typically + * representing a method. The entry method of this class is {@link + * #runMustCallInference(ResourceLeakAnnotatedTypeFactory, ControlFlowGraph, + * MustCallConsistencyAnalyzer)}, invoked from the {@link + * ResourceLeakAnnotatedTypeFactory#postAnalyze} method when Whole Program Inference is enabled. + * + *

    The algorithm determines if the @MustCall obligation of a field is satisfied along some path + * leading to the regular exit point of the method. If the obligation is satisfied, the algorithm + * adds an @Owning annotation on the field and an @EnsuresCalledMethods annotation on the method + * being analyzed. Additionally, if the method being analyzed satisfies the must-call obligation of + * all the enclosed owning fields, the algorithm adds a @InheritableMustCall annotation on the + * enclosing class. + * + *

    Note: This class makes the assumption that the must-call set has only one element. Must-call + * sets with more than one element may be supported in the future. + * + *

    See {@link ResourceLeakChecker#ENABLE_WPI_FOR_RLC} for an explanation of the meaning of the + * flags {@code -Ainfer} and {@code -AenableWpiForRlc}. + * + * @see Automatic + * Inference of Resource Leak Specifications + */ +public class MustCallInference { + + /** + * This map keeps the fields that have been inferred to be disposed within the current method. + * Keys represent inferred owning fields, and values contain the must-call method names (Note: + * currently this code assumes that the must-call set only contains one element). When inference + * finishes, all of the fields in this map will be given an @Owning annotation. Note that this + * map is not monotonically-increasing: fields may be added to this map and then removed during + * inference. For example, if a field's must-call method is called, it is added to this map. If, + * in a later statement in the same method, the same field is re-assigned, it will be removed + * from this map (since the field assignment invalidated the previously-inferred disposing of + * the obligation). + */ + private final Map disposedFields = new LinkedHashMap<>(); + + /** + * The owned fields. This includes: + * + *

      + *
    • fields with written {@code @Owning} annotations, and + *
    • the inferred owning fields in this analysis. + *
    + * + * This set is a superset of the key set in the {@code disposedFields} map. + */ + private final Set owningFields = new HashSet<>(); + + /** + * The type factory for the Resource Leak Checker, which is used to access the Must Call + * Checker. + */ + private final ResourceLeakAnnotatedTypeFactory resourceLeakAtf; + + /** The MustCallConsistencyAnalyzer. */ + private final MustCallConsistencyAnalyzer mcca; + + /** The {@link Owning} annotation. */ + protected final AnnotationMirror OWNING; + + /** The {@link NotOwning} annotation. */ + protected final AnnotationMirror NOTOWNING; + + /** The {@link MustCallAlias} annotation. */ + protected final AnnotationMirror MUSTCALLALIAS; + + /** + * The control flow graph of the current method. There is a separate MustCallInference for each + * method. + */ + private final ControlFlowGraph cfg; + + /** The MethodTree of the current method. */ + private final MethodTree methodTree; + + /** The element for the current method. */ + private final ExecutableElement methodElt; + + /** The tree for the enclosing class of the current method. */ + private final ClassTree classTree; + + /** + * The element for the enclosing class of the current method. It can be null for certain kinds + * of anonymous classes, such as PolyCollectorTypeVar.java in the all-systems test suite. + */ + private final @Nullable TypeElement classElt; + + /** + * This map is used to track must-alias relationships between the obligations that are + * resource-aliased to the return nodes and method parameters. The keys are the obligation of + * return nodes, and the values are the index of current method formal parameter (1-based) that + * is aliased with the return node. This map will be used to infer the {@link MustCallAlias} + * annotation for method parameters. + */ + private final Map returnObligationToParameter = new HashMap<>(); + + /** + * Creates a MustCallInference instance. + * + * @param resourceLeakAtf the type factory + * @param cfg the control flow graph of the method to check + * @param mcca the MustCallConsistencyAnalyzer + */ + /*package-private*/ MustCallInference( + ResourceLeakAnnotatedTypeFactory resourceLeakAtf, + ControlFlowGraph cfg, + MustCallConsistencyAnalyzer mcca) { + this.resourceLeakAtf = resourceLeakAtf; + this.mcca = mcca; + this.cfg = cfg; + this.OWNING = + AnnotationBuilder.fromClass(this.resourceLeakAtf.getElementUtils(), Owning.class); + this.NOTOWNING = + AnnotationBuilder.fromClass( + this.resourceLeakAtf.getElementUtils(), NotOwning.class); + this.MUSTCALLALIAS = + AnnotationBuilder.fromClass( + this.resourceLeakAtf.getElementUtils(), MustCallAlias.class); + this.methodTree = ((UnderlyingAST.CFGMethod) cfg.getUnderlyingAST()).getMethod(); + this.methodElt = TreeUtils.elementFromDeclaration(methodTree); + this.classTree = TreePathUtil.enclosingClass(resourceLeakAtf.getPath(methodTree)); + this.classElt = TreeUtils.elementFromDeclaration(classTree); + if (classElt != null) { + for (Element memberElt : classElt.getEnclosedElements()) { + if (memberElt.getKind().isField() && resourceLeakAtf.hasOwning(memberElt)) { + owningFields.add((VariableElement) memberElt); + } + } + } + } + + /** + * Creates a MustCallInference instance and runs the inference algorithm. This method is called + * by the {@link ResourceLeakAnnotatedTypeFactory#postAnalyze} method if Whole Program Inference + * is enabled. + * + * @param resourceLeakAtf the type factory + * @param cfg the control flow graph of the method to check + * @param mcca the MustCallConsistencyAnalyzer + */ + /*package-private*/ static void runMustCallInference( + ResourceLeakAnnotatedTypeFactory resourceLeakAtf, + ControlFlowGraph cfg, + MustCallConsistencyAnalyzer mcca) { + MustCallInference mustCallInferenceLogic = + new MustCallInference(resourceLeakAtf, cfg, mcca); + mustCallInferenceLogic.runInference(); + } + + /** + * Runs the inference algorithm on the current method (the {@link #cfg} field). It discovers + * annotations such as {@code @}{@link Owning} on owning fields and parameters, {@code @}{@link + * NotOwning} on return types, {@code @}{@link EnsuresCalledMethods} on methods, {@code @}{@link + * MustCallAlias} on parameters and return types, and {@code @}{@link InheritableMustCall} on + * class declarations. + * + *

    Operationally, it checks method invocations for fields and parameters (with + * non-empty @MustCall obligations) along all paths to the regular exit point. + */ + private void runInference() { + + Set visited = new HashSet<>(); + Deque worklist = new ArrayDeque<>(); + + BlockWithObligations entry = + new BlockWithObligations(cfg.getEntryBlock(), getNonEmptyMCParams(cfg)); + worklist.add(entry); + visited.add(entry); + + while (!worklist.isEmpty()) { + BlockWithObligations current = worklist.remove(); + + // Use a LinkedHashSet for determinism. + Set obligations = new LinkedHashSet<>(current.obligations); + + for (Node node : current.block.getNodes()) { + // The obligation set calculated for RLC differs from the Inference process. In the + // Inference process, it exclusively tracks parameters with non-empty must-call + // types, whether they have the @Owning annotation or not. However, there are some + // shared computations, such as updateObligationsWithInvocationResult, which is used + // during inference and could potentially affect the RLC result if it were called + // before the checking phase. However, calling + // updateObligationsWithInvocationResult() will not have any side effects on the + // outcome of the Resource Leak Checker. This is because the inference occurs within + // the postAnalyze method of the ResourceLeakAnnotatedTypeFactory, once the + // consistency analyzer has completed its process. + if (node instanceof MethodInvocationNode || node instanceof ObjectCreationNode) { + mcca.updateObligationsWithInvocationResult(obligations, node); + inferOwningFromInvocation(obligations, node); + } else if (node instanceof AssignmentNode) { + analyzeAssignmentNode(obligations, (AssignmentNode) node); + } else if (node instanceof ReturnNode) { + analyzeReturnNode(obligations, (ReturnNode) node); + } + } + + addNonExceptionalSuccessorsToWorklist(obligations, current.block, visited, worklist); + } + + addMemberAndClassAnnotations(); + } + + /** + * Analyzes a return statement and performs two computations. Does nothing if the return + * expression's type has an empty must-call obligation. + * + *

      + *
    • If the returned expression is a field with non-empty must-call obligations, adds a + * {@link NotOwning} annotation to the return type of the current method. Note the + * implication: if a method returns a field of this class at any return site, the + * return type is inferred to be non-owning. + *
    • Compute the index of the parameter that is an alias of the return node and add it the + * {@link #returnObligationToParameter} map. + *
    + * + * @param obligations the current set of tracked Obligations + * @param node the return node + */ + private void analyzeReturnNode(Set obligations, ReturnNode node) { + Node returnNode = node.getResult(); + returnNode = mcca.removeCastsAndGetTmpVarIfPresent(returnNode); + if (resourceLeakAtf.hasEmptyMustCallValue(returnNode.getTree())) { + return; + } + + if (returnNode instanceof FieldAccessNode) { + addNotOwningToMethodDecl(); + } else if (returnNode instanceof LocalVariableNode) { + Obligation returnNodeObligation = + MustCallConsistencyAnalyzer.getObligationForVar( + obligations, (LocalVariableNode) returnNode); + if (returnNodeObligation != null) { + returnObligationToParameter.put( + returnNodeObligation, getIndexOfParam(returnNodeObligation)); + } + } + } + + /** + * Adds inferred {@literal @Owning} annotations to fields, {@literal @EnsuresCalledMethods} + * annotations to the current method, {@literal @MustCallAlias} annotations to the parameter, + * and {@literal @InheritableMustCall} annotations to the enclosing class. + */ + private void addMemberAndClassAnnotations() { + WholeProgramInference wpi = resourceLeakAtf.getWholeProgramInference(); + assert wpi != null : "MustCallInference is running without WPI."; + for (VariableElement fieldElt : getOwningFields()) { + wpi.addFieldDeclarationAnnotation(fieldElt, OWNING); + } + if (!disposedFields.isEmpty()) { + addEnsuresCalledMethodsForDisposedFields(); + } + + // If all return statements alias the same parameter index, then add the @MustCallAlias + // annotation to that parameter and the return type. + if (!returnObligationToParameter.isEmpty()) { + if (returnObligationToParameter.values().stream().distinct().count() == 1) { + int indexOfParam = returnObligationToParameter.values().iterator().next(); + if (indexOfParam > 0) { + addMustCallAliasToFormalParameter(indexOfParam); + } + } + } + + addInheritableMustCallToClass(); + } + + /** + * Returns a set of obligations representing the formal parameters of the current method that + * have non-empty MustCall annotations. Returns an empty set if the given CFG doesn't correspond + * to a method body. + * + * @param cfg the control flow graph of the method to check + * @return a set of obligations representing the parameters with non-empty MustCall obligations + */ + private Set getNonEmptyMCParams(ControlFlowGraph cfg) { + // TODO what about lambdas? + if (cfg.getUnderlyingAST().getKind() != UnderlyingAST.Kind.METHOD) { + return Collections.emptySet(); + } + Set result = null; + for (VariableTree param : methodTree.getParameters()) { + if (resourceLeakAtf.declaredTypeHasMustCall(param)) { + VariableElement paramElement = TreeUtils.elementFromDeclaration(param); + if (result == null) { + result = new HashSet<>(2); + } + result.add( + new Obligation( + ImmutableSet.of( + new ResourceAlias( + new LocalVariable(paramElement), + paramElement, + param)), + Collections.singleton(MethodExitKind.NORMAL_RETURN))); + } + } + return result != null ? result : Collections.emptySet(); + } + + /** + * Retrieves the owning fields, including fields inferred as owning from the current iteration. + * + * @return the owning fields + */ + private Set getOwningFields() { + return owningFields; + } + + /** + * Adds an owning annotation to the formal parameter at the given index. + * + * @param index the index of a formal parameter of the current method (1-based) + */ + private void addOwningToParam(int index) { + WholeProgramInference wpi = resourceLeakAtf.getWholeProgramInference(); + wpi.addDeclarationAnnotationToFormalParameter(methodElt, index, OWNING); + } + + /** + * Adds the node to the disposedFields map and the owningFields set if it is a field and its + * must-call obligation is satisfied by the given method call. If so, it will be given + * an @Owning annotation later. + * + * @param node possibly an owning field + * @param invocation method invoked on the possible owning field + */ + private void inferOwningField(Node node, MethodInvocationNode invocation) { + Element nodeElt = TreeUtils.elementFromTree(node.getTree()); + if (nodeElt == null || !nodeElt.getKind().isField()) { + return; + } + if (resourceLeakAtf.isFieldWithNonemptyMustCallValue(nodeElt)) { + node = NodeUtils.removeCasts(node); + JavaExpression nodeJe = JavaExpression.fromNode(node); + AnnotationMirror cmAnno = getCalledMethodsAnno(invocation, nodeJe); + List mustCallValues = resourceLeakAtf.getMustCallValues(nodeElt); + if (mcca.calledMethodsSatisfyMustCall(mustCallValues, cmAnno)) { + assert !mustCallValues.isEmpty() + : "Must-call obligation of owning field " + nodeElt + " is empty."; + // TODO: generalize this to MustCall annotations with more than one element. + // Currently, this code assumes that the must-call set has only one element. + assert mustCallValues.size() == 1 + : "The must-call set of " + + nodeElt + + "should be a singleton: " + + mustCallValues; + disposedFields.put((VariableElement) nodeElt, mustCallValues.get(0)); + owningFields.add((VariableElement) nodeElt); + } + } + } + + /** + * Analyzes an assignment statement and performs the following computations. Does nothing if the + * rhs is not a local variable or parameter, or if the rhs has no must-call obligation. + * + *
      + *
    • 1) If the current method under analysis is a constructor, the left-hand side of the + * assignment is the only owning field of the enclosing class, and the rhs is an alias of + * a formal parameter, it adds an {@code @MustCallAlias} annotation to the formal + * parameter and the result type of the constructor. + *
    • 2) If the left-hand side of the assignment is an owning field, and the rhs is an alias + * of a formal parameter, it adds an {@code @Owning} annotation to the formal parameter. + *
    • 3) Otherwise, updates the set of tracked obligations to account for the + * (pseudo-)assignment, as in a gen-kill dataflow analysis problem. + *
    + * + * @param obligations the set of obligations to update + * @param assignmentNode the assignment statement + */ + private void analyzeAssignmentNode(Set obligations, AssignmentNode assignmentNode) { + Node lhs = assignmentNode.getTarget(); + Element lhsElement = TreeUtils.elementFromTree(lhs.getTree()); + // Use the temporary variable for the rhs if it exists. + Node rhs = mcca.removeCastsAndGetTmpVarIfPresent(assignmentNode.getExpression()); + + if (!(rhs instanceof LocalVariableNode)) { + return; + } + Obligation rhsObligation = + MustCallConsistencyAnalyzer.getObligationForVar( + obligations, (LocalVariableNode) rhs); + if (rhsObligation == null) { + return; + } + + if (lhsElement.getKind() == ElementKind.FIELD) { + if (!getOwningFields().contains(lhsElement)) { + return; + } + + // If the owning field is present in the disposedFields map and there is an assignment + // to the field, it must be removed from the set. This is essential since the + // disposedFields map is used for adding @EnsuresCalledMethods annotations to the + // current method later. Note that this removal doesn't affect the owning annotation we + // inferred for the field, as the owningField set is updated with the inferred owning + // field in the 'inferOwningField' method. + if (!TreeUtils.isConstructor(methodTree)) { + disposedFields.remove((VariableElement) lhsElement); + } + + int paramIndex = getIndexOfParam(rhsObligation); + if (paramIndex == -1) { + // We are only tracking formal parameter aliases. If the rhsObligation is not an + // alias of any of the formal parameters, it won't be present in the obligations + // set. Thus, skipping the rest of this method is fine. + return; + } + + if (TreeUtils.isConstructor(methodTree) && getOwningFields().size() == 1) { + // case 1 is satisfied. + addMustCallAliasToFormalParameter(paramIndex); + mcca.removeObligationsContainingVar(obligations, (LocalVariableNode) rhs); + } else { + // case 2 is satisfied. + addOwningToParam(paramIndex); + mcca.removeObligationsContainingVar(obligations, (LocalVariableNode) rhs); + } + + } else if (lhs instanceof LocalVariableNode) { + // Updates the set of tracked obligations. (case 4) + LocalVariableNode lhsVar = (LocalVariableNode) lhs; + mcca.updateObligationsForPseudoAssignment(obligations, assignmentNode, lhsVar, rhs); + } + } + + /** + * Return the (1-based) index of the method parameter that exist in the set of aliases of the + * given {@code obligation}, if one exists; otherwise, return -1. + * + * @param obligation the obligation + * @return the index of the current method parameter that exist in the set of aliases of the + * given obligation, if one exists; otherwise, return -1. + */ + private int getIndexOfParam(Obligation obligation) { + Set resourceAliases = obligation.resourceAliases; + List paramElts = + CollectionsPlume.mapList( + TreeUtils::elementFromDeclaration, methodTree.getParameters()); + for (ResourceAlias resourceAlias : resourceAliases) { + int paramIndex = paramElts.indexOf(resourceAlias.element); + if (paramIndex != -1) { + return paramIndex + 1; + } + } + + return -1; + } + + /** Adds a {@link NotOwning} annotation to the current method. */ + private void addNotOwningToMethodDecl() { + WholeProgramInference wpi = resourceLeakAtf.getWholeProgramInference(); + wpi.addMethodDeclarationAnnotation(methodElt, NOTOWNING); + } + + /** + * Adds a {@link MustCallAlias} annotation to the formal parameter at the given index. + * + * @param index the index of a formal parameter of the current method (1-based) + */ + private void addMustCallAliasToFormalParameter(int index) { + WholeProgramInference wpi = resourceLeakAtf.getWholeProgramInference(); + wpi.addMethodDeclarationAnnotation(methodElt, MUSTCALLALIAS); + wpi.addDeclarationAnnotationToFormalParameter(methodElt, index, MUSTCALLALIAS); + } + + /** + * Adds an {@link EnsuresCalledMethods} annotation to the current method for any owning field + * whose must-call obligation is satisfied within the current method, i.e., the fields in {@link + * #disposedFields}. + */ + private void addEnsuresCalledMethodsForDisposedFields() { + // The keys are the must-call method names, and the values are the set of fields on which + // those methods are called. This map is used to create a @EnsuresCalledMethods annotation + // for each set of fields that share the same must-call obligation. + Map> methodToFields = new LinkedHashMap<>(); + for (VariableElement disposedField : disposedFields.keySet()) { + String mustCallValue = disposedFields.get(disposedField); + String fieldName = "this." + disposedField.getSimpleName().toString(); + methodToFields + .computeIfAbsent(mustCallValue, k -> new LinkedHashSet<>()) + .add(fieldName); + } + + for (String mustCallValue : methodToFields.keySet()) { + Set fields = methodToFields.get(mustCallValue); + AnnotationMirror am = + createEnsuresCalledMethods( + fields.toArray(new String[fields.size()]), + new String[] {mustCallValue}); + WholeProgramInference wpi = resourceLeakAtf.getWholeProgramInference(); + wpi.addMethodDeclarationAnnotation(methodElt, am); + } + } + + /** + * Possibly adds an InheritableMustCall annotation on the enclosing class. + * + *

    Let the enclosing class be C. If C already has a non-empty MustCall type (that is written + * or inherited from one of its superclasses), this method preserves the exising must-call type + * to avoid infinite iteration. Otherwise, if the current method is not private and satisfies + * the must-call obligations of all the owning fields in C, it adds an InheritableMustCall + * annotation to C. + */ + private void addInheritableMustCallToClass() { + if (classElt == null) { + return; + } + + WholeProgramInference wpi = resourceLeakAtf.getWholeProgramInference(); + + List currentMustCallValues = resourceLeakAtf.getMustCallValues(classElt); + if (!currentMustCallValues.isEmpty()) { + // The class already has a MustCall annotation. + + // If it is inherited from a superclass, do nothing. + if (classElt.getSuperclass() != null) { + TypeMirror superType = classElt.getSuperclass(); + TypeElement superClassElt = TypesUtils.getTypeElement(superType); + if (superClassElt != null + && !resourceLeakAtf.getMustCallValues(superClassElt).isEmpty()) { + return; + } + } + + // If the enclosing class already has a non-empty @MustCall type, either added by + // programmers or inferred in previous iterations (not-inherited), we do not change it + // in the current analysis round to prevent potential inconsistencies and guarantee the + // termination of the inference algorithm. This becomes particularly important when + // multiple methods could satisfy the must-call obligation of the enclosing class. To + // ensure the existing @MustCall annotation is included in the inference result for this + // iteration, we re-add it. + assert currentMustCallValues.size() == 1 : "TODO: Handle multiple must-call values"; + AnnotationMirror am = + createInheritableMustCall(new String[] {currentMustCallValues.get(0)}); + wpi.addClassDeclarationAnnotation(classElt, am); + return; + } + + // If the current method is not private and satisfies the must-call obligation of all owning + // fields, then add (to the class) an InheritableMustCall annotation with the name of this + // method. + if (!methodTree.getModifiers().getFlags().contains(Modifier.PRIVATE)) { + // Since the result of getOwningFields() is a superset of the key set in disposedFields + // map, it is sufficient to check the equality of their sizes to determine if both sets + // are equal. + if (!disposedFields.isEmpty() && disposedFields.size() == getOwningFields().size()) { + AnnotationMirror am = + createInheritableMustCall(new String[] {methodTree.getName().toString()}); + wpi.addClassDeclarationAnnotation(classElt, am); + } + } + } + + /** + * Infers ownership transfer at the method call to infer @owning annotations for formal + * parameters of the current method, if the parameter is passed into the call and the + * corresponding formal parameter of the callee is @owning. + * + * @param obligations the current set of tracked Obligations + * @param invocation the method or constructor invocation + */ + private void inferOwningParamsViaOwnershipTransfer( + Set obligations, Node invocation) { + List paramsOfCurrentMethod = methodTree.getParameters(); + if (paramsOfCurrentMethod.isEmpty()) { + return; + } + List calleeParams = mcca.getParametersOfInvocation(invocation); + if (calleeParams.isEmpty()) { + return; + } + List arguments = mcca.getArgumentsOfInvocation(invocation); + + for (int i = 0; i < arguments.size(); i++) { + if (!resourceLeakAtf.hasOwning(calleeParams.get(i))) { + continue; + } + for (int j = 0; j < paramsOfCurrentMethod.size(); j++) { + VariableTree paramOfCurrMethod = paramsOfCurrentMethod.get(j); + if (resourceLeakAtf.hasEmptyMustCallValue(paramOfCurrMethod)) { + continue; + } + + Node arg = NodeUtils.removeCasts(arguments.get(i)); + VariableElement paramElt = TreeUtils.elementFromDeclaration(paramOfCurrMethod); + if (nodeAndElementResourceAliased(obligations, arg, paramElt)) { + addOwningToParam(j + 1); + break; + } + } + } + } + + /** + * Checks whether the given element is a resource alias of the given node in the provided set of + * obligations. + * + * @param obligations the current set of tracked Obligations + * @param node the node + * @param element the element + * @return true if {@code element} is a resource alias of {@code node} + */ + private boolean nodeAndElementResourceAliased( + Set obligations, Node node, VariableElement element) { + Set nodeAliases = getResourceAliasOfNode(obligations, node); + for (ResourceAlias nodeAlias : nodeAliases) { + Element nodeAliasElt = nodeAlias.element; + if (nodeAliasElt.equals(element)) { + return true; + } + } + return false; + } + + /** + * Infers @Owning annotations for fields or the parameters of the current method that are passed + * in the receiver or arguments position of a call if their must-call obligation is satisfied + * via the {@code invocation}. + * + * @param obligations the current set of tracked Obligations + * @param invocation a method invocation node to check + */ + private void inferOwningForRecieverOrFormalParamPassedToCall( + Set obligations, MethodInvocationNode invocation) { + Node receiver = invocation.getTarget().getReceiver(); + receiver = NodeUtils.removeCasts(receiver); + if (receiver.getTree() != null) { + inferOwningForParamOrField(obligations, invocation, receiver); + } + + for (Node argument : mcca.getArgumentsOfInvocation(invocation)) { + Node arg = mcca.removeCastsAndGetTmpVarIfPresent(argument); + // In the CFG, explicit passing of multiple arguments in the varargs position is + // represented via an ArrayCreationNode. In this case, it checks the called methods set + // of each argument passed in this position. + if (arg instanceof ArrayCreationNode) { + ArrayCreationNode varArgsNode = (ArrayCreationNode) arg; + for (Node varArgNode : varArgsNode.getInitializers()) { + inferOwningForParamOrField(obligations, invocation, varArgNode); + } + } else { + inferOwningForParamOrField(obligations, invocation, arg); + } + } + } + + /** + * Infers an @Owning annotation for the {@code arg} that can be a receiver or an argument passed + * into a method call if the must-call obligation of the {@code arg} is satisfied via the {@code + * invocation}. + * + * @param obligations the current set of tracked Obligations + * @param invocation the method invocation node to check + * @param arg a receiver or an argument passed to the method call + */ + private void inferOwningForParamOrField( + Set obligations, MethodInvocationNode invocation, Node arg) { + Element argElt = TreeUtils.elementFromTree(arg.getTree()); + // The must-call obligation of a field can be satisfied either through a call where it + // serves as a receiver or within the callee method when it is passed as an argument. + if (argElt != null && argElt.getKind().isField()) { + inferOwningField(arg, invocation); + return; + } + + List paramsOfCurrentMethod = methodTree.getParameters(); + outerLoop: + for (int i = 0; i < paramsOfCurrentMethod.size(); i++) { + VariableTree currentMethodParamTree = paramsOfCurrentMethod.get(i); + if (resourceLeakAtf.hasEmptyMustCallValue(currentMethodParamTree)) { + continue; + } + + VariableElement paramElt = TreeUtils.elementFromDeclaration(currentMethodParamTree); + if (!nodeAndElementResourceAliased(obligations, arg, paramElt)) { + continue; + } + + List mustCallValues = resourceLeakAtf.getMustCallValues(paramElt); + assert mustCallValues.size() <= 1 : "TODO: Handle larger must-call values sets"; + Set nodeAliases = getResourceAliasOfNode(obligations, arg); + for (ResourceAlias resourceAlias : nodeAliases) { + AnnotationMirror cmAnno = getCalledMethodsAnno(invocation, resourceAlias.reference); + if (mcca.calledMethodsSatisfyMustCall(mustCallValues, cmAnno)) { + addOwningToParam(i + 1); + break outerLoop; + } + } + } + } + + /** + * Returns the set of resource aliases associated with the given node, by looking up the + * corresponding obligation in the given set of obligations. + * + * @param obligations the set of obligations to search in + * @param node the node whose resource aliases are to be returned + * @return the resource aliases associated with the given node, or an empty set if the node has + * none + */ + private Set getResourceAliasOfNode(Set obligations, Node node) { + Node tempVar = mcca.getTempVarOrNode(node); + if (!(tempVar instanceof LocalVariableNode)) { + return Collections.emptySet(); + } + + Obligation argumentObligation = + MustCallConsistencyAnalyzer.getObligationForVar( + obligations, (LocalVariableNode) tempVar); + if (argumentObligation == null) { + return Collections.emptySet(); + } + return argumentObligation.resourceAliases; + } + + /** + * Infers @Owning or @MustCallAlias annotations for formal parameters of the enclosing method + * and @Owning annotations for fields of the enclosing class, as follows: + * + *

      + *
    • If a formal parameter is passed as an owning parameter, add an @Owning annotation to + * that formal parameter (see {@link #inferOwningParamsViaOwnershipTransfer}). + *
    • It calls {@link #inferOwningForRecieverOrFormalParamPassedToCall} to infer @Owning + * annotations for the receiver or arguments of a call by analyzing the called-methods set + * after the call. + *
    • It calls {@link #inferMustCallAliasFromThisOrSuperCall} to infer @MustCallAlias + * annotation for formal parameters and the result of the constructor. + *
    + * + * @param obligations the set of obligations to search in + * @param invocation the method or constructor invocation + */ + private void inferOwningFromInvocation(Set obligations, Node invocation) { + if (invocation instanceof ObjectCreationNode) { + // If the invocation corresponds to an object creation node, only ownership transfer + // checking is required, as constructor parameters may have an @Owning annotation. We + // do not handle @EnsuresCalledMethods annotations on constructors as we have not + // observed them in practice. + inferOwningParamsViaOwnershipTransfer(obligations, invocation); + } else if (invocation instanceof MethodInvocationNode) { + inferMustCallAliasFromThisOrSuperCall(obligations, (MethodInvocationNode) invocation); + inferOwningParamsViaOwnershipTransfer(obligations, invocation); + inferOwningForRecieverOrFormalParamPassedToCall( + obligations, (MethodInvocationNode) invocation); + } + } + + /** + * Adds the @MustCallAlias annotation to a method parameter when it is passed in + * a @MustCallAlias position during a constructor call using {@literal this} or {@literal + * super}. + * + * @param obligations the current set of tracked Obligations + * @param node a method invocation node + */ + private void inferMustCallAliasFromThisOrSuperCall( + Set obligations, MethodInvocationNode node) { + if (!TreeUtils.isSuperConstructorCall(node.getTree()) + && !TreeUtils.isThisConstructorCall(node.getTree())) { + return; + } + List calleeParams = mcca.getParametersOfInvocation(node); + List arguments = mcca.getArgumentsOfInvocation(node); + for (int i = 0; i < arguments.size(); i++) { + if (!resourceLeakAtf.hasMustCallAlias(calleeParams.get(i))) { + continue; + } + + Node arg = mcca.removeCastsAndGetTmpVarIfPresent(arguments.get(i)); + Obligation argObligation = + MustCallConsistencyAnalyzer.getObligationForVar( + obligations, (LocalVariableNode) arg); + if (argObligation == null) { + return; + } + int index = getIndexOfParam(argObligation); + if (index != -1) { + addMustCallAliasToFormalParameter(index); + break; + } + } + } + + /** + * Returns the called methods annotation for the given Java expression after the invocation + * node. + * + * @param invocation the MethodInvocationNode + * @param varJe a Java expression + * @return the called methods annotation for the {@code varJe} after the {@code invocation} node + */ + private AnnotationMirror getCalledMethodsAnno( + MethodInvocationNode invocation, JavaExpression varJe) { + AccumulationStore cmStoreAfter = resourceLeakAtf.getStoreAfter(invocation); + AccumulationValue cmValue = cmStoreAfter == null ? null : cmStoreAfter.getValue(varJe); + + AnnotationMirror cmAnno = null; + + if (cmValue != null) { + // The store contains the lhs. + Set accumulatedValues = cmValue.getAccumulatedValues(); + if (accumulatedValues != null) { // type variable or wildcard type + cmAnno = + resourceLeakAtf.createCalledMethods( + accumulatedValues.toArray(new String[0])); + } else { + for (AnnotationMirror anno : cmValue.getAnnotations()) { + if (AnnotationUtils.areSameByName( + anno, + "org.checkerframework.checker.calledmethods.qual.CalledMethods")) { + cmAnno = anno; + } + } + } + } + + if (cmAnno == null) { + cmAnno = resourceLeakAtf.top; + } + + return cmAnno; + } + + /** + * Adds all non-exceptional successors to {@code worklist}. + * + * @param obligations the current set of tracked Obligations + * @param curBlock the block whose successors to add to the worklist + * @param visited block-Obligations pairs already analyzed or already on the worklist + * @param worklist the worklist, which is side-effected by this method + */ + private void addNonExceptionalSuccessorsToWorklist( + Set obligations, + Block curBlock, + Set visited, + Deque worklist) { + + for (Block successor : getNonExceptionalSuccessors(curBlock)) { + // If successor is a special block, it must be the regular exit. + if (successor.getType() != Block.BlockType.SPECIAL_BLOCK) { + BlockWithObligations state = new BlockWithObligations(successor, obligations); + if (visited.add(state)) { + worklist.add(state); + } + } + } + } + + /** + * Returns the non-exceptional successors of a block. + * + * @param cur a block + * @return the successors of the given block + */ + private List getNonExceptionalSuccessors(Block cur) { + if (cur.getType() == Block.BlockType.CONDITIONAL_BLOCK) { + ConditionalBlock ccur = (ConditionalBlock) cur; + return Arrays.asList(ccur.getThenSuccessor(), ccur.getElseSuccessor()); + } + if (!(cur instanceof SingleSuccessorBlock)) { + throw new BugInCF("Not a conditional block nor a SingleSuccessorBlock: " + cur); + } + + Block successor = ((SingleSuccessorBlock) cur).getSuccessor(); + if (successor != null) { + return Collections.singletonList(successor); + } + return Collections.emptyList(); + } + + /** + * Creates an {@code @EnsuresCalledMethods} annotation with the given arguments. + * + * @param value the expressions that will have methods called on them + * @param methods the methods guaranteed to be invoked on the expressions + * @return an {@code @EnsuresCalledMethods} annotation with the given arguments + */ + private AnnotationMirror createEnsuresCalledMethods(String[] value, String[] methods) { + AnnotationBuilder builder = + new AnnotationBuilder( + resourceLeakAtf.getProcessingEnv(), EnsuresCalledMethods.class); + builder.setValue("value", value); + builder.setValue("methods", methods); + AnnotationMirror am = builder.build(); + return am; + } + + /** + * Creates an {@code @InheritableMustCall} annotation with the given arguments. + * + * @param methods methods that might need to be called on the expression whose type is annotated + * @return an {@code @InheritableMustCall} annotation with the given arguments + */ + private AnnotationMirror createInheritableMustCall(String[] methods) { + AnnotationBuilder builder = + new AnnotationBuilder( + resourceLeakAtf.getProcessingEnv(), InheritableMustCall.class); + Arrays.sort(methods); + builder.setValue("value", methods); + return builder.build(); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnalysis.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnalysis.java new file mode 100644 index 000000000000..0b68646c1c08 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnalysis.java @@ -0,0 +1,37 @@ +package org.checkerframework.checker.resourceleak; + +import org.checkerframework.checker.calledmethods.CalledMethodsAnalysis; +import org.checkerframework.checker.calledmethods.CalledMethodsAnnotatedTypeFactory; + +import javax.lang.model.type.TypeMirror; + +/** + * This variant of CFAnalysis extends the set of ignored exception types. + * + * @see ResourceLeakChecker#getIgnoredExceptions() + */ +public class ResourceLeakAnalysis extends CalledMethodsAnalysis { + + /** + * The set of exceptions to ignore, cached from {@link + * ResourceLeakChecker#getIgnoredExceptions()}. + */ + private final SetOfTypes ignoredExceptions; + + /** + * Creates a new {@code CalledMethodsAnalysis}. + * + * @param checker the checker + * @param factory the factory + */ + protected ResourceLeakAnalysis( + ResourceLeakChecker checker, CalledMethodsAnnotatedTypeFactory factory) { + super(checker, factory); + this.ignoredExceptions = checker.getIgnoredExceptions(); + } + + @Override + public boolean isIgnoredExceptionType(TypeMirror exceptionType) { + return ignoredExceptions.contains(getTypes(), exceptionType); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnnotatedTypeFactory.java new file mode 100644 index 000000000000..89d052cf7e29 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnnotatedTypeFactory.java @@ -0,0 +1,520 @@ +package org.checkerframework.checker.resourceleak; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.sun.source.tree.Tree; + +import org.checkerframework.checker.calledmethods.CalledMethodsAnnotatedTypeFactory; +import org.checkerframework.checker.calledmethods.EnsuresCalledMethodOnExceptionContract; +import org.checkerframework.checker.calledmethods.qual.CalledMethods; +import org.checkerframework.checker.calledmethods.qual.CalledMethodsBottom; +import org.checkerframework.checker.calledmethods.qual.CalledMethodsPredicate; +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.CreatesMustCallForElementSupplier; +import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; +import org.checkerframework.checker.mustcall.MustCallChecker; +import org.checkerframework.checker.mustcall.MustCallNoCreatesMustCallForChecker; +import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.MustCallAlias; +import org.checkerframework.checker.mustcall.qual.NotOwning; +import org.checkerframework.checker.mustcall.qual.Owning; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.framework.flow.CFStore; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.GenericAnnotatedTypeFactory; +import org.checkerframework.framework.util.Contract; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collections; +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.TypeElement; + +/** + * The type factory for the Resource Leak Checker. The main difference between this and the Called + * Methods type factory from which it is derived is that this version's {@link + * #postAnalyze(ControlFlowGraph)} method checks that must-call obligations are fulfilled. + */ +public class ResourceLeakAnnotatedTypeFactory extends CalledMethodsAnnotatedTypeFactory + implements CreatesMustCallForElementSupplier { + + /** The MustCall.value element/field. */ + private final ExecutableElement mustCallValueElement = + TreeUtils.getMethod(MustCall.class, "value", 0, processingEnv); + + /** The EnsuresCalledMethods.value element/field. */ + /*package-private*/ final ExecutableElement ensuresCalledMethodsValueElement = + TreeUtils.getMethod(EnsuresCalledMethods.class, "value", 0, processingEnv); + + /** The EnsuresCalledMethods.methods element/field. */ + /*package-private*/ final ExecutableElement ensuresCalledMethodsMethodsElement = + TreeUtils.getMethod(EnsuresCalledMethods.class, "methods", 0, processingEnv); + + /** The EnsuresCalledMethods.List.value element/field. */ + private final ExecutableElement ensuresCalledMethodsListValueElement = + TreeUtils.getMethod(EnsuresCalledMethods.List.class, "value", 0, processingEnv); + + /** The CreatesMustCallFor.List.value element/field. */ + private final ExecutableElement createsMustCallForListValueElement = + TreeUtils.getMethod(CreatesMustCallFor.List.class, "value", 0, processingEnv); + + /** The CreatesMustCallFor.value element/field. */ + private final ExecutableElement createsMustCallForValueElement = + TreeUtils.getMethod(CreatesMustCallFor.class, "value", 0, processingEnv); + + /** True if -AnoResourceAliases was passed on the command line. */ + private final boolean noResourceAliases; + + /** + * Bidirectional map to store temporary variables created for expressions with + * non-empty @MustCall obligations and the corresponding trees. Keys are the artificial local + * variable nodes created as temporary variables; values are the corresponding trees. + * + *

    Note that in an ideal world, this would be an {@code IdentityBiMap}: that is, a BiMap + * using {@link java.util.IdentityHashMap} as both of the backing maps. However, Guava doesn't + * have such a map AND their implementation is incompatible with IdentityHashMap as a backing + * map, because even their {@code AbstractBiMap} class uses {@code equals} calls in its + * implementation (and its documentation calls out that it and all its derived BiMaps are + * incompatible with IdentityHashMap as a backing map for this reason). Therefore, we use a + * regular BiMap. Doing so is safe iff 1) the LocalVariableNode keys all have different names, + * and 2) a standard Tree implementation that uses reference equality for equality (e.g., JCTree + * in javac) is used. + */ + private final BiMap tempVarToTree = HashBiMap.create(); + + /** + * Creates a new ResourceLeakAnnotatedTypeFactory. + * + * @param checker the checker associated with this type factory + */ + public ResourceLeakAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + this.noResourceAliases = checker.hasOption(MustCallChecker.NO_RESOURCE_ALIASES); + this.postInit(); + } + + /** + * Is the given element a candidate to be an owning field? A candidate owning field must have a + * non-empty must-call obligation. + * + * @param element a element + * @return true iff the given element is a field with non-empty @MustCall obligation + */ + /*package-private*/ boolean isFieldWithNonemptyMustCallValue(Element element) { + return element.getKind().isField() && !hasEmptyMustCallValue(element); + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return getBundledTypeQualifiers( + CalledMethods.class, CalledMethodsBottom.class, CalledMethodsPredicate.class); + } + + /** + * Creates a @CalledMethods annotation whose values are the given strings. + * + * @param val the methods that have been called + * @return an annotation indicating that the given methods have been called + */ + public AnnotationMirror createCalledMethods(String... val) { + return createAccumulatorAnnotation(Arrays.asList(val)); + } + + @Override + public void postAnalyze(ControlFlowGraph cfg) { + MustCallConsistencyAnalyzer mustCallConsistencyAnalyzer = + new MustCallConsistencyAnalyzer(this, (ResourceLeakAnalysis) this.analysis); + mustCallConsistencyAnalyzer.analyze(cfg); + + // Inferring owning annotations for @Owning fields/parameters, @EnsuresCalledMethods for + // finalizer methods and @InheritableMustCall annotations for the class declarations. + /* NO-AFU + if (getWholeProgramInference() != null) { + if (cfg.getUnderlyingAST().getKind() == UnderlyingAST.Kind.METHOD) { + MustCallInference.runMustCallInference(this, cfg, mustCallConsistencyAnalyzer); + } + } + */ + + super.postAnalyze(cfg); + tempVarToTree.clear(); + } + + @Override + protected ResourceLeakAnalysis createFlowAnalysis() { + return new ResourceLeakAnalysis((ResourceLeakChecker) checker, this); + } + + /** + * Returns whether the {@link MustCall#value} element/argument of the @MustCall annotation on + * the type of {@code tree} is definitely empty. + * + *

    This method only considers the declared type: it does not consider flow-sensitive + * refinement. + * + * @param tree a tree + * @return true if the Must Call type is non-empty or top + */ + /*package-private*/ boolean hasEmptyMustCallValue(Tree tree) { + MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory = + getTypeFactoryOfSubchecker(MustCallChecker.class); + AnnotatedTypeMirror mustCallAnnotatedType = + mustCallAnnotatedTypeFactory.getAnnotatedType(tree); + AnnotationMirror mustCallAnnotation = mustCallAnnotatedType.getAnnotation(MustCall.class); + if (mustCallAnnotation != null) { + return getMustCallValues(mustCallAnnotation).isEmpty(); + } else { + // Indicates @MustCallUnknown, which should be treated (conservatively) as if it + // contains + // some must call values. + return false; + } + } + + /** + * Returns whether the {@link MustCall#value} element/argument of the @MustCall annotation on + * the type of {@code element} is definitely empty. + * + *

    This method only considers the declared type: it does not consider flow-sensitive + * refinement. + * + * @param element an element + * @return true if the Must Call type is non-empty or top + */ + /*package-private*/ boolean hasEmptyMustCallValue(Element element) { + MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory = + getTypeFactoryOfSubchecker(MustCallChecker.class); + AnnotatedTypeMirror mustCallAnnotatedType = + mustCallAnnotatedTypeFactory.getAnnotatedType(element); + AnnotationMirror mustCallAnnotation = mustCallAnnotatedType.getAnnotation(MustCall.class); + if (mustCallAnnotation != null) { + return getMustCallValues(mustCallAnnotation).isEmpty(); + } else { + // Indicates @MustCallUnknown, which should be treated (conservatively) as if it + // contains + // some must call values. + return false; + } + } + + /** + * Returns the {@link MustCall#value} element/argument of the @MustCall annotation on the class + * type of {@code element}. If there is no such annotation, returns the empty list. + * + *

    Do not use this method to get the MustCall values of an {@link + * org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation}. Instead, + * use {@link + * org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation#getMustCallMethods(ResourceLeakAnnotatedTypeFactory, + * CFStore)}. + * + *

    Do not call {@link List#isEmpty()} on the result of this method: prefer to call {@link + * #hasEmptyMustCallValue(Element)}, which correctly accounts for @MustCallUnknown, instead. + * + * @param element an element + * @return the strings in its must-call type + */ + /*package-private*/ List getMustCallValues(Element element) { + MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory = + getTypeFactoryOfSubchecker(MustCallChecker.class); + AnnotatedTypeMirror mustCallAnnotatedType = + mustCallAnnotatedTypeFactory.getAnnotatedType(element); + AnnotationMirror mustCallAnnotation = mustCallAnnotatedType.getAnnotation(MustCall.class); + return getMustCallValues(mustCallAnnotation); + } + + /** + * Helper method for getting the must-call values from a must-call annotation. + * + * @param mustCallAnnotation a {@link MustCall} annotation, or null + * @return the strings in mustCallAnnotation's value element, or the empty list if + * mustCallAnnotation is null + */ + /*package-private*/ List getMustCallValues( + @Nullable AnnotationMirror mustCallAnnotation) { + if (mustCallAnnotation == null) { + return Collections.emptyList(); + } + return AnnotationUtils.getElementValueArray( + mustCallAnnotation, mustCallValueElement, String.class, Collections.emptyList()); + } + + /** + * Helper method to get the temporary variable that represents the given node, if one exists. + * + * @param node a node + * @return the tempvar for node's expression, or null if one does not exist + */ + /*package-private*/ @Nullable LocalVariableNode getTempVarForNode(Node node) { + return tempVarToTree.inverse().get(node.getTree()); + } + + /** + * Is the given node a temporary variable? + * + * @param node a node + * @return true iff the given node is a temporary variable + */ + /*package-private*/ boolean isTempVar(Node node) { + return tempVarToTree.containsKey(node); + } + + /** + * Gets the tree for a temporary variable + * + * @param node a node for a temporary variable + * @return the tree for {@code node} + */ + /*package-private*/ Tree getTreeForTempVar(Node node) { + if (!tempVarToTree.containsKey(node)) { + throw new TypeSystemError(node + " must be a temporary variable"); + } + return tempVarToTree.get(node); + } + + /** + * Registers a temporary variable by adding it to this type factory's tempvar map. + * + * @param tmpVar a temporary variable + * @param tree the tree of the expression the tempvar represents + */ + /*package-private*/ void addTempVar(LocalVariableNode tmpVar, Tree tree) { + if (!tempVarToTree.containsValue(tree)) { + tempVarToTree.put(tmpVar, tree); + } + } + + /** + * Returns true if the type of the tree includes a must-call annotation. Note that this method + * may not consider dataflow, and is only safe to use when you need the declared, rather than + * inferred, type of the tree. + * + *

    Do not use this method if you are trying to get the must-call obligations of the resource + * aliases of an {@link + * org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation}. Instead, + * use {@link + * org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation#getMustCallMethods(ResourceLeakAnnotatedTypeFactory, + * CFStore)}. + * + * @param tree a tree + * @return whether the tree has declared must-call obligations + */ + /*package-private*/ boolean declaredTypeHasMustCall(Tree tree) { + assert tree.getKind() == Tree.Kind.METHOD + || tree.getKind() == Tree.Kind.VARIABLE + || tree.getKind() == Tree.Kind.NEW_CLASS + || tree.getKind() == Tree.Kind.METHOD_INVOCATION + : "unexpected declaration tree kind: " + tree.getKind(); + return !hasEmptyMustCallValue(tree); + } + + /** + * Returns true if the given tree has an {@link MustCallAlias} annotation and resource-alias + * tracking is not disabled. + * + * @param tree a tree + * @return true if the given tree has an {@link MustCallAlias} annotation + */ + /*package-private*/ boolean hasMustCallAlias(Tree tree) { + Element elt = TreeUtils.elementFromTree(tree); + return hasMustCallAlias(elt); + } + + /** + * Returns true if the given element has an {@link MustCallAlias} annotation and resource-alias + * tracking is not disabled. + * + * @param elt an element + * @return true if the given element has an {@link MustCallAlias} annotation + */ + /*package-private*/ boolean hasMustCallAlias(Element elt) { + if (noResourceAliases) { + return false; + } + MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory = + getTypeFactoryOfSubchecker(MustCallChecker.class); + return mustCallAnnotatedTypeFactory.getDeclAnnotationNoAliases(elt, MustCallAlias.class) + != null; + } + + /** + * Returns true if the declaration of the method being invoked has one or more {@link + * CreatesMustCallFor} annotations. + * + * @param node a method invocation node + * @return true iff there is one or more @CreatesMustCallFor annotations on the declaration of + * the invoked method + */ + public boolean hasCreatesMustCallFor(MethodInvocationNode node) { + ExecutableElement decl = TreeUtils.elementFromUse(node.getTree()); + return getDeclAnnotation(decl, CreatesMustCallFor.class) != null + || getDeclAnnotation(decl, CreatesMustCallFor.List.class) != null; + } + + /** + * Does this type factory support {@link CreatesMustCallFor}? + * + * @return true iff the -AnoCreatesMustCallFor command-line argument was not supplied to the + * checker + */ + public boolean canCreateObligations() { + // Precomputing this call to `hasOption` causes a NullPointerException, so leave it as is. + return !checker.hasOption(MustCallChecker.NO_CREATES_MUSTCALLFOR); + } + + @Override + @SuppressWarnings("TypeParameterUnusedInFormals") // Intentional abuse + public > @Nullable T getTypeFactoryOfSubcheckerOrNull(Class subCheckerClass) { + if (subCheckerClass == MustCallChecker.class) { + if (!canCreateObligations()) { + return super.getTypeFactoryOfSubcheckerOrNull( + MustCallNoCreatesMustCallForChecker.class); + } + } + return super.getTypeFactoryOfSubcheckerOrNull(subCheckerClass); + } + + /** + * Returns the {@link EnsuresCalledMethods.List#value} element. + * + * @return the {@link EnsuresCalledMethods.List#value} element + */ + public ExecutableElement getEnsuresCalledMethodsListValueElement() { + return ensuresCalledMethodsListValueElement; + } + + /** + * Returns the {@link CreatesMustCallFor#value} element. + * + * @return the {@link CreatesMustCallFor#value} element + */ + @Override + public ExecutableElement getCreatesMustCallForValueElement() { + return createsMustCallForValueElement; + } + + /** + * Returns the {@link org.checkerframework.checker.mustcall.qual.CreatesMustCallFor.List#value} + * element. + * + * @return the {@link org.checkerframework.checker.mustcall.qual.CreatesMustCallFor.List#value} + * element + */ + @Override + public ExecutableElement getCreatesMustCallForListValueElement() { + return createsMustCallForListValueElement; + } + + /** + * Does the given element have an {@code @NotOwning} annotation (including in stub files)? + * + *

    Prefer this method to calling {@link #getDeclAnnotation(Element, Class)} on the type + * factory directly, which won't find this annotation in stub files (it only considers stub + * files loaded by this checker, not subcheckers). + * + * @param elt an element + * @return whether there is a NotOwning annotation on the given element + */ + public boolean hasNotOwning(Element elt) { + MustCallAnnotatedTypeFactory mcatf = getTypeFactoryOfSubchecker(MustCallChecker.class); + return mcatf.getDeclAnnotation(elt, NotOwning.class) != null; + } + + /** + * Does the given element have an {@code @Owning} annotation (including in stub files)? + * + *

    Prefer this method to calling {@link #getDeclAnnotation(Element, Class)} on the type + * factory directly, which won't find this annotation in stub files (it only considers stub + * files loaded by this checker, not subcheckers). + * + * @param elt an element + * @return whether there is an Owning annotation on the given element + */ + public boolean hasOwning(Element elt) { + MustCallAnnotatedTypeFactory mcatf = getTypeFactoryOfSubchecker(MustCallChecker.class); + return mcatf.getDeclAnnotation(elt, Owning.class) != null; + } + + @Override + public Set getExceptionalPostconditions( + ExecutableElement methodOrConstructor) { + Set result = + super.getExceptionalPostconditions(methodOrConstructor); + + // This override is a sneaky way to satisfy a few subtle design constraints: + // 1. The RLC requires destructors to close the class's @Owning fields even on exception + // (see ResourceLeakVisitor.checkOwningField). + // 2. In versions 3.39.0 and earlier, the RLC did not have the annotation + // @EnsuresCalledMethodsOnException, meaning that for destructors it had to treat + // a simple @EnsuresCalledMethods annotation as serving both purposes. + // + // As a result, there is a lot of code that is missing the "correct" + // @EnsuresCalledMethodsOnException annotations on its destructors. + // + // This override treats the @EnsuresCalledMethods annotations on destructors as if they + // were also @EnsuresCalledMethodsOnException for backwards compatibility. By overriding + // this method we get both directions of checking: destructor implementations have to + // satisfy these implicit contracts, and destructor callers get to benefit from them. + // + // It should be possible to remove this override entirely without sacrificing any soundness. + // However, that is undesirable at this point because it would be a breaking change. + // + // TODO: gradually remove this override. + // 1. When this override adds an implicit annotation, the Checker Framework should issue + // a warning along with a suggestion to add the right annotations. + // 2. After a few months we should remove this override and require proper annotations on + // all destructors. + + if (isMustCallMethod(methodOrConstructor)) { + Set normalPostconditions = + getContractsFromMethod().getPostconditions(methodOrConstructor); + for (Contract.Postcondition normalPostcondition : normalPostconditions) { + for (String method : getCalledMethods(normalPostcondition.annotation)) { + result.add( + new EnsuresCalledMethodOnExceptionContract( + normalPostcondition.expressionString, method)); + } + } + } + + return result; + } + + /** + * Returns true iff the {@code MustCall} annotation of the class that encloses the methodTree + * names this method. + * + * @param elt a method + * @return whether that method is one of the must-call methods for its enclosing class + */ + private boolean isMustCallMethod(ExecutableElement elt) { + TypeElement containingClass = ElementUtils.enclosingTypeElement(elt); + MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory = + getTypeFactoryOfSubchecker(MustCallChecker.class); + AnnotationMirror mcAnno = + mustCallAnnotatedTypeFactory + .getAnnotatedType(containingClass) + .getAnnotationInHierarchy(mustCallAnnotatedTypeFactory.TOP); + List mcValues = + AnnotationUtils.getElementValueArray( + mcAnno, + mustCallAnnotatedTypeFactory.getMustCallValueElement(), + String.class); + String methodName = elt.getSimpleName().toString(); + return mcValues.contains(methodName); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java new file mode 100644 index 000000000000..fdfd3435e3b0 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakChecker.java @@ -0,0 +1,307 @@ +package org.checkerframework.checker.resourceleak; + +import com.google.common.collect.ImmutableSet; + +import org.checkerframework.checker.calledmethods.CalledMethodsChecker; +import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; +import org.checkerframework.checker.mustcall.MustCallChecker; +import org.checkerframework.checker.mustcall.MustCallNoCreatesMustCallForChecker; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.qual.StubFiles; +import org.checkerframework.framework.source.SupportedOptions; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; + +/** + * The entry point for the Resource Leak Checker. This checker is a modifed {@link + * CalledMethodsChecker} that checks that the must-call obligations of each expression (as computed + * via the {@link org.checkerframework.checker.mustcall.MustCallChecker} have been fulfilled. + */ +@SupportedOptions({ + "permitStaticOwning", + "permitInitializationLeak", + ResourceLeakChecker.COUNT_MUST_CALL, + ResourceLeakChecker.IGNORED_EXCEPTIONS, + MustCallChecker.NO_CREATES_MUSTCALLFOR, + MustCallChecker.NO_LIGHTWEIGHT_OWNERSHIP, + MustCallChecker.NO_RESOURCE_ALIASES, + // NO-AFU ResourceLeakChecker.ENABLE_WPI_FOR_RLC, +}) +@StubFiles("IOUtils.astub") +public class ResourceLeakChecker extends CalledMethodsChecker { + + /** Creates a ResourceLeakChecker. */ + public ResourceLeakChecker() {} + + /** + * Command-line option for counting how many must-call obligations were checked by the Resource + * Leak Checker, and emitting the number after processing all files. Used for generating tables + * for a research paper. Not of interest to most users. + */ + public static final String COUNT_MUST_CALL = "countMustCall"; + + /** + * The exception types in this set are ignored in the CFG when determining if a resource leaks + * along an exceptional path. These kinds of errors fall into a few categories: runtime errors, + * errors that the JVM can issue on any statement, and errors that can be prevented by running + * some other CF checker. + */ + private static final SetOfTypes DEFAULT_IGNORED_EXCEPTIONS = + SetOfTypes.anyOfTheseNames( + ImmutableSet.of( + // Any method call has a CFG edge for Throwable/RuntimeException/Error + // to represent run-time misbehavior. Ignore it. + Throwable.class.getCanonicalName(), + Error.class.getCanonicalName(), + RuntimeException.class.getCanonicalName(), + // Use the Nullness Checker to prove this won't happen. + NullPointerException.class.getCanonicalName(), + // These errors can't be predicted statically, so ignore them and assume + // they won't happen. + ClassCircularityError.class.getCanonicalName(), + ClassFormatError.class.getCanonicalName(), + NoClassDefFoundError.class.getCanonicalName(), + OutOfMemoryError.class.getCanonicalName(), + // It's not our problem if the Java type system is wrong. + ClassCastException.class.getCanonicalName(), + // It's not our problem if the code is going to divide by zero. + ArithmeticException.class.getCanonicalName(), + // Use the Index Checker to prevent these errors. + ArrayIndexOutOfBoundsException.class.getCanonicalName(), + NegativeArraySizeException.class.getCanonicalName(), + // Most of the time, this exception is infeasible, as the charset used + // is guaranteed to be present by the Java spec (e.g., "UTF-8"). + // Eventually, this exclusion could be refined by looking at the charset + // being requested. + UnsupportedEncodingException.class.getCanonicalName())); + + /** + * Command-line option for controlling which exceptions are ignored. + * + * @see #DEFAULT_IGNORED_EXCEPTIONS + * @see #getIgnoredExceptions() + */ + public static final String IGNORED_EXCEPTIONS = "resourceLeakIgnoredExceptions"; + + /** + * A pattern that matches one or more consecutive commas, optionally preceded and followed by + * whitespace. + */ + private static final Pattern COMMAS = + Pattern.compile("\\s*(?:" + Pattern.quote(",") + "\\s*)+"); + + /** + * A pattern that matches an exception specifier for the {@link #IGNORED_EXCEPTIONS} option: an + * optional "=" followed by a qualified name. The whole thing can be padded with whitespace. + */ + private static final Pattern EXCEPTION_SPECIFIER = + Pattern.compile( + "^\\s*" + + "(" + + Pattern.quote("=") + + "\\s*" + + ")?" + + "(\\w+(?:\\.\\w+)*)" + + "\\s*$"); + + /* NO-AFU + * Ordinarily, when the -Ainfer flag is used, whole-program inference is run for every checker + * and sub-checker. However, the Resource Leak Checker is different. The -Ainfer flag enables + * the RLC's own (non-WPI) inference mechanism ({@link MustCallInference}). To use WPI in + * addition to this mechanism for its sub-checkers, use the -AenableWpiForRlc flag, which is + * intended only for testing and experiments. + * + public static final String ENABLE_WPI_FOR_RLC = "enableWpiForRlc"; + */ + + /** + * The number of expressions with must-call obligations that were checked. Incremented only if + * the {@link #COUNT_MUST_CALL} command-line option was supplied. + */ + /*package-private*/ int numMustCall = 0; + + /** + * The number of must-call-related errors issued. The count of verified must-call expressions is + * the difference between this and {@link #numMustCall}. + */ + private int numMustCallFailed = 0; + + /** + * The cached set of ignored exceptions parsed from {@link #IGNORED_EXCEPTIONS}. Caching this + * field prevents the checker from issuing duplicate warnings about missing exception types. + * + * @see #getIgnoredExceptions() + */ + private @MonotonicNonNull SetOfTypes ignoredExceptions = null; + + @Override + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); + + if (this.processingEnv.getOptions().containsKey(MustCallChecker.NO_CREATES_MUSTCALLFOR)) { + checkers.add(MustCallNoCreatesMustCallForChecker.class); + } else { + checkers.add(MustCallChecker.class); + } + + return checkers; + } + + @Override + protected BaseTypeVisitor createSourceVisitor() { + return new ResourceLeakVisitor(this); + } + + @Override + public void reportError( + @Nullable Object source, @CompilerMessageKey String messageKey, Object... args) { + if (messageKey.equals("required.method.not.called")) { + // This is safe because of the message key. + String qualifiedTypeName = (String) args[1]; + // Only count classes in the JDK, not user-defined classes. + if (MustCallConsistencyAnalyzer.isJdkClass(qualifiedTypeName)) { + numMustCallFailed++; + } + } + super.reportError(source, messageKey, args); + } + + @Override + public void typeProcessingOver() { + if (hasOption(COUNT_MUST_CALL)) { + message(Diagnostic.Kind.WARNING, "Found %d must call obligation(s).%n", numMustCall); + message( + Diagnostic.Kind.WARNING, + "Successfully verified %d must call obligation(s).%n", + numMustCall - numMustCallFailed); + } + super.typeProcessingOver(); + } + + /** + * Get the set of exceptions that should be ignored. This set comes from the {@link + * #IGNORED_EXCEPTIONS} option if it was provided, or {@link #DEFAULT_IGNORED_EXCEPTIONS} if + * not. + * + * @return the set of exceptions to ignore + */ + public SetOfTypes getIgnoredExceptions() { + SetOfTypes result = ignoredExceptions; + if (result == null) { + String ignoredExceptionsOptionValue = getOption(IGNORED_EXCEPTIONS); + result = + ignoredExceptionsOptionValue == null + ? DEFAULT_IGNORED_EXCEPTIONS + : parseIgnoredExceptions(ignoredExceptionsOptionValue); + ignoredExceptions = result; + } + return result; + } + + /** + * Parse the argument given for the {@link #IGNORED_EXCEPTIONS} option. Warnings will be issued + * for any problems in the argument, for instance if any of the named exceptions cannot be + * found. + * + * @param ignoredExceptionsOptionValue the value given for {@link #IGNORED_EXCEPTIONS} + * @return the set of ignored exceptions + */ + protected SetOfTypes parseIgnoredExceptions(String ignoredExceptionsOptionValue) { + String[] exceptions = COMMAS.split(ignoredExceptionsOptionValue); + List sets = new ArrayList<>(); + for (String e : exceptions) { + SetOfTypes set = parseExceptionSpecifier(e, ignoredExceptionsOptionValue); + if (set != null) { + sets.add(set); + } + } + return SetOfTypes.union(sets.toArray(new SetOfTypes[0])); + } + + /** + * Parse a single exception specifier from the {@link #IGNORED_EXCEPTIONS} option and issue + * warnings if it does not parse. See {@link #EXCEPTION_SPECIFIER} for a description of the + * syntax. + * + * @param exceptionSpecifier the exception specifier to parse + * @param ignoredExceptionsOptionValue the whole value of the {@link #IGNORED_EXCEPTIONS} + * option; only used for error reporting + * @return the parsed set of types, or null if the value does not parse + */ + @SuppressWarnings({ + // user input might not be a legal @CanonicalName, but it should be safe to pass to + // `SetOfTypes.anyOfTheseNames` + "signature:argument", + }) + protected @Nullable SetOfTypes parseExceptionSpecifier( + String exceptionSpecifier, String ignoredExceptionsOptionValue) { + Matcher m = EXCEPTION_SPECIFIER.matcher(exceptionSpecifier); + if (m.matches()) { + @Nullable String equalsSign = m.group(1); + String qualifiedName = m.group(2); + + if (qualifiedName.equalsIgnoreCase("default")) { + return DEFAULT_IGNORED_EXCEPTIONS; + } + TypeMirror type = checkCanonicalName(qualifiedName); + if (type == null) { + // There is a chance that the user named a real type, but the class is not + // accessible for some reason. We'll issue a warning (in case this was a typo) but + // add the type as ignored anyway (in case it's just an inaccessible type). + // + // Note that if the user asked to ignore subtypes of this exception, this code won't + // do it because we can't know what those subtypes are. We have to treat this as if + // it were "=qualifiedName" even if no equals sign was provided. + message( + Diagnostic.Kind.WARNING, + "The exception '%s' appears in the -A%s=%s option, but it does not seem to exist", + exceptionSpecifier, + IGNORED_EXCEPTIONS, + ignoredExceptionsOptionValue); + return SetOfTypes.anyOfTheseNames(ImmutableSet.of(qualifiedName)); + } else { + return equalsSign == null + ? SetOfTypes.allSubtypes(type) + : SetOfTypes.singleton(type); + } + } else if (!exceptionSpecifier.trim().isEmpty()) { + message( + Diagnostic.Kind.WARNING, + "The string '%s' appears in the -A%s=%s option, but it is not a legal exception specifier", + exceptionSpecifier, + IGNORED_EXCEPTIONS, + ignoredExceptionsOptionValue); + } + return null; + } + + /** + * Check if the given String refers to an actual type. + * + * @param s any string + * @return the referenced type, or null if it does not exist + */ + @SuppressWarnings({ + "signature:argument", // `s` is not a qualified name, but we pass it to getTypeElement + // anyway + }) + protected @Nullable TypeMirror checkCanonicalName(String s) { + TypeElement elem = getProcessingEnvironment().getElementUtils().getTypeElement(s); + if (elem == null) { + return null; + } + return types.getDeclaredType(elem); + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakTransfer.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakTransfer.java new file mode 100644 index 000000000000..91672e523f4f --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakTransfer.java @@ -0,0 +1,168 @@ +package org.checkerframework.checker.resourceleak; + +import org.checkerframework.checker.calledmethods.CalledMethodsTransfer; +import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression; +import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; +import org.checkerframework.checker.mustcall.MustCallChecker; +import org.checkerframework.common.accumulation.AccumulationStore; +import org.checkerframework.common.accumulation.AccumulationValue; +import org.checkerframework.dataflow.analysis.TransferInput; +import org.checkerframework.dataflow.analysis.TransferResult; +import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; +import org.checkerframework.dataflow.cfg.node.SwitchExpressionNode; +import org.checkerframework.dataflow.cfg.node.TernaryExpressionNode; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.javacutil.TypesUtils; + +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; + +/** The transfer function for the resource-leak extension to the called-methods type system. */ +public class ResourceLeakTransfer extends CalledMethodsTransfer { + + /** + * Shadowed because we must dispatch to the Resource Leak Checker's version of + * getTypefactoryOfSubchecker to get the correct MustCallAnnotatedTypeFactory. + */ + private final ResourceLeakAnnotatedTypeFactory rlTypeFactory; + + /** + * Create a new resource leak transfer function. + * + * @param analysis the analysis. Its type factory must be a {@link + * ResourceLeakAnnotatedTypeFactory}. + */ + public ResourceLeakTransfer(ResourceLeakAnalysis analysis) { + super(analysis); + this.rlTypeFactory = (ResourceLeakAnnotatedTypeFactory) analysis.getTypeFactory(); + } + + @Override + public TransferResult visitTernaryExpression( + TernaryExpressionNode node, TransferInput input) { + TransferResult result = + super.visitTernaryExpression(node, input); + if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { + // Add the synthetic variable created during CFG construction to the temporary + // variable map (rather than creating a redundant temp var) + rlTypeFactory.addTempVar(node.getTernaryExpressionVar(), node.getTree()); + } + return result; + } + + @Override + public TransferResult visitSwitchExpressionNode( + SwitchExpressionNode node, TransferInput input) { + TransferResult result = + super.visitSwitchExpressionNode(node, input); + if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { + // Add the synthetic variable created during CFG construction to the temporary + // variable map (rather than creating a redundant temp var) + rlTypeFactory.addTempVar(node.getSwitchExpressionVar(), node.getTree()); + } + return result; + } + + @Override + public TransferResult visitMethodInvocation( + MethodInvocationNode node, TransferInput input) { + + TransferResult result = + super.visitMethodInvocation(node, input); + + handleCreatesMustCallFor(node, result); + updateStoreWithTempVar(result, node); + + // If there is a temporary variable for the receiver, update its type. + Node receiver = node.getTarget().getReceiver(); + MustCallAnnotatedTypeFactory mcAtf = + rlTypeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + Node accumulationTarget = mcAtf.getTempVar(receiver); + if (accumulationTarget != null) { + String methodName = node.getTarget().getMethod().getSimpleName().toString(); + methodName = + rlTypeFactory.adjustMethodNameUsingValueChecker(methodName, node.getTree()); + accumulate(accumulationTarget, result, methodName); + } + + return result; + } + + /** + * Clears the called-methods store of all information about the target if an @CreatesMustCallFor + * method is invoked and the type factory can create obligations. Otherwise, does nothing. + * + * @param n a method invocation + * @param result the transfer result whose stores should be cleared of information + */ + private void handleCreatesMustCallFor( + MethodInvocationNode n, TransferResult result) { + if (!rlTypeFactory.canCreateObligations()) { + return; + } + + List targetExprs = + CreatesMustCallForToJavaExpression.getCreatesMustCallForExpressionsAtInvocation( + n, rlTypeFactory, rlTypeFactory); + AnnotationMirror defaultType = rlTypeFactory.top; + for (JavaExpression targetExpr : targetExprs) { + AccumulationValue defaultTypeValue = + analysis.createSingleAnnotationValue(defaultType, targetExpr.getType()); + if (result.containsTwoStores()) { + result.getThenStore().replaceValue(targetExpr, defaultTypeValue); + result.getElseStore().replaceValue(targetExpr, defaultTypeValue); + } else { + result.getRegularStore().replaceValue(targetExpr, defaultTypeValue); + } + } + } + + @Override + public TransferResult visitObjectCreation( + ObjectCreationNode node, TransferInput input) { + TransferResult result = + super.visitObjectCreation(node, input); + updateStoreWithTempVar(result, node); + return result; + } + + /** + * This method either creates or looks up the temp var t for node, and then updates the store to + * give t the same type as node. Temporary variables are supported for expressions throughout + * this checker (and the Must Call Checker) to enable refinement of their types. See the + * documentation of {@link MustCallConsistencyAnalyzer} for more details. + * + * @param node the node to be assigned to a temporary variable + * @param result the transfer result containing the store to be modified + */ + public void updateStoreWithTempVar( + TransferResult result, Node node) { + // Must-call obligations on primitives are not supported. + if (!TypesUtils.isPrimitiveOrBoxed(node.getType())) { + MustCallAnnotatedTypeFactory mcAtf = + rlTypeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + LocalVariableNode temp = mcAtf.getTempVar(node); + if (temp != null) { + rlTypeFactory.addTempVar(temp, node.getTree()); + JavaExpression localExp = JavaExpression.fromNode(temp); + AnnotationMirror anm = + rlTypeFactory + .getAnnotatedType(node.getTree()) + .getAnnotationInHierarchy(rlTypeFactory.top); + if (anm == null) { + anm = rlTypeFactory.top; + } + if (result.containsTwoStores()) { + result.getThenStore().insertValue(localExp, anm); + result.getElseStore().insertValue(localExp, anm); + } else { + result.getRegularStore().insertValue(localExp, anm); + } + } + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakVisitor.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakVisitor.java new file mode 100644 index 000000000000..8a2211d44557 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakVisitor.java @@ -0,0 +1,567 @@ +package org.checkerframework.checker.resourceleak; + +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.VariableTree; + +import org.checkerframework.checker.calledmethods.CalledMethodsVisitor; +import org.checkerframework.checker.calledmethods.EnsuresCalledMethodOnExceptionContract; +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.CreatesMustCallForToJavaExpression; +import org.checkerframework.checker.mustcall.MustCallAnnotatedTypeFactory; +import org.checkerframework.checker.mustcall.MustCallChecker; +import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; +import org.checkerframework.checker.mustcall.qual.NotOwning; +import org.checkerframework.checker.mustcall.qual.Owning; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.framework.util.JavaExpressionParseUtil; +import org.checkerframework.framework.util.StringToJavaExpression; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; +import org.checkerframework.javacutil.TypesUtils; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.VariableElement; + +/** + * The visitor for the Resource Leak Checker. Responsible for checking that the rules for {@link + * Owning} fields are satisfied, and for checking that {@link CreatesMustCallFor} overrides are + * valid. + */ +public class ResourceLeakVisitor extends CalledMethodsVisitor { + + /** True if errors related to static owning fields should be suppressed. */ + private final boolean permitStaticOwning; + + /** + * Because CalledMethodsVisitor doesn't have a type parameter, we need a reference to the type + * factory that has this static type to access the features that + * ResourceLeakAnnotatedTypeFactory implements but CalledMethodsAnnotatedTypeFactory does not. + */ + private final ResourceLeakAnnotatedTypeFactory rlTypeFactory; + + /** True if -AnoLightweightOwnership was supplied on the command line. */ + private final boolean noLightweightOwnership; + + /* NO-AFU + * True if -AenableWpiForRlc was passed on the command line. See {@link + * ResourceLeakChecker#ENABLE_WPI_FOR_RLC}. + * + private final boolean enableWpiForRlc; + */ + + /** + * Create the visitor. + * + * @param checker the type-checker associated with this visitor + */ + public ResourceLeakVisitor(BaseTypeChecker checker) { + super(checker); + rlTypeFactory = (ResourceLeakAnnotatedTypeFactory) atypeFactory; + permitStaticOwning = checker.hasOption("permitStaticOwning"); + noLightweightOwnership = checker.hasOption("noLightweightOwnership"); + // enableWpiForRlc = checker.hasOption(ResourceLeakChecker.ENABLE_WPI_FOR_RLC); + } + + @Override + protected ResourceLeakAnnotatedTypeFactory createTypeFactory() { + return new ResourceLeakAnnotatedTypeFactory(checker); + } + + @Override + public Void visitMethod(MethodTree tree, Void p) { + ExecutableElement elt = TreeUtils.elementFromDeclaration(tree); + MustCallAnnotatedTypeFactory mcAtf = + rlTypeFactory.getTypeFactoryOfSubchecker(MustCallChecker.class); + List cmcfValues = getCreatesMustCallForValues(elt, mcAtf, rlTypeFactory); + if (!cmcfValues.isEmpty()) { + checkCreatesMustCallForOverrides(tree, elt, mcAtf, cmcfValues); + checkCreatesMustCallForTargetsHaveNonEmptyMustCall(tree, mcAtf); + } + checkOwningOverrides(tree, elt, mcAtf); + return super.visitMethod(tree, p); + } + + /** + * checks that any created must-call obligation has a declared type with a non-empty + * {@code @MustCall} obligation + * + * @param tree the method + * @param mcAtf the type factory + */ + private void checkCreatesMustCallForTargetsHaveNonEmptyMustCall( + MethodTree tree, MustCallAnnotatedTypeFactory mcAtf) { + // Get all the JavaExpressions for all CreatesMustCallFor annotations + List createsMustCallExprs = + CreatesMustCallForToJavaExpression + .getCreatesMustCallForExpressionsAtMethodDeclaration(tree, mcAtf, mcAtf); + for (JavaExpression targetExpr : createsMustCallExprs) { + AnnotationMirror mustCallAnno = + mcAtf.getAnnotatedType(TypesUtils.getTypeElement(targetExpr.getType())) + .getAnnotationInHierarchy(mcAtf.TOP); + if (rlTypeFactory.getMustCallValues(mustCallAnno).isEmpty()) { + checker.reportError( + tree, + "creates.mustcall.for.invalid.target", + targetExpr.toString(), + targetExpr.getType().toString()); + } + } + } + + /** + * Check that an overriding method does not reduce the number of created must-call obligations + * + * @param tree overriding method + * @param elt element for overriding method + * @param mcAtf the type factory + * @param cmcfValues must call values created by overriding method + */ + private void checkCreatesMustCallForOverrides( + MethodTree tree, + ExecutableElement elt, + MustCallAnnotatedTypeFactory mcAtf, + List cmcfValues) { + // If this method overrides another method, it must create at least as many + // obligations. Without this check, dynamic dispatch might allow e.g. a field to be + // overwritten by a CMCF method, but the CMCF effect wouldn't occur. + for (ExecutableElement overridden : ElementUtils.getOverriddenMethods(elt, this.types)) { + List overriddenCmcfValues = + getCreatesMustCallForValues(overridden, mcAtf, rlTypeFactory); + if (!overriddenCmcfValues.containsAll(cmcfValues)) { + String foundCmcfValueString = String.join(", ", cmcfValues); + String neededCmcfValueString = String.join(", ", overriddenCmcfValues); + String actualClassname = ElementUtils.getEnclosingClassName(elt); + String overriddenClassname = ElementUtils.getEnclosingClassName(overridden); + checker.reportError( + tree, + "creates.mustcall.for.override.invalid", + actualClassname + "#" + elt, + overriddenClassname + "#" + overridden, + foundCmcfValueString, + neededCmcfValueString); + } + } + } + + /** + * Checks that overrides respect behavioral subtyping for @Owning and @NotOwning annotations. In + * particular, checks that 1) if an overridden method has an @Owning parameter, then that + * parameter is @Owning in the overrider, and 2) if an overridden method has an @NotOwning + * return, then the overrider also has an @NotOwning return. + * + * @param tree overriding method, for error reporting + * @param overrider element for overriding method + * @param mcAtf the type factory + */ + private void checkOwningOverrides( + MethodTree tree, ExecutableElement overrider, MustCallAnnotatedTypeFactory mcAtf) { + for (ExecutableElement overridden : + ElementUtils.getOverriddenMethods(overrider, this.types)) { + // Check for @Owning parameters. Must use an explicitly-indexed for loop so that the + // same parameter index can be accessed in the overrider's parameter list, which is the + // same length. + for (int i = 0; i < overridden.getParameters().size(); i++) { + if (mcAtf.getDeclAnnotation(overridden.getParameters().get(i), Owning.class) + != null) { + if (mcAtf.getDeclAnnotation(overrider.getParameters().get(i), Owning.class) + == null) { + checker.reportError( + tree, + "owning.override.param", + overrider.getParameters().get(i).getSimpleName().toString(), + overrider.getSimpleName().toString(), + ElementUtils.getEnclosingClassName(overrider), + overridden.getSimpleName().toString(), + ElementUtils.getEnclosingClassName(overridden)); + } + } + } + // Check for @NotOwning returns. + if (mcAtf.getDeclAnnotation(overridden, NotOwning.class) != null + && mcAtf.getDeclAnnotation(overrider, NotOwning.class) == null) { + checker.reportError( + tree, + "owning.override.return", + overrider.getSimpleName().toString(), + ElementUtils.getEnclosingClassName(overrider), + overridden.getSimpleName().toString(), + ElementUtils.getEnclosingClassName(overridden)); + } + } + } + + /* NO-AFU + @Override + protected boolean shouldPerformContractInference() { + return atypeFactory.getWholeProgramInference() != null && isWpiEnabledForRLC(); + } + */ + + /** + * Returns the {@link CreatesMustCallFor#value} element/argument of the + * given @CreatesMustCallFor annotation, or "this" if there is none. + * + *

    Does not vipewpoint-adaptation. + * + * @param createsMustCallFor an @CreatesMustCallFor annotation + * @param mcAtf a MustCallAnnotatedTypeFactory, to source the value element + * @return the string value + */ + private static String getCreatesMustCallForValue( + AnnotationMirror createsMustCallFor, MustCallAnnotatedTypeFactory mcAtf) { + return AnnotationUtils.getElementValue( + createsMustCallFor, + mcAtf.getCreatesMustCallForValueElement(), + String.class, + "this"); + } + + /** + * Returns all the {@link CreatesMustCallFor#value} elements/arguments of + * all @CreatesMustCallFor annotations on the given element. + * + *

    Does no viewpoint-adaptation, unlike {@link + * CreatesMustCallForToJavaExpression#getCreatesMustCallForExpressionsAtInvocation} which does. + * + * @param elt an executable element + * @param mcAtf a MustCallAnnotatedTypeFactory, to source the value element + * @param atypeFactory a ResourceLeakAnnotatedTypeFactory + * @return the literal strings present in the @CreatesMustCallFor annotation(s) of that element, + * substituting the default "this" for empty annotations. This method returns the empty list + * iff there are no @CreatesMustCallFor annotations on elt. The returned list is always + * modifiable if it is non-empty. + */ + /*package-private*/ static List getCreatesMustCallForValues( + ExecutableElement elt, + MustCallAnnotatedTypeFactory mcAtf, + ResourceLeakAnnotatedTypeFactory atypeFactory) { + AnnotationMirror createsMustCallForList = + atypeFactory.getDeclAnnotation(elt, CreatesMustCallFor.List.class); + List result = new ArrayList<>(4); + if (createsMustCallForList != null) { + List createsMustCallFors = + AnnotationUtils.getElementValueArray( + createsMustCallForList, + mcAtf.getCreatesMustCallForListValueElement(), + AnnotationMirror.class); + for (AnnotationMirror cmcf : createsMustCallFors) { + result.add(getCreatesMustCallForValue(cmcf, mcAtf)); + } + } + AnnotationMirror createsMustCallFor = + atypeFactory.getDeclAnnotation(elt, CreatesMustCallFor.class); + if (createsMustCallFor != null) { + result.add(getCreatesMustCallForValue(createsMustCallFor, mcAtf)); + } + return result; + } + + /** + * Get all {@link EnsuresCalledMethods} annotations on an element. + * + * @param elt an executable element that might have {@link EnsuresCalledMethods} annotations + * @param atypeFactory a ResourceLeakAnnotatedTypeFactory + * @return a set of {@link EnsuresCalledMethods} annotations + */ + @Pure + private static AnnotationMirrorSet getEnsuresCalledMethodsAnnotations( + ExecutableElement elt, ResourceLeakAnnotatedTypeFactory atypeFactory) { + AnnotationMirror ensuresCalledMethodsAnnos = + atypeFactory.getDeclAnnotation(elt, EnsuresCalledMethods.List.class); + AnnotationMirrorSet result = new AnnotationMirrorSet(); + if (ensuresCalledMethodsAnnos != null) { + result.addAll( + AnnotationUtils.getElementValueArray( + ensuresCalledMethodsAnnos, + atypeFactory.getEnsuresCalledMethodsListValueElement(), + AnnotationMirror.class)); + } + AnnotationMirror ensuresCalledMethod = + atypeFactory.getDeclAnnotation(elt, EnsuresCalledMethods.class); + if (ensuresCalledMethod != null) { + result.add(ensuresCalledMethod); + } + return result; + } + + @Override + public Void visitVariable(VariableTree tree, Void p) { + VariableElement varElement = TreeUtils.elementFromDeclaration(tree); + + if (varElement.getKind().isField() + && !noLightweightOwnership + && rlTypeFactory.getDeclAnnotation(varElement, Owning.class) != null) { + checkOwningField(varElement); + } + + return super.visitVariable(tree, p); + } + + /** + * An obligation that must be satisfied by a destructor. Helper type for {@link + * #checkOwningField(VariableElement)}. + */ + // TODO: In the future, this class should be a record. + private static final class DestructorObligation { + /** The method that must be called on the field. */ + final String mustCallMethod; + + /** When the method must be called. */ + final MustCallConsistencyAnalyzer.MethodExitKind exitKind; + + /** + * Create a new obligation. + * + * @param mustCallMethod the method that must be called + * @param exitKind when the method must be called + */ + public DestructorObligation( + String mustCallMethod, MustCallConsistencyAnalyzer.MethodExitKind exitKind) { + this.mustCallMethod = mustCallMethod; + this.exitKind = exitKind; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DestructorObligation that = (DestructorObligation) o; + return mustCallMethod.equals(that.mustCallMethod) && exitKind == that.exitKind; + } + + @Override + public int hashCode() { + return Objects.hash(mustCallMethod, exitKind); + } + } + + /** + * Checks validity of a field {@code field} with an {@code @}{@link Owning} annotation. Say the + * type of {@code field} is {@code @MustCall("m"}}. This method checks that the enclosing class + * of {@code field} has a type {@code @MustCall("m2")} for some method {@code m2}, and that + * {@code m2} has an annotation {@code @EnsuresCalledMethods(value = "this.field", methods = + * "m")}, guaranteeing that the {@code @MustCall} obligation of the field will be satisfied. + * + * @param field the declaration of the field to check + */ + private void checkOwningField(VariableElement field) { + + if (checker.shouldSkipUses(field)) { + return; + } + + Set modifiers = field.getModifiers(); + if (modifiers.contains(Modifier.STATIC)) { + if (permitStaticOwning) { + return; + } + if (modifiers.contains(Modifier.FINAL)) { + return; + } + } + + List mustCallObligationsOfOwningField = rlTypeFactory.getMustCallValues(field); + + if (mustCallObligationsOfOwningField.isEmpty()) { + return; + } + + // This value is side-effected. + Set unsatisfiedMustCallObligationsOfOwningField = + new LinkedHashSet<>(); + for (String mustCallMethod : mustCallObligationsOfOwningField) { + for (MustCallConsistencyAnalyzer.MethodExitKind exitKind : + MustCallConsistencyAnalyzer.MethodExitKind.values()) { + unsatisfiedMustCallObligationsOfOwningField.add( + new DestructorObligation(mustCallMethod, exitKind)); + } + } + + String error; + Element enclosingElement = field.getEnclosingElement(); + List enclosingMustCallValues = rlTypeFactory.getMustCallValues(enclosingElement); + + if (enclosingMustCallValues == null) { + error = + " The enclosing element " + + ElementUtils.getQualifiedName(enclosingElement) + + " doesn't have a @MustCall annotation"; + } else if (enclosingMustCallValues.isEmpty()) { + error = + " The enclosing element " + + ElementUtils.getQualifiedName(enclosingElement) + + " has an empty @MustCall annotation"; + } else { + error = " [[checkOwningField() did not find a reason!]]"; // should be reassigned + List siblingsOfOwningField = enclosingElement.getEnclosedElements(); + for (Element siblingElement : siblingsOfOwningField) { + if (siblingElement.getKind() == ElementKind.METHOD + && enclosingMustCallValues.contains( + siblingElement.getSimpleName().toString())) { + + ExecutableElement siblingMethod = (ExecutableElement) siblingElement; + + AnnotationMirrorSet allEnsuresCalledMethodsAnnos = + getEnsuresCalledMethodsAnnotations(siblingMethod, rlTypeFactory); + for (AnnotationMirror ensuresCalledMethodsAnno : allEnsuresCalledMethodsAnnos) { + List values = + AnnotationUtils.getElementValueArray( + ensuresCalledMethodsAnno, + rlTypeFactory.ensuresCalledMethodsValueElement, + String.class); + for (String value : values) { + if (expressionEqualsField(value, field)) { + List methods = + AnnotationUtils.getElementValueArray( + ensuresCalledMethodsAnno, + rlTypeFactory.ensuresCalledMethodsMethodsElement, + String.class); + for (String method : methods) { + unsatisfiedMustCallObligationsOfOwningField.remove( + new DestructorObligation( + method, + MustCallConsistencyAnalyzer.MethodExitKind + .NORMAL_RETURN)); + } + } + } + + Set exceptionalPostconds = + rlTypeFactory.getExceptionalPostconditions(siblingMethod); + for (EnsuresCalledMethodOnExceptionContract postcond : + exceptionalPostconds) { + if (expressionEqualsField(postcond.getExpression(), field)) { + unsatisfiedMustCallObligationsOfOwningField.remove( + new DestructorObligation( + postcond.getMethod(), + MustCallConsistencyAnalyzer.MethodExitKind + .EXCEPTIONAL_EXIT)); + } + } + + // Optimization: stop early as soon as we've exhausted the list of + // obligations + if (unsatisfiedMustCallObligationsOfOwningField.isEmpty()) { + return; + } + } + + if (!unsatisfiedMustCallObligationsOfOwningField.isEmpty()) { + // This variable could be set immediately before reporting the error, but + // IMO it is more clear to set it here. + error = + "Postconditions written on MustCall methods are missing: " + + formatMissingMustCallMethodPostconditions( + field, unsatisfiedMustCallObligationsOfOwningField); + } + } + } + } + + if (!unsatisfiedMustCallObligationsOfOwningField.isEmpty()) { + Set missingMethods = new LinkedHashSet<>(); + for (DestructorObligation obligation : unsatisfiedMustCallObligationsOfOwningField) { + missingMethods.add(obligation.mustCallMethod); + } + + checker.reportError( + field, + "required.method.not.called", + MustCallConsistencyAnalyzer.formatMissingMustCallMethods( + new ArrayList<>(missingMethods)), + "field " + field.getSimpleName().toString(), + field.asType().toString(), + error); + } + } + + /** + * Determine if the given expression e refers to this.field. + * + * @param e the expression + * @param field the field + * @return true if e refers to this.field + */ + private boolean expressionEqualsField(String e, VariableElement field) { + try { + JavaExpression je = StringToJavaExpression.atFieldDecl(e, field, this.checker); + return je instanceof FieldAccess && ((FieldAccess) je).getField().equals(field); + } catch (JavaExpressionParseUtil.JavaExpressionParseException ex) { + // The parsing error will be reported elsewhere, assuming e was derived from an + // annotation. + return false; + } + } + + /* NO-AFU + * Checks if WPI is enabled for the Resource Leak Checker inference. See {@link + * ResourceLeakChecker#ENABLE_WPI_FOR_RLC}. + * + * @return returns true if WPI is enabled for the Resource Leak Checker + * + protected boolean isWpiEnabledForRLC() { + return enableWpiForRlc; + } + */ + + /** + * Formats a list of must-call method post-conditions to be printed in an error message. + * + * @param field the value whose methods must be called + * @param mustCallVal the list of must-call strings + * @return a formatted string + */ + /*package-private*/ static String formatMissingMustCallMethodPostconditions( + Element field, Set mustCallVal) { + int size = mustCallVal.size(); + if (size == 0) { + throw new TypeSystemError("empty mustCallVal " + mustCallVal); + } + String fieldName = field.getSimpleName().toString(); + return mustCallVal.stream() + .map( + o -> + postconditionAnnotationFor(o.exitKind) + + "(value = \"" + + fieldName + + "\", methods = \"" + + o.mustCallMethod + + "\")") + .collect(Collectors.joining(", ")); + } + + /** + * Format a must-call post-condition to be printed in an error message. + * + * @param exitKind the kind of method exit + * @return the name of the annotation + */ + private static String postconditionAnnotationFor( + MustCallConsistencyAnalyzer.MethodExitKind exitKind) { + switch (exitKind) { + case NORMAL_RETURN: + return "@EnsuresCalledMethods"; + case EXCEPTIONAL_EXIT: + return "@EnsuresCalledMethodsOnException"; + default: + throw new UnsupportedOperationException(exitKind.toString()); + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/SetOfTypes.java b/checker/src/main/java/org/checkerframework/checker/resourceleak/SetOfTypes.java new file mode 100644 index 000000000000..185bbbf70e94 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/SetOfTypes.java @@ -0,0 +1,99 @@ +package org.checkerframework.checker.resourceleak; + +import com.google.common.collect.ImmutableSet; +import com.sun.tools.javac.code.Type; + +import org.checkerframework.checker.signature.qual.CanonicalName; +import org.checkerframework.dataflow.qual.Pure; + +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; + +/** + * A set of types. + * + *

    Important properties of this class: + * + *

      + *
    • No defined equality: in general, equality between these sets is prohibitively difficult to + * compute, and therefore this class uses reference equality. + *
    • Unknown size: it is not possible to know the true size of a set like {@link + * #allSubtypes(TypeMirror)}. By extension, it is not possible to iterate over a {@code + * SetOfTypes}. + *
    • Immutable: instances of this class can be created but not modified. + *
    + */ +public interface SetOfTypes { + + /** + * Test whether this set contains the given type. + * + * @param typeUtils a {@code Types} object for computing the relationships between types + * @param type the type in question + * @return true if this set contains {@code type}, or false otherwise + */ + @Pure + boolean contains(Types typeUtils, TypeMirror type); + + /** An empty set of types. */ + SetOfTypes EMPTY = (typeUtils, type) -> false; + + /** + * Create a set containing exactly the given type, but not its subtypes. + * + * @param t the type + * @return a set containing only {@code t} + */ + @Pure + static SetOfTypes singleton(TypeMirror t) { + return (typeUtils, u) -> typeUtils.isSameType(t, u); + } + + /** + * Create a set containing the given type and all of its subtypes. + * + * @param t the type + * @return a set containing {@code t} and its subtypes + */ + @Pure + static SetOfTypes allSubtypes(TypeMirror t) { + return (typeUtils, u) -> typeUtils.isSubtype(u, t); + } + + /** + * Create a set containing exactly the types with the given names, but not their subtypes. + * + * @param names the type names + * @return a set containing only the named types + */ + @Pure + static SetOfTypes anyOfTheseNames(ImmutableSet<@CanonicalName String> names) { + return (typeUtils, u) -> + u instanceof Type && names.contains(((Type) u).tsym.getQualifiedName().toString()); + } + + /** + * Create a set representing the union of all the given sets. + * + * @param typeSets an array of sets + * @return the union of the given sets + */ + @Pure + static SetOfTypes union(SetOfTypes... typeSets) { + switch (typeSets.length) { + case 0: + return EMPTY; + case 1: + return typeSets[0]; + default: + return (typeUtils, type) -> { + for (SetOfTypes set : typeSets) { + if (set.contains(typeUtils, type)) { + return true; + } + } + return false; + }; + } + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties new file mode 100644 index 000000000000..16a51267fc2a --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/resourceleak/messages.properties @@ -0,0 +1,11 @@ +required.method.not.called=@MustCall %s may not have been invoked on %s or any of its aliases.\nThe type of object is: %s.\nReason for going out of scope: %s +missing.creates.mustcall.for=Method %s re-assigns the non-final, owning field %s.%s, but does not have a corresponding @CreatesMustCallFor annotation. +incompatible.creates.mustcall.for=Method %s re-assigns the non-final, owning field %s.%s, but its @CreatesMustCallFor annotation targets %s. +reset.not.owning=Calling method %s resets the must-call obligations of the expression %s, which is non-owning. Either annotate its declaration with an @Owning annotation, extract it into a local variable, or write a corresponding @CreatesMustCallFor annotation on the method that encloses this statement. +creates.mustcall.for.override.invalid=Method %s cannot override method %s, which defines fewer @CreatesMustCallFor targets.\nfound: %s\nrequired: %s +creates.mustcall.for.invalid.target=Cannot create a must-call obligation for "%s" since its type %s has no must-call methods. +destructor.exceptional.postcondition=Method %s must resolve the must-call obligations of the owning field %s along all paths, including exceptional paths. On an exceptional path, the @EnsuresCalledMethods annotation was not satisfied.\nFound: %s\nRequired: %s +mustcallalias.out.of.scope=This @MustCallAlias parameter might go out of scope without being assigned into an owning field of this object (if this is a constructor) or returned.\nReason for going out of scope: %s +owning.override.param=Incompatible ownership for parameter %s.\nfound : no ownership annotation or @NotOwning\nrequired: @Owning\nConsequence: method %s in %s cannot override method %s in %s +owning.override.return=Incompatible ownership for return.\nfound : no ownership annotation or @Owning\nrequired: @NotOwning\nConsequence: method %s in %s cannot override method %s in %s +required.method.not.known=The checker cannot determine the must call methods of %s or any of its aliases, so it could not determine if they were called. Typically, this error indicates that you need to write an @MustCall annotation (often on an unconstrained generic type).\nThe type of object is: %s.\nReason for going out of scope: %s diff --git a/checker/src/main/java/org/checkerframework/checker/signature/SignatureAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/signature/SignatureAnnotatedTypeFactory.java index 165468b2a894..be385c7881b8 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/SignatureAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/signature/SignatureAnnotatedTypeFactory.java @@ -4,16 +4,17 @@ import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.LiteralTree; +import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.PrimitiveTypeTree; import com.sun.source.tree.Tree; -import java.lang.annotation.Annotation; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.ExecutableElement; + import org.checkerframework.checker.signature.qual.ArrayWithoutPackage; import org.checkerframework.checker.signature.qual.BinaryName; import org.checkerframework.checker.signature.qual.BinaryNameOrPrimitiveType; import org.checkerframework.checker.signature.qual.BinaryNameWithoutPackage; +import org.checkerframework.checker.signature.qual.CanonicalName; +import org.checkerframework.checker.signature.qual.CanonicalNameAndBinaryName; import org.checkerframework.checker.signature.qual.ClassGetName; import org.checkerframework.checker.signature.qual.ClassGetSimpleName; import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers; @@ -38,6 +39,19 @@ import org.checkerframework.framework.type.treeannotator.TreeAnnotator; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; +import org.plumelib.reflection.SignatureRegexes; + +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.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; // TODO: Does not yet handle method signature annotations, such as // @MethodDescriptor. @@ -48,31 +62,57 @@ public class SignatureAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { /** The {@literal @}{@link SignatureUnknown} annotation. */ protected final AnnotationMirror SIGNATURE_UNKNOWN = AnnotationBuilder.fromClass(elements, SignatureUnknown.class); + /** The {@literal @}{@link BinaryName} annotation. */ protected final AnnotationMirror BINARY_NAME = AnnotationBuilder.fromClass(elements, BinaryName.class); + /** The {@literal @}{@link InternalForm} annotation. */ protected final AnnotationMirror INTERNAL_FORM = AnnotationBuilder.fromClass(elements, InternalForm.class); + /** The {@literal @}{@link DotSeparatedIdentifiers} annotation. */ protected final AnnotationMirror DOT_SEPARATED_IDENTIFIERS = AnnotationBuilder.fromClass(elements, DotSeparatedIdentifiers.class); + /** The {@literal @}{@link CanonicalName} annotation. */ + protected final AnnotationMirror CANONICAL_NAME = + AnnotationBuilder.fromClass(elements, CanonicalName.class); + + /** The {@literal @}{@link CanonicalNameAndBinaryName} annotation. */ + protected final AnnotationMirror CANONICAL_NAME_AND_BINARY_NAME = + AnnotationBuilder.fromClass(elements, CanonicalNameAndBinaryName.class); + + /** The {@literal @}{@link PrimitiveType} annotation. */ + protected final AnnotationMirror PRIMITIVE_TYPE = + AnnotationBuilder.fromClass(elements, PrimitiveType.class); + /** The {@link String#replace(char, char)} method. */ private final ExecutableElement replaceCharChar = - TreeUtils.getMethod( - java.lang.String.class.getName(), "replace", processingEnv, "char", "char"); + TreeUtils.getMethod("java.lang.String", "replace", processingEnv, "char", "char"); /** The {@link String#replace(CharSequence, CharSequence)} method. */ private final ExecutableElement replaceCharSequenceCharSequence = TreeUtils.getMethod( - java.lang.String.class.getName(), + "java.lang.String", "replace", processingEnv, "java.lang.CharSequence", "java.lang.CharSequence"); - /** Creates a SignatureAnnotatedTypeFactory. */ + /** The {@link Class#getName()} method. */ + private final ExecutableElement classGetName = + TreeUtils.getMethod("java.lang.Class", "getName", processingEnv); + + /** The {@link Class#getCanonicalName()} method. */ + private final ExecutableElement classGetCanonicalName = + TreeUtils.getMethod(java.lang.Class.class, "getCanonicalName", processingEnv); + + /** + * Creates a SignatureAnnotatedTypeFactory. + * + * @param checker the type-checker associated with this type factory + */ public SignatureAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); @@ -111,56 +151,56 @@ private LiteralTreeAnnotator signatureLiteralTreeAnnotator(AnnotatedTypeFactory // constants such as effectively-final fields. So every `stringPatterns = "..."` would have // to be a literal string, which would be verbose ard hard to maintain. result.addStringPattern( - SignatureRegexes.ArrayWithoutPackage, + SignatureRegexes.ArrayWithoutPackageRegex, AnnotationBuilder.fromClass(elements, ArrayWithoutPackage.class)); result.addStringPattern( - SignatureRegexes.BinaryName, + SignatureRegexes.BinaryNameRegex, AnnotationBuilder.fromClass(elements, BinaryName.class)); result.addStringPattern( - SignatureRegexes.BinaryNameOrPrimitiveType, + SignatureRegexes.BinaryNameOrPrimitiveTypeRegex, AnnotationBuilder.fromClass(elements, BinaryNameOrPrimitiveType.class)); result.addStringPattern( - SignatureRegexes.BinaryNameWithoutPackage, + SignatureRegexes.BinaryNameWithoutPackageRegex, AnnotationBuilder.fromClass(elements, BinaryNameWithoutPackage.class)); result.addStringPattern( - SignatureRegexes.ClassGetName, + SignatureRegexes.ClassGetNameRegex, AnnotationBuilder.fromClass(elements, ClassGetName.class)); result.addStringPattern( - SignatureRegexes.ClassGetSimpleName, + SignatureRegexes.ClassGetSimpleNameRegex, AnnotationBuilder.fromClass(elements, ClassGetSimpleName.class)); result.addStringPattern( - SignatureRegexes.DotSeparatedIdentifiers, + SignatureRegexes.DotSeparatedIdentifiersRegex, AnnotationBuilder.fromClass(elements, DotSeparatedIdentifiers.class)); result.addStringPattern( - SignatureRegexes.DotSeparatedIdentifiersOrPrimitiveType, + SignatureRegexes.DotSeparatedIdentifiersOrPrimitiveTypeRegex, AnnotationBuilder.fromClass( elements, DotSeparatedIdentifiersOrPrimitiveType.class)); result.addStringPattern( - SignatureRegexes.FieldDescriptor, + SignatureRegexes.FieldDescriptorRegex, AnnotationBuilder.fromClass(elements, FieldDescriptor.class)); result.addStringPattern( - SignatureRegexes.FieldDescriptorForPrimitive, + SignatureRegexes.FieldDescriptorForPrimitiveRegex, AnnotationBuilder.fromClass(elements, FieldDescriptorForPrimitive.class)); result.addStringPattern( - SignatureRegexes.FieldDescriptorWithoutPackage, + SignatureRegexes.FieldDescriptorWithoutPackageRegex, AnnotationBuilder.fromClass(elements, FieldDescriptorWithoutPackage.class)); result.addStringPattern( - SignatureRegexes.FqBinaryName, + SignatureRegexes.FqBinaryNameRegex, AnnotationBuilder.fromClass(elements, FqBinaryName.class)); result.addStringPattern( - SignatureRegexes.FullyQualifiedName, + SignatureRegexes.FullyQualifiedNameRegex, AnnotationBuilder.fromClass(elements, FullyQualifiedName.class)); result.addStringPattern( - SignatureRegexes.Identifier, + SignatureRegexes.IdentifierRegex, AnnotationBuilder.fromClass(elements, Identifier.class)); result.addStringPattern( - SignatureRegexes.IdentifierOrPrimitiveType, + SignatureRegexes.IdentifierOrPrimitiveTypeRegex, AnnotationBuilder.fromClass(elements, IdentifierOrPrimitiveType.class)); result.addStringPattern( - SignatureRegexes.InternalForm, + SignatureRegexes.InternalFormRegex, AnnotationBuilder.fromClass(elements, InternalForm.class)); result.addStringPattern( - SignatureRegexes.PrimitiveType, + SignatureRegexes.PrimitiveTypeRegex, AnnotationBuilder.fromClass(elements, PrimitiveType.class)); return result; } @@ -174,31 +214,37 @@ public SignatureTreeAnnotator(AnnotatedTypeFactory atypeFactory) { @Override public Void visitBinary(BinaryTree tree, AnnotatedTypeMirror type) { if (TreeUtils.isStringConcatenation(tree)) { - type.removeAnnotationInHierarchy(SIGNATURE_UNKNOWN); // This could be made more precise. - type.addAnnotation(SignatureUnknown.class); + type.replaceAnnotation(SIGNATURE_UNKNOWN); } return null; // super.visitBinary(tree, type); } @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMirror type) { - if (TreeUtils.isStringCompoundConcatenation(node)) { - type.removeAnnotationInHierarchy(SIGNATURE_UNKNOWN); + public Void visitCompoundAssignment(CompoundAssignmentTree tree, AnnotatedTypeMirror type) { + if (TreeUtils.isStringCompoundConcatenation(tree)) { // This could be made more precise. - type.addAnnotation(SignatureUnknown.class); + type.replaceAnnotation(SIGNATURE_UNKNOWN); } - return null; // super.visitCompoundAssignment(node, type); + return null; // super.visitCompoundAssignment(tree, type); } /** * String.replace, when called with specific constant arguments, converts between internal - * form and binary name. + * form and binary name: * *
    
              * {@literal @}InternalForm String internalForm = binaryName.replace('.', '/');
              * {@literal @}BinaryName String binaryName = internalForm.replace('/', '.');
              * 
    + * + * Class.getName and Class.getCanonicalName(): Cwhen called on a primitive type ,the return + * a {@link PrimitiveType}. When called on a non-array, non-nested, non-primitive type, they + * return a {@link BinaryName}: + * + *
    
    +         * {@literal @}BinaryName String binaryName = MyClass.class.getName();
    +         * 
    */ @Override public Void visitMethodInvocation(MethodInvocationTree tree, AnnotatedTypeMirror type) { @@ -229,7 +275,7 @@ public Void visitMethodInvocation(MethodInvocationTree tree, AnnotatedTypeMirror } } ExpressionTree receiver = TreeUtils.getReceiverTree(tree); - final AnnotatedTypeMirror receiverType = getAnnotatedType(receiver); + AnnotatedTypeMirror receiverType = getAnnotatedType(receiver); if ((oldChar == '.' && newChar == '/') && receiverType.getAnnotation(BinaryName.class) != null) { type.replaceAnnotation(INTERNAL_FORM); @@ -237,7 +283,41 @@ public Void visitMethodInvocation(MethodInvocationTree tree, AnnotatedTypeMirror && receiverType.getAnnotation(InternalForm.class) != null) { type.replaceAnnotation(BINARY_NAME); } + } else { + boolean isClassGetName = + TreeUtils.isMethodInvocation(tree, classGetName, processingEnv); + boolean isClassGetCanonicalName = + TreeUtils.isMethodInvocation(tree, classGetCanonicalName, processingEnv); + if (isClassGetName || isClassGetCanonicalName) { + ExpressionTree receiver = TreeUtils.getReceiverTree(tree); + if (TreeUtils.isClassLiteral(receiver)) { + ExpressionTree classExpr = ((MemberSelectTree) receiver).getExpression(); + if (classExpr.getKind() == Tree.Kind.PRIMITIVE_TYPE) { + if (((PrimitiveTypeTree) classExpr).getPrimitiveTypeKind() + == TypeKind.VOID) { + // do nothing + } else { + type.replaceAnnotation(PRIMITIVE_TYPE); + } + } else { + // Binary name if non-array, non-primitive, non-nested. + TypeMirror literalType = TreeUtils.typeOf(classExpr); + if (literalType.getKind() == TypeKind.DECLARED) { + TypeElement typeElt = TypesUtils.getTypeElement(literalType); + Element enclosing = typeElt.getEnclosingElement(); + if (enclosing == null + || enclosing.getKind() == ElementKind.PACKAGE) { + type.replaceAnnotation( + isClassGetName + ? DOT_SEPARATED_IDENTIFIERS + : CANONICAL_NAME_AND_BINARY_NAME); + } + } + } + } + } } + return super.visitMethodInvocation(tree, type); } } diff --git a/checker/src/main/java/org/checkerframework/checker/signature/SignatureChecker.java b/checker/src/main/java/org/checkerframework/checker/signature/SignatureChecker.java index 9c23743312da..6b3bcfa4dfcf 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/SignatureChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/signature/SignatureChecker.java @@ -1,6 +1,7 @@ package org.checkerframework.checker.signature; import com.sun.source.tree.CompilationUnitTree; + import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.qual.StubFiles; diff --git a/checker/src/main/java/org/checkerframework/checker/signature/SignatureRegexes.java b/checker/src/main/java/org/checkerframework/checker/signature/SignatureRegexes.java deleted file mode 100644 index 155f55953188..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/signature/SignatureRegexes.java +++ /dev/null @@ -1,258 +0,0 @@ -package org.checkerframework.checker.signature; - -/** This class defines stringPattern regexes for the Signature Checker. */ -public class SignatureRegexes { - - /** Do not instantiate this class. */ - private SignatureRegexes() { - throw new Error("Do not instantiate"); - } - - /////////////////////////////////////////////////////////////////////////// - /// Functions on regular expressions - /// - - /** - * Create a capturing group. - * - * @param arg a regular expression - * @return the argument wrapped in a capturing group - */ - private static final String GROUPED(String arg) { - return "(" + arg + ")"; - } - - /** - * Create a regex matching zero or more of the given argument (Kleene star). - * - * @param arg a regular expression - * @return the argument, repeated zero or more times - */ - private static final String ANY(String arg) { - return GROUPED(arg) + "*"; - } - - /** - * Create a regex that must match the entire string. - * - * @param arg a regular expression - * @return the argument, made to match the entire string - */ - private static final String ANCHORED(String arg) { - return "^" + arg + "$"; - } - - /** - * An ungrouped alternation. - * - * @param args regular expressions - * @return a regex that matches any one of the arguments - */ - private static final String ALTERNATE(String... args) { - return String.join("|", args); - } - - /** - * A grouped alternation. - * - * @param args regular expressions - * @return a regex that matches any one of the arguments, wrapped in a capturing group - */ - private static final String GROUPED_ALTERNATE(String... args) { - return GROUPED(ALTERNATE(args)); - } - - /////////////////////////////////////////////////////////////////////////// - /// Building blocks for regular expressions - /// - - /** An unanchored regex that matches keywords, except primitive types. */ - private static final String KEYWORD_NON_PRIMITIVE_TYPE = - String.join( - "|", - "abstract", - "assert", - // "boolean", - "break", - // "byte", - "case", - "catch", - // "char", - "class", - "const", - "continue", - "default", - "do", - // "double", - "else", - "enum", - "extends", - "final", - "finally", - // "float", - "for", - "if", - "goto", - "implements", - "import", - "instanceof", - // "int", - "interface", - // "long", - "native", - "new", - "package", - "private", - "protected", - "public", - "return", - // "short", - "static", - "strictfp", - "super", - "switch", - "synchronized", - "this", - "throw", - "throws", - "transient", - "try", - "void", - "volatile", - "while"); - - /** An unanchored regex that matches primitive types. */ - private static final String PRIMITIVE_TYPE = - String.join("|", "boolean", "byte", "char", "double", "float", "int", "long", "short"); - - /** A regex that matches field descriptors for primitive types. */ - private static final String FD_PRIMITIVE = "[BCDFIJSZ]"; - - /** An unanchored regex that matches keywords. */ - private static final String KEYWORD = KEYWORD_NON_PRIMITIVE_TYPE + "|" + PRIMITIVE_TYPE; - - /** - * A regex that matches identifier tokens that are not identifiers (keywords, boolean literals, - * and the null literal). - */ - private static final String KEYWORD_OR_LITERAL = - String.join("|", KEYWORD, "true", "false", "null"); - - /** A regex that matches Java identifier tokens, as defined by the Java grammar. */ - private static final String IDENTIFIER_TOKEN = "[A-Za-z_][A-Za-z_0-9]*"; - - /** A grouped regex that matches identifiers. */ - private static final String IDENTIFIER = "(?!" + KEYWORD_OR_LITERAL + ")" + IDENTIFIER_TOKEN; - - /** An anchored regex that matches Identifier strings. */ - public static final String IDENTIFIER_OR_PRIMITIVE_TYPE = ALTERNATE(IDENTIFIER, PRIMITIVE_TYPE); - - /** An unanchored regex that matches DotSeparatedIdentifiers strings. */ - private static final String DOT_SEPARATED_IDENTIFIERS = IDENTIFIER + ANY("\\." + IDENTIFIER); - - /** An unanchored regex that matches slash-separated identifiers. */ - private static final String SLASH_SEPARATED_IDENTIFIERS = IDENTIFIER + ANY("/" + IDENTIFIER); - - /** A regex that matches the nested-class part of a class name, for one nested class. */ - private static final String NESTED_ONE = "\\$[A-Za-z_0-9]+"; - - /** A regex that matches the nested-class part of a class name. */ - private static final String NESTED = ANY(NESTED_ONE); - - /** An unanchored regex that matches BinaryName strings. */ - private static final String BINARY_NAME = DOT_SEPARATED_IDENTIFIERS + NESTED; - - /** A regex that matches the nested-class part of a class name. */ - private static final String ARRAY = "(\\[\\])*"; - - /** A regex that matches InternalForm strings. */ - public static final String INTERNAL_FORM = SLASH_SEPARATED_IDENTIFIERS + NESTED; - - /** A regex that matches ClassGetName, for non-primitive, non-array types. */ - private static final String CLASS_GET_NAME_NONPRIMITIVE_NONARRAY = - IDENTIFIER + "(\\." + IDENTIFIER + "|" + NESTED_ONE + ")*"; - - /////////////////////////////////////////////////////////////////////////// - // Regexes for literal Strings, one per annotation definitions. - - /** A regex that matches ArrayWithoutPackage strings. */ - public static final String ArrayWithoutPackage = - ANCHORED(GROUPED(IDENTIFIER_OR_PRIMITIVE_TYPE) + ARRAY); - - /** A regex that matches BinaryName strings. */ - public static final String BinaryName = ANCHORED(BINARY_NAME); - - /** A regex that matches BinaryNameWithoutPackage strings. */ - public static final String BinaryNameWithoutPackage = ANCHORED(IDENTIFIER + NESTED); - - /** A regex that matches BinaryNameOrPrimitiveType strings. */ - public static final String BinaryNameOrPrimitiveType = - ANCHORED(GROUPED_ALTERNATE(BINARY_NAME, PRIMITIVE_TYPE)); - - /** A regex that matches ClassGetName strings. */ - public static final String ClassGetName = - ANCHORED( - GROUPED_ALTERNATE( - // non-array - PRIMITIVE_TYPE, - CLASS_GET_NAME_NONPRIMITIVE_NONARRAY, - // array - ("\\[+" - + GROUPED_ALTERNATE( - FD_PRIMITIVE, - "L" + CLASS_GET_NAME_NONPRIMITIVE_NONARRAY + ";")))); - - /** A regex that matches ClassGetSimpleName strings. */ - public static final String ClassGetSimpleName = - ANCHORED( - GROUPED_ALTERNATE( - "", // empty string is a ClassGetSimpleName - IDENTIFIER_OR_PRIMITIVE_TYPE) - + ARRAY); - - /** A regex that matches DotSeparatedIdentifiers strings. */ - public static final String DotSeparatedIdentifiers = ANCHORED(DOT_SEPARATED_IDENTIFIERS); - - /** A regex that matches DotSeparatedIdentifiersOrPrimitiveType strings. */ - public static final String DotSeparatedIdentifiersOrPrimitiveType = - ANCHORED(GROUPED_ALTERNATE(DOT_SEPARATED_IDENTIFIERS, PRIMITIVE_TYPE)); - - /** A regex that matches FieldDescriptor strings. */ - public static final String FieldDescriptor = - ANCHORED("\\[*(" + FD_PRIMITIVE + "|L" + INTERNAL_FORM + ";)"); - - /** A regex that matches FieldDescriptorWithoutPackage strings. */ - public static final String FieldDescriptorWithoutPackage = - ANCHORED( - "(" - + FD_PRIMITIVE - + "|\\[+" - + FD_PRIMITIVE - + "|\\[L" - + IDENTIFIER - + NESTED - + ";)"); - - /** A regex that matches FieldDescriptorForPrimitive strings. */ - public static final String FieldDescriptorForPrimitive = ANCHORED("^[BCDFIJSZ]$"); - - /** A regex that matches FqBinaryName strings. */ - public static final String FqBinaryName = - ANCHORED("(" + PRIMITIVE_TYPE + "|" + BINARY_NAME + ")" + ARRAY); - - /** A regex that matches FullyQualifiedName strings. */ - public static final String FullyQualifiedName = - ANCHORED("(" + PRIMITIVE_TYPE + "|" + DOT_SEPARATED_IDENTIFIERS + ")" + ARRAY); - - /** A regex that matches Identifier strings. */ - public static final String Identifier = ANCHORED(IDENTIFIER); - - /** A regex that matches IdentifierOrPrimitiveType strings. */ - public static final String IdentifierOrPrimitiveType = ANCHORED(IDENTIFIER_OR_PRIMITIVE_TYPE); - - /** A regex that matches InternalForm strings. */ - public static final String InternalForm = ANCHORED(INTERNAL_FORM); - - /** A regex that matches PrimitiveType strings. */ - public static final String PrimitiveType = ANCHORED(PRIMITIVE_TYPE); -} diff --git a/checker/src/main/java/org/checkerframework/checker/signature/SignatureTransfer.java b/checker/src/main/java/org/checkerframework/checker/signature/SignatureTransfer.java new file mode 100644 index 000000000000..d9caf2cac549 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/signature/SignatureTransfer.java @@ -0,0 +1,65 @@ +package org.checkerframework.checker.signature; + +import org.checkerframework.checker.signature.qual.CanonicalNameOrEmpty; +import org.checkerframework.dataflow.analysis.ConditionalTransferResult; +import org.checkerframework.dataflow.analysis.TransferInput; +import org.checkerframework.dataflow.analysis.TransferResult; +import org.checkerframework.dataflow.cfg.node.MethodAccessNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.framework.flow.CFAnalysis; +import org.checkerframework.framework.flow.CFStore; +import org.checkerframework.framework.flow.CFTransfer; +import org.checkerframework.framework.flow.CFValue; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TypesUtils; + +import javax.lang.model.element.ExecutableElement; + +/** The transfer function for the Signature Checker. */ +public class SignatureTransfer extends CFTransfer { + + /** The annotated type factory for this transfer function. */ + private final SignatureAnnotatedTypeFactory atypeFactory; + + /** + * Create a new SignatureTransfer. + * + * @param analysis the analysis + */ + public SignatureTransfer(CFAnalysis analysis) { + super(analysis); + atypeFactory = (SignatureAnnotatedTypeFactory) analysis.getTypeFactory(); + } + + @Override + public TransferResult visitMethodInvocation( + MethodInvocationNode n, TransferInput in) { + TransferResult superResult = super.visitMethodInvocation(n, in); + + MethodAccessNode target = n.getTarget(); + ExecutableElement method = target.getMethod(); + Node receiver = target.getReceiver(); + if (TypesUtils.isString(receiver.getType()) + && ElementUtils.matchesElement(method, "isEmpty")) { + + AnnotatedTypeMirror receiverAtm = atypeFactory.getAnnotatedType(receiver.getTree()); + if (receiverAtm.hasAnnotation(CanonicalNameOrEmpty.class)) { + + CFStore thenStore = superResult.getRegularStore(); + CFStore elseStore = thenStore.copy(); + ConditionalTransferResult result = + new ConditionalTransferResult<>( + superResult.getResultValue(), thenStore, elseStore); + // The refined expression is the receiver of the method call. + JavaExpression refinedExpr = JavaExpression.fromNode(receiver); + + elseStore.insertValue(refinedExpr, atypeFactory.CANONICAL_NAME); + return result; + } + } + return superResult; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/signature/javac.astub b/checker/src/main/java/org/checkerframework/checker/signature/javac.astub index 585b24120616..07b263a1f1ae 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/javac.astub +++ b/checker/src/main/java/org/checkerframework/checker/signature/javac.astub @@ -2,33 +2,8 @@ import org.checkerframework.checker.signature.qual.*; package javax.lang.model.element; -interface TypeElement { - // TODO: Is this correct?? or is it a @BinaryName? - @DotSeparatedIdentifiers Name getQualifiedName(); -} - -// This does not work because Name does not define an override of toString. +// This fake override does not work yet because it depends on an annotation on a formal parameter, +// and currently fake overrides affect only return types. // interface Name { // @PolySignature String toString(@PolySignature Name this); // } -// Writing those annotations on java.lang.Object does not work either, because -// then vast numbers of toString overrides fail to typecheck. -// See https://tinyurl.com/cfissue/3094 . - -package com.sun.tools.javac.code; - -class Type { - @FullyQualifiedName String toString(); -} - -class Symbol { - @DotSeparatedIdentifiers Name getQualifiedName(); - - @BinaryName Name flatname(); - - class ClassSymbol { - @DotSeparatedIdentifiers Name getQualifiedName(); - - @BinaryName Name flatname(); - } -} diff --git a/checker/src/main/java/org/checkerframework/checker/signature/javaparser.astub b/checker/src/main/java/org/checkerframework/checker/signature/javaparser.astub index 49a6690d7a42..85c3aa167c89 100644 --- a/checker/src/main/java/org/checkerframework/checker/signature/javaparser.astub +++ b/checker/src/main/java/org/checkerframework/checker/signature/javaparser.astub @@ -3,5 +3,22 @@ import org.checkerframework.checker.signature.qual.*; package com.github.javaparser.ast; class PackageDeclaration { - @DotSeparatedIdentifiers String getNameAsString(); + @DotSeparatedIdentifiers Name getName(); +} + +package com.github.javaparser.ast; + +class ImportDeclaration { + @DotSeparatedIdentifiers Name getName(); +} + +// Class is documented as "A node with a (qualified) name." +interface NodeWithName { + @FullyQualifiedName Name getName(); +} + +package com.github.javaparser.ast.expr; + +class AnnotationExpr { + @FullyQualifiedName Name getName(); } diff --git a/checker/src/main/java/org/checkerframework/checker/signature/jdk11.astub b/checker/src/main/java/org/checkerframework/checker/signature/jdk11.astub new file mode 100644 index 000000000000..62a4ccaf981c --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/signature/jdk11.astub @@ -0,0 +1,25 @@ +// This file is for classes that appear in JDK 11 but not in JDK 17. + +package com.sun.javadoc; + +import org.checkerframework.checker.signature.qual.BinaryName; +import org.checkerframework.checker.signature.qual.BinaryNameOrPrimitiveType; +import org.checkerframework.checker.signature.qual.FqBinaryName; +import org.checkerframework.checker.signature.qual.IdentifierOrPrimitiveType; + +public interface Parameter { + + @FqBinaryName String typeName(); +} + +public interface ProgramElementDoc extends Doc { + + @BinaryName String qualifiedName(); +} + +public interface Type { + + @BinaryNameOrPrimitiveType String qualifiedTypeName(); + + @IdentifierOrPrimitiveType String simpleTypeName(); +} 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 e3f5d8daff52..71f29f5183db 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessAnnotatedTypeFactory.java @@ -2,16 +2,19 @@ import com.sun.source.tree.BinaryTree; import com.sun.source.tree.CompoundAssignmentTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LiteralTree; import com.sun.source.tree.Tree; -import java.lang.annotation.Annotation; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.util.TreePath; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.signedness.qual.PolySigned; 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.checker.signedness.qual.Unsigned; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.value.ValueAnnotatedTypeFactory; @@ -20,14 +23,31 @@ 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.AnnotatedExecutableType; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable; +import org.checkerframework.framework.type.poly.DefaultQualifierPolymorphism; +import org.checkerframework.framework.type.poly.QualifierPolymorphism; 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.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeKindUtils; +import org.checkerframework.javacutil.TypesUtils; + +import java.io.Serializable; +import java.util.List; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; /** * The type factory for the Signedness Checker. @@ -36,149 +56,192 @@ */ 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); + protected final AnnotationMirror SIGNED = AnnotationBuilder.fromClass(elements, Signed.class); + + /** The @Unsigned annotation. */ + protected final AnnotationMirror UNSIGNED = + AnnotationBuilder.fromClass(elements, Unsigned.class); + + /** The @SignednessGlb annotation. Do not use @SignedPositive; use this instead. */ + protected final AnnotationMirror SIGNEDNESS_GLB = + AnnotationBuilder.fromClass(elements, SignednessGlb.class); + + /** The @SignedPositive annotation. */ + protected final AnnotationMirror SIGNED_POSITIVE = + AnnotationBuilder.fromClass(elements, SignedPositive.class); + + /** The @SignednessBottom annotation. */ + protected final AnnotationMirror SIGNEDNESS_BOTTOM = + AnnotationBuilder.fromClass(elements, SignednessBottom.class); + + /** The @PolySigned annotation. */ + protected final AnnotationMirror POLY_SIGNED = + AnnotationBuilder.fromClass(elements, PolySigned.class); /** The @NonNegative annotation of the Index Checker, as represented by the Value Checker. */ private final AnnotationMirror INT_RANGE_FROM_NON_NEGATIVE = AnnotationBuilder.fromClass(elements, IntRangeFromNonNegative.class); + /** The @Positive annotation of the Index Checker, as represented by the Value Checker. */ private final AnnotationMirror INT_RANGE_FROM_POSITIVE = AnnotationBuilder.fromClass(elements, IntRangeFromPositive.class); - ValueAnnotatedTypeFactory valueFactory = getTypeFactoryOfSubchecker(ValueChecker.class); + /** The Serializable type mirror. */ + private final TypeMirror serializableTM = + elements.getTypeElement(Serializable.class.getCanonicalName()).asType(); + + /** The Comparable type mirror. */ + private final TypeMirror comparableTM = + elements.getTypeElement(Comparable.class.getCanonicalName()).asType(); + + /** The Number type mirror. */ + private final TypeMirror numberTM = + elements.getTypeElement(Number.class.getCanonicalName()).asType(); + + /** A set containing just {@code @Signed}. */ + private final AnnotationMirrorSet SIGNED_SINGLETON = new AnnotationMirrorSet(SIGNED); - /** Create a SignednessAnnotatedTypeFactory. */ + /** A set containing just {@code @Unsigned}. */ + private final AnnotationMirrorSet UNSIGNED_SINGLETON = new AnnotationMirrorSet(UNSIGNED); + + /** + * Create a SignednessAnnotatedTypeFactory. + * + * @param checker the type-checker associated with this type factory + */ public SignednessAnnotatedTypeFactory(BaseTypeChecker checker) { super(checker); - addAliasedAnnotation(SignedPositive.class, SIGNEDNESS_GLB); + addAliasedTypeAnnotation("jdk.jfr.Unsigned", UNSIGNED); postInit(); } - @Override - protected Set> createSupportedTypeQualifiers() { - Set> result = getBundledTypeQualifiers(); - result.remove(SignedPositive.class); // this method should not return aliases - return result; - } - @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); + Tree.Kind treeKind = tree.getKind(); + if (treeKind == Tree.Kind.INT_LITERAL) { + int literalValue = (int) ((LiteralTree) tree).getValue(); + if (literalValue >= 0) { + type.replaceAnnotation(SIGNED_POSITIVE); + } else { + type.replaceAnnotation(SIGNEDNESS_GLB); + } + } else if (treeKind == Tree.Kind.LONG_LITERAL) { + long literalValue = (long) ((LiteralTree) tree).getValue(); + if (literalValue >= 0) { + type.replaceAnnotation(SIGNED_POSITIVE); + } else { + type.replaceAnnotation(SIGNEDNESS_GLB); + } + } else if (!isComputingAnnotatedTypeMirrorOfLhs()) { + addSignedPositiveAnnotation(tree, type); } super.addComputedTypeAnnotations(tree, type, iUseFlow); } /** - * True when the AnnotatedTypeMirror currently being computed is the left hand side of an - * assignment or pseudo-assignment. - * - * @see #addComputedTypeAnnotations(Tree, AnnotatedTypeMirror, boolean) - * @see #getAnnotatedTypeLhs(Tree) - */ - private boolean computingAnnotatedTypeMirrorOfLHS = false; - - @Override - public AnnotatedTypeMirror getAnnotatedTypeLhs(Tree lhsTree) { - boolean oldComputingAnnotatedTypeMirrorOfLHS = computingAnnotatedTypeMirrorOfLHS; - computingAnnotatedTypeMirrorOfLHS = true; - AnnotatedTypeMirror result = super.getAnnotatedTypeLhs(lhsTree); - computingAnnotatedTypeMirrorOfLHS = oldComputingAnnotatedTypeMirrorOfLHS; - return result; - } - - /** - * Refines an integer expression to @SignednessGlb if its value is within the signed positive - * range (i.e. its MSB is zero). + * Refines an integer expression to @SignedPositive if its value is within the signed positive + * range (i.e. its MSB is zero). Does not refine the type of cast expressions. * * @param tree an AST node, whose type may be refined * @param type the type of the tree */ - private void addSignednessGlbAnnotation(Tree tree, AnnotatedTypeMirror type) { + private void addSignedPositiveAnnotation(Tree tree, AnnotatedTypeMirror type) { + if (tree.getKind() == Tree.Kind.TYPE_CAST) { + return; + } TypeMirror javaType = type.getUnderlyingType(); TypeKind javaTypeKind = javaType.getKind(); - if (tree.getKind() != Tree.Kind.VARIABLE) { - if (javaTypeKind == TypeKind.BYTE - || javaTypeKind == TypeKind.CHAR - || javaTypeKind == TypeKind.SHORT - || javaTypeKind == TypeKind.INT - || javaTypeKind == TypeKind.LONG) { - AnnotatedTypeMirror valueATM = valueFactory.getAnnotatedType(tree); - // These annotations are trusted rather than checked. Maybe have an option to - // disable using them? - if ((valueATM.hasAnnotation(INT_RANGE_FROM_NON_NEGATIVE) - || valueATM.hasAnnotation(INT_RANGE_FROM_POSITIVE)) - && type.hasAnnotation(SIGNED)) { - type.replaceAnnotation(SIGNEDNESS_GLB); - } else { - Range treeRange = ValueCheckerUtils.getPossibleValues(valueATM, valueFactory); - - if (treeRange != null) { - switch (javaType.getKind()) { - case BYTE: - case CHAR: - if (treeRange.isWithin(0, Byte.MAX_VALUE)) { - type.replaceAnnotation(SIGNEDNESS_GLB); - } - break; - case SHORT: - if (treeRange.isWithin(0, Short.MAX_VALUE)) { - type.replaceAnnotation(SIGNEDNESS_GLB); - } - break; - case INT: - if (treeRange.isWithin(0, Integer.MAX_VALUE)) { - type.replaceAnnotation(SIGNEDNESS_GLB); - } - break; - case LONG: - if (treeRange.isWithin(0, Long.MAX_VALUE)) { - type.replaceAnnotation(SIGNEDNESS_GLB); - } - break; - default: - // Nothing + if (tree.getKind() == Tree.Kind.VARIABLE) { + return; + } + if (!(javaTypeKind == TypeKind.BYTE + || javaTypeKind == TypeKind.CHAR + || javaTypeKind == TypeKind.SHORT + || javaTypeKind == TypeKind.INT + || javaTypeKind == TypeKind.LONG)) { + return; + } + ValueAnnotatedTypeFactory valueFactory = getTypeFactoryOfSubchecker(ValueChecker.class); + AnnotatedTypeMirror valueATM = valueFactory.getAnnotatedType(tree); + // These annotations are trusted rather than checked. Maybe have an option to + // disable using them? + if ((valueATM.hasAnnotation(INT_RANGE_FROM_NON_NEGATIVE) + || valueATM.hasAnnotation(INT_RANGE_FROM_POSITIVE)) + && type.hasAnnotation(SIGNED)) { + type.replaceAnnotation(SIGNED_POSITIVE); + } else { + Range treeRange = ValueCheckerUtils.getPossibleValues(valueATM, valueFactory); + + if (treeRange != null) { + switch (javaType.getKind()) { + case BYTE: + case CHAR: + if (treeRange.isWithin(0, Byte.MAX_VALUE)) { + type.replaceAnnotation(SIGNED_POSITIVE); } - } + break; + case SHORT: + if (treeRange.isWithin(0, Short.MAX_VALUE)) { + type.replaceAnnotation(SIGNED_POSITIVE); + } + break; + case INT: + if (treeRange.isWithin(0, Integer.MAX_VALUE)) { + type.replaceAnnotation(SIGNED_POSITIVE); + } + break; + case LONG: + if (treeRange.isWithin(0, Long.MAX_VALUE)) { + type.replaceAnnotation(SIGNED_POSITIVE); + } + break; + default: + // Nothing } } } } - /** - * 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 + public AnnotationMirrorSet getWidenedAnnotations( + AnnotationMirrorSet annos, TypeKind typeKind, TypeKind widenedTypeKind) { + assert annos.size() == 1; + + AnnotationMirrorSet result = new AnnotationMirrorSet(); + if (TypeKindUtils.isFloatingPoint(widenedTypeKind)) { + result.add(SIGNED); + return result; } + if (widenedTypeKind == TypeKind.CHAR) { + result.add(UNSIGNED); + return result; + } + if ((widenedTypeKind == TypeKind.INT || widenedTypeKind == TypeKind.LONG) + && typeKind == TypeKind.CHAR) { + result.add(SIGNED_POSITIVE); + return result; + } + return annos; + } + + @Override + public AnnotationMirrorSet getNarrowedAnnotations( + AnnotationMirrorSet annos, TypeKind typeKind, TypeKind narrowedTypeKind) { + assert annos.size() == 1; + + AnnotationMirrorSet result = new AnnotationMirrorSet(); + + if (narrowedTypeKind == TypeKind.CHAR) { + result.add(SIGNED); + return result; + } + + return annos; } @Override @@ -187,11 +250,25 @@ protected TreeAnnotator createTreeAnnotator() { new SignednessTreeAnnotator(this), super.createTreeAnnotator()); } + @Override + public AnnotationMirrorSet annotationsForIrrelevantJavaType(TypeMirror tm) { + if (TypesUtils.isCharType(tm)) { + return UNSIGNED_SINGLETON; + } else { + return SIGNED_SINGLETON; + } + } + /** - * 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. + *
    • casts take types related to widening + *
    */ private class SignednessTreeAnnotator extends TreeAnnotator { @@ -199,40 +276,174 @@ public SignednessTreeAnnotator(AnnotatedTypeFactory atypeFactory) { super(atypeFactory); } - /** - * Change the type of booleans to {@code @UnknownSignedness} so that the {@link - * PropagationTreeAnnotator} does not change the type of them. - */ - private void annotateBooleanAsUnknownSignedness(AnnotatedTypeMirror type) { - switch (type.getKind()) { - case BOOLEAN: - type.addAnnotation(UNKNOWN_SIGNEDNESS); - break; - default: - // Nothing for other cases. - } - } - @Override public Void visitBinary(BinaryTree tree, AnnotatedTypeMirror type) { switch (tree.getKind()) { case LEFT_SHIFT: case RIGHT_SHIFT: case UNSIGNED_RIGHT_SHIFT: - AnnotatedTypeMirror lht = getAnnotatedType(tree.getLeftOperand()); - type.replaceAnnotations(lht.getAnnotations()); + TreePath path = getPath(tree); + if (path != null + && (SignednessShifts.isMaskedShiftEitherSignedness(tree, path) + || SignednessShifts.isCastedShiftEitherSignedness( + tree, path))) { + type.replaceAnnotation(SIGNED_POSITIVE); + } else { + AnnotatedTypeMirror lht = getAnnotatedType(tree.getLeftOperand()); + type.replaceAnnotations(lht.getAnnotations()); + } break; default: // Do nothing } - annotateBooleanAsUnknownSignedness(type); return null; } @Override public Void visitCompoundAssignment(CompoundAssignmentTree tree, AnnotatedTypeMirror type) { - annotateBooleanAsUnknownSignedness(type); + if (TreeUtils.isStringCompoundConcatenation(tree)) { + if (TypesUtils.isCharType(TreeUtils.typeOf(tree.getExpression()))) { + type.replaceAnnotation(SIGNED); + } + } + return null; + } + + @Override + public Void visitTypeCast(TypeCastTree tree, AnnotatedTypeMirror type) { + // Don't change the annotation on a cast with an explicit annotation. + if (TypesUtils.isCharType(type.getUnderlyingType())) { + type.replaceAnnotation(UNSIGNED); + } else if (type.getAnnotations().isEmpty() && !maybeIntegral(type)) { + AnnotatedTypeMirror exprType = atypeFactory.getAnnotatedType(tree.getExpression()); + if ((type.getKind() != TypeKind.TYPEVAR || exprType.getKind() != TypeKind.TYPEVAR) + && !AnnotationUtils.containsSame( + exprType.getEffectiveAnnotations(), UNSIGNED)) { + type.addAnnotation(SIGNED); + } + } + log("SATF.visitTypeCast(%s, ...) final: %s%n", tree, type); + log("SATF: treeAnnotator=%s%n", treeAnnotator); return null; } } + + /** + * Returns true if {@code type}'s underlying type might be integral: it is a number, char, or a + * supertype of them. + * + * @param type a type + * @return true if {@code type}'s underlying type might be integral + */ + public boolean maybeIntegral(AnnotatedTypeMirror type) { + + TypeKind kind = type.getKind(); + + switch (kind) { + case BOOLEAN: + return false; + case BYTE: + case SHORT: + case INT: + case LONG: + case CHAR: + return true; + case FLOAT: + case DOUBLE: + return false; + + case DECLARED: + case TYPEVAR: + case WILDCARD: + TypeMirror erasedType = types.erasure(type.getUnderlyingType()); + return (TypesUtils.isBoxedPrimitive(erasedType) + || TypesUtils.isObject(erasedType) + || TypesUtils.isErasedSubtype(numberTM, erasedType, types) + || TypesUtils.isErasedSubtype(serializableTM, erasedType, types) + || TypesUtils.isErasedSubtype(comparableTM, erasedType, types)); + + default: + return false; + } + } + + @Override + protected void adaptGetClassReturnTypeToReceiver( + AnnotatedExecutableType getClassType, + AnnotatedTypeMirror receiverType, + ExpressionTree tree) { + super.adaptGetClassReturnTypeToReceiver(getClassType, receiverType, tree); + // Make the captured wildcard always @Signed, regardless of the declared type. + AnnotatedDeclaredType returnAdt = (AnnotatedDeclaredType) getClassType.getReturnType(); + List typeArgs = returnAdt.getTypeArguments(); + AnnotatedTypeVariable classWildcardArg = (AnnotatedTypeVariable) typeArgs.get(0); + classWildcardArg.getUpperBound().replaceAnnotation(SIGNED); + } + + @Override + protected void addAnnotationsFromDefaultForType( + @Nullable Element element, AnnotatedTypeMirror type) { + TypeMirror underlying = type.getUnderlyingType(); + if (TypesUtils.isFloatingPrimitive(underlying) + || TypesUtils.isBoxedFloating(underlying) + || TypesUtils.isCharType(underlying)) { + // Floats are always signed and chars are always unsigned. + super.addAnnotationsFromDefaultForType(null, type); + } else { + super.addAnnotationsFromDefaultForType(element, type); + } + } + + /** + * Requires that, when two formal parameter types are annotated with {@code @PolySigned}, the + * two arguments must have the same signedness type annotation. + */ + // Not static because it references SIGNEDNESS_BOTTOM. + private class SignednessQualifierPolymorphism extends DefaultQualifierPolymorphism { + /** + * Creates a {@link SignednessQualifierPolymorphism}. + * + * @param env the processing environment + * @param factory the factory for the current checker + */ + public SignednessQualifierPolymorphism( + ProcessingEnvironment env, AnnotatedTypeFactory factory) { + super(env, factory); + } + + /** + * Combines the two annotations. If they are comparable, return the lub. If they are + * incomparable, return @SignednessBottom. + * + * @param polyQual the polymorphic qualifier + * @param a1 the first annotation to compare + * @param a2 the second annotation to compare + * @return the lub, unless the annotations are incomparable + */ + @Override + protected AnnotationMirror combine( + AnnotationMirror polyQual, AnnotationMirror a1, AnnotationMirror a2) { + if (a1 == null) { + return a2; + } else if (a2 == null) { + return a1; + } else if (AnnotationUtils.areSame(a1, a2)) { + return a1; + } else if (qualHierarchy.isSubtypeQualifiersOnly(a1, a2)) { + return a2; + } else if (qualHierarchy.isSubtypeQualifiersOnly(a2, a1)) { + return a1; + } else + // The two annotations are incomparable + // TODO: Issue a warning at the proper code location. + // TODO: Returning bottom leads to obscure error messages. It would probably be + // better to issue a warning in this method, then return lub as usual. + return SIGNEDNESS_BOTTOM; + } + } + + @Override + protected QualifierPolymorphism createQualifierPolymorphism() { + return new SignednessQualifierPolymorphism(processingEnv, this); + } } diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessChecker.java b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessChecker.java index 5eb118babbd4..a7823bd45686 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessChecker.java @@ -1,9 +1,11 @@ package org.checkerframework.checker.signedness; -import java.util.LinkedHashSet; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.value.ValueChecker; import org.checkerframework.framework.qual.RelevantJavaTypes; +import org.checkerframework.framework.qual.StubFiles; + +import java.util.Set; /** * A type-checker that prevents mixing of unsigned and signed values, and prevents meaningless @@ -11,13 +13,27 @@ * * @checker_framework.manual #signedness-checker Signedness Checker */ -@RelevantJavaTypes({Number.class, Character.class}) +// Character and char are omitted here because they are always @Unsigned, and the user is not +// allowed to write @Signed or @Unsigned on them. +@RelevantJavaTypes({ + Byte.class, + Short.class, + Integer.class, + Long.class, + byte.class, + short.class, + int.class, + long.class, +}) +@StubFiles({"junit-assertions.astub"}) public class SignednessChecker extends BaseTypeChecker { + /** Creates a new SignednessChecker. */ + public SignednessChecker() {} + @Override - protected LinkedHashSet> getImmediateSubcheckerClasses() { - LinkedHashSet> checkers = - super.getImmediateSubcheckerClasses(); + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); checkers.add(ValueChecker.class); return checkers; } diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessShifts.java b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessShifts.java new file mode 100644 index 000000000000..697589069713 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessShifts.java @@ -0,0 +1,304 @@ +package org.checkerframework.checker.signedness; + +import com.sun.source.tree.AnnotatedTypeTree; +import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.LiteralTree; +import com.sun.source.tree.PrimitiveTypeTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.util.TreePath; + +import org.checkerframework.checker.interning.qual.InternedDistinct; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.javacutil.TreePathUtil; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; +import org.plumelib.util.IPair; + +import javax.lang.model.type.TypeKind; + +/** + * This file contains code to special-case shifts whose result does not depend on the MSB of the + * first argument, due to subsequent masking or casts. + * + * @checker_framework.manual #signedness-checker Signedness Checker + */ +public class SignednessShifts { + + /** Do not instantiate SignednessShifts. */ + private SignednessShifts() { + throw new Error("Do not instantiate SignednessShifts"); + } + + /** + * Returns true iff the given tree node is a mask operation (& or |). + * + * @param tree a tree to test + * @return true iff node is a mask operation (& or |) + */ + private static boolean isMask(Tree tree) { + Tree.Kind kind = tree.getKind(); + + return kind == Tree.Kind.AND || kind == Tree.Kind.OR; + } + + // TODO: Return a TypeKind rather than a PrimitiveTypeTree? + /** + * Returns the type of a primitive cast, or null if the argument is not a cast to a primitive. + * + * @param tree a tree that might be a cast to a primitive + * @return type of a primitive cast, or null if not a cast to a primitive + */ + private static @Nullable PrimitiveTypeTree primitiveTypeCast(Tree tree) { + if (tree.getKind() != Tree.Kind.TYPE_CAST) { + return null; + } + + TypeCastTree cast = (TypeCastTree) tree; + Tree castType = cast.getType(); + + Tree underlyingType; + if (castType.getKind() == Tree.Kind.ANNOTATED_TYPE) { + underlyingType = ((AnnotatedTypeTree) castType).getUnderlyingType(); + } else { + underlyingType = castType; + } + + if (underlyingType.getKind() != Tree.Kind.PRIMITIVE_TYPE) { + return null; + } + + return (PrimitiveTypeTree) underlyingType; + } + + /** + * Returns true iff the given tree is a literal. + * + * @param expr a tree to test + * @return true iff expr is a literal + */ + private static boolean isLiteral(ExpressionTree expr) { + return expr instanceof LiteralTree; + } + + /** + * Returns the long value of an Integer or a Long + * + * @param obj either an Integer or a Long + * @return the long value of obj + */ + private static long getLong(Object obj) { + return ((Number) obj).longValue(); + } + + /** + * Given a masking operation of the form {@code expr & maskLit} or {@code expr | maskLit}, + * return true iff the masking operation results in the same output regardless of the value of + * the shiftAmount most significant bits of expr. This is if the shiftAmount most significant + * bits of mask are 0 for AND, and 1 for OR. For example, assuming that shiftAmount is 4, the + * following is true about AND and OR masks: + * + *

    {@code expr & 0x0[anything] == 0x0[something] ;} + * + *

    {@code expr | 0xF[anything] == 0xF[something] ;} + * + * @param maskKind the kind of mask (AND or OR) + * @param shiftAmountLit the LiteralTree whose value is shiftAmount + * @param maskLit the LiteralTree whose value is mask + * @param shiftedTypeKind the type of shift operation; int or long + * @return true iff the shiftAmount most significant bits of mask are 0 for AND, and 1 for OR + */ + private static boolean maskIgnoresMSB( + Tree.Kind maskKind, + LiteralTree shiftAmountLit, + LiteralTree maskLit, + TypeKind shiftedTypeKind) { + long shiftAmount = getLong(shiftAmountLit.getValue()); + + // Shift of zero is a nop + if (shiftAmount == 0) { + return true; + } + + long mask = getLong(maskLit.getValue()); + // Shift the shiftAmount most significant bits to become the shiftAmount least significant + // bits, zeroing out the rest. + if (shiftedTypeKind == TypeKind.INT) { + mask <<= 32; + } + mask >>>= (64 - shiftAmount); + + if (maskKind == Tree.Kind.AND) { + // Check that the shiftAmount most significant bits of the mask were 0. + return mask == 0; + } else if (maskKind == Tree.Kind.OR) { + // Check that the shiftAmount most significant bits of the mask were 1. + return mask == (1 << shiftAmount) - 1; + } else { + throw new TypeSystemError("Invalid Masking Operation"); + } + } + + /** + * Given a casted right shift of the form {@code (type) (baseExpr >> shiftAmount)} or {@code + * (type) (baseExpr >>> shiftAmount)}, return true iff the expression's value is the same + * regardless of the type of right shift (signed or unsigned). This is true if the cast ignores + * the shiftAmount most significant bits of the shift result -- that is, if the cast ignores all + * the new bits that the right shift introduced on the left. + * + *

    For example, the function returns true for + * + *

    {@code (short) (myInt >> 16)}
    + * + * and for + * + *
    {@code (short) (myInt >>> 16)}
    + * + * because these two expressions are guaranteed to have the same result. + * + * @param shiftTypeKind the kind of the type of the shift literal (BYTE, CHAR, SHORT, INT, or + * LONG) + * @param castTypeKind the kind of the cast target type (BYTE, CHAR, SHORT, INT, or LONG) + * @param shiftAmountLit the LiteralTree whose value is shiftAmount + * @return true iff introduced bits are discarded + */ + private static boolean castIgnoresMSB( + TypeKind shiftTypeKind, TypeKind castTypeKind, LiteralTree shiftAmountLit) { + + // Determine number of bits in the shift type, note shifts upcast to int. + // Also determine the shift amount as it is dependent on the shift type. + long shiftBits; + long shiftAmount; + switch (shiftTypeKind) { + case INT: + shiftBits = 32; + // When the LHS of the shift is an int, the 5 lower order bits of the RHS are used. + shiftAmount = 0x1F & getLong(shiftAmountLit.getValue()); + break; + case LONG: + shiftBits = 64; + // When the LHS of the shift is a long, the 6 lower order bits of the RHS are used. + shiftAmount = 0x3F & getLong(shiftAmountLit.getValue()); + break; + default: + throw new TypeSystemError("Invalid shift type"); + } + + // Determine number of bits in the cast type + long castBits; + switch (castTypeKind) { + case BYTE: + castBits = 8; + break; + case CHAR: + castBits = 8; + break; + case SHORT: + castBits = 16; + break; + case INT: + castBits = 32; + break; + case LONG: + castBits = 64; + break; + default: + throw new TypeSystemError("Invalid cast target"); + } + + long bitsDiscarded = shiftBits - castBits; + + return shiftAmount <= bitsDiscarded || shiftAmount == 0; + } + + /** + * Returns true if a right shift operation, {@code >>} or {@code >>>}, is masked with a masking + * operation of the form {@code shiftExpr & maskLit} or {@code shiftExpr | maskLit} such that + * the mask renders the shift signedness ({@code >>} vs {@code >>>}) irrelevant by destroying + * the bits duplicated into the shift result. For example, the following pairs of right shifts + * on {@code byte b} both produce the same results under any input, because of their masks: + * + *

    {@code (b >> 4) & 0x0F == (b >>> 4) & 0x0F;} + * + *

    {@code (b >> 4) | 0xF0 == (b >>> 4) | 0xF0;} + * + * @param shiftExpr a right shift expression: {@code expr1 >> expr2} or {@code expr1 >>> expr2} + * @param path the path to {@code shiftExpr} + * @return true iff the right shift is masked such that a signed or unsigned right shift has the + * same effect + */ + /*package-private*/ static boolean isMaskedShiftEitherSignedness( + BinaryTree shiftExpr, TreePath path) { + IPair enclosingPair = TreePathUtil.enclosingNonParen(path); + // 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. + @SuppressWarnings("interning:assignment.type.incompatible") // comparing AST nodes + @InternedDistinct Tree enclosingChild = enclosingPair.second; + + if (!isMask(enclosing)) { + return false; + } + + BinaryTree maskExpr = (BinaryTree) enclosing; + ExpressionTree shiftAmountExpr = shiftExpr.getRightOperand(); + + // Determine which child of maskExpr leads to shiftExpr. The other one is the mask. + ExpressionTree mask = + maskExpr.getRightOperand() == enclosingChild + ? maskExpr.getLeftOperand() + : maskExpr.getRightOperand(); + + // Strip away the parentheses from the mask if any exist + mask = TreeUtils.withoutParens(mask); + + if (!isLiteral(shiftAmountExpr) || !isLiteral(mask)) { + return false; + } + + LiteralTree shiftLit = (LiteralTree) shiftAmountExpr; + LiteralTree maskLit = (LiteralTree) mask; + + return maskIgnoresMSB( + maskExpr.getKind(), shiftLit, maskLit, TreeUtils.typeOf(shiftExpr).getKind()); + } + + /** + * Returns true if a right shift operation, {@code >>} or {@code >>>}, is type casted such that + * the cast renders the shift signedness ({@code >>} vs {@code >>>}) irrelevant by discarding + * the bits duplicated into the shift result. For example, the following pair of right shifts on + * {@code short s} both produce the same results under any input, because of type casting: + * + *

    {@code (byte)(s >> 8) == (byte)(b >>> 8);} + * + * @param shiftExpr a right shift expression: {@code expr1 >> expr2} or {@code expr1 >>> expr2} + * @param path the path to {@code shiftExpr} + * @return true iff the right shift is type casted such that a signed or unsigned right shift + * has the same effect + */ + /*package-private*/ static boolean isCastedShiftEitherSignedness( + BinaryTree shiftExpr, TreePath path) { + // enclosing immediately contains shiftExpr or a parenthesized version of shiftExpr + Tree enclosing = TreePathUtil.enclosingNonParen(path).first; + + PrimitiveTypeTree castPrimitiveType = primitiveTypeCast(enclosing); + if (castPrimitiveType == null) { + return false; + } + TypeKind castTypeKind = castPrimitiveType.getPrimitiveTypeKind(); + + // Determine the type of the shift result + TypeKind shiftTypeKind = TreeUtils.typeOf(shiftExpr).getKind(); + + // Determine shift literal + ExpressionTree shiftAmountExpr = shiftExpr.getRightOperand(); + if (!isLiteral(shiftAmountExpr)) { + return false; + } + LiteralTree shiftLit = (LiteralTree) shiftAmountExpr; + + boolean result = castIgnoresMSB(shiftTypeKind, castTypeKind, shiftLit); + return result; + } +} 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 d2b3687d6d8f..dc0f41b66241 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/SignednessVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/signedness/SignednessVisitor.java @@ -1,24 +1,30 @@ package org.checkerframework.checker.signedness; -import com.sun.source.tree.AnnotatedTypeTree; import com.sun.source.tree.BinaryTree; import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.LiteralTree; -import com.sun.source.tree.PrimitiveTypeTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; -import com.sun.source.tree.TypeCastTree; -import javax.lang.model.type.TypeKind; + +import org.checkerframework.checker.interning.InterningVisitor; +import org.checkerframework.checker.interning.qual.EqualsMethod; import org.checkerframework.checker.signedness.qual.PolySigned; import org.checkerframework.checker.signedness.qual.Signed; import org.checkerframework.checker.signedness.qual.Unsigned; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import org.checkerframework.javacutil.AnnotationMirrorSet; import org.checkerframework.javacutil.BugInCF; -import org.checkerframework.javacutil.Pair; import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; +import org.plumelib.util.IPair; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeMirror; /** * The SignednessVisitor enforces the Signedness Checker rules. These rules are described in the @@ -32,252 +38,8 @@ public SignednessVisitor(BaseTypeChecker checker) { super(checker); } - /** @return true iff node is a mask operation (& or |) */ - private boolean isMask(Tree node) { - Kind kind = node.getKind(); - - return kind == Kind.AND || kind == Kind.OR; - } - /** - * @return type of an explicitly annotated primitive cast. Return null if not an explicitly - * annotated cast to a primitive. - */ - private PrimitiveTypeTree primitiveTypeCast(Tree node) { - if (node.getKind() != Kind.TYPE_CAST) { - return null; - } - - TypeCastTree cast = (TypeCastTree) node; - Tree castType = cast.getType(); - - // We only care if the cast has an annotation. - if (castType.getKind() != Kind.ANNOTATED_TYPE) { - return null; - } - AnnotatedTypeTree annotatedType = (AnnotatedTypeTree) castType; - ExpressionTree underlyingType = annotatedType.getUnderlyingType(); - - if (underlyingType.getKind() != Kind.PRIMITIVE_TYPE) { - return null; - } - - return (PrimitiveTypeTree) underlyingType; - } - - /** @return true iff expr is a literal */ - private boolean isLiteral(ExpressionTree expr) { - return expr instanceof LiteralTree; - } - - /** - * @param obj either an Integer or a Long - * @return the long value of obj - */ - private long getLong(Object obj) { - return ((Number) obj).longValue(); - } - - /** - * Given a masking operation of the form {@code expr & maskLit} or {@code expr | maskLit}, - * return true iff the masking operation results in the same output regardless of the value of - * the shiftAmount most significant bits of expr. This is if the shiftAmount most significant - * bits of mask are 0 for AND, and 1 for OR. For example, assuming that shiftAmount is 4, the - * following is true about AND and OR masks: - * - *

    {@code expr & 0x0[anything] == 0x0[something] ;} - * - *

    {@code expr | 0xF[anything] == 0xF[something] ;} - * - * @param maskKind the kind of mask (AND or OR) - * @param shiftAmountLit the LiteralTree whose value is shiftAmount - * @param maskLit the LiteralTree whose value is mask - * @return true iff the shiftAmount most significant bits of mask are 0 for AND, and 1 for OR - */ - private boolean maskIgnoresMSB(Kind maskKind, LiteralTree shiftAmountLit, LiteralTree maskLit) { - long shiftAmount = getLong(shiftAmountLit.getValue()); - long mask = getLong(maskLit.getValue()); - - // Shift of zero is a nop - if (shiftAmount == 0) { - return true; - } - - // Shift the shiftAmount most significant bits to become the shiftAmount least significant - // bits, zeroing out the rest. - if (maskLit.getKind() != Kind.LONG_LITERAL) { - mask <<= 32; - } - mask >>>= (64 - shiftAmount); - - if (maskKind == Kind.AND) { - // Check that the shiftAmount most significant bits of the mask were 0. - return mask == 0; - } else if (maskKind == Kind.OR) { - // Check that the shiftAmount most significant bits of the mask were 1. - return mask == (1 << shiftAmount) - 1; - } else { - throw new BugInCF("Invalid Masking Operation"); - } - } - - /** - * Given a casted right shift of the form {@code (type) (baseExpr >> shiftAmount)} or {@code - * (type) (baseExpr >>> shiftAmount)}, return true iff the expression's value is the same - * regardless of the type of right shift (signed or unsigned). This is true if the cast ignores - * the shiftAmount most significant bits of the shift result -- that is, if the cast ignores all - * the new bits that the right shift introduced on the left. - * - *

    For example, the function returns true for - * - *

    {@code (short) (myInt >> 16)}
    - * - * and for - * - *
    {@code (short) (myInt >>> 16)}
    - * - * because these two expressions are guaranteed to have the same result. - * - * @param shiftTypeKind the kind of the type of the shift literal (BYTE, CHAR, SHORT, INT, or - * LONG) - * @param castTypeKind the kind of the cast target type (BYTE, CHAR, SHORT, INT, or LONG) - * @param shiftAmountLit the LiteralTree whose value is shiftAmount - * @return true iff introduced bits are discarded - */ - private boolean castIgnoresMSB( - TypeKind shiftTypeKind, TypeKind castTypeKind, LiteralTree shiftAmountLit) { - // Determine number of bits in the shift type, note shifts upcast to int. - // Also determine the shift amount as it is dependent on the shift type. - long shiftBits; - long shiftAmount; - switch (shiftTypeKind) { - case INT: - shiftBits = 32; - // When the LHS of the shift is an int, the 5 lower order bits of the RHS are used. - shiftAmount = 0x1F & getLong(shiftAmountLit.getValue()); - break; - case LONG: - shiftBits = 64; - // When the LHS of the shift is a long, the 6 lower order bits of the RHS are used. - shiftAmount = 0x3F & getLong(shiftAmountLit.getValue()); - break; - default: - throw new BugInCF("Invalid shift type"); - } - - // Determine number of bits in the cast type - long castBits; - switch (castTypeKind) { - case BYTE: - castBits = 8; - break; - case CHAR: - castBits = 8; - break; - case SHORT: - castBits = 16; - break; - case INT: - castBits = 32; - break; - case LONG: - castBits = 64; - break; - default: - throw new BugInCF("Invalid cast target"); - } - - long bitsDiscarded = shiftBits - castBits; - - return shiftAmount <= bitsDiscarded || shiftAmount == 0; - } - - /** - * Determines if a right shift operation, {@code >>} or {@code >>>}, is masked with a masking - * operation of the form {@code shiftExpr & maskLit} or {@code shiftExpr | maskLit} such that - * the mask renders the shift signedness ({@code >>} vs {@code >>>}) irrelevent by destroying - * the bits duplicated into the shift result. For example, the following pairs of right shifts - * on {@code byte b} both produce the same results under any input, because of their masks: - * - *

    {@code (b >> 4) & 0x0F == (b >>> 4) & 0x0F;} - * - *

    {@code (b >> 4) | 0xF0 == (b >>> 4) | 0xF0;} - * - * @param shiftExpr a right shift expression: {@code expr1 >> expr2} or {@code expr1 >>> expr2} - * @return true iff the right shift is masked such that a signed or unsigned right shift has the - * same effect - */ - private boolean isMaskedShiftEitherSignedness(BinaryTree shiftExpr) { - Pair enclosingPair = TreeUtils.enclosingNonParen(visitorState.getPath()); - // 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; - - if (!isMask(enclosing)) { - return false; - } - - BinaryTree maskExpr = (BinaryTree) enclosing; - ExpressionTree shiftAmountExpr = shiftExpr.getRightOperand(); - - // Determine which child of maskExpr leads to shiftExpr. The other one is the mask. - ExpressionTree mask = - maskExpr.getRightOperand() == enclosingChild - ? maskExpr.getLeftOperand() - : maskExpr.getRightOperand(); - - // Strip away the parentheses from the mask if any exist - mask = TreeUtils.withoutParens(mask); - - if (!isLiteral(shiftAmountExpr) || !isLiteral(mask)) { - return false; - } - - LiteralTree shiftLit = (LiteralTree) shiftAmountExpr; - LiteralTree maskLit = (LiteralTree) mask; - - return maskIgnoresMSB(maskExpr.getKind(), shiftLit, maskLit); - } - - /** - * Determines if a right shift operation, {@code >>} or {@code >>>}, is type casted such that - * the cast renders the shift signedness ({@code >>} vs {@code >>>}) irrelevent by discarding - * the bits duplicated into the shift result. For example, the following pair of right shifts on - * {@code short s} both produce the same results under any input, because of type casting: - * - *

    {@code (byte)(s >> 8) == (byte)(b >>> 8);} - * - * @param shiftExpr a right shift expression: {@code expr1 >> expr2} or {@code expr1 >>> expr2} - * @return true iff the right shift is type casted such that a signed or unsigned right shift - * has the same effect - */ - private boolean isCastedShiftEitherSignedness(BinaryTree shiftExpr) { - // enclosing immediately contains shiftExpr or a parenthesized version of shiftExpr - Tree enclosing = TreeUtils.enclosingNonParen(visitorState.getPath()).first; - - PrimitiveTypeTree castPrimitiveType = primitiveTypeCast(enclosing); - if (castPrimitiveType == null) { - return false; - } - TypeKind castTypeKind = castPrimitiveType.getPrimitiveTypeKind(); - - // Determine the type of the shift result - TypeKind shiftTypeKind = - atypeFactory.getAnnotatedType(shiftExpr).getUnderlyingType().getKind(); - - // Determine shift literal - ExpressionTree shiftAmountExpr = shiftExpr.getRightOperand(); - if (!isLiteral(shiftAmountExpr)) { - return false; - } - LiteralTree shiftLit = (LiteralTree) shiftAmountExpr; - - return castIgnoresMSB(shiftTypeKind, castTypeKind, shiftLit); - } - - /** - * Determines if an annotated type is annotated as {@link Unsigned} or {@link PolySigned} + * Returns true if an annotated type is annotated as {@link Unsigned} or {@link PolySigned} * * @param type the annotated type to be checked * @return true if the annotated type is annotated as {@link Unsigned} or {@link PolySigned} @@ -287,7 +49,7 @@ private boolean hasUnsignedAnnotation(AnnotatedTypeMirror type) { } /** - * Determines if an annotated type is annotated as {@link Signed} or {@link PolySigned} + * Returns true if an annotated type is annotated as {@link Signed} or {@link PolySigned} * * @param type the annotated type to be checked * @return true if the annotated type is annotated as {@link Signed} or {@link PolySigned} @@ -312,38 +74,45 @@ private boolean hasSignedAnnotation(AnnotatedTypeMirror type) { * */ @Override - public Void visitBinary(BinaryTree node, Void p) { + public Void visitBinary(BinaryTree tree, Void p) { + // Used in diagnostic messages. + ExpressionTree leftOp = tree.getLeftOperand(); + ExpressionTree rightOp = tree.getRightOperand(); - ExpressionTree leftOp = node.getLeftOperand(); - ExpressionTree rightOp = node.getRightOperand(); - AnnotatedTypeMirror leftOpType = atypeFactory.getAnnotatedType(leftOp); - AnnotatedTypeMirror rightOpType = atypeFactory.getAnnotatedType(rightOp); + IPair argTypes = + atypeFactory.binaryTreeArgTypes(tree); + AnnotatedTypeMirror leftOpType = argTypes.first; + AnnotatedTypeMirror rightOpType = argTypes.second; - Kind kind = node.getKind(); + Tree.Kind kind = tree.getKind(); switch (kind) { case DIVIDE: case REMAINDER: if (hasUnsignedAnnotation(leftOpType)) { - checker.reportError(leftOp, "operation.unsignedlhs", kind); + checker.reportError( + leftOp, "operation.unsignedlhs", kind, leftOpType, rightOpType); } else if (hasUnsignedAnnotation(rightOpType)) { - checker.reportError(rightOp, "operation.unsignedrhs", kind); + checker.reportError( + rightOp, "operation.unsignedrhs", kind, leftOpType, rightOpType); } break; case RIGHT_SHIFT: if (hasUnsignedAnnotation(leftOpType) - && !isMaskedShiftEitherSignedness(node) - && !isCastedShiftEitherSignedness(node)) { - checker.reportError(leftOp, "shift.signed", kind); + && !SignednessShifts.isMaskedShiftEitherSignedness(tree, getCurrentPath()) + && !SignednessShifts.isCastedShiftEitherSignedness( + tree, getCurrentPath())) { + checker.reportError(leftOp, "shift.signed", kind, leftOpType, rightOpType); } break; case UNSIGNED_RIGHT_SHIFT: if (hasSignedAnnotation(leftOpType) - && !isMaskedShiftEitherSignedness(node) - && !isCastedShiftEitherSignedness(node)) { - checker.reportError(leftOp, "shift.unsigned", kind); + && !SignednessShifts.isMaskedShiftEitherSignedness(tree, getCurrentPath()) + && !SignednessShifts.isCastedShiftEitherSignedness( + tree, getCurrentPath())) { + checker.reportError(leftOp, "shift.unsigned", kind, leftOpType, rightOpType); } break; @@ -355,38 +124,143 @@ public Void visitBinary(BinaryTree node, Void p) { case LESS_THAN: case LESS_THAN_EQUAL: if (hasUnsignedAnnotation(leftOpType)) { - checker.reportError(leftOp, "comparison.unsignedlhs"); + checker.reportError(leftOp, "comparison.unsignedlhs", leftOpType, rightOpType); } else if (hasUnsignedAnnotation(rightOpType)) { - checker.reportError(rightOp, "comparison.unsignedrhs"); + checker.reportError(rightOp, "comparison.unsignedrhs", leftOpType, rightOpType); } break; case EQUAL_TO: case NOT_EQUAL_TO: - if (leftOpType.hasAnnotation(Unsigned.class) - && rightOpType.hasAnnotation(Signed.class)) { - checker.reportError(node, "comparison.mixed.unsignedlhs"); - } else if (leftOpType.hasAnnotation(Signed.class) - && rightOpType.hasAnnotation(Unsigned.class)) { - checker.reportError(node, "comparison.mixed.unsignedrhs"); + if (!atypeFactory.maybeIntegral(leftOpType) + || !atypeFactory.maybeIntegral(rightOpType)) { + break; + } + if (leftOpType.hasEffectiveAnnotation(Unsigned.class) + && rightOpType.hasEffectiveAnnotation(Signed.class)) { + checker.reportError( + tree, "comparison.mixed.unsignedlhs", leftOpType, rightOpType); + } else if (leftOpType.hasEffectiveAnnotation(Signed.class) + && rightOpType.hasEffectiveAnnotation(Unsigned.class)) { + checker.reportError( + tree, "comparison.mixed.unsignedrhs", leftOpType, rightOpType); } break; + case PLUS: + if (TreeUtils.isStringConcatenation(tree)) { + // Note that leftOpType.getUnderlyingType() and rightOpType.getUnderlyingType() + // are always java.lang.String. Please refer to binaryTreeArgTypes for more + // details. + // Here, the original types of operands can be something different from string. + // For example, "123" + obj is a string concatenation in which the original type + // of the right operand is java.lang.Object. + TypeMirror leftOpTM = TreeUtils.typeOf(leftOp); + AnnotationMirror leftAnno = + leftOpType.getEffectiveAnnotationInHierarchy(atypeFactory.SIGNED); + TypeMirror rightOpTM = TreeUtils.typeOf(rightOp); + AnnotationMirror rightAnno = + rightOpType.getEffectiveAnnotationInHierarchy(atypeFactory.SIGNED); + if (!TypesUtils.isCharType(leftOpTM) + && !qualHierarchy.isSubtypeQualifiersOnly( + leftAnno, atypeFactory.SIGNED)) { + checker.reportError(leftOp, "unsigned.concat"); + } else if (!TypesUtils.isCharType(rightOpTM) + && !qualHierarchy.isSubtypeQualifiersOnly( + rightAnno, atypeFactory.SIGNED)) { + checker.reportError(rightOp, "unsigned.concat"); + } + break; + } + // Other plus binary trees should be handled in the default case. + // fall through default: - if (leftOpType.hasAnnotation(Unsigned.class) + if (leftOpType.hasEffectiveAnnotation(Unsigned.class) + && rightOpType.hasEffectiveAnnotation(Signed.class)) { + checker.reportError( + tree, "operation.mixed.unsignedlhs", kind, leftOpType, rightOpType); + } else if (leftOpType.hasEffectiveAnnotation(Signed.class) + && rightOpType.hasEffectiveAnnotation(Unsigned.class)) { + checker.reportError( + tree, "operation.mixed.unsignedrhs", kind, leftOpType, rightOpType); + } + break; + } + return super.visitBinary(tree, p); + } + + // Ensure that method annotations are not written on methods they don't apply to. + // Copied from InterningVisitor + @Override + public Void visitMethod(MethodTree tree, Void p) { + ExecutableElement methElt = TreeUtils.elementFromDeclaration(tree); + boolean hasEqualsMethodAnno = + atypeFactory.getDeclAnnotation(methElt, EqualsMethod.class) != null; + int params = methElt.getParameters().size(); + if (hasEqualsMethodAnno && !(params == 1 || params == 2)) { + checker.reportError( + tree, "invalid.method.annotation", "@EqualsMethod", "1 or 2", methElt, params); + } + + return super.visitMethod(tree, p); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { + ExecutableElement methElt = TreeUtils.elementFromUse(tree); + boolean hasEqualsMethodAnno = + atypeFactory.getDeclAnnotation(methElt, EqualsMethod.class) != null; + if (hasEqualsMethodAnno || InterningVisitor.isInvocationOfEquals(tree)) { + int params = methElt.getParameters().size(); + if (!(params == 1 || params == 2)) { + checker.reportError( + tree, + "invalid.method.annotation", + "@EqualsMethod", + "1 or 2", + methElt, + params); + } else { + AnnotatedTypeMirror leftOpType; + AnnotatedTypeMirror rightOpType; + if (params == 1) { + leftOpType = atypeFactory.getReceiverType(tree); + rightOpType = atypeFactory.getAnnotatedType(tree.getArguments().get(0)); + } else if (params == 2) { + leftOpType = atypeFactory.getAnnotatedType(tree.getArguments().get(0)); + rightOpType = atypeFactory.getAnnotatedType(tree.getArguments().get(1)); + } else { + throw new BugInCF("Checked that params is 1 or 2"); + } + if (!atypeFactory.maybeIntegral(leftOpType) + || !atypeFactory.maybeIntegral(rightOpType)) { + // nothing to do + } else if (leftOpType.hasAnnotation(Unsigned.class) && rightOpType.hasAnnotation(Signed.class)) { - checker.reportError(node, "operation.mixed.unsignedlhs", kind); + checker.reportError( + tree, "comparison.mixed.unsignedlhs", leftOpType, rightOpType); } else if (leftOpType.hasAnnotation(Signed.class) && rightOpType.hasAnnotation(Unsigned.class)) { - checker.reportError(node, "operation.mixed.unsignedrhs", kind); + checker.reportError( + tree, "comparison.mixed.unsignedrhs", leftOpType, rightOpType); } - break; + } + // Don't check against the annotated method declaration (which super would do). + return null; } - return super.visitBinary(node, p); + + return super.visitMethodInvocation(tree, p); } - /** @return a string representation of kind, with trailing _ASSIGNMENT stripped off if any */ - private String kindWithoutAssignment(Kind kind) { + /** + * Returns a string representation of {@code kind}, with trailing _ASSIGNMENT stripped off if + * any. + * + * @param kind a tree kind + * @return a string representation of {@code kind}, with trailing _ASSIGNMENT stripped off if + * any + */ + private String kindWithoutAssignment(Tree.Kind kind) { String result = kind.toString(); if (result.endsWith("_ASSIGNMENT")) { return result.substring(0, result.length() - "_ASSIGNMENT".length()); @@ -409,14 +283,17 @@ private String kindWithoutAssignment(Kind kind) { * */ @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { + public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { + + ExpressionTree var = tree.getVariable(); + ExpressionTree expr = tree.getExpression(); - ExpressionTree var = node.getVariable(); - ExpressionTree expr = node.getExpression(); - AnnotatedTypeMirror varType = atypeFactory.getAnnotatedType(var); - AnnotatedTypeMirror exprType = atypeFactory.getAnnotatedType(expr); + IPair argTypes = + atypeFactory.compoundAssignmentTreeArgTypes(tree); + AnnotatedTypeMirror varType = argTypes.first; + AnnotatedTypeMirror exprType = argTypes.second; - Kind kind = node.getKind(); + Tree.Kind kind = tree.getKind(); switch (kind) { case DIVIDE_ASSIGNMENT: @@ -425,12 +302,16 @@ public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { checker.reportError( var, "compound.assignment.unsigned.variable", - kindWithoutAssignment(kind)); + kindWithoutAssignment(kind), + varType, + exprType); } else if (hasUnsignedAnnotation(exprType)) { checker.reportError( expr, "compound.assignment.unsigned.expression", - kindWithoutAssignment(kind)); + kindWithoutAssignment(kind), + varType, + exprType); } break; @@ -440,7 +321,8 @@ public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { var, "compound.assignment.shift.signed", kindWithoutAssignment(kind), - "unsigned"); + varType, + exprType); } break; @@ -450,28 +332,67 @@ public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { var, "compound.assignment.shift.unsigned", kindWithoutAssignment(kind), - "signed"); + varType, + exprType); } break; case LEFT_SHIFT_ASSIGNMENT: break; + case PLUS_ASSIGNMENT: + if (TreeUtils.isStringCompoundConcatenation(tree)) { + // Note that exprType.getUnderlyingType() is always java.lang.String. + // Please refer to compoundAssignmentTreeArgTypes for more details. + if (TypesUtils.isCharType(TreeUtils.typeOf(expr))) { + break; + } + AnnotationMirror exprAnno = + exprType.getEffectiveAnnotationInHierarchy(atypeFactory.SIGNED); + if (!qualHierarchy.isSubtypeQualifiersOnly(exprAnno, atypeFactory.SIGNED)) { + checker.reportError(tree.getExpression(), "unsigned.concat"); + } + break; + } + // Other plus binary trees should be handled in the default case. + // fall through default: if (varType.hasAnnotation(Unsigned.class) && exprType.hasAnnotation(Signed.class)) { checker.reportError( expr, "compound.assignment.mixed.unsigned.variable", - kindWithoutAssignment(kind)); + kindWithoutAssignment(kind), + varType, + exprType); } else if (varType.hasAnnotation(Signed.class) && exprType.hasAnnotation(Unsigned.class)) { checker.reportError( expr, "compound.assignment.mixed.unsigned.expression", - kindWithoutAssignment(kind)); + kindWithoutAssignment(kind), + varType, + exprType); } break; } - return super.visitCompoundAssignment(node, p); + return super.visitCompoundAssignment(tree, p); } + + @Override + protected boolean isTypeCastSafe(AnnotatedTypeMirror castType, AnnotatedTypeMirror exprType) { + if (!atypeFactory.maybeIntegral(castType)) { + // If the cast is not a number or a char, then it is legal. + return true; + } + return super.isTypeCastSafe(castType, exprType); + } + + @Override + protected AnnotationMirrorSet getExceptionParameterLowerBoundAnnotations() { + return new AnnotationMirrorSet(atypeFactory.SIGNED); + } + + @Override + protected void checkConstructorResult( + AnnotatedExecutableType constructorType, ExecutableElement constructorElement) {} } diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/junit-assertions.astub b/checker/src/main/java/org/checkerframework/checker/signedness/junit-assertions.astub new file mode 100644 index 000000000000..ef0171f01159 --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/signedness/junit-assertions.astub @@ -0,0 +1,593 @@ +package org.junit.jupiter.api; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.checkerframework.checker.signedness.qual.UnknownSignedness; + +public class Assertions { + + public static V fail(); + + public static V fail(String message); + + public static V fail(String message, Throwable cause); + + public static V fail(Throwable cause); + + public static V fail(Supplier messageSupplier); + + public static void assertTrue(boolean condition); + + public static void assertTrue(boolean condition, Supplier messageSupplier); + + public static void assertTrue(BooleanSupplier booleanSupplier); + + public static void assertTrue(BooleanSupplier booleanSupplier, String message); + + public static void assertTrue(boolean condition, String message); + + public static void assertTrue(BooleanSupplier booleanSupplier, Supplier messageSupplier); + + public static void assertFalse(boolean condition); + + public static void assertFalse(boolean condition, String message); + + public static void assertFalse(boolean condition, Supplier messageSupplier); + + public static void assertFalse(BooleanSupplier booleanSupplier); + + public static void assertFalse(BooleanSupplier booleanSupplier, String message); + + public static void assertFalse(BooleanSupplier booleanSupplier, Supplier messageSupplier); + + public static void assertNull(@UnknownSignedness Object actual); + + public static void assertNull(@UnknownSignedness Object actual, String message); + + public static void assertNull(@UnknownSignedness Object actual, Supplier messageSupplier); + + public static void assertNotNull(@UnknownSignedness Object actual); + + public static void assertNotNull(@UnknownSignedness Object actual, String message); + + public static void assertNotNull(@UnknownSignedness Object actual, Supplier messageSupplier); + + public static void assertEquals(short expected, short actual); + + public static void assertEquals(short expected, Short actual); + + public static void assertEquals(Short expected, short actual); + + public static void assertEquals(Short expected, Short actual); + + public static void assertEquals(short expected, short actual, String message); + + public static void assertEquals(short expected, Short actual, String message); + + public static void assertEquals(Short expected, short actual, String message); + + public static void assertEquals(Short expected, Short actual, String message); + + public static void assertEquals(short expected, short actual, Supplier messageSupplier); + + public static void assertEquals(short expected, Short actual, Supplier messageSupplier); + + public static void assertEquals(Short expected, short actual, Supplier messageSupplier); + + public static void assertEquals(Short expected, Short actual, Supplier messageSupplier); + + public static void assertEquals(byte expected, byte actual); + + public static void assertEquals(byte expected, Byte actual); + + public static void assertEquals(Byte expected, byte actual); + + public static void assertEquals(Byte expected, Byte actual); + + public static void assertEquals(byte expected, byte actual, String message); + + public static void assertEquals(byte expected, Byte actual, String message); + + public static void assertEquals(Byte expected, byte actual, String message); + + public static void assertEquals(Byte expected, Byte actual, String message); + + public static void assertEquals(byte expected, byte actual, Supplier messageSupplier); + + public static void assertEquals(byte expected, Byte actual, Supplier messageSupplier); + + public static void assertEquals(Byte expected, byte actual, Supplier messageSupplier); + + public static void assertEquals(Byte expected, Byte actual, Supplier messageSupplier); + + public static void assertEquals(int expected, int actual); + + public static void assertEquals(int expected, Integer actual); + + public static void assertEquals(Integer expected, int actual); + + public static void assertEquals(Integer expected, Integer actual); + + public static void assertEquals(int expected, int actual, String message); + + public static void assertEquals(int expected, Integer actual, String message); + + public static void assertEquals(Integer expected, int actual, String message); + + public static void assertEquals(Integer expected, Integer actual, String message); + + public static void assertEquals(int expected, int actual, Supplier messageSupplier); + + public static void assertEquals(int expected, Integer actual, Supplier messageSupplier); + + public static void assertEquals(Integer expected, int actual, Supplier messageSupplier); + + public static void assertEquals(Integer expected, Integer actual, Supplier messageSupplier); + + public static void assertEquals(long expected, long actual); + + public static void assertEquals(long expected, Long actual); + + public static void assertEquals(Long expected, long actual); + + public static void assertEquals(Long expected, Long actual); + + public static void assertEquals(long expected, long actual, String message); + + public static void assertEquals(long expected, Long actual, String message); + + public static void assertEquals(Long expected, long actual, String message); + + public static void assertEquals(Long expected, Long actual, String message); + + public static void assertEquals(long expected, long actual, Supplier messageSupplier); + + public static void assertEquals(long expected, Long actual, Supplier messageSupplier); + + public static void assertEquals(Long expected, long actual, Supplier messageSupplier); + + public static void assertEquals(Long expected, Long actual, Supplier messageSupplier); + + public static void assertEquals(float expected, float actual); + + public static void assertEquals(float expected, Float actual); + + public static void assertEquals(Float expected, float actual); + + public static void assertEquals(Float expected, Float actual); + + public static void assertEquals(float expected, float actual, String message); + + public static void assertEquals(float expected, Float actual, String message); + + public static void assertEquals(Float expected, float actual, String message); + + public static void assertEquals(Float expected, Float actual, String message); + + public static void assertEquals(float expected, float actual, Supplier messageSupplier); + + public static void assertEquals(float expected, Float actual, Supplier messageSupplier); + + public static void assertEquals(Float expected, float actual, Supplier messageSupplier); + + public static void assertEquals(Float expected, Float actual, Supplier messageSupplier); + + public static void assertEquals(float expected, float actual, float delta); + + public static void assertEquals(float expected, float actual, float delta, String message); + + public static void assertEquals(float expected, float actual, float delta, Supplier messageSupplier); + + public static void assertEquals(double expected, double actual); + + public static void assertEquals(double expected, Double actual); + + public static void assertEquals(Double expected, double actual); + + public static void assertEquals(Double expected, Double actual); + + public static void assertEquals(double expected, double actual, String message); + + public static void assertEquals(double expected, Double actual, String message); + + public static void assertEquals(Double expected, double actual, String message); + + public static void assertEquals(Double expected, Double actual, String message); + + public static void assertEquals(double expected, double actual, Supplier messageSupplier); + + public static void assertEquals(double expected, Double actual, Supplier messageSupplier); + + public static void assertEquals(Double expected, double actual, Supplier messageSupplier); + + public static void assertEquals(Double expected, Double actual, Supplier messageSupplier); + + public static void assertEquals(double expected, double actual, double delta); + + public static void assertEquals(double expected, double actual, double delta, String message); + + public static void assertEquals(double expected, double actual, double delta, Supplier messageSupplier); + + public static void assertEquals(char expected, char actual); + + public static void assertEquals(char expected, Character actual); + + public static void assertEquals(Character expected, char actual); + + public static void assertEquals(Character expected, Character actual); + + public static void assertEquals(char expected, char actual, String message); + + public static void assertEquals(char expected, Character actual, String message); + + public static void assertEquals(Character expected, char actual, String message); + + public static void assertEquals(Character expected, Character actual, String message); + + public static void assertEquals(char expected, char actual, Supplier messageSupplier); + + public static void assertEquals(char expected, Character actual, Supplier messageSupplier); + + public static void assertEquals(Character expected, char actual, Supplier messageSupplier); + + public static void assertEquals(Character expected, Character actual, Supplier messageSupplier); + + public static void assertEquals(@UnknownSignedness Object expected, @UnknownSignedness Object actual); + + public static void assertEquals(@UnknownSignedness Object expected, @UnknownSignedness Object actual, String message); + + public static void assertEquals(@UnknownSignedness Object expected, @UnknownSignedness Object actual, Supplier messageSupplier); + + public static void assertArrayEquals(boolean [] expected, boolean [] actual); + + public static void assertArrayEquals(boolean [] expected, boolean [] actual, String message); + + public static void assertArrayEquals(boolean [] expected, boolean [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(char [] expected, char [] actual); + + public static void assertArrayEquals(char [] expected, char [] actual, String message); + + public static void assertArrayEquals(char [] expected, char [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(byte [] expected, byte [] actual); + + public static void assertArrayEquals(byte [] expected, byte [] actual, String message); + + public static void assertArrayEquals(byte [] expected, byte [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(short [] expected, short [] actual); + + public static void assertArrayEquals(short [] expected, short [] actual, String message); + + public static void assertArrayEquals(short [] expected, short [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(int [] expected, int [] actual); + + public static void assertArrayEquals(int [] expected, int [] actual, String message); + + public static void assertArrayEquals(int [] expected, int [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(long [] expected, long [] actual); + + public static void assertArrayEquals(long [] expected, long [] actual, String message); + + public static void assertArrayEquals(long [] expected, long [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(float [] expected, float [] actual); + + public static void assertArrayEquals(float [] expected, float [] actual, String message); + + public static void assertArrayEquals(float [] expected, float [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(float[] expected, float[] actual, float delta); + + public static void assertArrayEquals(float[] expected, float[] actual, float delta, String message); + + public static void assertArrayEquals(float[] expected, float[] actual, float delta, + Supplier messageSupplier); + + public static void assertArrayEquals(double [] expected, double [] actual); + + public static void assertArrayEquals(double [] expected, double [] actual, String message); + + public static void assertArrayEquals(double [] expected, double [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(double[] expected, double[] actual, double delta); + + public static void assertArrayEquals(double[] expected, double[] actual, double delta, String message); + + public static void assertArrayEquals(double[] expected, double[] actual, double delta, + Supplier messageSupplier); + + public static void assertArrayEquals(@UnknownSignedness Object [] expected, @UnknownSignedness Object [] actual); + + public static void assertArrayEquals(@UnknownSignedness Object [] expected, @UnknownSignedness Object [] actual, String message); + + public static void assertArrayEquals(@UnknownSignedness Object [] expected, @UnknownSignedness Object [] actual, Supplier messageSupplier); + + public static void assertIterableEquals(Iterable expected, Iterable actual); + + public static void assertIterableEquals(Iterable expected, Iterable actual, String message); + + public static void assertIterableEquals(Iterable expected, Iterable actual, + Supplier messageSupplier); + + public static void assertLinesMatch(List expectedLines, List actualLines); + + public static void assertLinesMatch(List expectedLines, List actualLines, String message); + + public static void assertLinesMatch(List expectedLines, List actualLines, + Supplier messageSupplier); + + public static void assertLinesMatch(Stream expectedLines, Stream actualLines); + + public static void assertLinesMatch(Stream expectedLines, Stream actualLines, String message); + + public static void assertLinesMatch(Stream expectedLines, Stream actualLines, + Supplier messageSupplier); + + public static void assertNotEquals(byte unexpected, byte actual); + + public static void assertNotEquals(byte unexpected, Byte actual); + + public static void assertNotEquals(Byte unexpected, byte actual); + + public static void assertNotEquals(Byte unexpected, Byte actual); + + public static void assertNotEquals(byte unexpected, byte actual, String message); + + public static void assertNotEquals(byte unexpected, Byte actual, String message); + + public static void assertNotEquals(Byte unexpected, byte actual, String message); + + public static void assertNotEquals(Byte unexpected, Byte actual, String message); + + public static void assertNotEquals(byte unexpected, byte actual, Supplier messageSupplier); + + public static void assertNotEquals(byte unexpected, Byte actual, Supplier messageSupplier); + + public static void assertNotEquals(Byte unexpected, byte actual, Supplier messageSupplier); + + public static void assertNotEquals(Byte unexpected, Byte actual, Supplier messageSupplier); + + public static void assertNotEquals(short unexpected, short actual); + + public static void assertNotEquals(short unexpected, Short actual); + + public static void assertNotEquals(Short unexpected, short actual); + + public static void assertNotEquals(Short unexpected, Short actual); + + public static void assertNotEquals(short unexpected, short actual, String message); + + public static void assertNotEquals(short unexpected, Short actual, String message); + + public static void assertNotEquals(Short unexpected, short actual, String message); + + public static void assertNotEquals(Short unexpected, Short actual, String message); + + public static void assertNotEquals(short unexpected, short actual, Supplier messageSupplier); + + public static void assertNotEquals(short unexpected, Short actual, Supplier messageSupplier); + + public static void assertNotEquals(Short unexpected, short actual, Supplier messageSupplier); + + public static void assertNotEquals(Short unexpected, Short actual, Supplier messageSupplier); + + public static void assertNotEquals(int unexpected, int actual); + + public static void assertNotEquals(int unexpected, Integer actual); + + public static void assertNotEquals(Integer unexpected, int actual); + + public static void assertNotEquals(Integer unexpected, Integer actual); + + public static void assertNotEquals(int unexpected, int actual, String message); + + public static void assertNotEquals(int unexpected, Integer actual, String message); + + public static void assertNotEquals(Integer unexpected, int actual, String message); + + public static void assertNotEquals(Integer unexpected, Integer actual, String message); + + public static void assertNotEquals(int unexpected, int actual, Supplier messageSupplier); + + public static void assertNotEquals(int unexpected, Integer actual, Supplier messageSupplier); + + public static void assertNotEquals(Integer unexpected, int actual, Supplier messageSupplier); + + public static void assertNotEquals(Integer unexpected, Integer actual, Supplier messageSupplier); + + public static void assertNotEquals(long unexpected, long actual); + + public static void assertNotEquals(long unexpected, Long actual); + + public static void assertNotEquals(Long unexpected, long actual); + + public static void assertNotEquals(Long unexpected, Long actual); + + public static void assertNotEquals(long unexpected, long actual, String message); + + public static void assertNotEquals(long unexpected, Long actual, String message); + + public static void assertNotEquals(Long unexpected, long actual, String message); + + public static void assertNotEquals(Long unexpected, Long actual, String message); + + public static void assertNotEquals(long unexpected, long actual, Supplier messageSupplier); + + public static void assertNotEquals(long unexpected, Long actual, Supplier messageSupplier); + + public static void assertNotEquals(Long unexpected, long actual, Supplier messageSupplier); + + public static void assertNotEquals(Long unexpected, Long actual, Supplier messageSupplier); + + public static void assertNotEquals(float unexpected, float actual); + + public static void assertNotEquals(float unexpected, Float actual); + + public static void assertNotEquals(Float unexpected, float actual); + + public static void assertNotEquals(Float unexpected, Float actual); + + public static void assertNotEquals(float unexpected, float actual, String message); + + public static void assertNotEquals(float unexpected, Float actual, String message); + + public static void assertNotEquals(Float unexpected, float actual, String message); + + public static void assertNotEquals(Float unexpected, Float actual, String message); + + public static void assertNotEquals(float unexpected, float actual, Supplier messageSupplier); + + public static void assertNotEquals(float unexpected, Float actual, Supplier messageSupplier); + + public static void assertNotEquals(Float unexpected, float actual, Supplier messageSupplier); + + public static void assertNotEquals(Float unexpected, Float actual, Supplier messageSupplier); + + public static void assertNotEquals(float unexpected, float actual, float delta); + + public static void assertNotEquals(float unexpected, float actual, float delta, String message); + + public static void assertNotEquals(float unexpected, float actual, float delta, Supplier messageSupplier); + + public static void assertNotEquals(double unexpected, double actual); + + public static void assertNotEquals(double unexpected, Double actual); + + public static void assertNotEquals(Double unexpected, double actual); + + public static void assertNotEquals(Double unexpected, Double actual); + + public static void assertNotEquals(double unexpected, double actual, String message); + + public static void assertNotEquals(double unexpected, Double actual, String message); + + public static void assertNotEquals(Double unexpected, double actual, String message); + + public static void assertNotEquals(Double unexpected, Double actual, String message); + + public static void assertNotEquals(double unexpected, double actual, Supplier messageSupplier); + + public static void assertNotEquals(double unexpected, Double actual, Supplier messageSupplier); + + public static void assertNotEquals(Double unexpected, double actual, Supplier messageSupplier); + + public static void assertNotEquals(Double unexpected, Double actual, Supplier messageSupplier); + + public static void assertNotEquals(double unexpected, double actual, double delta); + + public static void assertNotEquals(double unexpected, double actual, double delta, String message); + + public static void assertNotEquals(double unexpected, double actual, double delta, + Supplier messageSupplier); + + public static void assertNotEquals(char unexpected, char actual); + + public static void assertNotEquals(char unexpected, Character actual); + + public static void assertNotEquals(Character unexpected, char actual); + + public static void assertNotEquals(Character unexpected, Character actual); + + public static void assertNotEquals(char unexpected, char actual, String message); + + public static void assertNotEquals(char unexpected, Character actual, String message); + + public static void assertNotEquals(Character unexpected, char actual, String message); + + public static void assertNotEquals(Character unexpected, Character actual, String message); + + public static void assertNotEquals(char unexpected, char actual, Supplier messageSupplier); + + public static void assertNotEquals(char unexpected, Character actual, Supplier messageSupplier); + + public static void assertNotEquals(Character unexpected, char actual, Supplier messageSupplier); + + public static void assertNotEquals(Character unexpected, Character actual, Supplier messageSupplier); + + public static void assertNotEquals(@UnknownSignedness Object unexpected, @UnknownSignedness Object actual); + + public static void assertNotEquals(@UnknownSignedness Object unexpected, @UnknownSignedness Object actual, String message); + + public static void assertNotEquals(@UnknownSignedness Object unexpected, @UnknownSignedness Object actual, Supplier messageSupplier); + + public static void assertSame(@UnknownSignedness Object expected, @UnknownSignedness Object actual); + + public static void assertSame(@UnknownSignedness Object expected, @UnknownSignedness Object actual, String message); + + public static void assertSame(@UnknownSignedness Object expected, @UnknownSignedness Object actual, Supplier messageSupplier); + + public static void assertNotSame(@UnknownSignedness Object unexpected, @UnknownSignedness Object actual); + + public static void assertNotSame(@UnknownSignedness Object unexpected, @UnknownSignedness Object actual, String message); + + public static void assertNotSame(@UnknownSignedness Object unexpected, @UnknownSignedness Object actual, Supplier messageSupplier); + + public static void assertAll(Executable... executables) throws MultipleFailuresError; + + public static void assertAll(String heading, Executable... executables) throws MultipleFailuresError; + + public static void assertAll(Collection executables) throws MultipleFailuresError; + + public static void assertAll(String heading, Collection executables) throws MultipleFailuresError; + + public static void assertAll(Stream executables) throws MultipleFailuresError; + + public static void assertAll(String heading, Stream executables) throws MultipleFailuresError; + + public static T assertThrows(Class expectedType, Executable executable); + + public static T assertThrows(Class expectedType, Executable executable, String message); + + public static T assertThrows(Class expectedType, Executable executable, + Supplier messageSupplier); + + public static void assertDoesNotThrow(Executable executable); + + public static void assertDoesNotThrow(Executable executable, String message); + + public static void assertDoesNotThrow(Executable executable, Supplier messageSupplier); + + public static T assertDoesNotThrow(ThrowingSupplier supplier); + + public static T assertDoesNotThrow(ThrowingSupplier supplier, String message); + + public static T assertDoesNotThrow(ThrowingSupplier supplier, Supplier messageSupplier); + + public static void assertTimeout(Duration timeout, Executable executable); + + public static void assertTimeout(Duration timeout, Executable executable, String message); + + public static void assertTimeout(Duration timeout, Executable executable, Supplier messageSupplier); + + public static T assertTimeout(Duration timeout, ThrowingSupplier supplier); + + public static T assertTimeout(Duration timeout, ThrowingSupplier supplier, String message); + + public static T assertTimeout(Duration timeout, ThrowingSupplier supplier, + Supplier messageSupplier); + + public static void assertTimeoutPreemptively(Duration timeout, Executable executable); + + public static void assertTimeoutPreemptively(Duration timeout, Executable executable, String message); + + public static void assertTimeoutPreemptively(Duration timeout, Executable executable, + Supplier messageSupplier); + + public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier); + + public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, String message); + + public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, + Supplier messageSupplier); +} diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/messages.properties b/checker/src/main/java/org/checkerframework/checker/signedness/messages.properties index 97ffe4fc66b0..437eaaf6e612 100644 --- a/checker/src/main/java/org/checkerframework/checker/signedness/messages.properties +++ b/checker/src/main/java/org/checkerframework/checker/signedness/messages.properties @@ -1,17 +1,18 @@ ## Error messages for the Signedness Checker -operation.unsignedrhs=%s operation has an unsigned RHS -operation.unsignedlhs=%s operation has an unsigned LHS -operation.mixed.unsignedlhs=%s operation on unsigned LHS and signed RHS values -operation.mixed.unsignedrhs=%s operation on signed LHS and unsigned RHS values -shift.signed=%s operation on an unsigned value -shift.unsigned=%s operation on a signed value -comparison.unsignedlhs=comparison has an unsigned LHS -comparison.unsignedrhs=comparison has an unsigned RHS -comparison.mixed.unsignedlhs=comparison between unsigned LHS and signed RHS values -comparison.mixed.unsignedrhs=comparison between signed LHS and unsigned RHS values -compound.assignment.unsigned.variable=%s has unsigned LHS -compound.assignment.unsigned.expression=%s has unsigned RHS -compound.assignment.mixed.unsigned.variable=%s operation has unsigned LHS and signed RHS -compound.assignment.mixed.unsigned.expression=%s operation has signed LHS and unsigned RHS -compound.assignment.shift.signed=%s has unsigned LHS -compound.assignment.shift.unsigned=%s has signed LHS +operation.unsignedrhs=%s operation has an unsigned RHS%nlhs: %s%nrhs: %s +operation.unsignedlhs=%s operation has an unsigned LHS%nlhs: %s%nrhs: %s +operation.mixed.unsignedlhs=%s operation on unsigned LHS and signed RHS values%nlhs: %s%nrhs: %s +operation.mixed.unsignedrhs=%s operation on signed LHS and unsigned RHS values%nlhs: %s%nrhs: %s +shift.signed=%s operation on an unsigned value%nlhs: %s%nrhs: %s +shift.unsigned=%s operation on a signed value%nlhs: %s%nrhs: %s +comparison.unsignedlhs=comparison has an unsigned LHS%nlhs: %s%nrhs: %s +comparison.unsignedrhs=comparison has an unsigned RHS%nlhs: %s%nrhs: %s +comparison.mixed.unsignedlhs=comparison between unsigned LHS and signed RHS values%nlhs: %s%nrhs: %s +comparison.mixed.unsignedrhs=comparison between signed LHS and unsigned RHS values%nlhs: %s%nrhs: %s +compound.assignment.unsigned.variable=%s has unsigned LHS%nlhs: %s%nrhs: %s +compound.assignment.unsigned.expression=%s has unsigned RHS%nlhs: %s%nrhs: %s +compound.assignment.mixed.unsigned.variable=%s operation has unsigned LHS and signed RHS%nlhs: %s%nrhs: %s +compound.assignment.mixed.unsigned.expression=%s operation has signed LHS and unsigned RHS%nlhs: %s%nrhs: %s +compound.assignment.shift.signed=%s has unsigned LHS%nlhs: %s%nrhs: %s +compound.assignment.shift.unsigned=%s has signed LHS%nlhs: %s%nrhs: %s +unsigned.concat=string concatenation on an unsigned value 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 deleted file mode 100644 index 3af02116d872..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/signedness/qual/Signed.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.checkerframework.checker.signedness.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.DefaultFor; -import org.checkerframework.framework.qual.SubtypeOf; -import org.checkerframework.framework.qual.TypeKind; - -/** - * The value is to be interpreted as signed. That is, if the most significant bit in the bitwise - * representation is set, then the bits should be interpreted as a negative number. - * - * @checker_framework.manual #signedness-checker Signedness Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf({UnknownSignedness.class}) -@DefaultFor( - typeKinds = { - TypeKind.BYTE, - TypeKind.INT, - TypeKind.LONG, - TypeKind.SHORT, - TypeKind.FLOAT, - TypeKind.DOUBLE, - TypeKind.CHAR - } - - // This is commented out until implicitly signed boxed types are implemented - // correctly. - - /*, - types = { - java.lang.Byte.class, - java.lang.Short.class, - java.lang.Integer.class, - java.lang.Long.class - }*/ ) -public @interface Signed {} diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/qual/SignedPositive.java b/checker/src/main/java/org/checkerframework/checker/signedness/qual/SignedPositive.java deleted file mode 100644 index ef4ef02410df..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/signedness/qual/SignedPositive.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.checkerframework.checker.signedness.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; - -/** - * The expression's value is in the signed positive range; that is, its most significant bit is not - * set. The value has the same interpretation as {@link Signed} and {@link Unsigned} — both - * interpretations are equivalent. - * - *

    Programmers should rarely write {@code @SignedPositive}. Instead, the programmer should write - * {@link Signed} or {@link Unsigned} to indicate how the programmer intends the value to be - * interpreted. - * - *

    Internally, this is translated to the {@code @}{@link SignednessGlb} annotation. This means - * that programmers do not see this annotation in error messages. - * - * @see SignednessGlb - * @checker_framework.manual #signedness-checker Signedness Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -public @interface SignedPositive {} diff --git a/checker/src/main/java/org/checkerframework/checker/signedness/qual/SignednessGlb.java b/checker/src/main/java/org/checkerframework/checker/signedness/qual/SignednessGlb.java deleted file mode 100644 index 1a7150d14b91..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/signedness/qual/SignednessGlb.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.checkerframework.checker.signedness.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.LiteralKind; -import org.checkerframework.framework.qual.QualifierForLiterals; -import org.checkerframework.framework.qual.SubtypeOf; - -/** - * Client code may interpret the value either as {@link Signed} or as {@link Unsigned}. This - * primarily applies to values whose most significant bit is not set {@link SignedPositive}, and - * thus the value has the same interpretation as signed or unsigned. - * - *

    As a special case, the Signedness Checker also applies this annotation to manifest literals. - * This permits a value like {@code -1} or {@code 255} or {@code 0xFF} to be used in both signed and - * unsigned contexts. The Signedness Checker has no way of knowing how a programmer intended a - * literal to be used, so it does not issue warnings for any uses of a literal. (An alternate design - * would require the programmer to explicitly annotate every manifest literal whose most significant - * bit is set. That might detect more errors, at the cost of much greater programmer annotation - * effort.) - * - *

    The programmer should not write this annotation. Instead, the programmer should write {@link - * Signed} or {@link Unsigned} to indicate how the programmer intends the value to be interpreted. - * For a value whose most significant bit is not set and different clients may treat it differently - * (say, the return value of certain library routines, or certain constant fields), the programmer - * should write {@code @}{@link SignedPositive} instead of {@code @SignednessGlb}. - * - *

    The "Glb" in the name stands for "greatest lower bound", because this type is the greatest - * lower bound of the types {@link Signed} and {@link Unsigned}; that is, this type is a subtype of - * both of those types. - * - * @see SignedPositive - * @checker_framework.manual #signedness-checker Signedness Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf({Unsigned.class, Signed.class}) -@QualifierForLiterals({LiteralKind.INT, LiteralKind.LONG, LiteralKind.CHAR}) -public @interface SignednessGlb {} 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 deleted file mode 100644 index be27a217a1a3..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/signedness/qual/Unsigned.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.checkerframework.checker.signedness.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.SubtypeOf; - -/** - * The value is to be interpreted as unsigned. That is, if the most significant bit in the bitwise - * representation is set, then the bits should be interpreted as a large positive number rather than - * as a negative number. - * - * @checker_framework.manual #signedness-checker Signedness Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf({UnknownSignedness.class}) -public @interface Unsigned {} diff --git a/checker/src/main/java/org/checkerframework/checker/tainting/TaintingAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/tainting/TaintingAnnotatedTypeFactory.java new file mode 100644 index 000000000000..a92ec7a5f4fe --- /dev/null +++ b/checker/src/main/java/org/checkerframework/checker/tainting/TaintingAnnotatedTypeFactory.java @@ -0,0 +1,38 @@ +package org.checkerframework.checker.tainting; + +import org.checkerframework.checker.tainting.qual.Untainted; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationMirrorSet; + +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; + +/** Annotated type factory for the Tainting Checker. */ +public class TaintingAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + + /** The {@code @}{@link Untainted} annotation mirror. */ + private final AnnotationMirror UNTAINTED; + + /** A singleton set containing the {@code @}{@link Untainted} annotation mirror. */ + private final AnnotationMirrorSet setOfUntainted; + + /** + * Creates a {@link TaintingAnnotatedTypeFactory}. + * + * @param checker the tainting checker + */ + public TaintingAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + this.UNTAINTED = AnnotationBuilder.fromClass(getElementUtils(), Untainted.class); + this.setOfUntainted = AnnotationMirrorSet.singleton(UNTAINTED); + postInit(); + } + + @Override + protected Set getEnumConstructorQualifiers() { + return setOfUntainted; + } +} diff --git a/checker/src/main/java/org/checkerframework/checker/tainting/TaintingVisitor.java b/checker/src/main/java/org/checkerframework/checker/tainting/TaintingVisitor.java index 7691a225214b..15f5937aca6b 100644 --- a/checker/src/main/java/org/checkerframework/checker/tainting/TaintingVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/tainting/TaintingVisitor.java @@ -1,11 +1,12 @@ package org.checkerframework.checker.tainting; -import javax.lang.model.element.ExecutableElement; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; +import javax.lang.model.element.ExecutableElement; + /** Visitor for the {@link TaintingChecker}. */ public class TaintingVisitor extends BaseTypeVisitor { diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotatedTypeFactory.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotatedTypeFactory.java index 052a388bd5ec..97d878d97d2a 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotatedTypeFactory.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotatedTypeFactory.java @@ -4,15 +4,14 @@ import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.Tree; -import java.lang.annotation.Annotation; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Name; -import javax.tools.Diagnostic.Kind; + +import org.checkerframework.checker.initialization.qual.UnderInitialization; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.checker.signature.qual.BinaryName; +import org.checkerframework.checker.signature.qual.CanonicalName; import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers; import org.checkerframework.checker.units.qual.MixedUnits; import org.checkerframework.checker.units.qual.Prefix; @@ -21,33 +20,53 @@ import org.checkerframework.checker.units.qual.UnknownUnits; import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.qual.AnnotatedFor; import org.checkerframework.framework.type.AnnotatedTypeFactory; import org.checkerframework.framework.type.AnnotatedTypeFormatter; import org.checkerframework.framework.type.AnnotatedTypeMirror; import org.checkerframework.framework.type.AnnotationClassLoader; +import org.checkerframework.framework.type.MostlyNoElementQualifierHierarchy; import org.checkerframework.framework.type.QualifierHierarchy; import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; import org.checkerframework.framework.type.treeannotator.LiteralTreeAnnotator; import org.checkerframework.framework.type.treeannotator.PropagationTreeAnnotator; import org.checkerframework.framework.type.treeannotator.TreeAnnotator; -import org.checkerframework.framework.util.GraphQualifierHierarchy; -import org.checkerframework.framework.util.MultiGraphQualifierHierarchy.MultiGraphFactory; +import org.checkerframework.framework.util.DefaultQualifierKindHierarchy; +import org.checkerframework.framework.util.QualifierKind; +import org.checkerframework.framework.util.QualifierKindHierarchy; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.InternalUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; import org.checkerframework.javacutil.UserError; import org.plumelib.reflection.Signatures; +import java.io.File; +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; +import javax.lang.model.util.Elements; +import javax.tools.Diagnostic; + /** * Annotated type factory for the Units Checker. * *

    Handles multiple names for the same unit, with different prefixes, e.g. @kg is the same * as @g(Prefix.kilo). * - *

    Supports relations between units, e.g. if "m" is a variable of type "@m" and "s" is a variable - * of type "@s", the division "m/s" is automatically annotated as "mPERs", the correct unit for the - * result. + *

    Supports relations between units. If {@code m} is a variable of type "@m" and {@code s} is a + * variable of type "@s", the division {@code m / s} is automatically annotated as "@mPERs", the + * correct unit for the result. */ public class UnitsAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { private static final Class @@ -60,13 +79,30 @@ public class UnitsAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { protected final AnnotationMirror BOTTOM = AnnotationBuilder.fromClass(elements, UnitsBottom.class); + /** The UnitsMultiple.prefix argument/element. */ + private final ExecutableElement unitsMultiplePrefixElement = + TreeUtils.getMethod(UnitsMultiple.class, "prefix", 0, processingEnv); + + /** The UnitsMultiple.quantity argument/element. */ + private final ExecutableElement unitsMultipleQuantityElement = + TreeUtils.getMethod(UnitsMultiple.class, "quantity", 0, processingEnv); + + /** The UnitsRelations.value argument/element. */ + private final ExecutableElement unitsRelationsValueElement = + TreeUtils.getMethod( + org.checkerframework.checker.units.qual.UnitsRelations.class, + "value", + 0, + processingEnv); + /** * Map from canonical class name to the corresponding UnitsRelations instance. We use the string * to prevent instantiating the UnitsRelations multiple times. */ - private Map unitsRel; + private @MonotonicNonNull Map<@CanonicalName String, UnitsRelations> unitsRel; - private static final Map> externalQualsMap = + /** Map from canonical name of external qualifiers, to their Class. */ + private static final Map<@CanonicalName String, Class> externalQualsMap = new HashMap<>(); private static final Map aliasMap = new HashMap<>(); @@ -78,9 +114,8 @@ public UnitsAnnotatedTypeFactory(BaseTypeChecker checker) { this.postInit(); } - // In Units Checker, we always want to print out the Invisible Qualifiers - // (UnknownUnits), and to format the print out of qualifiers by removing - // Prefix.one + // In Units Checker, we always want to print out the Invisible Qualifiers (UnknownUnits), and to + // format the print out of qualifiers by removing Prefix.one @Override protected AnnotatedTypeFormatter createAnnotatedTypeFormatter() { return new UnitsAnnotatedTypeFormatter(checker); @@ -91,7 +126,7 @@ protected AnnotatedTypeFormatter createAnnotatedTypeFormatter() { @Override public AnnotationMirror canonicalAnnotation(AnnotationMirror anno) { // Get the name of the aliased annotation - String aname = anno.getAnnotationType().toString(); + String aname = AnnotationUtils.annotationName(anno); // See if we already have a map from this aliased annotation to its corresponding base unit // annotation @@ -109,11 +144,13 @@ public AnnotationMirror canonicalAnnotation(AnnotationMirror anno) { if (isUnitsMultiple(metaAnno)) { // retrieve the Class of the base unit annotation Name baseUnitAnnoClass = - AnnotationUtils.getElementValueClassName(metaAnno, "quantity", true); + AnnotationUtils.getElementValueClassName( + metaAnno, unitsMultipleQuantityElement); // retrieve the SI Prefix of the aliased annotation Prefix prefix = - AnnotationUtils.getElementValueEnum(metaAnno, "prefix", Prefix.class, true); + AnnotationUtils.getElementValueEnum( + metaAnno, unitsMultiplePrefixElement, Prefix.class, Prefix.one); // Build a base unit annotation with the prefix applied result = @@ -144,11 +181,16 @@ public AnnotationMirror canonicalAnnotation(AnnotationMirror anno) { return super.canonicalAnnotation(anno); } - /** Return a map from canonical class name to the corresponding UnitsRelations instance. */ - protected Map getUnitsRel() { + /** + * Returns a map from canonical class name to the corresponding UnitsRelations instance. + * + * @return a map from canonical class name to the corresponding UnitsRelations instance + */ + protected Map<@CanonicalName String, UnitsRelations> getUnitsRel() { if (unitsRel == null) { unitsRel = new HashMap<>(); // Always add the default units relations, for the standard units. + // Other code adds more relations. unitsRel.put( UnitsRelationsDefault.class.getCanonicalName(), new UnitsRelationsDefault().init(processingEnv)); @@ -164,37 +206,32 @@ protected AnnotationClassLoader createAnnotationClassLoader() { @Override protected Set> createSupportedTypeQualifiers() { - // get all the loaded annotations + // Get all the loaded annotations. Set> qualSet = getBundledTypeQualifiers(); - // load all the external units + // Load all the units specified on the command line. loadAllExternalUnits(); - - // copy all loaded external Units to qual set qualSet.addAll(externalQualsMap.values()); return qualSet; } + /** Loads all the externnal units specified on the command line. */ private void loadAllExternalUnits() { // load external individually named units - String qualNames = checker.getOption("units"); - if (qualNames != null) { - for (String qualName : qualNames.split(",")) { - if (!Signatures.isBinaryName(qualName)) { - throw new UserError( - "Malformed qualifier name \"%s\" in -Aunits=%s", qualName, qualNames); - } - loadExternalUnit(qualName); + for (String qualName : checker.getStringsOption("units", ',')) { + if (!Signatures.isBinaryName(qualName)) { + throw new UserError("Malformed qualifier name \"%s\" in -Aunits", qualName); } + loadExternalUnit(qualName); } // load external directories of units - String qualDirectories = checker.getOption("unitsDirs"); - if (qualDirectories != null) { - for (String directoryName : qualDirectories.split(":")) { - loadExternalDirectory(directoryName); + for (String directoryName : checker.getStringsOption("unitsDirs", ':')) { + if (!new File(directoryName).exists()) { + throw new UserError("Nonexistent directory in -AunitsDirs: " + directoryName); } + loadExternalDirectory(directoryName); } } @@ -222,7 +259,7 @@ private void loadExternalDirectory(String directoryName) { } /** Adds the annotation class to the external qualifier map if it is not an alias annotation. */ - private void addUnitToExternalQualMap(final Class annoClass) { + private void addUnitToExternalQualMap(Class annoClass) { AnnotationMirror mirror = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix( processingEnv, annoClass.getCanonicalName()); @@ -237,7 +274,7 @@ private void addUnitToExternalQualMap(final Class annoClas // if it is an aliased annotation else { // ensure it has a base unit - @DotSeparatedIdentifiers Name baseUnitClass = getBaseUnitAnno(mirror); + @CanonicalName Name baseUnitClass = getBaseUnitAnno(mirror); if (baseUnitClass != null) { // if the base unit isn't already added, add that first @SuppressWarnings("signature") // https://tinyurl.com/cfissue/658 @@ -284,7 +321,7 @@ private boolean isAliasedAnnotation(AnnotationMirror anno) { * @param anno the annotation to examine * @return the annotation's name, if it is meta-annotated with UnitsMultiple; otherwise null */ - private @Nullable @DotSeparatedIdentifiers Name getBaseUnitAnno(AnnotationMirror anno) { + private @Nullable @CanonicalName Name getBaseUnitAnno(AnnotationMirror anno) { // loop through the meta annotations of the annotation, look for UnitsMultiple for (AnnotationMirror metaAnno : anno.getAnnotationType().asElement().getAnnotationMirrors()) { @@ -293,7 +330,8 @@ private boolean isAliasedAnnotation(AnnotationMirror anno) { // TODO: does every alias have to have Prefix? // Retrieve the base unit annotation. Name baseUnitAnnoClass = - AnnotationUtils.getElementValueClassName(metaAnno, "quantity", true); + AnnotationUtils.getElementValueClassName( + metaAnno, unitsMultipleQuantityElement); return baseUnitAnnoClass; } } @@ -310,6 +348,10 @@ private boolean isUnitsMultiple(AnnotationMirror metaAnno) { return areSameByClass(metaAnno, UnitsMultiple.class); } + /** A class loader for looking up annotations. */ + private static final ClassLoader CLASSLOADER = + InternalUtils.getClassLoaderForClass(AnnotationUtils.class); + /** * Look for an @UnitsRelations annotation on the qualifier and add it to the list of * UnitsRelations. @@ -322,17 +364,17 @@ private void addUnitsRelations(Class qual) { for (AnnotationMirror ama : am.getAnnotationType().asElement().getAnnotationMirrors()) { if (areSameByClass(ama, unitsRelationsAnnoClass)) { String theclassname = - AnnotationUtils.getElementValueClassName(ama, "value", true).toString(); + AnnotationUtils.getElementValueClassName(ama, unitsRelationsValueElement) + .toString(); if (!Signatures.isClassGetName(theclassname)) { throw new UserError( - "Malformed class name \"%s\" should be in ClassGetName format in annotation %s", + "Malformed class name \"%s\" should be in ClassGetName format in" + + " annotation %s", theclassname, ama); } Class valueElement; try { - ClassLoader classLoader = - InternalUtils.getClassLoaderForClass(AnnotationUtils.class); - valueElement = Class.forName(theclassname, true, classLoader); + valueElement = Class.forName(theclassname, true, CLASSLOADER); } catch (ClassNotFoundException e) { String msg = String.format( @@ -361,7 +403,7 @@ private void addUnitsRelations(Class qual) { .newInstance() .init(processingEnv)); } catch (Throwable e) { - throw new BugInCF("Throwable when instantiating UnitsRelations", e); + throw new TypeSystemError("Throwable when instantiating UnitsRelations", e); } } } @@ -386,13 +428,13 @@ public UnitsPropagationTreeAnnotator(AnnotatedTypeFactory atypeFactory) { // Handled completely by UnitsTreeAnnotator @Override - public Void visitBinary(BinaryTree node, AnnotatedTypeMirror type) { + public Void visitBinary(BinaryTree tree, AnnotatedTypeMirror type) { return null; } // Handled completely by UnitsTreeAnnotator @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMirror type) { + public Void visitCompoundAssignment(CompoundAssignmentTree tree, AnnotatedTypeMirror type) { return null; } } @@ -405,10 +447,10 @@ private class UnitsTreeAnnotator extends TreeAnnotator { } @Override - public Void visitBinary(BinaryTree node, AnnotatedTypeMirror type) { - AnnotatedTypeMirror lht = getAnnotatedType(node.getLeftOperand()); - AnnotatedTypeMirror rht = getAnnotatedType(node.getRightOperand()); - Tree.Kind kind = node.getKind(); + public Void visitBinary(BinaryTree tree, AnnotatedTypeMirror type) { + AnnotatedTypeMirror lht = getAnnotatedType(tree.getLeftOperand()); + AnnotatedTypeMirror rht = getAnnotatedType(tree.getRightOperand()); + Tree.Kind kind = tree.getKind(); // Remove Prefix.one if (UnitsRelationsTools.getPrefix(lht) == Prefix.one) { @@ -424,12 +466,12 @@ public Void visitBinary(BinaryTree node, AnnotatedTypeMirror type) { if (bestres != null && res != null && !bestres.equals(res)) { checker.message( - Kind.WARNING, + Diagnostic.Kind.WARNING, "UnitsRelation mismatch, taking neither! Previous: " + bestres + " and current: " + res); - return null; // super.visitBinary(node, type); + return null; // super.visitBinary(tree, type); } if (res != null) { @@ -441,7 +483,7 @@ public Void visitBinary(BinaryTree node, AnnotatedTypeMirror type) { type.replaceAnnotation(bestres); } else { // If none of the units relations classes could resolve the units, then apply - // default rules + // default rules. switch (kind) { case MINUS: @@ -500,119 +542,179 @@ public Void visitBinary(BinaryTree node, AnnotatedTypeMirror type) { } @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, AnnotatedTypeMirror type) { - ExpressionTree var = node.getVariable(); + public Void visitCompoundAssignment(CompoundAssignmentTree tree, AnnotatedTypeMirror type) { + ExpressionTree var = tree.getVariable(); AnnotatedTypeMirror varType = getAnnotatedType(var); type.replaceAnnotations(varType.getAnnotations()); return null; } - private AnnotationMirror useUnitsRelation( + private @Nullable AnnotationMirror useUnitsRelation( Tree.Kind kind, UnitsRelations ur, AnnotatedTypeMirror lht, AnnotatedTypeMirror rht) { - AnnotationMirror res = null; if (ur != null) { switch (kind) { case DIVIDE: - res = ur.division(lht, rht); - break; + return ur.division(lht, rht); case MULTIPLY: - res = ur.multiplication(lht, rht); - break; + return ur.multiplication(lht, rht); default: // Do nothing } } - return res; + return null; } } /** Set the Bottom qualifier as the bottom of the hierarchy. */ @Override - public QualifierHierarchy createQualifierHierarchy(MultiGraphFactory factory) { - return new UnitsQualifierHierarchy( - factory, AnnotationBuilder.fromClass(elements, UnitsBottom.class)); + protected QualifierHierarchy createQualifierHierarchy() { + return new UnitsQualifierHierarchy(); } /** Qualifier Hierarchy for the Units Checker. */ - protected class UnitsQualifierHierarchy extends GraphQualifierHierarchy { - + @AnnotatedFor("nullness") + protected class UnitsQualifierHierarchy extends MostlyNoElementQualifierHierarchy { /** Constructor. */ - public UnitsQualifierHierarchy(MultiGraphFactory mgf, AnnotationMirror unitsBottom) { - super(mgf, unitsBottom); + public UnitsQualifierHierarchy() { + super( + UnitsAnnotatedTypeFactory.this.getSupportedTypeQualifiers(), + elements, + UnitsAnnotatedTypeFactory.this); } @Override - public boolean isSubtype(AnnotationMirror subAnno, AnnotationMirror superAnno) { - if (AnnotationUtils.areSameByName(superAnno, subAnno)) { - return AnnotationUtils.sameElementValues(superAnno, subAnno); - } - superAnno = removePrefix(superAnno); - subAnno = removePrefix(subAnno); - - return super.isSubtype(subAnno, superAnno); + protected QualifierKindHierarchy createQualifierKindHierarchy( + @UnderInitialization UnitsQualifierHierarchy this, + Collection> qualifierClasses) { + return new UnitsQualifierKindHierarchy(qualifierClasses, elements); } - // Overriding leastUpperBound due to the fact that alias annotations are - // not placed in the Supported Type Qualifiers set, instead, their base - // SI units are in the set. - // Whenever an alias annotation or prefix-multiple of a base SI unit is - // used in ternary statements or through mismatched PolyUnit method - // parameters, we handle the LUB resolution here so that these units can - // correctly resolve to an LUB Unit. @Override - public AnnotationMirror leastUpperBound(AnnotationMirror a1, AnnotationMirror a2) { - AnnotationMirror result; + protected boolean isSubtypeWithElements( + AnnotationMirror subAnno, + QualifierKind subKind, + AnnotationMirror superAnno, + QualifierKind superKind) { + return AnnotationUtils.areSame(subAnno, superAnno); + } - // if the prefix is Prefix.one, automatically strip it for LUB checking - if (UnitsRelationsTools.getPrefix(a1) == Prefix.one) { - a1 = removePrefix(a1); - } - if (UnitsRelationsTools.getPrefix(a2) == Prefix.one) { - a2 = removePrefix(a2); + @Override + protected AnnotationMirror leastUpperBoundWithElements( + AnnotationMirror a1, + QualifierKind qualifierKind1, + AnnotationMirror a2, + QualifierKind qualifierKind2, + QualifierKind lubKind) { + if (qualifierKind1.isBottom()) { + return a2; + } else if (qualifierKind2.isBottom()) { + return a1; + } else if (qualifierKind1 == qualifierKind2) { + if (AnnotationUtils.areSame(a1, a2)) { + return a1; + } else { + @SuppressWarnings({ + "nullness:assignment.type.incompatible" // Every qualifier kind is a + // key in directSuperQualifierMap. + }) + @NonNull AnnotationMirror lub = + ((UnitsQualifierKindHierarchy) qualifierKindHierarchy) + .directSuperQualifierMap.get(qualifierKind1); + return lub; + } } + throw new TypeSystemError( + "Unexpected QualifierKinds: %s %s", qualifierKind1, qualifierKind2); + } - // if the two units have the same base SI unit - // TODO: it is possible to rewrite these two lines to use UnitsRelationsTools, will it - // have worse performance? - if (AnnotationUtils.areSameByName(a1, a2)) { - // and if they have the same Prefix, it means it is the same unit - if (AnnotationUtils.sameElementValues(a1, a2)) { - // return the unit - result = a1; - } + @Override + protected AnnotationMirror greatestLowerBoundWithElements( + AnnotationMirror a1, + QualifierKind qualifierKind1, + AnnotationMirror a2, + QualifierKind qualifierKind2, + QualifierKind glbKind) { + return UnitsAnnotatedTypeFactory.this.BOTTOM; + } + } - // if they don't have the same Prefix, find the LUB - else { - // check if a1 is a prefixed multiple of a base unit - boolean a1Prefixed = !UnitsRelationsTools.hasNoPrefix(a1); - // check if a2 is a prefixed multiple of a base unit - boolean a2Prefixed = !UnitsRelationsTools.hasNoPrefix(a2); - - // when calling findLub(), the left AnnoMirror has to be a type within the - // supertypes Map this means it has to be one of the base SI units, so always - // strip the left unit or ensure it has no prefix - if (a1Prefixed && a2Prefixed) { - // if both are prefixed, strip the left and find LUB - result = this.findLub(removePrefix(a1), a2); - } else if (a1Prefixed && !a2Prefixed) { - // if only the left is prefixed, swap order and find LUB - result = this.findLub(a2, a1); - } else { - // else (only right is prefixed), just find the LUB - result = this.findLub(a1, a2); - } + /** UnitsQualifierKindHierarchy. */ + @AnnotatedFor("nullness") + protected static class UnitsQualifierKindHierarchy extends DefaultQualifierKindHierarchy { + + /** + * Mapping from QualifierKind to an AnnotationMirror that represents its direct super + * qualifier. Every qualifier kind maps to a nonnull AnnotationMirror. + */ + private final Map directSuperQualifierMap; + + /** + * Creates a UnitsQualifierKindHierarchy. + * + * @param qualifierClasses classes of annotations that are the qualifiers for this hierarchy + * @param elements element utils + */ + public UnitsQualifierKindHierarchy( + Collection> qualifierClasses, Elements elements) { + super(qualifierClasses, UnitsBottom.class); + directSuperQualifierMap = createDirectSuperQualifierMap(elements); + } + + /** + * Creates the direct super qualifier map. + * + * @param elements element utils + * @return the map + */ + @RequiresNonNull("this.qualifierKinds") + private Map createDirectSuperQualifierMap( + @UnderInitialization UnitsQualifierKindHierarchy this, Elements elements) { + Map directSuperType = new TreeMap<>(); + for (QualifierKind qualifierKind : qualifierKinds) { + QualifierKind directSuperTypeKind = getDirectSuperQualifierKind(qualifierKind); + AnnotationMirror directSuperTypeAnno; + try { + directSuperTypeAnno = + AnnotationBuilder.fromName(elements, directSuperTypeKind.getName()); + } catch (BugInCF ex) { + throw new TypeSystemError( + "Unit annotations must have a default for all elements."); } - } else { - // if they don't have the same base SI unit, let super find it - result = super.leastUpperBound(a1, a2); + if (directSuperTypeAnno == null) { + throw new TypeSystemError( + "Could not create AnnotationMirror: %s", directSuperTypeAnno); + } + directSuperType.put(qualifierKind, directSuperTypeAnno); } + return directSuperType; + } - return result; + /** + * Get the direct super qualifier for the given qualifier kind. + * + * @param qualifierKind qualifier kind + * @return direct super qualifier kind + */ + private QualifierKind getDirectSuperQualifierKind( + @UnderInitialization UnitsQualifierKindHierarchy this, + QualifierKind qualifierKind) { + if (qualifierKind.isTop()) { + return qualifierKind; + } + Set superQuals = new TreeSet<>(qualifierKind.getStrictSuperTypes()); + while (superQuals.size() > 0) { + Set lowest = findLowestQualifiers(superQuals); + if (lowest.size() == 1) { + return lowest.iterator().next(); + } + superQuals.removeAll(lowest); + } + throw new TypeSystemError("No direct super qualifier found for %s", qualifierKind); } } diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotatedTypeFormatter.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotatedTypeFormatter.java index 1eb1b153e463..e4e017decfba 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotatedTypeFormatter.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotatedTypeFormatter.java @@ -1,25 +1,35 @@ package org.checkerframework.checker.units; -import java.util.Collection; -import java.util.Collections; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.util.Elements; import org.checkerframework.checker.units.qual.Prefix; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.DefaultAnnotatedTypeFormatter; import org.checkerframework.framework.util.AnnotationFormatter; import org.checkerframework.framework.util.DefaultAnnotationFormatter; -import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import java.util.Collection; +import java.util.Collections; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.util.Elements; + +/** Formats units-of-measure annotations. */ public class UnitsAnnotatedTypeFormatter extends DefaultAnnotatedTypeFormatter { + /** The checker. */ protected final BaseTypeChecker checker; + + /** Javac element utilities. */ protected final Elements elements; + /** + * Create a UnitsAnnotatedTypeFormatter. + * + * @param checker the checker + */ public UnitsAnnotatedTypeFormatter(BaseTypeChecker checker) { - // Utilize the Default Type Formatter, but force it to print out Invisible Qualifiers - // keep super call in sync with implementation in DefaultAnnotatedTypeFormatter - // keep checker options in sync with implementation in AnnotatedTypeFactory + // Utilize the Default Type Formatter, but force it to print out Invisible Qualifiers. + // Keep super call in sync with implementation in DefaultAnnotatedTypeFormatter. + // Keep checker options in sync with implementation in AnnotatedTypeFactory. super( new UnitsFormattingVisitor( checker, @@ -62,7 +72,7 @@ public UnitsAnnotationFormatter(BaseTypeChecker checker) { public String formatAnnotationString( Collection annos, boolean printInvisible) { // create an empty annotation set - Set trimmedAnnoSet = AnnotationUtils.createAnnotationSet(); + AnnotationMirrorSet trimmedAnnoSet = new AnnotationMirrorSet(); // loop through all the annotation mirrors to see if they use Prefix.one, remove // Prefix.one if it does diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotationClassLoader.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotationClassLoader.java index 649decb3c6ae..0c36171bfb34 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotationClassLoader.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsAnnotationClassLoader.java @@ -1,13 +1,15 @@ package org.checkerframework.checker.units; -import java.lang.annotation.Annotation; -import javax.lang.model.element.AnnotationMirror; import org.checkerframework.checker.units.qual.UnitsMultiple; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.type.AnnotationClassLoader; import org.checkerframework.javacutil.AnnotationBuilder; import org.checkerframework.javacutil.AnnotationUtils; +import java.lang.annotation.Annotation; + +import javax.lang.model.element.AnnotationMirror; + public class UnitsAnnotationClassLoader extends AnnotationClassLoader { public UnitsAnnotationClassLoader(BaseTypeChecker checker) { @@ -35,10 +37,11 @@ protected boolean isSupportedAnnotationClass(Class annoCla initialResult.getAnnotationType().asElement().getAnnotationMirrors()) { // TODO : special treatment of invisible qualifiers? - // if the annotation is a SI prefix multiple of some base unit, then return false - // classic Units checker does not need to load the annotations of SI prefix multiples of - // base units - if (AnnotationUtils.areSameByClass(metaAnno, UnitsMultiple.class)) { + // If the annotation is a SI prefix multiple of some base unit, then return false. + // Units checker does not need to load the annotations of SI prefix multiples of base + // units. + if (AnnotationUtils.areSameByName( + metaAnno, "org.checkerframework.checker.units.qual.UnitsMultiple")) { return false; } } diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsChecker.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsChecker.java index 4f40167b9773..e084ae7c65f9 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsChecker.java @@ -1,10 +1,12 @@ package org.checkerframework.checker.units; -import java.util.SortedSet; -import javax.annotation.processing.SupportedOptions; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.subtyping.SubtypingChecker; +import java.util.NavigableSet; + +import javax.annotation.processing.SupportedOptions; + /** * Units Checker main class. * @@ -18,15 +20,8 @@ @SupportedOptions({"units", "unitsDirs"}) public class UnitsChecker extends BaseTypeChecker { - /* - @Override - public void initChecker() { - super.initChecker(); - } - */ - @Override - public SortedSet getSuppressWarningsPrefixes() { + public NavigableSet getSuppressWarningsPrefixes() { return SubtypingChecker.getSuppressWarningsPrefixes( this.visitor, super.getSuppressWarningsPrefixes()); } diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsRelations.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsRelations.java index a761de516ff3..e71c6d58e706 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsRelations.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsRelations.java @@ -1,11 +1,16 @@ package org.checkerframework.checker.units; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.AnnotationMirror; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.type.AnnotatedTypeMirror; -/** Interface that is used to specify the relation between units. */ +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; + +/** + * Interface that is used to specify the relation between units. A class that implements this + * interface is the argument to the {@link org.checkerframework.checker.units.qual.UnitsRelations} + * annotation. + */ public interface UnitsRelations { /** * Initialize the object. Needs to be called before any other method. diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsDefault.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsDefault.java index 2b480ee58e5b..2706f9bc787c 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsDefault.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsDefault.java @@ -1,30 +1,49 @@ package org.checkerframework.checker.units; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.util.Elements; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.units.qual.N; import org.checkerframework.checker.units.qual.Prefix; +import org.checkerframework.checker.units.qual.g; import org.checkerframework.checker.units.qual.h; +import org.checkerframework.checker.units.qual.kg; import org.checkerframework.checker.units.qual.km2; +import org.checkerframework.checker.units.qual.km3; import org.checkerframework.checker.units.qual.kmPERh; import org.checkerframework.checker.units.qual.m; import org.checkerframework.checker.units.qual.m2; +import org.checkerframework.checker.units.qual.m3; import org.checkerframework.checker.units.qual.mPERs; import org.checkerframework.checker.units.qual.mPERs2; import org.checkerframework.checker.units.qual.mm2; +import org.checkerframework.checker.units.qual.mm3; import org.checkerframework.checker.units.qual.s; +import org.checkerframework.checker.units.qual.t; import org.checkerframework.framework.type.AnnotatedTypeMirror; -/** - * Default relations between SI units. - * - *

    TODO: what relations are missing? - */ +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.util.Elements; + +/** Default relations between SI units. */ public class UnitsRelationsDefault implements UnitsRelations { - /** SI units. */ - protected AnnotationMirror m, km, mm, m2, km2, mm2, s, h, mPERs, kmPERh, mPERs2; + /** SI base units. */ + @SuppressWarnings("nullness:initialization.field.uninitialized") // init() method + protected AnnotationMirror m, km, mm, s, g, kg; + + /** Derived SI units without special names */ + @SuppressWarnings("nullness:initialization.field.uninitialized") // init() method + protected AnnotationMirror m2, km2, mm2, m3, km3, mm3, mPERs, mPERs2; + + /** Derived SI units with special names */ + @SuppressWarnings("nullness:initialization.field.uninitialized") // init() method + protected AnnotationMirror N, kN; + + /** Non-SI units */ + @SuppressWarnings("nullness:initialization.field.uninitialized") // init() method + protected AnnotationMirror h, kmPERh, t; + /** The Element Utilities from the Units Checker's processing environment. */ + @SuppressWarnings("nullness:initialization.field.uninitialized") // init() method protected Elements elements; /** @@ -43,6 +62,10 @@ public UnitsRelations init(ProcessingEnvironment env) { km2 = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, km2.class); mm2 = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, mm2.class); + m3 = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, m3.class); + km3 = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, km3.class); + mm3 = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, mm3.class); + s = UnitsRelationsTools.buildAnnoMirrorWithDefaultPrefix(env, s.class); h = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, h.class); @@ -51,6 +74,12 @@ public UnitsRelations init(ProcessingEnvironment env) { mPERs2 = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, mPERs2.class); + g = UnitsRelationsTools.buildAnnoMirrorWithDefaultPrefix(env, g.class); + kg = UnitsRelationsTools.buildAnnoMirrorWithSpecificPrefix(env, g.class, Prefix.kilo); + t = UnitsRelationsTools.buildAnnoMirrorWithNoPrefix(env, t.class); + N = UnitsRelationsTools.buildAnnoMirrorWithDefaultPrefix(env, N.class); + kN = UnitsRelationsTools.buildAnnoMirrorWithSpecificPrefix(env, N.class, Prefix.kilo); + return this; } @@ -87,6 +116,12 @@ public UnitsRelations init(ProcessingEnvironment env) { } else { return null; } + } else if (havePairOfUnitsIgnoringOrder(lht, m, rht, m2)) { + return m3; + } else if (havePairOfUnitsIgnoringOrder(lht, km, rht, km2)) { + return km3; + } else if (havePairOfUnitsIgnoringOrder(lht, mm, rht, mm2)) { + return mm3; } else if (havePairOfUnitsIgnoringOrder(lht, s, rht, mPERs)) { // s * mPERs or mPERs * s => m return m; @@ -96,6 +131,12 @@ public UnitsRelations init(ProcessingEnvironment env) { } else if (havePairOfUnitsIgnoringOrder(lht, h, rht, kmPERh)) { // h * kmPERh or kmPERh * h => km return km; + } else if (havePairOfUnitsIgnoringOrder(lht, kg, rht, mPERs2)) { + // kg * mPERs2 or mPERs2 * kg = N + return N; + } else if (havePairOfUnitsIgnoringOrder(lht, t, rht, mPERs2)) { + // t * mPERs2 or mPERs2 * t = kN + return kN; } else { return null; } @@ -122,6 +163,24 @@ public UnitsRelations init(ProcessingEnvironment env) { } else if (havePairOfUnits(lht, mm2, rht, mm)) { // mm2 / mm => mm return mm; + } else if (havePairOfUnits(lht, m3, rht, m)) { + // m3 / m => m2 + return m2; + } else if (havePairOfUnits(lht, km3, rht, km)) { + // km3 / km => km2 + return km2; + } else if (havePairOfUnits(lht, mm3, rht, mm)) { + // mm3 / mm => mm2 + return mm2; + } else if (havePairOfUnits(lht, m3, rht, m2)) { + // m3 / m2 => m + return m; + } else if (havePairOfUnits(lht, km3, rht, km2)) { + // km3 / km2 => km + return km; + } else if (havePairOfUnits(lht, mm3, rht, mm2)) { + // mm3 / mm2 => mm + return mm; } else if (havePairOfUnits(lht, m, rht, mPERs)) { // m / mPERs => s return s; @@ -134,6 +193,24 @@ public UnitsRelations init(ProcessingEnvironment env) { } else if (havePairOfUnits(lht, mPERs, rht, mPERs2)) { // mPERs / mPERs2 => s (velocity / acceleration == time) return s; + } else if (UnitsRelationsTools.hasSpecificUnit(lht, N)) { + if (UnitsRelationsTools.hasSpecificUnit(rht, kg)) { + // N / kg => mPERs2 + return mPERs2; + } else if (UnitsRelationsTools.hasSpecificUnit(rht, mPERs2)) { + // N / mPERs2 => kg + return kg; + } + return null; + } else if (UnitsRelationsTools.hasSpecificUnit(lht, kN)) { + if (UnitsRelationsTools.hasSpecificUnit(rht, t)) { + // kN / t => mPERs2 + return mPERs2; + } else if (UnitsRelationsTools.hasSpecificUnit(rht, mPERs2)) { + // kN / mPERs2 => t + return t; + } + return null; } else { return null; } diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsTools.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsTools.java index f5e74986ede3..134af783d4ff 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsTools.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsRelationsTools.java @@ -1,19 +1,22 @@ package org.checkerframework.checker.units; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.signature.qual.FullyQualifiedName; +import org.checkerframework.checker.units.qual.Prefix; +import org.checkerframework.checker.units.qual.UnknownUnits; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; + import java.lang.annotation.Annotation; import java.util.Map; -import java.util.Set; + import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.ExecutableElement; import javax.lang.model.util.Elements; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.checker.units.qual.Prefix; -import org.checkerframework.checker.units.qual.UnknownUnits; -import org.checkerframework.framework.type.AnnotatedTypeMirror; -import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationUtils; /** * A helper class for UnitsRelations, providing numerous methods which help process Annotations and @@ -34,7 +37,7 @@ public class UnitsRelationsTools { * constructed */ public static @Nullable AnnotationMirror buildAnnoMirrorWithSpecificPrefix( - final ProcessingEnvironment env, final CharSequence annoClass, final Prefix p) { + ProcessingEnvironment env, @FullyQualifiedName CharSequence annoClass, Prefix p) { AnnotationBuilder builder = new AnnotationBuilder(env, annoClass); builder.setValue("value", p); return builder.build(); @@ -50,7 +53,7 @@ public class UnitsRelationsTools { * @return an AnnotationMirror of the Unit with no prefix, or null if it cannot be constructed */ public static @Nullable AnnotationMirror buildAnnoMirrorWithNoPrefix( - final ProcessingEnvironment env, final CharSequence annoClass) { + ProcessingEnvironment env, @FullyQualifiedName CharSequence annoClass) { return AnnotationBuilder.fromName(env.getElementUtils(), annoClass); } @@ -60,7 +63,7 @@ public class UnitsRelationsTools { * @param annoType an AnnotatedTypeMirror representing a Units Annotated Type * @return a Prefix value (including Prefix.one), or null if it has none */ - public static @Nullable Prefix getPrefix(final AnnotatedTypeMirror annoType) { + public static @Nullable Prefix getPrefix(AnnotatedTypeMirror annoType) { Prefix result = null; // go through each Annotation of an Annotated Type, find the prefix and return it @@ -83,7 +86,7 @@ public class UnitsRelationsTools { * @param unitsAnnotation an AnnotationMirror representing a Units Annotation * @return a Prefix value (including Prefix.one), or null if it has none */ - public static @Nullable Prefix getPrefix(final AnnotationMirror unitsAnnotation) { + public static @Nullable Prefix getPrefix(AnnotationMirror unitsAnnotation) { AnnotationValue annotationValue = getAnnotationMirrorPrefix(unitsAnnotation); // if this Annotation has no prefix, return null @@ -110,7 +113,7 @@ public class UnitsRelationsTools { * @param annoType an AnnotatedTypeMirror representing a Units Annotated Type * @return true if it has no prefix, false otherwise */ - public static boolean hasNoPrefix(final AnnotatedTypeMirror annoType) { + public static boolean hasNoPrefix(AnnotatedTypeMirror annoType) { for (AnnotationMirror mirror : annoType.getAnnotations()) { // if any Annotation has a prefix, return false if (!hasNoPrefix(mirror)) { @@ -127,12 +130,12 @@ public static boolean hasNoPrefix(final AnnotatedTypeMirror annoType) { * @param unitsAnnotation an AnnotationMirror representing a Units Annotation * @return true if it has no prefix, false otherwise */ - public static boolean hasNoPrefix(final AnnotationMirror unitsAnnotation) { + public static boolean hasNoPrefix(AnnotationMirror unitsAnnotation) { AnnotationValue annotationValue = getAnnotationMirrorPrefix(unitsAnnotation); return hasNoPrefix(annotationValue); } - private static boolean hasNoPrefix(final AnnotationValue annotationValue) { + private static boolean hasNoPrefix(AnnotationValue annotationValue) { // Annotation has no element value (ie no SI prefix) if (annotationValue == null) { return true; @@ -146,7 +149,7 @@ private static boolean hasNoPrefix(final AnnotationValue annotationValue) { * otherwise returns null. */ private static @Nullable AnnotationValue getAnnotationMirrorPrefix( - final AnnotationMirror unitsAnnotation) { + AnnotationMirror unitsAnnotation) { Map elementValues = unitsAnnotation.getElementValues(); @@ -170,18 +173,18 @@ private static boolean hasNoPrefix(final AnnotationValue annotationValue) { * @return the base SI Unit's AnnotationMirror, or null if the base SI Unit cannot be * constructed */ - public static @Nullable AnnotationMirror removePrefix( - final Elements elements, final AnnotationMirror unitsAnnotation) { + public static AnnotationMirror removePrefix( + Elements elements, AnnotationMirror unitsAnnotation) { if (hasNoPrefix(unitsAnnotation)) { // Optimization, though the else case would also work. return unitsAnnotation; } else { + String unitsAnnoName = AnnotationUtils.annotationName(unitsAnnotation); // In the Units Checker, the only annotation value is the prefix value. Therefore, // fromName (which creates an annotation with no values) is acceptable. // TODO: refine sensitivity of removal for extension units, in case extension // Annotations have more than just Prefix in its values. - return AnnotationBuilder.fromName( - elements, unitsAnnotation.getAnnotationType().toString()); + return AnnotationBuilder.fromName(elements, unitsAnnoName); } } @@ -195,12 +198,12 @@ private static boolean hasNoPrefix(final AnnotationValue annotationValue) { * @return a copy of the Annotated Type without the prefix */ public static AnnotatedTypeMirror removePrefix( - final Elements elements, final AnnotatedTypeMirror annoType) { + Elements elements, AnnotatedTypeMirror annoType) { // deep copy the Annotated Type Mirror without any of the Annotations AnnotatedTypeMirror result = annoType.deepCopy(false); // get all of the original Annotations in the Annotated Type - Set annos = annoType.getAnnotations(); + AnnotationMirrorSet annos = annoType.getAnnotations(); // loop through all the Annotations to see if they use Prefix.one, remove Prefix.one if it // does @@ -230,7 +233,7 @@ public static AnnotatedTypeMirror removePrefix( * @param annoType an AnnotatedTypeMirror representing a Units Annotated Type * @return true if the Type has no units, false otherwise */ - public static boolean hasNoUnits(final AnnotatedTypeMirror annoType) { + public static boolean hasNoUnits(AnnotatedTypeMirror annoType) { return (annoType.getAnnotation(UnknownUnits.class) != null); } @@ -243,7 +246,7 @@ public static boolean hasNoUnits(final AnnotatedTypeMirror annoType) { * @return true if the Type has the specific unit, false otherwise */ public static boolean hasSpecificUnit( - final AnnotatedTypeMirror annoType, final AnnotationMirror unitsAnnotation) { + AnnotatedTypeMirror annoType, AnnotationMirror unitsAnnotation) { return AnnotationUtils.containsSame(annoType.getAnnotations(), unitsAnnotation); } @@ -256,7 +259,7 @@ public static boolean hasSpecificUnit( * @return true if the Type has the specific unit, false otherwise */ public static boolean hasSpecificUnitIgnoringPrefix( - final AnnotatedTypeMirror annoType, final AnnotationMirror unitsAnnotation) { + AnnotatedTypeMirror annoType, AnnotationMirror unitsAnnotation) { return AnnotationUtils.containsSameByName(annoType.getAnnotations(), unitsAnnotation); } @@ -275,10 +278,8 @@ public static boolean hasSpecificUnitIgnoringPrefix( * constructed */ public static @Nullable AnnotationMirror buildAnnoMirrorWithSpecificPrefix( - final ProcessingEnvironment env, - final Class annoClass, - final Prefix p) { - AnnotationBuilder builder = new AnnotationBuilder(env, annoClass.getCanonicalName()); + ProcessingEnvironment env, Class annoClass, Prefix p) { + AnnotationBuilder builder = new AnnotationBuilder(env, annoClass); builder.setValue("value", p); return builder.build(); } @@ -296,7 +297,7 @@ public static boolean hasSpecificUnitIgnoringPrefix( * @return an AnnotationMirror of the Unit with Prefix.one, or null if it cannot be constructed */ public static @Nullable AnnotationMirror buildAnnoMirrorWithDefaultPrefix( - final ProcessingEnvironment env, final Class annoClass) { + ProcessingEnvironment env, Class annoClass) { return buildAnnoMirrorWithSpecificPrefix(env, annoClass, Prefix.one); } @@ -312,7 +313,7 @@ public static boolean hasSpecificUnitIgnoringPrefix( * @return an AnnotationMirror of the Unit with no prefix, or null if it cannot be constructed */ static @Nullable AnnotationMirror buildAnnoMirrorWithNoPrefix( - final ProcessingEnvironment env, final Class annoClass) { + ProcessingEnvironment env, Class annoClass) { return AnnotationBuilder.fromClass(env.getElementUtils(), annoClass); } } diff --git a/checker/src/main/java/org/checkerframework/checker/units/UnitsVisitor.java b/checker/src/main/java/org/checkerframework/checker/units/UnitsVisitor.java index bb84912aae15..7937c850cb5f 100644 --- a/checker/src/main/java/org/checkerframework/checker/units/UnitsVisitor.java +++ b/checker/src/main/java/org/checkerframework/checker/units/UnitsVisitor.java @@ -2,7 +2,8 @@ import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.Tree.Kind; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.units.qual.UnknownUnits; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.common.basetype.BaseTypeVisitor; @@ -19,24 +20,24 @@ public UnitsVisitor(BaseTypeChecker checker) { } @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { - ExpressionTree var = node.getVariable(); - ExpressionTree expr = node.getExpression(); + public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { + ExpressionTree var = tree.getVariable(); + ExpressionTree expr = tree.getExpression(); AnnotatedTypeMirror varType = atypeFactory.getAnnotatedType(var); AnnotatedTypeMirror exprType = atypeFactory.getAnnotatedType(expr); - Kind kind = node.getKind(); + Tree.Kind kind = tree.getKind(); - if ((kind == Kind.PLUS_ASSIGNMENT || kind == Kind.MINUS_ASSIGNMENT)) { - if (!atypeFactory.getTypeHierarchy().isSubtype(exprType, varType)) { + if ((kind == Tree.Kind.PLUS_ASSIGNMENT || kind == Tree.Kind.MINUS_ASSIGNMENT)) { + if (!typeHierarchy.isSubtypeShallowEffective(exprType, varType)) { checker.reportError( - node, "compound.assignment.type.incompatible", varType, exprType); + tree, "compound.assignment.type.incompatible", varType, exprType); } - } else if (exprType.getAnnotation(UnknownUnits.class) == null) { + } else if (!exprType.hasAnnotation(UnknownUnits.class)) { // Only allow mul/div with unqualified units - checker.reportError(node, "compound.assignment.type.incompatible", varType, exprType); + checker.reportError(tree, "compound.assignment.type.incompatible", varType, exprType); } - return null; // super.visitCompoundAssignment(node, p); + return null; // super.visitCompoundAssignment(tree, p); } } diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/A.java b/checker/src/main/java/org/checkerframework/checker/units/qual/A.java deleted file mode 100644 index 270ad8cd5108..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/A.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.checkerframework.checker.units.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.SubtypeOf; - -/** - * Ampere. - * - * @checker_framework.manual #units-checker Units Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(Current.class) -public @interface A { - Prefix value() default Prefix.one; -} diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/C.java b/checker/src/main/java/org/checkerframework/checker/units/qual/C.java deleted file mode 100644 index bdda8f0e55d6..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/C.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.checkerframework.checker.units.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.SubtypeOf; - -/** - * Degree Centigrade (Celsius). - * - * @checker_framework.manual #units-checker Units Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) -@SubtypeOf(Temperature.class) -public @interface C {} diff --git a/checker/src/main/java/org/checkerframework/checker/units/qual/UnitsRelations.java b/checker/src/main/java/org/checkerframework/checker/units/qual/UnitsRelations.java deleted file mode 100644 index 562d65c2703f..000000000000 --- a/checker/src/main/java/org/checkerframework/checker/units/qual/UnitsRelations.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.checkerframework.checker.units.qual; - -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * Specify the class that knows how to handle the meta-annotated unit when put in relation (plus, - * multiply, ...) with another unit. - * - * @see org.checkerframework.checker.units.UnitsRelations - * @checker_framework.manual #units-checker Units Checker - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -public @interface UnitsRelations { - /** - * Returns the UnitsRelations subclass to use. - * - * @return the UnitsRelations subclass to use - */ - // The more precise type is Class, - // but org.checkerframework.checker.units.UnitsRelations is not in checker-qual.jar, nor can - // it be since it uses AnnotatedTypeMirrors. So use a less precise type and check that it - // is a subclass in UnitsAnnotatedTypeFactory. - Class value(); -} diff --git a/checker/src/main/resources/guava-assertions.astub b/checker/src/main/resources/guava-assertions.astub new file mode 100644 index 000000000000..8a00e9fad5e9 --- /dev/null +++ b/checker/src/main/resources/guava-assertions.astub @@ -0,0 +1,438 @@ +package com.google.common.base; + +import org.checkerframework.dataflow.qual.AssertMethod; +import org.checkerframework.dataflow.qual.Pure; + +public final class Preconditions { + private Preconditions() {} + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument(boolean expression); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument(boolean expression,Object errorMessage); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, + String errorMessageTemplate, + Object... errorMessageArgs); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument(boolean expression, String errorMessageTemplate, char p1); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument(boolean expression, String errorMessageTemplate, int p1); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument(boolean expression, String errorMessageTemplate, long p1); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate,Object p1); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate, char p1, char p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate, char p1, int p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate, char p1, long p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate, char p1,Object p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate, int p1, char p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate, int p1, int p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate, int p1, long p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate, int p1,Object p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate, long p1, char p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate, long p1, int p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate, long p1, long p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate, long p1,Object p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate,Object p1, char p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate,Object p1, int p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, String errorMessageTemplate,Object p1, long p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, + String errorMessageTemplate, + Object p1, + Object p2); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, + String errorMessageTemplate, + Object p1, + Object p2, + Object p3); + + @AssertMethod(IllegalArgumentException.class) + public static void checkArgument( + boolean expression, + String errorMessageTemplate, + Object p1, + Object p2, + Object p3, + Object p4); + + @AssertMethod(IllegalStateException.class) + public static void checkState(boolean expression); + + @AssertMethod(IllegalStateException.class) + public static void checkState(boolean expression,Object errorMessage); + + @AssertMethod(IllegalStateException.class) + public static void checkState( + boolean expression, + String errorMessageTemplate, + Object... errorMessageArgs); + + @AssertMethod(IllegalStateException.class) + public static void checkState(boolean expression, String errorMessageTemplate, char p1); + + @AssertMethod(IllegalStateException.class) + public static void checkState(boolean expression, String errorMessageTemplate, int p1); + + @AssertMethod(IllegalStateException.class) + public static void checkState(boolean expression, String errorMessageTemplate, long p1); + + @AssertMethod(IllegalStateException.class) + public static void checkState( + boolean expression, String errorMessageTemplate,Object p1); + + @AssertMethod(IllegalStateException.class) + public static void checkState(boolean expression, String errorMessageTemplate, char p1, char p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState(boolean expression, String errorMessageTemplate, char p1, int p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState(boolean expression, String errorMessageTemplate, char p1, long p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState( + boolean expression, String errorMessageTemplate, char p1,Object p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState(boolean expression, String errorMessageTemplate, int p1, char p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState(boolean expression, String errorMessageTemplate, int p1, int p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState(boolean expression, String errorMessageTemplate, int p1, long p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState( + boolean expression, String errorMessageTemplate, int p1,Object p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState(boolean expression, String errorMessageTemplate, long p1, char p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState(boolean expression, String errorMessageTemplate, long p1, int p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState(boolean expression, String errorMessageTemplate, long p1, long p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState( + boolean expression, String errorMessageTemplate, long p1,Object p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState( + boolean expression, String errorMessageTemplate,Object p1, char p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState( + boolean expression, String errorMessageTemplate,Object p1, int p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState( + boolean expression, String errorMessageTemplate,Object p1, long p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState( + boolean expression, + String errorMessageTemplate, + Object p1, + Object p2); + + @AssertMethod(IllegalStateException.class) + public static void checkState( + boolean expression, + String errorMessageTemplate, + Object p1, + Object p2, + Object p3); + + @AssertMethod(IllegalStateException.class) + public static void checkState( + boolean expression, + String errorMessageTemplate, + Object p1, + Object p2, + Object p3, + Object p4); + + public static T checkNotNull(T reference); + + public static T checkNotNull(T reference,Object errorMessage); + + public static T checkNotNull( + T reference, + String errorMessageTemplate, + Object... errorMessageArgs); + + public static T checkNotNull( + T reference, String errorMessageTemplate, char p1); + + public static T checkNotNull(T reference, String errorMessageTemplate, int p1); + + public static T checkNotNull( + T reference, String errorMessageTemplate, long p1); + + public static T checkNotNull( + T reference, String errorMessageTemplate,Object p1); + + public static T checkNotNull( + T reference, String errorMessageTemplate, char p1, char p2); + + public static T checkNotNull( + T reference, String errorMessageTemplate, char p1, int p2); + + public static T checkNotNull( + T reference, String errorMessageTemplate, char p1, long p2); + + public static T checkNotNull( + T reference, String errorMessageTemplate, char p1,Object p2); + + public static T checkNotNull( + T reference, String errorMessageTemplate, int p1, char p2); + + public static T checkNotNull( + T reference, String errorMessageTemplate, int p1, int p2); + + public static T checkNotNull( + T reference, String errorMessageTemplate, int p1, long p2); + + public static T checkNotNull( + T reference, String errorMessageTemplate, int p1,Object p2); + + public static T checkNotNull( + T reference, String errorMessageTemplate, long p1, char p2); + + public static T checkNotNull( + T reference, String errorMessageTemplate, long p1, int p2); + + public static T checkNotNull( + T reference, String errorMessageTemplate, long p1, long p2); + + public static T checkNotNull( + T reference, String errorMessageTemplate, long p1,Object p2); + + public static T checkNotNull( + T reference, String errorMessageTemplate,Object p1, char p2); + + public static T checkNotNull( + T reference, String errorMessageTemplate,Object p1, int p2); + + public static T checkNotNull( + T reference, String errorMessageTemplate,Object p1, long p2); + + public static T checkNotNull( + T reference, + String errorMessageTemplate, + Object p1, + Object p2); + + public static T checkNotNull( + T reference, + String errorMessageTemplate, + Object p1, + Object p2, + Object p3); + + public static T checkNotNull( + T reference, + String errorMessageTemplate, + Object p1, + Object p2, + Object p3, + Object p4); + + public static int checkElementIndex(int index, int size); + + public static int checkElementIndex(int index, int size, String desc); + + private static String badElementIndex(int index, int size, String desc); + + public static int checkPositionIndex(int index, int size); + + public static int checkPositionIndex(int index, int size, String desc); + + private static String badPositionIndex(int index, int size, String desc); + + public static void checkPositionIndexes(int start, int end, int size); + + private static String badPositionIndexes(int start, int end, int size); +} + +public final class Verify { + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate, Object... errorMessageArgs); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate, char p1); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate, int p1); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate, long p1); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate, Object p1); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate, char p1, char p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate, int p1, char p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate, long p1, char p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate,Object p1, char p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate, char p1, int p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate, int p1, int p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate, long p1, int p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify( + boolean expression, String errorMessageTemplate,Object p1, int p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate, char p1, long p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate, int p1, long p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify(boolean expression, String errorMessageTemplate, long p1, long p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify( + boolean expression, String errorMessageTemplate,Object p1, long p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify( + boolean expression, String errorMessageTemplate, char p1,Object p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify( + boolean expression, String errorMessageTemplate, int p1,Object p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify( + boolean expression, String errorMessageTemplate, long p1,Object p2); + @AssertMethod(VerifyException.class) + @Pure + public static void verify( + boolean expression, + String errorMessageTemplate, + Object p1, + Object p2); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify( + boolean expression, + String errorMessageTemplate, + Object p1, + Object p2, + Object p3); + + @AssertMethod(VerifyException.class) + @Pure + public static void verify( + boolean expression, + String errorMessageTemplate, + Object p1, + Object p2, + Object p3, + Object p4); + +} diff --git a/checker/src/main/resources/junit-assertions.astub b/checker/src/main/resources/junit-assertions.astub new file mode 100644 index 000000000000..853400216849 --- /dev/null +++ b/checker/src/main/resources/junit-assertions.astub @@ -0,0 +1,600 @@ +package org.junit.jupiter.api; + +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.checkerframework.dataflow.qual.AssertMethod; +import org.opentest4j.AssertionFailedError; + +public class Assertions { + + public static V fail(); + + public static V fail(String message); + + public static V fail(String message, Throwable cause); + + public static V fail(Throwable cause); + + public static V fail(Supplier messageSupplier); + + @AssertMethod(AssertionFailedError.class) + public static void assertTrue(boolean condition); + + @AssertMethod(AssertionFailedError.class) + public static void assertTrue(boolean condition, Supplier messageSupplier); + + public static void assertTrue(BooleanSupplier booleanSupplier); + + public static void assertTrue(BooleanSupplier booleanSupplier, String message); + + @AssertMethod(AssertionFailedError.class) + public static void assertTrue(boolean condition, String message); + + public static void assertTrue(BooleanSupplier booleanSupplier, Supplier messageSupplier); + + @AssertMethod(value = AssertionFailedError.class, isAssertFalse = true) + public static void assertFalse(boolean condition); + + @AssertMethod(value = AssertionFailedError.class, isAssertFalse = true) + public static void assertFalse(boolean condition, String message); + + @AssertMethod(value = AssertionFailedError.class, isAssertFalse = true) + public static void assertFalse(boolean condition, Supplier messageSupplier); + + public static void assertFalse(BooleanSupplier booleanSupplier); + + public static void assertFalse(BooleanSupplier booleanSupplier, String message); + + public static void assertFalse(BooleanSupplier booleanSupplier, Supplier messageSupplier); + + public static void assertNull(Object actual); + + public static void assertNull(Object actual, String message); + + public static void assertNull(Object actual, Supplier messageSupplier); + + public static void assertNotNull(Object actual); + + public static void assertNotNull(Object actual, String message); + + public static void assertNotNull(Object actual, Supplier messageSupplier); + + public static void assertEquals(short expected, short actual); + + public static void assertEquals(short expected, Short actual); + + public static void assertEquals(Short expected, short actual); + + public static void assertEquals(Short expected, Short actual); + + public static void assertEquals(short expected, short actual, String message); + + public static void assertEquals(short expected, Short actual, String message); + + public static void assertEquals(Short expected, short actual, String message); + + public static void assertEquals(Short expected, Short actual, String message); + + public static void assertEquals(short expected, short actual, Supplier messageSupplier); + + public static void assertEquals(short expected, Short actual, Supplier messageSupplier); + + public static void assertEquals(Short expected, short actual, Supplier messageSupplier); + + public static void assertEquals(Short expected, Short actual, Supplier messageSupplier); + + public static void assertEquals(byte expected, byte actual); + + public static void assertEquals(byte expected, Byte actual); + + public static void assertEquals(Byte expected, byte actual); + + public static void assertEquals(Byte expected, Byte actual); + + public static void assertEquals(byte expected, byte actual, String message); + + public static void assertEquals(byte expected, Byte actual, String message); + + public static void assertEquals(Byte expected, byte actual, String message); + + public static void assertEquals(Byte expected, Byte actual, String message); + + public static void assertEquals(byte expected, byte actual, Supplier messageSupplier); + + public static void assertEquals(byte expected, Byte actual, Supplier messageSupplier); + + public static void assertEquals(Byte expected, byte actual, Supplier messageSupplier); + + public static void assertEquals(Byte expected, Byte actual, Supplier messageSupplier); + + public static void assertEquals(int expected, int actual); + + public static void assertEquals(int expected, Integer actual); + + public static void assertEquals(Integer expected, int actual); + + public static void assertEquals(Integer expected, Integer actual); + + public static void assertEquals(int expected, int actual, String message); + + public static void assertEquals(int expected, Integer actual, String message); + + public static void assertEquals(Integer expected, int actual, String message); + + public static void assertEquals(Integer expected, Integer actual, String message); + + public static void assertEquals(int expected, int actual, Supplier messageSupplier); + + public static void assertEquals(int expected, Integer actual, Supplier messageSupplier); + + public static void assertEquals(Integer expected, int actual, Supplier messageSupplier); + + public static void assertEquals(Integer expected, Integer actual, Supplier messageSupplier); + + public static void assertEquals(long expected, long actual); + + public static void assertEquals(long expected, Long actual); + + public static void assertEquals(Long expected, long actual); + + public static void assertEquals(Long expected, Long actual); + + public static void assertEquals(long expected, long actual, String message); + + public static void assertEquals(long expected, Long actual, String message); + + public static void assertEquals(Long expected, long actual, String message); + + public static void assertEquals(Long expected, Long actual, String message); + + public static void assertEquals(long expected, long actual, Supplier messageSupplier); + + public static void assertEquals(long expected, Long actual, Supplier messageSupplier); + + public static void assertEquals(Long expected, long actual, Supplier messageSupplier); + + public static void assertEquals(Long expected, Long actual, Supplier messageSupplier); + + public static void assertEquals(float expected, float actual); + + public static void assertEquals(float expected, Float actual); + + public static void assertEquals(Float expected, float actual); + + public static void assertEquals(Float expected, Float actual); + + public static void assertEquals(float expected, float actual, String message); + + public static void assertEquals(float expected, Float actual, String message); + + public static void assertEquals(Float expected, float actual, String message); + + public static void assertEquals(Float expected, Float actual, String message); + + public static void assertEquals(float expected, float actual, Supplier messageSupplier); + + public static void assertEquals(float expected, Float actual, Supplier messageSupplier); + + public static void assertEquals(Float expected, float actual, Supplier messageSupplier); + + public static void assertEquals(Float expected, Float actual, Supplier messageSupplier); + + public static void assertEquals(float expected, float actual, float delta); + + public static void assertEquals(float expected, float actual, float delta, String message); + + public static void assertEquals(float expected, float actual, float delta, Supplier messageSupplier); + + public static void assertEquals(double expected, double actual); + + public static void assertEquals(double expected, Double actual); + + public static void assertEquals(Double expected, double actual); + + public static void assertEquals(Double expected, Double actual); + + public static void assertEquals(double expected, double actual, String message); + + public static void assertEquals(double expected, Double actual, String message); + + public static void assertEquals(Double expected, double actual, String message); + + public static void assertEquals(Double expected, Double actual, String message); + + public static void assertEquals(double expected, double actual, Supplier messageSupplier); + + public static void assertEquals(double expected, Double actual, Supplier messageSupplier); + + public static void assertEquals(Double expected, double actual, Supplier messageSupplier); + + public static void assertEquals(Double expected, Double actual, Supplier messageSupplier); + + public static void assertEquals(double expected, double actual, double delta); + + public static void assertEquals(double expected, double actual, double delta, String message); + + public static void assertEquals(double expected, double actual, double delta, Supplier messageSupplier); + + public static void assertEquals(char expected, char actual); + + public static void assertEquals(char expected, Character actual); + + public static void assertEquals(Character expected, char actual); + + public static void assertEquals(Character expected, Character actual); + + public static void assertEquals(char expected, char actual, String message); + + public static void assertEquals(char expected, Character actual, String message); + + public static void assertEquals(Character expected, char actual, String message); + + public static void assertEquals(Character expected, Character actual, String message); + + public static void assertEquals(char expected, char actual, Supplier messageSupplier); + + public static void assertEquals(char expected, Character actual, Supplier messageSupplier); + + public static void assertEquals(Character expected, char actual, Supplier messageSupplier); + + public static void assertEquals(Character expected, Character actual, Supplier messageSupplier); + + public static void assertEquals(Object expected, Object actual); + + public static void assertEquals(Object expected, Object actual, String message); + + public static void assertEquals(Object expected, Object actual, Supplier messageSupplier); + + public static void assertArrayEquals(boolean [] expected, boolean [] actual); + + public static void assertArrayEquals(boolean [] expected, boolean [] actual, String message); + + public static void assertArrayEquals(boolean [] expected, boolean [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(char [] expected, char [] actual); + + public static void assertArrayEquals(char [] expected, char [] actual, String message); + + public static void assertArrayEquals(char [] expected, char [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(byte [] expected, byte [] actual); + + public static void assertArrayEquals(byte [] expected, byte [] actual, String message); + + public static void assertArrayEquals(byte [] expected, byte [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(short [] expected, short [] actual); + + public static void assertArrayEquals(short [] expected, short [] actual, String message); + + public static void assertArrayEquals(short [] expected, short [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(int [] expected, int [] actual); + + public static void assertArrayEquals(int [] expected, int [] actual, String message); + + public static void assertArrayEquals(int [] expected, int [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(long [] expected, long [] actual); + + public static void assertArrayEquals(long [] expected, long [] actual, String message); + + public static void assertArrayEquals(long [] expected, long [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(float [] expected, float [] actual); + + public static void assertArrayEquals(float [] expected, float [] actual, String message); + + public static void assertArrayEquals(float [] expected, float [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(float[] expected, float[] actual, float delta); + + public static void assertArrayEquals(float[] expected, float[] actual, float delta, String message); + + public static void assertArrayEquals(float[] expected, float[] actual, float delta, + Supplier messageSupplier); + + public static void assertArrayEquals(double [] expected, double [] actual); + + public static void assertArrayEquals(double [] expected, double [] actual, String message); + + public static void assertArrayEquals(double [] expected, double [] actual, Supplier messageSupplier); + + public static void assertArrayEquals(double[] expected, double[] actual, double delta); + + public static void assertArrayEquals(double[] expected, double[] actual, double delta, String message); + + public static void assertArrayEquals(double[] expected, double[] actual, double delta, + Supplier messageSupplier); + + public static void assertArrayEquals(Object [] expected, Object [] actual); + + public static void assertArrayEquals(Object [] expected, Object [] actual, String message); + + public static void assertArrayEquals(Object [] expected, Object [] actual, Supplier messageSupplier); + + public static void assertIterableEquals(Iterable expected, Iterable actual); + + public static void assertIterableEquals(Iterable expected, Iterable actual, String message); + + public static void assertIterableEquals(Iterable expected, Iterable actual, + Supplier messageSupplier); + + public static void assertLinesMatch(List expectedLines, List actualLines); + + public static void assertLinesMatch(List expectedLines, List actualLines, String message); + + public static void assertLinesMatch(List expectedLines, List actualLines, + Supplier messageSupplier); + + public static void assertLinesMatch(Stream expectedLines, Stream actualLines); + + public static void assertLinesMatch(Stream expectedLines, Stream actualLines, String message); + + public static void assertLinesMatch(Stream expectedLines, Stream actualLines, + Supplier messageSupplier); + + public static void assertNotEquals(byte unexpected, byte actual); + + public static void assertNotEquals(byte unexpected, Byte actual); + + public static void assertNotEquals(Byte unexpected, byte actual); + + public static void assertNotEquals(Byte unexpected, Byte actual); + + public static void assertNotEquals(byte unexpected, byte actual, String message); + + public static void assertNotEquals(byte unexpected, Byte actual, String message); + + public static void assertNotEquals(Byte unexpected, byte actual, String message); + + public static void assertNotEquals(Byte unexpected, Byte actual, String message); + + public static void assertNotEquals(byte unexpected, byte actual, Supplier messageSupplier); + + public static void assertNotEquals(byte unexpected, Byte actual, Supplier messageSupplier); + + public static void assertNotEquals(Byte unexpected, byte actual, Supplier messageSupplier); + + public static void assertNotEquals(Byte unexpected, Byte actual, Supplier messageSupplier); + + public static void assertNotEquals(short unexpected, short actual); + + public static void assertNotEquals(short unexpected, Short actual); + + public static void assertNotEquals(Short unexpected, short actual); + + public static void assertNotEquals(Short unexpected, Short actual); + + public static void assertNotEquals(short unexpected, short actual, String message); + + public static void assertNotEquals(short unexpected, Short actual, String message); + + public static void assertNotEquals(Short unexpected, short actual, String message); + + public static void assertNotEquals(Short unexpected, Short actual, String message); + + public static void assertNotEquals(short unexpected, short actual, Supplier messageSupplier); + + public static void assertNotEquals(short unexpected, Short actual, Supplier messageSupplier); + + public static void assertNotEquals(Short unexpected, short actual, Supplier messageSupplier); + + public static void assertNotEquals(Short unexpected, Short actual, Supplier messageSupplier); + + public static void assertNotEquals(int unexpected, int actual); + + public static void assertNotEquals(int unexpected, Integer actual); + + public static void assertNotEquals(Integer unexpected, int actual); + + public static void assertNotEquals(Integer unexpected, Integer actual); + + public static void assertNotEquals(int unexpected, int actual, String message); + + public static void assertNotEquals(int unexpected, Integer actual, String message); + + public static void assertNotEquals(Integer unexpected, int actual, String message); + + public static void assertNotEquals(Integer unexpected, Integer actual, String message); + + public static void assertNotEquals(int unexpected, int actual, Supplier messageSupplier); + + public static void assertNotEquals(int unexpected, Integer actual, Supplier messageSupplier); + + public static void assertNotEquals(Integer unexpected, int actual, Supplier messageSupplier); + + public static void assertNotEquals(Integer unexpected, Integer actual, Supplier messageSupplier); + + public static void assertNotEquals(long unexpected, long actual); + + public static void assertNotEquals(long unexpected, Long actual); + + public static void assertNotEquals(Long unexpected, long actual); + + public static void assertNotEquals(Long unexpected, Long actual); + + public static void assertNotEquals(long unexpected, long actual, String message); + + public static void assertNotEquals(long unexpected, Long actual, String message); + + public static void assertNotEquals(Long unexpected, long actual, String message); + + public static void assertNotEquals(Long unexpected, Long actual, String message); + + public static void assertNotEquals(long unexpected, long actual, Supplier messageSupplier); + + public static void assertNotEquals(long unexpected, Long actual, Supplier messageSupplier); + + public static void assertNotEquals(Long unexpected, long actual, Supplier messageSupplier); + + public static void assertNotEquals(Long unexpected, Long actual, Supplier messageSupplier); + + public static void assertNotEquals(float unexpected, float actual); + + public static void assertNotEquals(float unexpected, Float actual); + + public static void assertNotEquals(Float unexpected, float actual); + + public static void assertNotEquals(Float unexpected, Float actual); + + public static void assertNotEquals(float unexpected, float actual, String message); + + public static void assertNotEquals(float unexpected, Float actual, String message); + + public static void assertNotEquals(Float unexpected, float actual, String message); + + public static void assertNotEquals(Float unexpected, Float actual, String message); + + public static void assertNotEquals(float unexpected, float actual, Supplier messageSupplier); + + public static void assertNotEquals(float unexpected, Float actual, Supplier messageSupplier); + + public static void assertNotEquals(Float unexpected, float actual, Supplier messageSupplier); + + public static void assertNotEquals(Float unexpected, Float actual, Supplier messageSupplier); + + public static void assertNotEquals(float unexpected, float actual, float delta); + + public static void assertNotEquals(float unexpected, float actual, float delta, String message); + + public static void assertNotEquals(float unexpected, float actual, float delta, Supplier messageSupplier); + + public static void assertNotEquals(double unexpected, double actual); + + public static void assertNotEquals(double unexpected, Double actual); + + public static void assertNotEquals(Double unexpected, double actual); + + public static void assertNotEquals(Double unexpected, Double actual); + + public static void assertNotEquals(double unexpected, double actual, String message); + + public static void assertNotEquals(double unexpected, Double actual, String message); + + public static void assertNotEquals(Double unexpected, double actual, String message); + + public static void assertNotEquals(Double unexpected, Double actual, String message); + + public static void assertNotEquals(double unexpected, double actual, Supplier messageSupplier); + + public static void assertNotEquals(double unexpected, Double actual, Supplier messageSupplier); + + public static void assertNotEquals(Double unexpected, double actual, Supplier messageSupplier); + + public static void assertNotEquals(Double unexpected, Double actual, Supplier messageSupplier); + + public static void assertNotEquals(double unexpected, double actual, double delta); + + public static void assertNotEquals(double unexpected, double actual, double delta, String message); + + public static void assertNotEquals(double unexpected, double actual, double delta, + Supplier messageSupplier); + + public static void assertNotEquals(char unexpected, char actual); + + public static void assertNotEquals(char unexpected, Character actual); + + public static void assertNotEquals(Character unexpected, char actual); + + public static void assertNotEquals(Character unexpected, Character actual); + + public static void assertNotEquals(char unexpected, char actual, String message); + + public static void assertNotEquals(char unexpected, Character actual, String message); + + public static void assertNotEquals(Character unexpected, char actual, String message); + + public static void assertNotEquals(Character unexpected, Character actual, String message); + + public static void assertNotEquals(char unexpected, char actual, Supplier messageSupplier); + + public static void assertNotEquals(char unexpected, Character actual, Supplier messageSupplier); + + public static void assertNotEquals(Character unexpected, char actual, Supplier messageSupplier); + + public static void assertNotEquals(Character unexpected, Character actual, Supplier messageSupplier); + + public static void assertNotEquals(Object unexpected, Object actual); + + public static void assertNotEquals(Object unexpected, Object actual, String message); + + public static void assertNotEquals(Object unexpected, Object actual, Supplier messageSupplier); + + public static void assertSame(Object expected, Object actual); + + public static void assertSame(Object expected, Object actual, String message); + + public static void assertSame(Object expected, Object actual, Supplier messageSupplier); + + public static void assertNotSame(Object unexpected, Object actual); + + public static void assertNotSame(Object unexpected, Object actual, String message); + + public static void assertNotSame(Object unexpected, Object actual, Supplier messageSupplier); + + public static void assertAll(Executable... executables) throws MultipleFailuresError; + + public static void assertAll(String heading, Executable... executables) throws MultipleFailuresError; + + public static void assertAll(Collection executables) throws MultipleFailuresError; + + public static void assertAll(String heading, Collection executables) throws MultipleFailuresError; + + public static void assertAll(Stream executables) throws MultipleFailuresError; + + public static void assertAll(String heading, Stream executables) throws MultipleFailuresError; + + public static T assertThrows(Class expectedType, Executable executable); + + public static T assertThrows(Class expectedType, Executable executable, String message); + + public static T assertThrows(Class expectedType, Executable executable, + Supplier messageSupplier); + + public static void assertDoesNotThrow(Executable executable); + + public static void assertDoesNotThrow(Executable executable, String message); + + public static void assertDoesNotThrow(Executable executable, Supplier messageSupplier); + + public static T assertDoesNotThrow(ThrowingSupplier supplier); + + public static T assertDoesNotThrow(ThrowingSupplier supplier, String message); + + public static T assertDoesNotThrow(ThrowingSupplier supplier, Supplier messageSupplier); + + public static void assertTimeout(Duration timeout, Executable executable); + + public static void assertTimeout(Duration timeout, Executable executable, String message); + + public static void assertTimeout(Duration timeout, Executable executable, Supplier messageSupplier); + + public static T assertTimeout(Duration timeout, ThrowingSupplier supplier); + + public static T assertTimeout(Duration timeout, ThrowingSupplier supplier, String message); + + public static T assertTimeout(Duration timeout, ThrowingSupplier supplier, + Supplier messageSupplier); + + public static void assertTimeoutPreemptively(Duration timeout, Executable executable); + + public static void assertTimeoutPreemptively(Duration timeout, Executable executable, String message); + + public static void assertTimeoutPreemptively(Duration timeout, Executable executable, + Supplier messageSupplier); + + public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier); + + public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, String message); + + public static T assertTimeoutPreemptively(Duration timeout, ThrowingSupplier supplier, + Supplier messageSupplier); +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsAutoValueTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsAutoValueTest.java new file mode 100644 index 000000000000..7605df2a60b0 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsAutoValueTest.java @@ -0,0 +1,34 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.calledmethods.CalledMethodsChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** Test case for Called Methods Checker's AutoValue support. */ +public class CalledMethodsAutoValueTest extends CheckerFrameworkPerDirectoryTest { + + public CalledMethodsAutoValueTest(List testFiles) { + super( + testFiles, + Arrays.asList( + "com.google.auto.value.extension.memoized.processor.MemoizedValidator", + "com.google.auto.value.processor.AutoAnnotationProcessor", + "com.google.auto.value.processor.AutoOneOfProcessor", + "com.google.auto.value.processor.AutoValueBuilderProcessor", + "com.google.auto.value.processor.AutoValueProcessor", + CalledMethodsChecker.class.getName()), + "calledmethods-autovalue", + Collections.emptyList(), + "-nowarn"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"calledmethods-autovalue"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsDisableReturnsReceiverTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsDisableReturnsReceiverTest.java new file mode 100644 index 000000000000..05c1d9841835 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsDisableReturnsReceiverTest.java @@ -0,0 +1,26 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.calledmethods.CalledMethodsChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** Basic tests for the Called Methods Checker. */ +public class CalledMethodsDisableReturnsReceiverTest extends CheckerFrameworkPerDirectoryTest { + public CalledMethodsDisableReturnsReceiverTest(List testFiles) { + super( + testFiles, + CalledMethodsChecker.class, + "calledmethods-disablereturnsreceiver", + "-AdisableReturnsReceiver", + "-encoding", + "UTF-8"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"calledmethods-disablereturnsreceiver"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsDisableframeworksTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsDisableframeworksTest.java new file mode 100644 index 000000000000..da5a50cc2607 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsDisableframeworksTest.java @@ -0,0 +1,37 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.calledmethods.CalledMethodsChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class CalledMethodsDisableframeworksTest extends CheckerFrameworkPerDirectoryTest { + + public CalledMethodsDisableframeworksTest(List testFiles) { + super( + testFiles, + Arrays.asList( + "com.google.auto.value.extension.memoized.processor.MemoizedValidator", + "com.google.auto.value.processor.AutoAnnotationProcessor", + "com.google.auto.value.processor.AutoOneOfProcessor", + "com.google.auto.value.processor.AutoValueBuilderProcessor", + "com.google.auto.value.processor.AutoValueProcessor", + CalledMethodsChecker.class.getName()), + "calledmethods-disableframeworks", + Collections.emptyList(), + "-AdisableBuilderFrameworkSupports=autovalue,lombok", + // The next option is so that we can run the usevaluechecker tests under this + // configuration. + "-ACalledMethodsChecker_useValueChecker", + "-nowarn"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"calledmethods-disableframeworks", "calledmethods-usevaluechecker"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsLombokTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsLombokTest.java new file mode 100644 index 000000000000..4f73eb6a5017 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsLombokTest.java @@ -0,0 +1,25 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.calledmethods.CalledMethodsChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** Test that the Called Methods Checker's support for Lombok works correctly. */ +public class CalledMethodsLombokTest extends CheckerFrameworkPerDirectoryTest { + public CalledMethodsLombokTest(List testFiles) { + super( + testFiles, + CalledMethodsChecker.class, + "calledmethods-delomboked", + "-nowarn", + "-AsuppressWarnings=type.anno.before.modifier"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"calledmethods-delomboked"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsNoDelombokTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsNoDelombokTest.java new file mode 100644 index 000000000000..558ab47f4d0c --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsNoDelombokTest.java @@ -0,0 +1,64 @@ +package org.checkerframework.checker.test.junit; + +import com.google.common.collect.ImmutableList; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.checkerframework.framework.test.TestConfiguration; +import org.checkerframework.framework.test.TestConfigurationBuilder; +import org.checkerframework.framework.test.TestUtilities; +import org.checkerframework.framework.test.TypecheckExecutor; +import org.checkerframework.framework.test.TypecheckResult; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +/** + * This test suite exists to demonstrate and keep a record of the unsoundness that occurs when + * Lombok and the Checker Framework are run in the same invocation of javac. + */ +public class CalledMethodsNoDelombokTest extends CheckerFrameworkPerDirectoryTest { + + private static final ImmutableList ANNOTATION_PROCS = + ImmutableList.of( + "lombok.launch.AnnotationProcessorHider$AnnotationProcessor", + "lombok.launch.AnnotationProcessorHider$ClaimingProcessor", + org.checkerframework.checker.calledmethods.CalledMethodsChecker.class + .getName()); + + public CalledMethodsNoDelombokTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.calledmethods.CalledMethodsChecker.class, + "lombok", + "-nowarn"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"calledmethods-nodelombok"}; + } + + /** + * copy-pasted code from {@link CheckerFrameworkPerDirectoryTest#run()}, except that we change + * the annotation processors to {@link #ANNOTATION_PROCS} + */ + @Override + public void run() { + boolean shouldEmitDebugInfo = TestUtilities.getShouldEmitDebugInfo(); + List customizedOptions = + customizeOptions(Collections.unmodifiableList(checkerOptions)); + TestConfiguration config = + TestConfigurationBuilder.buildDefaultConfiguration( + testDir, + testFiles, + classpathExtra, + ANNOTATION_PROCS, + customizedOptions, + shouldEmitDebugInfo); + TypecheckResult testResult = new TypecheckExecutor().runTest(config); + TypecheckResult adjustedTestResult = adjustTypecheckResult(testResult); + TestUtilities.assertTestDidNotFail(adjustedTestResult); + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsTest.java new file mode 100644 index 000000000000..ac396d83db18 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsTest.java @@ -0,0 +1,28 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.calledmethods.CalledMethodsChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** Basic tests for the Called Methods Checker. */ +public class CalledMethodsTest extends CheckerFrameworkPerDirectoryTest { + public CalledMethodsTest(List testFiles) { + super( + testFiles, + CalledMethodsChecker.class, + "calledmethods", + "-nowarn", + // Ignore the test suite's usage of qualifiers in illegal locations. + "-AignoreTargetLocations", + "-encoding", + "UTF-8"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"calledmethods"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsUseValueCheckerTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsUseValueCheckerTest.java new file mode 100644 index 000000000000..651932cc8fa3 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/CalledMethodsUseValueCheckerTest.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.calledmethods.CalledMethodsChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized; + +import java.io.File; +import java.util.List; + +public class CalledMethodsUseValueCheckerTest extends CheckerFrameworkPerDirectoryTest { + public CalledMethodsUseValueCheckerTest(List testFiles) { + super( + testFiles, + CalledMethodsChecker.class, + "calledmethods-usevaluechecker", + "-AuseValueChecker", + "-nowarn"); + } + + @Parameterized.Parameters + public static String[] getTestDirs() { + return new String[] {"calledmethods-usevaluechecker"}; + } +} diff --git a/checker/src/test/java/tests/CompilerMessagesTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/CompilerMessagesTest.java similarity index 94% rename from checker/src/test/java/tests/CompilerMessagesTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/CompilerMessagesTest.java index 1fb165936e9e..6ace53a5b48d 100644 --- a/checker/src/test/java/tests/CompilerMessagesTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/CompilerMessagesTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Compiler Messages Checker. Depends on the compiler.properties file. */ public class CompilerMessagesTest extends CheckerFrameworkPerDirectoryTest { @@ -18,7 +19,6 @@ public CompilerMessagesTest(List testFiles) { testFiles, org.checkerframework.checker.compilermsgs.CompilerMessagesChecker.class, "compilermsg", - "-Anomsgtext", "-Apropfiles=tests/compilermsg/compiler.properties"); } diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/CustomAliasTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/CustomAliasTest.java new file mode 100644 index 000000000000..caa4bdb64ad4 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/CustomAliasTest.java @@ -0,0 +1,32 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** JUnit tests for the custom aliasing. */ +public class CustomAliasTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a CustomAliasTest with the Nullness Checker and the Purity Checker. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public CustomAliasTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.nullness.NullnessChecker.class, + "custom-alias", + "-AaliasedTypeAnnos=org.checkerframework.checker.nullness.qual.NonNull:custom.alias.NonNull;" + + "org.checkerframework.checker.nullness.qual.Nullable:custom.alias.Nullable", + "-AaliasedDeclAnnos=org.checkerframework.dataflow.qual.Pure:custom.alias.Pure", + "-AcheckPurityAnnotations"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"custom-alias"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/DisbarUseTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/DisbarUseTest.java new file mode 100644 index 000000000000..a14c14b9fbbb --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/DisbarUseTest.java @@ -0,0 +1,35 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.testchecker.disbaruse.DisbarUseChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +public class DisbarUseTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a DisbarUseTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public DisbarUseTest(List testFiles) { + super( + testFiles, + DisbarUseChecker.class, + "disbaruse-records", + "-Astubs=tests/disbaruse-records", + "-AstubWarnIfNotFound"); + } + + @Parameters + public static String[] getTestDirs() { + // Check for JDK 16+ without using a library: + if (System.getProperty("java.version").matches("^(1[6-9]|[2-9][0-9])(\\..*)?")) { + return new String[] {"disbaruse-records"}; + } else { + return new String[] {}; + } + } +} diff --git a/checker/src/test/java/tests/FenumSwingTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/FenumSwingTest.java similarity index 84% rename from checker/src/test/java/tests/FenumSwingTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/FenumSwingTest.java index 4f9caa75d820..dfc7f55fb4c9 100644 --- a/checker/src/test/java/tests/FenumSwingTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/FenumSwingTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + public class FenumSwingTest extends CheckerFrameworkPerDirectoryTest { /** @@ -17,8 +18,9 @@ public FenumSwingTest(List testFiles) { testFiles, org.checkerframework.checker.fenum.FenumChecker.class, "fenum", - "-Anomsgtext", - "-Aquals=org.checkerframework.checker.fenum.qual.SwingVerticalOrientation,org.checkerframework.checker.fenum.qual.SwingHorizontalOrientation,org.checkerframework.checker.fenum.qual.SwingBoxOrientation,org.checkerframework.checker.fenum.qual.SwingCompassDirection,org.checkerframework.checker.fenum.qual.SwingElementOrientation,org.checkerframework.checker.fenum.qual.SwingTextOrientation"); + "-Aquals=org.checkerframework.checker.fenum.qual.SwingVerticalOrientation,org.checkerframework.checker.fenum.qual.SwingHorizontalOrientation,org.checkerframework.checker.fenum.qual.SwingBoxOrientation,org.checkerframework.checker.fenum.qual.SwingCompassDirection,org.checkerframework.checker.fenum.qual.SwingElementOrientation,org.checkerframework.checker.fenum.qual.SwingTextOrientation", + // Ignore the test suite's usage of qualifiers in illegal locations. + "-AignoreTargetLocations"); // TODO: check all qualifiers } diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/FenumTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/FenumTest.java new file mode 100644 index 000000000000..777086a832a0 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/FenumTest.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +public class FenumTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a FenumTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public FenumTest(List testFiles) { + super(testFiles, org.checkerframework.checker.fenum.FenumChecker.class, "fenum"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"fenum", "all-systems"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/FormatterLubGlbCheckerTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/FormatterLubGlbCheckerTest.java new file mode 100644 index 000000000000..e5d9b4267531 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/FormatterLubGlbCheckerTest.java @@ -0,0 +1,29 @@ +package org.checkerframework.checker.test.junit; + +// Test case for issue 691. +// https://github.com/typetools/checker-framework/issues/691 +// This exists to just run the FormatterLubGlbChecker. + +import org.checkerframework.checker.testchecker.lubglb.FormatterLubGlbChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +public class FormatterLubGlbCheckerTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a FormatterLubGlbCheckerTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public FormatterLubGlbCheckerTest(List testFiles) { + super(testFiles, FormatterLubGlbChecker.class, "", "-AcheckPurityAnnotations"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"formatter-lubglb"}; + } +} diff --git a/checker/src/test/java/tests/FormatterTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/FormatterTest.java similarity index 89% rename from checker/src/test/java/tests/FormatterTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/FormatterTest.java index ae7b79576ede..58f88dc12d33 100644 --- a/checker/src/test/java/tests/FormatterTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/FormatterTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + public class FormatterTest extends CheckerFrameworkPerDirectoryTest { /** * Create a FormatterTest. @@ -15,8 +16,7 @@ public FormatterTest(List testFiles) { super( testFiles, org.checkerframework.checker.formatter.FormatterChecker.class, - "formatter", - "-Anomsgtext"); + "formatter"); } @Parameters diff --git a/checker/src/test/java/tests/FormatterUncheckedDefaultsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/FormatterUncheckedDefaultsTest.java similarity index 94% rename from checker/src/test/java/tests/FormatterUncheckedDefaultsTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/FormatterUncheckedDefaultsTest.java index d0ecf7bc571f..4096dfd74033 100644 --- a/checker/src/test/java/tests/FormatterUncheckedDefaultsTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/FormatterUncheckedDefaultsTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + public class FormatterUncheckedDefaultsTest extends CheckerFrameworkPerDirectoryTest { /** * Create a FormatterUncheckedDefaultsTest. @@ -16,7 +17,6 @@ public FormatterUncheckedDefaultsTest(List testFiles) { testFiles, org.checkerframework.checker.formatter.FormatterChecker.class, "formatter", - "-Anomsgtext", "-AuseConservativeDefaultsForUncheckedCode=-source,bytecode"); } diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/FormatterUnitTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/FormatterUnitTest.java new file mode 100644 index 000000000000..20b67868668a --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/FormatterUnitTest.java @@ -0,0 +1,45 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.formatter.util.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("% testFiles) { super( testFiles, org.checkerframework.checker.guieffect.GuiEffectChecker.class, - "guieffect", - "-Anomsgtext"); + "guieffect"); // , "-Alint=debugSpew"); } diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/I18nFormatterLubGlbCheckerTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/I18nFormatterLubGlbCheckerTest.java new file mode 100644 index 000000000000..22f65cd19989 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/I18nFormatterLubGlbCheckerTest.java @@ -0,0 +1,29 @@ +package org.checkerframework.checker.test.junit; + +// Test case for issue 723. +// https://github.com/typetools/checker-framework/issues/723 +// This exists to just run the I18nFormatterLubGlbChecker. + +import org.checkerframework.checker.testchecker.lubglb.I18nFormatterLubGlbChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +public class I18nFormatterLubGlbCheckerTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create an I18nFormatterLubGlbCheckerTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public I18nFormatterLubGlbCheckerTest(List testFiles) { + super(testFiles, I18nFormatterLubGlbChecker.class, "", "-AcheckPurityAnnotations"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"i18n-formatter-lubglb"}; + } +} diff --git a/checker/src/test/java/tests/I18nFormatterTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/I18nFormatterTest.java similarity index 89% rename from checker/src/test/java/tests/I18nFormatterTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/I18nFormatterTest.java index 9b6e450760c0..003de4dff678 100644 --- a/checker/src/test/java/tests/I18nFormatterTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/I18nFormatterTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + public class I18nFormatterTest extends CheckerFrameworkPerDirectoryTest { /** @@ -16,8 +17,7 @@ public I18nFormatterTest(List testFiles) { super( testFiles, org.checkerframework.checker.i18nformatter.I18nFormatterChecker.class, - "i18n-formatter", - "-Anomsgtext"); + "i18n-formatter"); } @Parameters diff --git a/checker/src/test/java/tests/I18nFormatterUncheckedDefaultsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/I18nFormatterUncheckedDefaultsTest.java similarity index 94% rename from checker/src/test/java/tests/I18nFormatterUncheckedDefaultsTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/I18nFormatterUncheckedDefaultsTest.java index 3ea1d6ee5050..04cbbe0abcf0 100644 --- a/checker/src/test/java/tests/I18nFormatterUncheckedDefaultsTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/I18nFormatterUncheckedDefaultsTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + public class I18nFormatterUncheckedDefaultsTest extends CheckerFrameworkPerDirectoryTest { /** @@ -17,7 +18,6 @@ public I18nFormatterUncheckedDefaultsTest(List testFiles) { testFiles, org.checkerframework.checker.i18nformatter.I18nFormatterChecker.class, "i18n-formatter", - "-Anomsgtext", "-AuseConservativeDefaultsForUncheckedCode=-source,bytecode"); } diff --git a/checker/src/test/java/tests/I18nFormatterUnitTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/I18nFormatterUnitTest.java similarity index 98% rename from checker/src/test/java/tests/I18nFormatterUnitTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/I18nFormatterUnitTest.java index b56e42ba5b02..ca85671f9dc4 100644 --- a/checker/src/test/java/tests/I18nFormatterUnitTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/I18nFormatterUnitTest.java @@ -1,7 +1,7 @@ -package tests; +package org.checkerframework.checker.test.junit; -import org.checkerframework.checker.i18nformatter.I18nFormatUtil; import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory; +import org.checkerframework.checker.i18nformatter.util.I18nFormatUtil; import org.junit.Assert; import org.junit.Test; diff --git a/checker/src/test/java/tests/I18nTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/I18nTest.java similarity index 79% rename from checker/src/test/java/tests/I18nTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/I18nTest.java index 83131990ddee..bd0e3687a652 100644 --- a/checker/src/test/java/tests/I18nTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/I18nTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + public class I18nTest extends CheckerFrameworkPerDirectoryTest { /** @@ -17,7 +18,8 @@ public I18nTest(List testFiles) { testFiles, org.checkerframework.checker.i18n.I18nChecker.class, "i18n", - "-Anomsgtext"); + // Ignore the test suite's usage of qualifiers in illegal locations. + "-AignoreTargetLocations"); } @Parameters diff --git a/checker/src/test/java/tests/I18nUncheckedDefaultsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/I18nUncheckedDefaultsTest.java similarity index 94% rename from checker/src/test/java/tests/I18nUncheckedDefaultsTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/I18nUncheckedDefaultsTest.java index 94cb6c6e3011..b93127e08b30 100644 --- a/checker/src/test/java/tests/I18nUncheckedDefaultsTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/I18nUncheckedDefaultsTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + public class I18nUncheckedDefaultsTest extends CheckerFrameworkPerDirectoryTest { /** @@ -17,7 +18,6 @@ public I18nUncheckedDefaultsTest(List testFiles) { testFiles, org.checkerframework.checker.i18n.I18nChecker.class, "i18n", - "-Anomsgtext", "-AuseConservativeDefaultsForUncheckedCode=-source,bytecode"); } diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/IndexInitializedFieldsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/IndexInitializedFieldsTest.java new file mode 100644 index 000000000000..cdd19fb988ad --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/IndexInitializedFieldsTest.java @@ -0,0 +1,36 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** JUnit tests for the Index Checker when running together with the InitializedFields Checker. */ +public class IndexInitializedFieldsTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create an IndexTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public IndexInitializedFieldsTest(List testFiles) { + super( + testFiles, + Arrays.asList( + "org.checkerframework.checker.index.IndexChecker", + "org.checkerframework.common.initializedfields.InitializedFieldsChecker"), + "index-initializedfields", + Collections.emptyList(), + "-Aajava=tests/index-initializedfields/input-annotation-files/", + // Ignore the test suite's usage of qualifiers in illegal locations. + "-AignoreTargetLocations"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"index-initializedfields"}; + } +} diff --git a/checker/src/test/java/tests/IndexTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/IndexTest.java similarity index 80% rename from checker/src/test/java/tests/IndexTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/IndexTest.java index a48600c583d7..4cae656a319d 100644 --- a/checker/src/test/java/tests/IndexTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/IndexTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Index Checker. */ public class IndexTest extends CheckerFrameworkPerDirectoryTest { @@ -18,7 +19,8 @@ public IndexTest(List testFiles) { testFiles, org.checkerframework.checker.index.IndexChecker.class, "index", - "-Anomsgtext"); + // Ignore the test suite's usage of qualifiers in illegal locations. + "-AignoreTargetLocations"); } @Parameters diff --git a/checker/src/test/java/tests/InterningTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/InterningTest.java similarity index 90% rename from checker/src/test/java/tests/InterningTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/InterningTest.java index e3580b47070b..b20d0dc2a7d9 100644 --- a/checker/src/test/java/tests/InterningTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/InterningTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Interning Checker, which tests the Interned annotation. */ public class InterningTest extends CheckerFrameworkPerDirectoryTest { @@ -17,8 +18,7 @@ public InterningTest(List testFiles) { super( testFiles, org.checkerframework.checker.interning.InterningChecker.class, - "interning", - "-Anomsgtext"); + "interning"); } @Parameters diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/InterningWarnRedundantAnnotationsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/InterningWarnRedundantAnnotationsTest.java new file mode 100644 index 000000000000..20636c135513 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/InterningWarnRedundantAnnotationsTest.java @@ -0,0 +1,29 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** JUnit tests for the Interning checker when AwarnRedundantAnnotations is used. */ +public class InterningWarnRedundantAnnotationsTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a InterningWarnRedundantAnnotationsTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public InterningWarnRedundantAnnotationsTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.interning.InterningChecker.class, + "interning-warnredundantannotations", + "-AwarnRedundantAnnotations"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"interning-warnredundantannotations"}; + } +} diff --git a/checker/src/test/java/tests/LockSafeDefaultsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/LockSafeDefaultsTest.java similarity index 94% rename from checker/src/test/java/tests/LockSafeDefaultsTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/LockSafeDefaultsTest.java index adfe10a91e7c..ab7bed9d0d60 100644 --- a/checker/src/test/java/tests/LockSafeDefaultsTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/LockSafeDefaultsTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Lock checker when using safe defaults for unchecked source code. */ public class LockSafeDefaultsTest extends CheckerFrameworkPerDirectoryTest { @@ -18,8 +19,7 @@ public LockSafeDefaultsTest(List testFiles) { testFiles, org.checkerframework.checker.lock.LockChecker.class, "lock", - "-AuseConservativeDefaultsForUncheckedCode=source", - "-Anomsgtext"); + "-AuseConservativeDefaultsForUncheckedCode=source"); } @Parameters diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/LockTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/LockTest.java new file mode 100644 index 000000000000..7327a1546327 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/LockTest.java @@ -0,0 +1,34 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +public class LockTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a LockTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public LockTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.lock.LockChecker.class, + "lock", + // Ignore the test suite's usage of qualifiers in illegal locations. + "-AignoreTargetLocations"); + } + + @Parameters + public static String[] getTestDirs() { + // Check for JDK 16+ without using a library: + if (System.getProperty("java.version").matches("^(1[6-9]|[2-9][0-9])(\\..*)?")) { + return new String[] {"lock", "lock-records", "all-systems"}; + } else { + return new String[] {"lock", "all-systems"}; + } + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/MustCallNoLightweightOwnershipTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/MustCallNoLightweightOwnershipTest.java new file mode 100644 index 000000000000..18d9f7e05197 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/MustCallNoLightweightOwnershipTest.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +public class MustCallNoLightweightOwnershipTest extends CheckerFrameworkPerDirectoryTest { + public MustCallNoLightweightOwnershipTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.mustcall.MustCallChecker.class, + "mustcall-nolightweightownership", + "-AnoLightweightOwnership", + // "-AstubDebug"); + "-nowarn"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"mustcall-nolightweightownership"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/MustCallTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/MustCallTest.java new file mode 100644 index 000000000000..970fda0e6059 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/MustCallTest.java @@ -0,0 +1,23 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +public class MustCallTest extends CheckerFrameworkPerDirectoryTest { + public MustCallTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.mustcall.MustCallChecker.class, + "mustcall", + // "-AstubDebug"); + "-nowarn"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"mustcall"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NestedAggregateCheckerTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NestedAggregateCheckerTest.java new file mode 100644 index 000000000000..c2731b570459 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NestedAggregateCheckerTest.java @@ -0,0 +1,29 @@ +package org.checkerframework.checker.test.junit; + +// Test case for issue 343. +// https://github.com/typetools/checker-framework/issues/343 +// This exists to just run the NestedAggregateChecker. + +import org.checkerframework.checker.testchecker.NestedAggregateChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +public class NestedAggregateCheckerTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a NestedAggregateCheckerTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public NestedAggregateCheckerTest(List testFiles) { + super(testFiles, NestedAggregateChecker.class, "", "-AcheckPurityAnnotations"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"aggregate", "all-systems"}; + } +} diff --git a/checker/src/test/java/tests/NullnessAssertsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessAssertsTest.java similarity index 96% rename from checker/src/test/java/tests/NullnessAssertsTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/NullnessAssertsTest.java index 82945629b653..ee3c12300b9b 100644 --- a/checker/src/test/java/tests/NullnessAssertsTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessAssertsTest.java @@ -1,11 +1,12 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.checker.nullness.NullnessChecker; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Nullness checker. */ public class NullnessAssertsTest extends CheckerFrameworkPerDirectoryTest { @@ -24,7 +25,6 @@ public NullnessAssertsTest(List testFiles) { "nullness", "-AcheckPurityAnnotations", "-AassumeAssertionsAreEnabled", - "-Anomsgtext", "-Xlint:deprecation", "-Alint=soundArrayCreationNullness," + NullnessChecker.LINT_REDUNDANTNULLCOMPARISON); diff --git a/checker/src/test/java/tests/NullnessAssumeAssertionsAreDisabledTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessAssumeAssertionsAreDisabledTest.java similarity index 94% rename from checker/src/test/java/tests/NullnessAssumeAssertionsAreDisabledTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/NullnessAssumeAssertionsAreDisabledTest.java index 90f542f35ab8..c3716e3c1993 100644 --- a/checker/src/test/java/tests/NullnessAssumeAssertionsAreDisabledTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessAssumeAssertionsAreDisabledTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Nullness Checker. */ public class NullnessAssumeAssertionsAreDisabledTest extends CheckerFrameworkPerDirectoryTest { @@ -19,7 +20,6 @@ public NullnessAssumeAssertionsAreDisabledTest(List testFiles) { org.checkerframework.checker.nullness.NullnessChecker.class, "nullness", "-AassumeAssertionsAreDisabled", - "-Anomsgtext", "-Xlint:deprecation"); } diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessAssumeInitializedTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessAssumeInitializedTest.java new file mode 100644 index 000000000000..a5df11dcee7b --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessAssumeInitializedTest.java @@ -0,0 +1,38 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** JUnit tests for the Nullness Checker without the Initialization Checker. */ +public class NullnessAssumeInitializedTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a NullnessAssumeInitializedTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public NullnessAssumeInitializedTest(List testFiles) { + // TODO: remove soundArrayCreationNullness option once it's no + // longer needed. See issue #986: + // https://github.com/typetools/checker-framework/issues/986 + super( + testFiles, + org.checkerframework.checker.nullness.NullnessChecker.class, + "nullness", + "-AcheckPurityAnnotations", + "-AassumeInitialized", + "-AconservativeArgumentNullnessAfterInvocation=true", + "-Xlint:deprecation", + "-Alint=soundArrayCreationNullness," + + NullnessChecker.LINT_REDUNDANTNULLCOMPARISON); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"nullness", "nullness-assumeinitialized", "all-systems"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessAssumeKeyForTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessAssumeKeyForTest.java new file mode 100644 index 000000000000..4dc3ce056d2a --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessAssumeKeyForTest.java @@ -0,0 +1,37 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** JUnit tests for the Nullness checker. */ +public class NullnessAssumeKeyForTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a NullnessAssumeKeyForTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public NullnessAssumeKeyForTest(List testFiles) { + // TODO: remove soundArrayCreationNullness option once it's no + // longer needed. See issue #986: + // https://github.com/typetools/checker-framework/issues/986 + super( + testFiles, + org.checkerframework.checker.nullness.NullnessChecker.class, + "nullness", + "-AcheckPurityAnnotations", + "-AassumeKeyFor", + "-Xlint:deprecation", + "-Alint=soundArrayCreationNullness," + + NullnessChecker.LINT_REDUNDANTNULLCOMPARISON); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"nullness-assumekeyfor"}; + } +} diff --git a/checker/src/test/java/tests/NullnessCheckCastElementTypeTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessCheckCastElementTypeTest.java similarity index 90% rename from checker/src/test/java/tests/NullnessCheckCastElementTypeTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/NullnessCheckCastElementTypeTest.java index 21ded74cd359..165d35d9f31d 100644 --- a/checker/src/test/java/tests/NullnessCheckCastElementTypeTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessCheckCastElementTypeTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Nullness checker when checkCastElementType is used. */ public class NullnessCheckCastElementTypeTest extends CheckerFrameworkPerDirectoryTest { @@ -18,8 +19,7 @@ public NullnessCheckCastElementTypeTest(List testFiles) { testFiles, org.checkerframework.checker.nullness.NullnessChecker.class, "nullness", - "-AcheckCastElementType", - "-Anomsgtext"); + "-AcheckCastElementType"); } @Parameters diff --git a/checker/src/test/java/tests/NullnessConcurrentTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessConcurrentTest.java similarity index 89% rename from checker/src/test/java/tests/NullnessConcurrentTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/NullnessConcurrentTest.java index 414fe2f2597b..e556a67c1ab1 100644 --- a/checker/src/test/java/tests/NullnessConcurrentTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessConcurrentTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Nullness checker when running with concurrent semantics. */ public class NullnessConcurrentTest extends CheckerFrameworkPerDirectoryTest { @@ -18,8 +19,7 @@ public NullnessConcurrentTest(List testFiles) { testFiles, org.checkerframework.checker.nullness.NullnessChecker.class, "nullness", - "-AconcurrentSemantics", - "-Anomsgtext"); + "-AconcurrentSemantics"); } @Parameters diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessEnclosingExprTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessEnclosingExprTest.java new file mode 100644 index 000000000000..4976c79e5b3b --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessEnclosingExprTest.java @@ -0,0 +1,29 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * JUnit test for the Nullness Checker with checking of enclosing expressions of inner class + * instantiations enabled. + */ +public class NullnessEnclosingExprTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a NullnessEnclosingExprTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public NullnessEnclosingExprTest(List testFiles) { + super(testFiles, NullnessChecker.class, "nullness", "-AcheckEnclosingExpr"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"nullness-enclosingexpr"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessGenericWildcardTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessGenericWildcardTest.java new file mode 100644 index 000000000000..82f28abaab7c --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessGenericWildcardTest.java @@ -0,0 +1,65 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.checkerframework.framework.test.TestConfiguration; +import org.checkerframework.framework.test.TestConfigurationBuilder; +import org.checkerframework.framework.test.TestUtilities; +import org.checkerframework.framework.test.TypecheckExecutor; +import org.checkerframework.framework.test.TypecheckResult; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +/** JUnit tests for the Nullness checker for issue #511. */ +public class NullnessGenericWildcardTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a NullnessGenericWildcardTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public NullnessGenericWildcardTest(List testFiles) { + super( + testFiles, + NullnessChecker.class, + "nullness", + // This test reads bytecode .class files created by NullnessGenericWildcardLibTest + "-cp", + "dist/checker.jar:tests/build/testclasses/"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"nullness-genericwildcard"}; + } + + @Override + public void run() { + boolean shouldEmitDebugInfo = TestUtilities.getShouldEmitDebugInfo(); + List customizedOptions1 = customizeOptions(Collections.emptyList()); + TestConfiguration config1 = + TestConfigurationBuilder.buildDefaultConfiguration( + "tests/nullness-genericwildcardlib", + new File("tests/nullness-genericwildcardlib", "GwiParent.java"), + NullnessChecker.class, + customizedOptions1, + shouldEmitDebugInfo); + TypecheckResult testResult1 = new TypecheckExecutor().runTest(config1); + TestUtilities.assertTestDidNotFail(testResult1); + + List customizedOptions2 = + customizeOptions(Collections.unmodifiableList(checkerOptions)); + TestConfiguration config2 = + TestConfigurationBuilder.buildDefaultConfiguration( + testDir, + testFiles, + Collections.singleton(NullnessChecker.class.getName()), + customizedOptions2, + shouldEmitDebugInfo); + TypecheckResult testResult2 = new TypecheckExecutor().runTest(config2); + TestUtilities.assertTestDidNotFail(testResult2); + } +} diff --git a/checker/src/test/java/tests/NullnessInvariantArraysTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessInvariantArraysTest.java similarity index 90% rename from checker/src/test/java/tests/NullnessInvariantArraysTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/NullnessInvariantArraysTest.java index 767a3a0078ed..15acf45cca6b 100644 --- a/checker/src/test/java/tests/NullnessInvariantArraysTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessInvariantArraysTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Nullness checker when array subtyping is invariant. */ public class NullnessInvariantArraysTest extends CheckerFrameworkPerDirectoryTest { @@ -18,8 +19,7 @@ public NullnessInvariantArraysTest(List testFiles) { testFiles, org.checkerframework.checker.nullness.NullnessChecker.class, "nullness", - "-AinvariantArrays", - "-Anomsgtext"); + "-AinvariantArrays"); } @Parameters diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessJavacErrorsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessJavacErrorsTest.java new file mode 100644 index 000000000000..57886c339e2c --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessJavacErrorsTest.java @@ -0,0 +1,30 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerFileTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; + +/** JUnit tests for the Nullness checker that issue javac errors. */ +public class NullnessJavacErrorsTest extends CheckerFrameworkPerFileTest { + + public NullnessJavacErrorsTest(File testFile) { + // TODO: remove soundArrayCreationNullness option once it's no + // longer needed. See issue #986: + // https://github.com/typetools/checker-framework/issues/986 + super( + testFile, + org.checkerframework.checker.nullness.NullnessChecker.class, + "nullness", + "-AcheckPurityAnnotations", + "-Xlint:deprecation", + "-Alint=soundArrayCreationNullness," + + NullnessChecker.LINT_REDUNDANTNULLCOMPARISON); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"nullness-javac-errors"}; + } +} diff --git a/checker/src/test/java/tests/NullnessJavadocTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessJavadocTest.java similarity index 76% rename from checker/src/test/java/tests/NullnessJavadocTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/NullnessJavadocTest.java index b1b0ae9a6d1f..5751d82f3ff6 100644 --- a/checker/src/test/java/tests/NullnessJavadocTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessJavadocTest.java @@ -1,31 +1,34 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.Collections; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.checkerframework.javacutil.SystemUtil; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.Collections; +import java.util.List; + /** * JUnit tests for the Nullness Checker -- testing type-checking of code that uses Javadoc classes. */ public class NullnessJavadocTest extends CheckerFrameworkPerDirectoryTest { - /** @param testFiles the files containing test code, which will be type-checked */ + /** + * @param testFiles the files containing test code, which will be type-checked + */ public NullnessJavadocTest(List testFiles) { super( testFiles, org.checkerframework.checker.nullness.NullnessChecker.class, "nullness", - toolsJarList(), - "-Anomsgtext"); + // required for JDK 8 (maybe not required for JDK 11, but it does no harm) + toolsJarList()); } /** * Return a list that contains the pathname to the tools.jar file, if it exists. * - * @returns a list that contains the pathname to the tools.jar file, if it exists + * @return a list that contains the pathname to the tools.jar file, if it exists */ private static List toolsJarList() { String toolsJar = SystemUtil.getToolsJar(); diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessNoDelombokTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessNoDelombokTest.java new file mode 100644 index 000000000000..7c5a0d5e95d1 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessNoDelombokTest.java @@ -0,0 +1,63 @@ +package org.checkerframework.checker.test.junit; + +import com.google.common.collect.ImmutableList; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.checkerframework.framework.test.TestConfiguration; +import org.checkerframework.framework.test.TestConfigurationBuilder; +import org.checkerframework.framework.test.TestUtilities; +import org.checkerframework.framework.test.TypecheckExecutor; +import org.checkerframework.framework.test.TypecheckResult; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +/** + * This test suite exists to demonstrate and keep a record of the unsoundness that occurs when + * Lombok and the Checker Framework are run in the same invocation of javac. + */ +public class NullnessNoDelombokTest extends CheckerFrameworkPerDirectoryTest { + + private static final ImmutableList ANNOTATION_PROCS = + ImmutableList.of( + "lombok.launch.AnnotationProcessorHider$AnnotationProcessor", + "lombok.launch.AnnotationProcessorHider$ClaimingProcessor", + org.checkerframework.checker.nullness.NullnessChecker.class.getName()); + + public NullnessNoDelombokTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.nullness.NullnessChecker.class, + "nullness-nodelombok", + "-nowarn"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"nullness-nodelombok"}; + } + + /** + * copy-pasted code from {@link CheckerFrameworkPerDirectoryTest#run()}, except that we change + * the annotation processors to {@link #ANNOTATION_PROCS} + */ + @Override + public void run() { + boolean shouldEmitDebugInfo = TestUtilities.getShouldEmitDebugInfo(); + List customizedOptions = + customizeOptions(Collections.unmodifiableList(checkerOptions)); + TestConfiguration config = + TestConfigurationBuilder.buildDefaultConfiguration( + testDir, + testFiles, + classpathExtra, + ANNOTATION_PROCS, + customizedOptions, + shouldEmitDebugInfo); + TypecheckResult testResult = new TypecheckExecutor().runTest(config); + TypecheckResult adjustedTestResult = adjustTypecheckResult(testResult); + TestUtilities.assertTestDidNotFail(adjustedTestResult); + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessNullMarkedTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessNullMarkedTest.java new file mode 100644 index 000000000000..df697164a87c --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessNullMarkedTest.java @@ -0,0 +1,40 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.checkerframework.framework.test.TestUtilities; +import org.junit.Test; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** JUnit tests for the Nullness checker. */ +public class NullnessNullMarkedTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a NullnessNullMarkedTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public NullnessNullMarkedTest(List testFiles) { + super(testFiles, org.checkerframework.checker.nullness.NullnessChecker.class, "nullness"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"nullness-nullmarked"}; + } + + @Override + @Test + public void run() { + /* + * Skip under JDK8: checker/bin-devel/build.sh doesn't build JSpecify under that version + * (since the JSpecify build requires JDK9+), so there would be no JSpecify jar, and tests + * would fail on account of the missing classes. + */ + if (TestUtilities.IS_AT_LEAST_9_JVM) { + super.run(); + } + } +} diff --git a/checker/src/test/java/tests/NullnessPermitClearPropertyTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessPermitClearPropertyTest.java similarity index 83% rename from checker/src/test/java/tests/NullnessPermitClearPropertyTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/NullnessPermitClearPropertyTest.java index 5cb85b2b0318..9cb896513cab 100644 --- a/checker/src/test/java/tests/NullnessPermitClearPropertyTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessPermitClearPropertyTest.java @@ -1,23 +1,25 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** * JUnit tests for the Nullness Checker -- testing {@code -Alint=permitClearProperty} command-line * argument. */ public class NullnessPermitClearPropertyTest extends CheckerFrameworkPerDirectoryTest { - /** @param testFiles the files containing test code, which will be type-checked */ + /** + * @param testFiles the files containing test code, which will be type-checked + */ public NullnessPermitClearPropertyTest(List testFiles) { super( testFiles, org.checkerframework.checker.nullness.NullnessChecker.class, "nullness", - "-Anomsgtext", "-Alint=permitClearProperty"); } diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessRecordsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessRecordsTest.java new file mode 100644 index 000000000000..0637242b65b7 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessRecordsTest.java @@ -0,0 +1,37 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** JUnit tests for the Nullness checker with records (JDK16+ only). */ +public class NullnessRecordsTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a NullnessRecordsTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public NullnessRecordsTest(List testFiles) { + super( + testFiles, + NullnessChecker.class, + "nullness-records", + "-AcheckPurityAnnotations", + "-Xlint:deprecation"); + } + + @Parameters + public static String[] getTestDirs() { + // Check for JDK 16+ without using a library: + // There is no decimal point in the JDK 17 version number. + if (System.getProperty("java.version").matches("^(1[6-9]|[2-9][0-9])(\\..*)?")) { + return new String[] {"nullness-records"}; + } else { + return new String[] {}; + } + } +} diff --git a/checker/src/test/java/tests/NullnessReflectionTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessReflectionTest.java similarity index 90% rename from checker/src/test/java/tests/NullnessReflectionTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/NullnessReflectionTest.java index 1a4a8583e2f8..525a81bfa7da 100644 --- a/checker/src/test/java/tests/NullnessReflectionTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessReflectionTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Nullness checker when reflection resolution is enabled. */ public class NullnessReflectionTest extends CheckerFrameworkPerDirectoryTest { @@ -18,8 +19,7 @@ public NullnessReflectionTest(List testFiles) { testFiles, org.checkerframework.checker.nullness.NullnessChecker.class, "nullness", - "-AresolveReflection", - "-Anomsgtext"); + "-AresolveReflection"); } @Parameters diff --git a/checker/src/test/java/tests/NullnessSafeDefaultsBytecodeTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessSafeDefaultsBytecodeTest.java similarity index 94% rename from checker/src/test/java/tests/NullnessSafeDefaultsBytecodeTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/NullnessSafeDefaultsBytecodeTest.java index cda4212c92a1..d5ffae2deabf 100644 --- a/checker/src/test/java/tests/NullnessSafeDefaultsBytecodeTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessSafeDefaultsBytecodeTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Nullness checker when using safe defaults for unannotated bytecode. */ public class NullnessSafeDefaultsBytecodeTest extends CheckerFrameworkPerDirectoryTest { @@ -18,8 +19,7 @@ public NullnessSafeDefaultsBytecodeTest(List testFiles) { testFiles, org.checkerframework.checker.nullness.NullnessChecker.class, "nullness", - "-AuseConservativeDefaultsForUncheckedCode=bytecode", - "-Anomsgtext"); + "-AuseConservativeDefaultsForUncheckedCode=bytecode"); } @Parameters diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessSafeDefaultsSourceCodeTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessSafeDefaultsSourceCodeTest.java new file mode 100644 index 000000000000..605a3a022180 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessSafeDefaultsSourceCodeTest.java @@ -0,0 +1,68 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.checkerframework.framework.test.TestConfiguration; +import org.checkerframework.framework.test.TestConfigurationBuilder; +import org.checkerframework.framework.test.TestUtilities; +import org.checkerframework.framework.test.TypecheckExecutor; +import org.checkerframework.framework.test.TypecheckResult; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** JUnit tests for the Nullness checker when using safe defaults for unannotated source code. */ +public class NullnessSafeDefaultsSourceCodeTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a NullnessSafeDefaultsSourceCodeTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public NullnessSafeDefaultsSourceCodeTest(List testFiles) { + super( + testFiles, + NullnessChecker.class, + "nullness", + "-AuseConservativeDefaultsForUncheckedCode=source", + "-cp", + "dist/checker.jar:tests/build/testclasses/"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"nullness-safedefaultssourcecode"}; + } + + @Override + public void run() { + boolean shouldEmitDebugInfo = TestUtilities.getShouldEmitDebugInfo(); + List customizedOptions1 = + customizeOptions( + Arrays.asList("-AuseConservativeDefaultsForUncheckedCode=source,bytecode")); + TestConfiguration config1 = + TestConfigurationBuilder.buildDefaultConfiguration( + "tests/nullness-safedefaultssourcecodelib", + new File("tests/nullness-safedefaultssourcecodelib", "Lib.java"), + NullnessChecker.class, + customizedOptions1, + shouldEmitDebugInfo); + TypecheckResult testResult1 = new TypecheckExecutor().runTest(config1); + TestUtilities.assertTestDidNotFail(testResult1); + + List customizedOptions2 = + customizeOptions(Collections.unmodifiableList(checkerOptions)); + TestConfiguration config2 = + TestConfigurationBuilder.buildDefaultConfiguration( + testDir, + testFiles, + Collections.singleton(NullnessChecker.class.getName()), + customizedOptions2, + shouldEmitDebugInfo); + TypecheckResult testResult2 = new TypecheckExecutor().runTest(config2); + TestUtilities.assertTestDidNotFail(testResult2); + } +} diff --git a/checker/src/test/java/tests/NullnessSkipDefsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessSkipDefsTest.java similarity index 94% rename from checker/src/test/java/tests/NullnessSkipDefsTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/NullnessSkipDefsTest.java index ebc4725124c9..7865131017c1 100644 --- a/checker/src/test/java/tests/NullnessSkipDefsTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessSkipDefsTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Nullness Checker -- testing {@code -AskipDefs} command-line argument. */ public class NullnessSkipDefsTest extends CheckerFrameworkPerDirectoryTest { @@ -18,7 +19,6 @@ public NullnessSkipDefsTest(List testFiles) { testFiles, org.checkerframework.checker.nullness.NullnessChecker.class, "nullness", - "-Anomsgtext", "-AskipDefs=SkipMe"); } diff --git a/checker/src/test/java/tests/NullnessSkipUsesTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessSkipUsesTest.java similarity index 94% rename from checker/src/test/java/tests/NullnessSkipUsesTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/NullnessSkipUsesTest.java index 594a7eabd1af..077fc55365fd 100644 --- a/checker/src/test/java/tests/NullnessSkipUsesTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessSkipUsesTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Nullness Checker -- testing {@code -AskipUses} command-line argument. */ public class NullnessSkipUsesTest extends CheckerFrameworkPerDirectoryTest { @@ -18,7 +19,6 @@ public NullnessSkipUsesTest(List testFiles) { testFiles, org.checkerframework.checker.nullness.NullnessChecker.class, "nullness", - "-Anomsgtext", "-AskipUses=SkipMe"); } diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessStubfileTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessStubfileTest.java new file mode 100644 index 000000000000..02579ed0cb86 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessStubfileTest.java @@ -0,0 +1,33 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +public class NullnessStubfileTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a NullnessStubfileTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public NullnessStubfileTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.nullness.NullnessChecker.class, + "nullness", + "-Astubs=" + + String.join( + ":", + "tests/nullness-stubfile/stubfile1.astub", + "tests/nullness-stubfile/stubfile2.astub", + "tests/nullness-stubfile/requireNonNull.astub")); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"nullness-stubfile"}; + } +} diff --git a/checker/src/test/java/tests/NullnessTempTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessTempTest.java similarity index 80% rename from checker/src/test/java/tests/NullnessTempTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/NullnessTempTest.java index 5922964fc487..c7d54b0cb94f 100644 --- a/checker/src/test/java/tests/NullnessTempTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessTempTest.java @@ -1,11 +1,12 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.checker.nullness.NullnessChecker; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Nullness Checker. */ public class NullnessTempTest extends CheckerFrameworkPerDirectoryTest { @@ -15,14 +16,10 @@ public class NullnessTempTest extends CheckerFrameworkPerDirectoryTest { * @param testFiles the files containing test code, which will be type-checked */ public NullnessTempTest(List testFiles) { - // TODO: remove soundArrayCreationNullness option once it's no - // longer needed. See issue #986: - // https://github.com/typetools/checker-framework/issues/986 super( testFiles, org.checkerframework.checker.nullness.NullnessChecker.class, "nullness", - "-Anomsgtext", "-Alint=soundArrayCreationNullness," + NullnessChecker.LINT_REDUNDANTNULLCOMPARISON); } diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessTest.java new file mode 100644 index 000000000000..c4e519e96c16 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessTest.java @@ -0,0 +1,50 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * JUnit tests for the Nullness Checker with the Initialization Checker. + * + *

    Since the Initialization Checker cannot be run by itself, this covers + * + *

      + *
    • test cases for the Nullness Checker that depend on the Initialization Checker (in directory + * {@code nullness-initialization}), + *
    • test cases for the Nullness Checker that should behave the same regardless of whether + * initialization checking is on or off (in directory {@code nullness}; these are run both by + * this test and by the {@link NullnessAssumeInitializedTest}, + *
    • test cases for the Initialization Checker that do not involve any nullness annotations (in + * directory {@code initialization}) + *
    + */ +public class NullnessTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a NullnessTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public NullnessTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.nullness.NullnessChecker.class, + "nullness", + "-AcheckPurityAnnotations", + "-AconservativeArgumentNullnessAfterInvocation=true", + "-Xlint:deprecation", + "-Alint=soundArrayCreationNullness," + + NullnessChecker.LINT_REDUNDANTNULLCOMPARISON); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] { + "nullness", "nullness-initialization", "initialization", "all-systems" + }; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessWarnRedundantAnnotationsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessWarnRedundantAnnotationsTest.java new file mode 100644 index 000000000000..3f44fd1e4694 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/NullnessWarnRedundantAnnotationsTest.java @@ -0,0 +1,29 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** JUnit tests for the Nullness checker when AwarnRedundantAnnotations is used. */ +public class NullnessWarnRedundantAnnotationsTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a NullnessWarnRedundantAnnotationsTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public NullnessWarnRedundantAnnotationsTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.nullness.NullnessChecker.class, + "nullness-warnredundantannotations", + "-AwarnRedundantAnnotations"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"nullness-warnredundantannotations"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/OptionalPureGettersTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/OptionalPureGettersTest.java new file mode 100644 index 000000000000..ca7d19aeacf3 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/OptionalPureGettersTest.java @@ -0,0 +1,29 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** JUnit tests for the Optional Checker, which has the {@code @Present} annotation. */ +public class OptionalPureGettersTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create an OptionalPureGettersTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public OptionalPureGettersTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.optional.OptionalChecker.class, + "optional", + "-AassumePureGetters"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"optional-pure-getters"}; + } +} diff --git a/checker/src/test/java/tests/OptionalTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/OptionalTest.java similarity index 89% rename from checker/src/test/java/tests/OptionalTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/OptionalTest.java index a20637a85ddc..e9e36acf9a57 100644 --- a/checker/src/test/java/tests/OptionalTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/OptionalTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Optional Checker, which has the {@code @Present} annotation. */ public class OptionalTest extends CheckerFrameworkPerDirectoryTest { @@ -18,7 +19,7 @@ public OptionalTest(List testFiles) { testFiles, org.checkerframework.checker.optional.OptionalChecker.class, "optional", - "-Anomsgtext"); + "-AoptionalMapAssumeNonNull"); } @Parameters diff --git a/checker/src/test/java/tests/ParseAllJdkTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ParseAllJdkTest.java similarity index 88% rename from checker/src/test/java/tests/ParseAllJdkTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/ParseAllJdkTest.java index 8f753d740d03..0694f4fafec2 100644 --- a/checker/src/test/java/tests/ParseAllJdkTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ParseAllJdkTest.java @@ -1,12 +1,13 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.checker.nullness.NullnessChecker; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; -/** Tests -AparseAllJdk option. */ +import java.io.File; +import java.util.List; + +/** Tests {@code -AparseAllJdk} option. */ public class ParseAllJdkTest extends CheckerFrameworkPerDirectoryTest { /** diff --git a/checker/src/test/java/tests/README b/checker/src/test/java/org/checkerframework/checker/test/junit/README similarity index 100% rename from checker/src/test/java/tests/README rename to checker/src/test/java/org/checkerframework/checker/test/junit/README diff --git a/checker/src/test/java/tests/RegexTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/RegexTest.java similarity index 75% rename from checker/src/test/java/tests/RegexTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/RegexTest.java index d7f2782b3bc5..7d9f86886134 100644 --- a/checker/src/test/java/tests/RegexTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/RegexTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + public class RegexTest extends CheckerFrameworkPerDirectoryTest { /** @@ -13,11 +14,7 @@ public class RegexTest extends CheckerFrameworkPerDirectoryTest { * @param testFiles the files containing test code, which will be type-checked */ public RegexTest(List testFiles) { - super( - testFiles, - org.checkerframework.checker.regex.RegexChecker.class, - "regex", - "-Anomsgtext"); + super(testFiles, org.checkerframework.checker.regex.RegexChecker.class, "regex"); } @Parameters diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCustomIgnoredExceptionsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCustomIgnoredExceptionsTest.java new file mode 100644 index 000000000000..bfecdb05d977 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakCustomIgnoredExceptionsTest.java @@ -0,0 +1,26 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized; + +import java.io.File; +import java.util.List; + +public class ResourceLeakCustomIgnoredExceptionsTest extends CheckerFrameworkPerDirectoryTest { + public ResourceLeakCustomIgnoredExceptionsTest(List testFiles) { + super( + testFiles, + ResourceLeakChecker.class, + "resourceleak-customignoredexceptions", + "-AresourceLeakIgnoredExceptions=java.lang.Error, =java.lang.NullPointerException", + "-AwarnUnneededSuppressions", + "-encoding", + "UTF-8"); + } + + @Parameterized.Parameters + public static String[] getTestDirs() { + return new String[] {"resourceleak-customignoredexceptions"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakExtraIgnoredExceptionsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakExtraIgnoredExceptionsTest.java new file mode 100644 index 000000000000..42addc33c03b --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakExtraIgnoredExceptionsTest.java @@ -0,0 +1,26 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized; + +import java.io.File; +import java.util.List; + +public class ResourceLeakExtraIgnoredExceptionsTest extends CheckerFrameworkPerDirectoryTest { + public ResourceLeakExtraIgnoredExceptionsTest(List testFiles) { + super( + testFiles, + ResourceLeakChecker.class, + "resourceleak-extraignoredexceptions", + "-AresourceLeakIgnoredExceptions=default,java.lang.IllegalStateException", + "-AwarnUnneededSuppressions", + "-encoding", + "UTF-8"); + } + + @Parameterized.Parameters + public static String[] getTestDirs() { + return new String[] {"resourceleak-extraignoredexceptions"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakNoCreatesMustCallForTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakNoCreatesMustCallForTest.java new file mode 100644 index 000000000000..6b73c882cb55 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakNoCreatesMustCallForTest.java @@ -0,0 +1,27 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** Tests for the Resource Leak Checker. */ +public class ResourceLeakNoCreatesMustCallForTest extends CheckerFrameworkPerDirectoryTest { + public ResourceLeakNoCreatesMustCallForTest(List testFiles) { + super( + testFiles, + ResourceLeakChecker.class, + "resourceleak-nocreatesmustcallfor", + "-AnoCreatesMustCallFor", + "-AwarnUnneededSuppressions", + "-encoding", + "UTF-8"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"resourceleak-nocreatesmustcallfor"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakNoLightweightOwnershipTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakNoLightweightOwnershipTest.java new file mode 100644 index 000000000000..3f0354b894d8 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakNoLightweightOwnershipTest.java @@ -0,0 +1,27 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** Tests for the Resource Leak Checker. */ +public class ResourceLeakNoLightweightOwnershipTest extends CheckerFrameworkPerDirectoryTest { + public ResourceLeakNoLightweightOwnershipTest(List testFiles) { + super( + testFiles, + ResourceLeakChecker.class, + "resourceleak-nolightweightownership", + "-AnoLightweightOwnership", + "-nowarn", + "-encoding", + "UTF-8"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"resourceleak-nolightweightownership"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakNoResourceAliasesTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakNoResourceAliasesTest.java new file mode 100644 index 000000000000..99d77e202be7 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakNoResourceAliasesTest.java @@ -0,0 +1,27 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** Tests for the Resource Leak Checker. */ +public class ResourceLeakNoResourceAliasesTest extends CheckerFrameworkPerDirectoryTest { + public ResourceLeakNoResourceAliasesTest(List testFiles) { + super( + testFiles, + ResourceLeakChecker.class, + "resourceleak-noresourcealiases", + "-AnoResourceAliases", + "-nowarn", + "-encoding", + "UTF-8"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"resourceleak-noresourcealiases"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakPermitInitializationLeak.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakPermitInitializationLeak.java new file mode 100644 index 000000000000..668217f1f083 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakPermitInitializationLeak.java @@ -0,0 +1,27 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** Tests for the Resource Leak Checker. */ +public class ResourceLeakPermitInitializationLeak extends CheckerFrameworkPerDirectoryTest { + public ResourceLeakPermitInitializationLeak(List testFiles) { + super( + testFiles, + ResourceLeakChecker.class, + "resourceleak-permitinitializationleak", + "-ApermitInitializationLeak", + "-AwarnUnneededSuppressions", + "-encoding", + "UTF-8"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"resourceleak-permitinitializationleak"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakPermitStaticOwning.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakPermitStaticOwning.java new file mode 100644 index 000000000000..41347bd58d8f --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakPermitStaticOwning.java @@ -0,0 +1,27 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** Tests for the Resource Leak Checker. */ +public class ResourceLeakPermitStaticOwning extends CheckerFrameworkPerDirectoryTest { + public ResourceLeakPermitStaticOwning(List testFiles) { + super( + testFiles, + ResourceLeakChecker.class, + "resourceleak-permitstaticowning", + "-ApermitStaticOwning", + "-AwarnUnneededSuppressions", + "-encoding", + "UTF-8"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"resourceleak-permitstaticowning"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakTest.java new file mode 100644 index 000000000000..e0868c6f63ca --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ResourceLeakTest.java @@ -0,0 +1,26 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** Tests for the Resource Leak Checker. */ +public class ResourceLeakTest extends CheckerFrameworkPerDirectoryTest { + public ResourceLeakTest(List testFiles) { + super( + testFiles, + ResourceLeakChecker.class, + "resourceleak", + "-AwarnUnneededSuppressions", + "-encoding", + "UTF-8"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"resourceleak"}; + } +} diff --git a/checker/src/test/java/tests/SignatureTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/SignatureTest.java similarity index 89% rename from checker/src/test/java/tests/SignatureTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/SignatureTest.java index bb20dead484b..c649e50fb318 100644 --- a/checker/src/test/java/tests/SignatureTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/SignatureTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + public class SignatureTest extends CheckerFrameworkPerDirectoryTest { /** @@ -16,8 +17,7 @@ public SignatureTest(List testFiles) { super( testFiles, org.checkerframework.checker.signature.SignatureChecker.class, - "signature", - "-Anomsgtext"); + "signature"); } @Parameters diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/SignednessInitializedFieldsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/SignednessInitializedFieldsTest.java new file mode 100644 index 000000000000..09ae2e1fe62a --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/SignednessInitializedFieldsTest.java @@ -0,0 +1,33 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class SignednessInitializedFieldsTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a SignednessInitializedFieldsTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public SignednessInitializedFieldsTest(List testFiles) { + super( + testFiles, + Arrays.asList( + "org.checkerframework.common.initializedfields.InitializedFieldsChecker", + "org.checkerframework.checker.signedness.SignednessChecker"), + "signedness-initialized-fields", + Collections.emptyList() // classpathextra + ); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"signedness-initialized-fields", "all-systems"}; + } +} diff --git a/checker/src/test/java/tests/SignednessTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/SignednessTest.java similarity index 80% rename from checker/src/test/java/tests/SignednessTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/SignednessTest.java index 93f2666221ef..c5f0946897b3 100644 --- a/checker/src/test/java/tests/SignednessTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/SignednessTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + public class SignednessTest extends CheckerFrameworkPerDirectoryTest { /** @@ -17,7 +18,8 @@ public SignednessTest(List testFiles) { testFiles, org.checkerframework.checker.signedness.SignednessChecker.class, "signedness", - "-Anomsgtext"); + // Ignore the test suite's usage of qualifiers in illegal locations. + "-AignoreTargetLocations"); } @Parameters diff --git a/checker/src/test/java/tests/SignednessUncheckedDefaultsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/SignednessUncheckedDefaultsTest.java similarity index 94% rename from checker/src/test/java/tests/SignednessUncheckedDefaultsTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/SignednessUncheckedDefaultsTest.java index 4a1b6f1dac3a..c75554f9e8bb 100644 --- a/checker/src/test/java/tests/SignednessUncheckedDefaultsTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/SignednessUncheckedDefaultsTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + public class SignednessUncheckedDefaultsTest extends CheckerFrameworkPerDirectoryTest { /** @@ -17,7 +18,6 @@ public SignednessUncheckedDefaultsTest(List testFiles) { testFiles, org.checkerframework.checker.signedness.SignednessChecker.class, "signedness", - "-Anomsgtext", "-AuseConservativeDefaultsForUncheckedCode=-source,bytecode"); } diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserNullnessTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserNullnessTest.java new file mode 100644 index 000000000000..f64fe68d0dd9 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserNullnessTest.java @@ -0,0 +1,29 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized; + +import java.io.File; +import java.util.List; + +/** Tests for stub parsing. */ +public class StubparserNullnessTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a StubparserNullnessTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public StubparserNullnessTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.nullness.NullnessChecker.class, + "stubparser-nullness", + "-Astubs=tests/stubparser-nullness"); + } + + @Parameterized.Parameters + public static String[] getTestDirs() { + return new String[] {"stubparser-nullness"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserRecordTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserRecordTest.java new file mode 100644 index 000000000000..3b170c6a60ae --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserRecordTest.java @@ -0,0 +1,35 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized; + +import java.io.File; +import java.util.List; + +/** Tests for stub parsing with records. */ +public class StubparserRecordTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a StubparserRecordTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public StubparserRecordTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.nullness.NullnessChecker.class, + "stubparser-records", + "-Astubs=tests/stubparser-records"); + } + + @Parameterized.Parameters + public static String[] getTestDirs() { + // Check for JDK 16+ without using a library: + // There is no decimal point in the JDK 17 version number. + if (System.getProperty("java.version").matches("^(1[6-9]|[2-9][0-9])(\\..*)?")) { + return new String[] {"stubparser-records"}; + } else { + return new String[] {}; + } + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserTaintingTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserTaintingTest.java new file mode 100644 index 000000000000..b9e7508207c9 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/StubparserTaintingTest.java @@ -0,0 +1,30 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized; + +import java.io.File; +import java.util.List; + +/** Tests for stub parsing. */ +public class StubparserTaintingTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a StubparserTaintingTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public StubparserTaintingTest(List testFiles) { + super( + testFiles, + org.checkerframework.checker.tainting.TaintingChecker.class, + "stubparser-tainting", + "-AmergeStubsWithSource", + "-Astubs=tests/stubparser-tainting"); + } + + @Parameterized.Parameters + public static String[] getTestDirs() { + return new String[] {"stubparser-tainting", "all-systems"}; + } +} diff --git a/checker/src/test/java/tests/TaintingTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/TaintingTest.java similarity index 84% rename from checker/src/test/java/tests/TaintingTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/TaintingTest.java index c137bb053730..1b6ddb9136a8 100644 --- a/checker/src/test/java/tests/TaintingTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/TaintingTest.java @@ -1,11 +1,12 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.checker.tainting.TaintingChecker; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + public class TaintingTest extends CheckerFrameworkPerDirectoryTest { /** @@ -14,7 +15,7 @@ public class TaintingTest extends CheckerFrameworkPerDirectoryTest { * @param testFiles the files containing test code, which will be type-checked */ public TaintingTest(List testFiles) { - super(testFiles, TaintingChecker.class, "tainting", "-Anomsgtext"); + super(testFiles, TaintingChecker.class, "tainting"); } @Parameters diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/UnitsTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/UnitsTest.java new file mode 100644 index 000000000000..f2f377670059 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/UnitsTest.java @@ -0,0 +1,24 @@ +package org.checkerframework.checker.test.junit; + +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +public class UnitsTest extends CheckerFrameworkPerDirectoryTest { + + /** + * Create a UnitsTest. + * + * @param testFiles the files containing test code, which will be type-checked + */ + public UnitsTest(List testFiles) { + super(testFiles, org.checkerframework.checker.units.UnitsChecker.class, "units"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"units", "all-systems"}; + } +} diff --git a/checker/src/test/java/tests/ValueIndexInteractionTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ValueIndexInteractionTest.java similarity index 82% rename from checker/src/test/java/tests/ValueIndexInteractionTest.java rename to checker/src/test/java/org/checkerframework/checker/test/junit/ValueIndexInteractionTest.java index 84c41c37a9d5..edcaa5767f45 100644 --- a/checker/src/test/java/tests/ValueIndexInteractionTest.java +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ValueIndexInteractionTest.java @@ -1,10 +1,11 @@ -package tests; +package org.checkerframework.checker.test.junit; -import java.io.File; -import java.util.List; import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; import org.junit.runners.Parameterized.Parameters; +import java.io.File; +import java.util.List; + /** JUnit tests for the Value Checker's interactions with the Index Checker. */ public class ValueIndexInteractionTest extends CheckerFrameworkPerDirectoryTest { @@ -18,7 +19,8 @@ public ValueIndexInteractionTest(List testFiles) { testFiles, org.checkerframework.common.value.ValueChecker.class, "value-index-interaction", - "-Anomsgtext"); + // Ignore the test suite's usage of qualifiers in illegal locations. + "-AignoreTargetLocations"); } @Parameters diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferIndexAjavaGenerationTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferIndexAjavaGenerationTest.java new file mode 100644 index 000000000000..b84d0bf8cadf --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferIndexAjavaGenerationTest.java @@ -0,0 +1,40 @@ +package org.checkerframework.checker.test.junit.ainferrunners; + +import org.checkerframework.checker.index.IndexChecker; +import org.checkerframework.framework.test.AinferGeneratePerDirectoryTest; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * Tests whole-program inference with the aid of ajava files. This test is the first pass on the + * test data, which generates the ajava files. This specific test suite is designed to elicit + * problems with ajava parsing that only occur when an aggregate checker is in use. + * + *

    IMPORTANT: The errors captured in the tests located in tests/ainfer-index/ are not relevant. + * The meaning of this test class is to test if the generated ajava files are similar to the + * expected ones. The errors on .java files must be ignored. + */ +@Category(AinferIndexAjavaGenerationTest.class) +public class AinferIndexAjavaGenerationTest extends AinferGeneratePerDirectoryTest { + + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public AinferIndexAjavaGenerationTest(List testFiles) { + super( + testFiles, + IndexChecker.class, + "ainfer-index/non-annotated", + "-Ainfer=ajava", + // "-Aajava=tests/ainfer-index/input-annotation-files/", + "-Awarns"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"ainfer-index/non-annotated"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferIndexAjavaValidationTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferIndexAjavaValidationTest.java new file mode 100644 index 000000000000..87cc53d482ec --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferIndexAjavaValidationTest.java @@ -0,0 +1,37 @@ +package org.checkerframework.checker.test.junit.ainferrunners; + +import org.checkerframework.checker.index.IndexChecker; +import org.checkerframework.framework.test.AinferValidatePerDirectoryTest; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * Tests whole-program type inference with ajava files. This test is the second pass, which ensures + * that with the ajava files in place, the errors that those annotations remove are no longer + * issued. + */ +@Category(AinferIndexAjavaGenerationTest.class) +public class AinferIndexAjavaValidationTest extends AinferValidatePerDirectoryTest { + + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public AinferIndexAjavaValidationTest(List testFiles) { + super( + testFiles, + IndexChecker.class, + "index", + "ainfer-index/annotated", + AinferIndexAjavaGenerationTest.class, + ajavaArgFromFiles(testFiles, "index"), + "-Awarns"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"ainfer-index/annotated/"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferNullnessAjavaGenerationTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferNullnessAjavaGenerationTest.java new file mode 100644 index 000000000000..e55b10de9cf7 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferNullnessAjavaGenerationTest.java @@ -0,0 +1,40 @@ +package org.checkerframework.checker.test.junit.ainferrunners; + +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.framework.test.AinferGeneratePerDirectoryTest; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * Tests whole-program inference with the aid of ajava files. This test is the first pass on the + * test data, which generates the ajava files. This specific test suite is designed to elicit + * problems with ajava parsing that only occur when the Nullness Checker is run. + * + *

    IMPORTANT: The errors captured in the tests located in tests/ainfer-index/ are not relevant. + * The meaning of this test class is to test if the generated ajava files are similar to the + * expected ones. The errors on .java files must be ignored. + */ +@Category(AinferNullnessAjavaGenerationTest.class) +public class AinferNullnessAjavaGenerationTest extends AinferGeneratePerDirectoryTest { + + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public AinferNullnessAjavaGenerationTest(List testFiles) { + super( + testFiles, + NullnessChecker.class, + "ainfer-nullness/non-annotated", + "-Ainfer=ajava", + "-Aajava=tests/ainfer-nullness/input-annotation-files/", + "-Awarns"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"ainfer-nullness/non-annotated"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferNullnessAjavaValidationTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferNullnessAjavaValidationTest.java new file mode 100644 index 000000000000..334acae4f71b --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferNullnessAjavaValidationTest.java @@ -0,0 +1,37 @@ +package org.checkerframework.checker.test.junit.ainferrunners; + +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.framework.test.AinferValidatePerDirectoryTest; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * Tests whole-program type inference with ajava files. This test is the second pass, which ensures + * that with the ajava files in place, the errors that those annotations remove are no longer + * issued. + */ +@Category(AinferNullnessAjavaGenerationTest.class) +public class AinferNullnessAjavaValidationTest extends AinferValidatePerDirectoryTest { + + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public AinferNullnessAjavaValidationTest(List testFiles) { + super( + testFiles, + NullnessChecker.class, + "nullness", + "ainfer-nullness/annotated", + AinferNullnessAjavaGenerationTest.class, + ajavaArgFromFiles(testFiles, "nullness"), + "-Awarns"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"ainfer-nullness/annotated/"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferNullnessJaifsGenerationTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferNullnessJaifsGenerationTest.java new file mode 100644 index 000000000000..090023bc1231 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferNullnessJaifsGenerationTest.java @@ -0,0 +1,37 @@ +package org.checkerframework.checker.test.junit.ainferrunners; + +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * Runs whole-program inference and inserts annotations into source code. + * + *

    IMPORTANT: The errors captured in the tests located in tests/ainfer-nullness/ are not + * relevant. The meaning of this test class is to test if the generated .jaif files are similar to + * the expected ones. The errors on .java files must be ignored. + */ +@Category(AinferNullnessJaifsGenerationTest.class) +public class AinferNullnessJaifsGenerationTest extends CheckerFrameworkPerDirectoryTest { + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public AinferNullnessJaifsGenerationTest(List testFiles) { + super( + testFiles, + NullnessChecker.class, + "nullness", + "-Ainfer=jaifs", + "-Awarns", + "-Aajava=tests/ainfer-nullness/input-annotation-files/"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"ainfer-nullness/non-annotated"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferNullnessJaifsValidationTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferNullnessJaifsValidationTest.java new file mode 100644 index 000000000000..d9613d66b667 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferNullnessJaifsValidationTest.java @@ -0,0 +1,39 @@ +package org.checkerframework.checker.test.junit.ainferrunners; + +import org.checkerframework.checker.nullness.NullnessChecker; +import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * Tests whole-program type inference with the aid of .jaif files. This test is the second pass, + * which ensures that with the annotations inserted, the errors are no longer issued. + */ +@Category(AinferNullnessJaifsGenerationTest.class) +public class AinferNullnessJaifsValidationTest extends CheckerFrameworkPerDirectoryTest { + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public AinferNullnessJaifsValidationTest(List testFiles) { + super(testFiles, NullnessChecker.class, "nullness"); + } + + @Override + public void run() { + // Only run if annotated files have been created. + // See ainferTest task. + if (!new File("tests/ainfer-nullness/annotated/").exists()) { + throw new RuntimeException( + AinferNullnessJaifsGenerationTest.class + " must be run before this test."); + } + super.run(); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"ainfer-nullness/annotated/"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferResourceLeakAjavaGenerationTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferResourceLeakAjavaGenerationTest.java new file mode 100644 index 000000000000..19760313b7ad --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferResourceLeakAjavaGenerationTest.java @@ -0,0 +1,41 @@ +package org.checkerframework.checker.test.junit.ainferrunners; + +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.framework.test.AinferGeneratePerDirectoryTest; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * Tests RLC-specific inference features with the aid of ajava files. This test is the first pass on + * the test data, which generates the ajava files. + * + *

    This does not run WPI; it just runs one round of type-checking with {@code -Ainfer} enabled. + * + *

    IMPORTANT: The errors captured in the tests located in tests/ainfer-resourceleak/ are not + * relevant. The meaning of this test class is to test if the generated ajava files are similar to + * the expected ones. The errors on .java files must be ignored. + */ +@Category(AinferResourceLeakAjavaGenerationTest.class) +public class AinferResourceLeakAjavaGenerationTest extends AinferGeneratePerDirectoryTest { + + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public AinferResourceLeakAjavaGenerationTest(List testFiles) { + super( + testFiles, + ResourceLeakChecker.class, + "ainfer-resourceleak/non-annotated", + "-Ainfer=ajava", + // "-Aajava=tests/ainfer-resourceleak/input-annotation-files/", + "-Awarns"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"ainfer-resourceleak/non-annotated"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferResourceLeakAjavaValidationTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferResourceLeakAjavaValidationTest.java new file mode 100644 index 000000000000..88739d56fd6a --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferResourceLeakAjavaValidationTest.java @@ -0,0 +1,37 @@ +package org.checkerframework.checker.test.junit.ainferrunners; + +import org.checkerframework.checker.resourceleak.ResourceLeakChecker; +import org.checkerframework.framework.test.AinferValidatePerDirectoryTest; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * Tests RLC-specific inference features with ajava files. This test is the second pass, which + * ensures that with the ajava files in place, the errors that those annotations remove are no + * longer issued. + */ +@Category(AinferResourceLeakAjavaGenerationTest.class) +public class AinferResourceLeakAjavaValidationTest extends AinferValidatePerDirectoryTest { + + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public AinferResourceLeakAjavaValidationTest(List testFiles) { + super( + testFiles, + ResourceLeakChecker.class, + "resourceleak", + "ainfer-resourceleak/annotated", + AinferResourceLeakAjavaGenerationTest.class, + ajavaArgFromFiles(testFiles, "resourceleak"), + "-Awarns"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"ainfer-resourceleak/annotated/"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerAjavaGenerationTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerAjavaGenerationTest.java new file mode 100644 index 000000000000..3807a5307568 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerAjavaGenerationTest.java @@ -0,0 +1,39 @@ +package org.checkerframework.checker.test.junit.ainferrunners; + +import org.checkerframework.checker.testchecker.ainfer.AinferTestChecker; +import org.checkerframework.framework.test.AinferGeneratePerDirectoryTest; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * Tests whole-program inference with the aid of ajava files. This test is the first pass on the + * test data, which generates the ajava files. + * + *

    IMPORTANT: The errors captured in the tests located in tests/ainfer-testchecker/ are not + * relevant. The meaning of this test class is to test if the generated ajava files are similar to + * the expected ones. The errors on .java files must be ignored. + */ +@Category(AinferTestCheckerAjavaGenerationTest.class) +public class AinferTestCheckerAjavaGenerationTest extends AinferGeneratePerDirectoryTest { + + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public AinferTestCheckerAjavaGenerationTest(List testFiles) { + super( + testFiles, + AinferTestChecker.class, + "ainfer-testchecker/non-annotated", + "-Ainfer=ajava", + "-Aajava=tests/ainfer-testchecker/input-annotation-files/ExistingPurityAnnotations-org.checkerframework.checker.testchecker.ainfer.AinferTestChecker.ajava", + "-Awarns"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"ainfer-testchecker/non-annotated"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerAjavaValidationTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerAjavaValidationTest.java new file mode 100644 index 000000000000..9cb1997fb73f --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerAjavaValidationTest.java @@ -0,0 +1,38 @@ +package org.checkerframework.checker.test.junit.ainferrunners; + +import org.checkerframework.checker.testchecker.ainfer.AinferTestChecker; +import org.checkerframework.framework.test.AinferValidatePerDirectoryTest; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * Tests whole-program type inference with ajava files. This test is the second pass, which ensures + * that with the ajava files in place, the errors that those annotations remove are no longer + * issued. + */ +@Category(AinferTestCheckerAjavaGenerationTest.class) +public class AinferTestCheckerAjavaValidationTest extends AinferValidatePerDirectoryTest { + + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public AinferTestCheckerAjavaValidationTest(List testFiles) { + super( + testFiles, + AinferTestChecker.class, + "testchecker", + "ainfer-testchecker/annotated", + AinferTestCheckerAjavaGenerationTest.class, + ajavaArgFromFiles(testFiles, "testchecker"), + "-AcheckPurityAnnotations", + "-Awarns"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"ainfer-testchecker/annotated/"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerJaifsGenerationTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerJaifsGenerationTest.java new file mode 100644 index 000000000000..eba528c96343 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerJaifsGenerationTest.java @@ -0,0 +1,41 @@ +package org.checkerframework.checker.test.junit.ainferrunners; + +import org.checkerframework.checker.testchecker.ainfer.AinferTestChecker; +import org.checkerframework.framework.test.AinferGeneratePerDirectoryTest; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * Runs whole-program inference and inserts annotations into source code. + * + *

    IMPORTANT: The errors captured in the tests located in tests/ainfer-testchecker/ are not + * relevant. The meaning of this test class is to test if the generated .jaif files are similar to + * the expected ones. The errors on .java files must be ignored. + */ +@Category(AinferTestCheckerJaifsGenerationTest.class) +public class AinferTestCheckerJaifsGenerationTest extends AinferGeneratePerDirectoryTest { + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public AinferTestCheckerJaifsGenerationTest(List testFiles) { + super( + testFiles, + AinferTestChecker.class, + "ainfer-testchecker/non-annotated", + "-Ainfer=jaifs", + // Use a stub file here, even though this is a JAIF test. This test can't pass + // without an external file that specifies that a method is pure, and there is no + // way to directly pass a JAIF file (in a real WPI run, the JAIF file's annotations + // would have been inserted into the source). + "-Astubs=tests/ainfer-testchecker/input-annotation-files/ExistingPurityAnnotations-org.checkerframework.checker.testchecker.ainfer.AinferTestChecker.astub", + "-Awarns"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"ainfer-testchecker/non-annotated"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerJaifsValidationTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerJaifsValidationTest.java new file mode 100644 index 000000000000..1fe779d8fadd --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerJaifsValidationTest.java @@ -0,0 +1,35 @@ +package org.checkerframework.checker.test.junit.ainferrunners; + +import org.checkerframework.checker.testchecker.ainfer.AinferTestChecker; +import org.checkerframework.framework.test.AinferValidatePerDirectoryTest; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * Tests whole-program type inference with the aid of .jaif files. This test is the second pass, + * which ensures that with the annotations inserted, the errors are no longer issued. + */ +@Category(AinferTestCheckerJaifsGenerationTest.class) +public class AinferTestCheckerJaifsValidationTest extends AinferValidatePerDirectoryTest { + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public AinferTestCheckerJaifsValidationTest(List testFiles) { + super( + testFiles, + AinferTestChecker.class, + "testchecker", + "ainfer-testchecker/non-annotated", + AinferTestCheckerJaifsGenerationTest.class, + "-Awarns", + "-AskipDefs=TestPure"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"ainfer-testchecker/annotated/"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerStubsGenerationTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerStubsGenerationTest.java new file mode 100644 index 000000000000..23f8becb43b6 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerStubsGenerationTest.java @@ -0,0 +1,39 @@ +package org.checkerframework.checker.test.junit.ainferrunners; + +import org.checkerframework.checker.testchecker.ainfer.AinferTestChecker; +import org.checkerframework.framework.test.AinferGeneratePerDirectoryTest; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * Tests whole-program inference with the aid of stub files. This test is the first pass on the test + * data, which generates the stubs. + * + *

    IMPORTANT: The errors captured in the tests located in tests/ainfer-testchecker/ are not + * relevant. The meaning of this test class is to test if the generated stub files are similar to + * the expected ones. The errors on .java files must be ignored. + */ +@Category(AinferTestCheckerStubsGenerationTest.class) +public class AinferTestCheckerStubsGenerationTest extends AinferGeneratePerDirectoryTest { + + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public AinferTestCheckerStubsGenerationTest(List testFiles) { + super( + testFiles, + AinferTestChecker.class, + "ainfer-testchecker/non-annotated", + "-Ainfer=stubs", + "-Astubs=tests/ainfer-testchecker/input-annotation-files/ExistingPurityAnnotations-org.checkerframework.checker.testchecker.ainfer.AinferTestChecker.astub", + "-Awarns"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"ainfer-testchecker/non-annotated"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerStubsValidationTest.java b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerStubsValidationTest.java new file mode 100644 index 000000000000..778b91e3c77b --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/AinferTestCheckerStubsValidationTest.java @@ -0,0 +1,39 @@ +package org.checkerframework.checker.test.junit.ainferrunners; + +import org.checkerframework.checker.testchecker.ainfer.AinferTestChecker; +import org.checkerframework.framework.test.AinferValidatePerDirectoryTest; +import org.junit.experimental.categories.Category; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.util.List; + +/** + * Tests whole-program type inference with stub files. This test is the second pass, which ensures + * that with the stubs in place, the errors that those annotations remove are no longer issued. + */ +@Category(AinferTestCheckerStubsGenerationTest.class) +public class AinferTestCheckerStubsValidationTest extends AinferValidatePerDirectoryTest { + + /** + * @param testFiles the files containing test code, which will be type-checked + */ + public AinferTestCheckerStubsValidationTest(List testFiles) { + super( + testFiles, + AinferTestChecker.class, + "testchecker", + "ainfer-testchecker/annotated", + AinferTestCheckerStubsGenerationTest.class, + astubsArgFromFiles(testFiles, "testchecker"), + // "-AstubDebug", + "-AmergeStubsWithSource", + "-Awarns", + "-AskipDefs=TestPure"); + } + + @Parameters + public static String[] getTestDirs() { + return new String[] {"ainfer-testchecker/annotated/"}; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/README.md b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/README.md new file mode 100644 index 000000000000..c8e05293ddb7 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/test/junit/ainferrunners/README.md @@ -0,0 +1,5 @@ +This package contains the test runners for testing the -Ainfer command-line +argument. +They are in a separate package so that they don't run by default; they should +only run when they're invoked directly by their corresponding build rules, which +are in checker/build.gradle (ainferTest). diff --git a/checker/src/test/java/testlib/NestedAggregateChecker.java b/checker/src/test/java/org/checkerframework/checker/testchecker/NestedAggregateChecker.java similarity index 95% rename from checker/src/test/java/testlib/NestedAggregateChecker.java rename to checker/src/test/java/org/checkerframework/checker/testchecker/NestedAggregateChecker.java index 39eb67244209..567af32fc0bd 100644 --- a/checker/src/test/java/testlib/NestedAggregateChecker.java +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/NestedAggregateChecker.java @@ -1,10 +1,8 @@ -package testlib; +package org.checkerframework.checker.testchecker; // Test case for Issue 343 // https://github.com/typetools/checker-framework/issues/343 -import java.util.ArrayList; -import java.util.Collection; import org.checkerframework.checker.fenum.FenumChecker; import org.checkerframework.checker.i18n.I18nChecker; import org.checkerframework.checker.nullness.NullnessChecker; @@ -12,6 +10,9 @@ import org.checkerframework.framework.source.AggregateChecker; import org.checkerframework.framework.source.SourceChecker; +import java.util.ArrayList; +import java.util.Collection; + public class NestedAggregateChecker extends AggregateChecker { @Override protected Collection> getSupportedCheckers() { diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/AinferTestAnnotatedTypeFactory.java b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/AinferTestAnnotatedTypeFactory.java new file mode 100644 index 000000000000..385f1d9b6fcc --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/AinferTestAnnotatedTypeFactory.java @@ -0,0 +1,300 @@ +package org.checkerframework.checker.testchecker.ainfer; + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferBottom; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferDefaultType; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferImplicitAnno; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferParent; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSiblingWithFields; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferTop; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferTreatAsSibling1; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.framework.qual.LiteralKind; +import org.checkerframework.framework.type.AnnotatedTypeFactory; +import org.checkerframework.framework.type.AnnotatedTypeMirror; +import org.checkerframework.framework.type.MostlyNoElementQualifierHierarchy; +import org.checkerframework.framework.type.QualifierHierarchy; +import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator; +import org.checkerframework.framework.type.treeannotator.LiteralTreeAnnotator; +import org.checkerframework.framework.type.treeannotator.PropagationTreeAnnotator; +import org.checkerframework.framework.type.treeannotator.TreeAnnotator; +import org.checkerframework.framework.util.QualifierKind; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationMirrorSet; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypeSystemError; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.util.Elements; + +/** + * AnnotatedTypeFactory to test whole-program inference using .jaif files. + * + *

    The used qualifier hierarchy is only intended for test purposes. It is: + * + *

    {@code
    + *                   AinferTop
    + *                      |
    + *               AinferDefaultType
    + *                      |
    + *                 AinferParent
    + *               /      |       \
    + *  AinferSibling AinferSibling2 AinferSiblingWithFields
    + *               \      |       /
    + *              AinferImplicitAnno
    + *                      |
    + *                 AinferBottom
    + *
    + * AinferTreatAsSibling1 : a declaration annotation
    + * AinferToIgnore : unused
    + * }
    + */ +public class AinferTestAnnotatedTypeFactory extends BaseAnnotatedTypeFactory { + + private final AnnotationMirror PARENT = + new AnnotationBuilder(processingEnv, AinferParent.class).build(); + private final AnnotationMirror BOTTOM = + new AnnotationBuilder(processingEnv, AinferBottom.class).build(); + private final AnnotationMirror IMPLICIT_ANNO = + new AnnotationBuilder(processingEnv, AinferImplicitAnno.class).build(); + + private final AnnotationMirror SIBLING1 = + new AnnotationBuilder(processingEnv, AinferSibling1.class).build(); + + private final AnnotationMirror TREAT_AS_SIBLING1 = + new AnnotationBuilder(processingEnv, AinferTreatAsSibling1.class).build(); + + /** The AinferSiblingWithFields.value field/element. */ + private final ExecutableElement siblingWithFieldsValueElement = + TreeUtils.getMethod(AinferSiblingWithFields.class, "value", 0, processingEnv); + + /** The AinferSiblingWithFields.value2 field/element. */ + private final ExecutableElement siblingWithFieldsValue2Element = + TreeUtils.getMethod(AinferSiblingWithFields.class, "value2", 0, processingEnv); + + /** + * Creates an AinferTestAnnotatedTypeFactory. + * + * @param checker the checker + */ + public AinferTestAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + // Support a declaration annotation that has the same meaning as @Sibling1, to test that the + // WPI feature allowing inference of declaration annotations works as intended. + addAliasedTypeAnnotation(AinferTreatAsSibling1.class, SIBLING1); + postInit(); + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new HashSet>( + Arrays.asList( + AinferParent.class, + AinferDefaultType.class, + AinferTop.class, + AinferSibling1.class, + AinferSibling2.class, + AinferBottom.class, + AinferSiblingWithFields.class, + AinferImplicitAnno.class)); + } + + @Override + public TreeAnnotator createTreeAnnotator() { + LiteralTreeAnnotator literalTreeAnnotator = new LiteralTreeAnnotator(this); + literalTreeAnnotator.addLiteralKind(LiteralKind.INT, BOTTOM); + literalTreeAnnotator.addStandardLiteralQualifiers(); + + return new ListTreeAnnotator( + new PropagationTreeAnnotator(this), + literalTreeAnnotator, + new AinferTestTreeAnnotator(this)); + } + + protected class AinferTestTreeAnnotator extends TreeAnnotator { + + /** + * Create a new AinferTestTreeAnnotator. + * + * @param atypeFactory the type factory + */ + protected AinferTestTreeAnnotator(AnnotatedTypeFactory atypeFactory) { + super(atypeFactory); + } + + /* NO-AFU + @Override + public Void visitClass(ClassTree classTree, AnnotatedTypeMirror type) { + WholeProgramInference wpi = atypeFactory.getWholeProgramInference(); + TypeElement classElt = TreeUtils.elementFromDeclaration(classTree); + if (wpi != null && classElt.getSimpleName().contentEquals("IShouldBeSibling1")) { + wpi.addClassDeclarationAnnotation(classElt, SIBLING1); + } + return super.visitClass(classTree, type); + } + + @Override + public Void visitVariable(VariableTree variableTree, AnnotatedTypeMirror type) { + WholeProgramInference wpi = atypeFactory.getWholeProgramInference(); + VariableElement varElt = TreeUtils.elementFromDeclaration(variableTree); + if (wpi != null && varElt.getSimpleName().contentEquals("iShouldBeTreatedAsSibling1")) { + wpi.addFieldDeclarationAnnotation(varElt, TREAT_AS_SIBLING1); + } + return super.visitVariable(variableTree, type); + } + + @Override + public Void visitMethod(MethodTree methodTree, AnnotatedTypeMirror type) { + WholeProgramInference wpi = atypeFactory.getWholeProgramInference(); + if (wpi != null) { + ExecutableElement execElt = TreeUtils.elementFromDeclaration(methodTree); + int numParams = execElt.getParameters().size(); + for (int i = 0; i < numParams; ++i) { + VariableElement param = execElt.getParameters().get(i); + if (param.getSimpleName().contentEquals("iShouldBeTreatedAsSibling1")) { + wpi.addDeclarationAnnotationToFormalParameter(execElt, i + 1, TREAT_AS_SIBLING1); + } + } + } + return super.visitMethod(methodTree, type); + } + end NO-AFU */ + } + + @Override + public void addComputedTypeAnnotations(Element elt, AnnotatedTypeMirror type) { + super.addComputedTypeAnnotations(elt, type); + // If an element has an @AinferTreatAsSibling1 annotation, replace its type with + // @AinferSibling1. + // This should be handled by the fact that @AinferTreatAsSibling1 and @AinferSibling1 are + // aliases, but by default the CF does not look for declaration annotations + // that are aliases of type annotations in annotation files. + // TODO: is that a bug in the CF or expected behavior? + if (getDeclAnnotation(elt, AinferTreatAsSibling1.class) != null) { + type.replaceAnnotation(SIBLING1); + } + } + + @Override + protected QualifierHierarchy createQualifierHierarchy() { + return new AinferTestQualifierHierarchy(this.getSupportedTypeQualifiers(), elements); + } + + /** + * Using a MultiGraphQualifierHierarchy to enable tests with Annotations that contain fields. + * + * @see AinferSiblingWithFields + */ + protected class AinferTestQualifierHierarchy extends MostlyNoElementQualifierHierarchy { + + private final QualifierKind SIBLING_WITH_FIELDS_KIND; + + /** + * Creates a AinferTestQualifierHierarchy from the given classes. + * + * @param qualifierClasses classes of annotations that are the qualifiers for this hierarchy + * @param elements element utils + */ + protected AinferTestQualifierHierarchy( + Collection> qualifierClasses, Elements elements) { + super(qualifierClasses, elements, AinferTestAnnotatedTypeFactory.this); + SIBLING_WITH_FIELDS_KIND = + getQualifierKind(AinferSiblingWithFields.class.getCanonicalName()); + } + + @Override + public AnnotationMirror getBottomAnnotation(AnnotationMirror start) { + return BOTTOM; + } + + @Override + public AnnotationMirrorSet getBottomAnnotations() { + return new AnnotationMirrorSet(BOTTOM); + } + + @Override + protected AnnotationMirror greatestLowerBoundWithElements( + AnnotationMirror a1, + QualifierKind qualifierKind1, + AnnotationMirror a2, + QualifierKind qualifierKind2, + QualifierKind glbKind) { + if (qualifierKind1 == qualifierKind2 && qualifierKind1 == SIBLING_WITH_FIELDS_KIND) { + if (isSubtypeWithElements(a1, qualifierKind1, a2, qualifierKind2)) { + return a1; + } else { + return IMPLICIT_ANNO; + } + } else if (qualifierKind1 == SIBLING_WITH_FIELDS_KIND) { + return a1; + } else if (qualifierKind2 == SIBLING_WITH_FIELDS_KIND) { + return a2; + } + throw new TypeSystemError("Unexpected qualifiers: %s %s", a1, a2); + } + + @Override + protected AnnotationMirror leastUpperBoundWithElements( + AnnotationMirror a1, + QualifierKind qualifierKind1, + AnnotationMirror a2, + QualifierKind qualifierKind2, + QualifierKind lubKind) { + if (qualifierKind1 == qualifierKind2 && qualifierKind1 == SIBLING_WITH_FIELDS_KIND) { + if (isSubtypeWithElements(a1, qualifierKind1, a2, qualifierKind2)) { + return a1; + } else { + return PARENT; + } + } else if (qualifierKind1 == SIBLING_WITH_FIELDS_KIND) { + return a1; + } else if (qualifierKind2 == SIBLING_WITH_FIELDS_KIND) { + return a2; + } + throw new TypeSystemError("Unexpected qualifiers: %s %s", a1, a2); + } + + @Override + protected boolean isSubtypeWithElements( + AnnotationMirror subAnno, + QualifierKind subKind, + AnnotationMirror superAnno, + QualifierKind superKind) { + if (subKind == SIBLING_WITH_FIELDS_KIND && superKind == SIBLING_WITH_FIELDS_KIND) { + List subVal1 = + AnnotationUtils.getElementValueArray( + subAnno, + siblingWithFieldsValueElement, + String.class, + Collections.emptyList()); + List supVal1 = + AnnotationUtils.getElementValueArray( + superAnno, + siblingWithFieldsValueElement, + String.class, + Collections.emptyList()); + String subVal2 = + AnnotationUtils.getElementValue( + subAnno, siblingWithFieldsValue2Element, String.class, ""); + String supVal2 = + AnnotationUtils.getElementValue( + superAnno, siblingWithFieldsValue2Element, String.class, ""); + return subVal1.equals(supVal1) && subVal2.equals(supVal2); + } + throw new TypeSystemError("Unexpected qualifiers: %s %s", subAnno, superAnno); + } + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/AinferTestChecker.java b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/AinferTestChecker.java new file mode 100644 index 000000000000..1149f3a65c1b --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/AinferTestChecker.java @@ -0,0 +1,27 @@ +package org.checkerframework.checker.testchecker.ainfer; + +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.common.value.ValueChecker; + +import java.util.Set; + +/** + * Checker for a simple type system to test whole-program inference. Uses the Value Checker as a + * subchecker to ensure that generated files contain annotations both from this checker and from the + * Value Checker, to make certain that subchecker outputs aren't overwritten. + */ +public class AinferTestChecker extends BaseTypeChecker { + + @Override + protected BaseTypeVisitor createSourceVisitor() { + return new AinferTestVisitor(this); + } + + @Override + protected Set> getImmediateSubcheckerClasses() { + Set> checkers = super.getImmediateSubcheckerClasses(); + checkers.add(ValueChecker.class); + return checkers; + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/AinferTestVisitor.java b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/AinferTestVisitor.java new file mode 100644 index 000000000000..af24c61b1f81 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/AinferTestVisitor.java @@ -0,0 +1,41 @@ +package org.checkerframework.checker.testchecker.ainfer; + +import com.sun.source.tree.AnnotationTree; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeInfo; + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferDefaultType; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; + +/** Visitor for a simple type system to test whole-program inference using .jaif files. */ +public class AinferTestVisitor extends BaseTypeVisitor { + + public AinferTestVisitor(BaseTypeChecker checker) { + super(checker); + } + + @Override + protected AinferTestAnnotatedTypeFactory createTypeFactory() { + return new AinferTestAnnotatedTypeFactory(checker); + } + + @Override + public Void visitAnnotation(AnnotationTree tree, Void p) { + Element anno = TreeInfo.symbol((JCTree) tree.getAnnotationType()); + if (anno.toString().equals(AinferDefaultType.class.getName())) { + checker.reportError(tree, "annotation.not.allowed.in.src", anno.toString()); + } + return super.visitAnnotation(tree, p); + } + + @Override + protected void checkConstructorResult( + AnnotatedExecutableType constructorType, ExecutableElement constructorElement) { + // Skip this check. + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferBottom.java b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferBottom.java new file mode 100644 index 000000000000..17af7931bc53 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferBottom.java @@ -0,0 +1,20 @@ +package org.checkerframework.checker.testchecker.ainfer.qual; + +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TargetLocations; +import org.checkerframework.framework.qual.TypeUseLocation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Toy type system for testing field inference. + * + * @see AinferSibling1, AinferSibling2, AinferParent + */ +@SubtypeOf({AinferImplicitAnno.class}) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@TargetLocations({TypeUseLocation.LOWER_BOUND, TypeUseLocation.UPPER_BOUND}) +@DefaultFor(TypeUseLocation.LOWER_BOUND) +public @interface AinferBottom {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferDefaultType.java b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferDefaultType.java new file mode 100644 index 000000000000..ac18c134ec7d --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferDefaultType.java @@ -0,0 +1,21 @@ +package org.checkerframework.checker.testchecker.ainfer.qual; + +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * AinferDefaultType is used to test the relaxInference option. Toy type system for testing field + * inference. This annotation cannot be used in source code. + * + * @see AinferSibling1 + * @see AinferSibling2 + * @see AinferParent + * @see AinferTop + */ +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@SubtypeOf({AinferTop.class}) +@DefaultQualifierInHierarchy +public @interface AinferDefaultType {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferImplicitAnno.java b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferImplicitAnno.java new file mode 100644 index 000000000000..e58726d9770b --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferImplicitAnno.java @@ -0,0 +1,19 @@ +package org.checkerframework.checker.testchecker.ainfer.qual; + +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.IgnoreInWholeProgramInference; +import org.checkerframework.framework.qual.SubtypeOf; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Toy type system for testing field inference. + * + * @see AinferSibling1, AinferSibling2, AinferParent + */ +@SubtypeOf({AinferSibling1.class, AinferSibling2.class, AinferSiblingWithFields.class}) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +@IgnoreInWholeProgramInference +@DefaultFor(types = java.lang.StringBuffer.class) +public @interface AinferImplicitAnno {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferParent.java b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferParent.java new file mode 100644 index 000000000000..b3b44ed3a196 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferParent.java @@ -0,0 +1,15 @@ +package org.checkerframework.checker.testchecker.ainfer.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Toy type system for testing field inference. + * + * @see AinferSibling1, AinferSibling2, AinferParent + */ +@SubtypeOf({AinferDefaultType.class}) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface AinferParent {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferSibling1.java b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferSibling1.java new file mode 100644 index 000000000000..ae40f248b0d1 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferSibling1.java @@ -0,0 +1,15 @@ +package org.checkerframework.checker.testchecker.ainfer.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Toy type system for testing field inference. + * + * @see AinferSibling1, AinferSibling2, AinferParent + */ +@SubtypeOf(AinferParent.class) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface AinferSibling1 {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferSibling2.java b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferSibling2.java new file mode 100644 index 000000000000..bf7072755c74 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferSibling2.java @@ -0,0 +1,15 @@ +package org.checkerframework.checker.testchecker.ainfer.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Toy type system for testing field inference. + * + * @see AinferSibling1, AinferSibling2, AinferParent + */ +@SubtypeOf(AinferParent.class) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface AinferSibling2 {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferSiblingWithFields.java b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferSiblingWithFields.java new file mode 100644 index 000000000000..667ba02d2ab6 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferSiblingWithFields.java @@ -0,0 +1,19 @@ +package org.checkerframework.checker.testchecker.ainfer.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Toy type system for testing field inference. + * + * @see AinferSibling1, AinferSibling2, AinferParent + */ +@SubtypeOf(AinferParent.class) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface AinferSiblingWithFields { + String[] value() default {}; + + String value2() default ""; +} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferToIgnore.java b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferToIgnore.java new file mode 100644 index 000000000000..d2e5c3d45132 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferToIgnore.java @@ -0,0 +1,11 @@ +package org.checkerframework.checker.testchecker.ainfer.qual; + +import org.checkerframework.framework.qual.IgnoreInWholeProgramInference; + +/** + * Toy type system for testing field inference. + * + * @see AinferSibling1, AinferSibling2, AinferParent + */ +@IgnoreInWholeProgramInference +public @interface AinferToIgnore {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferTop.java b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferTop.java new file mode 100644 index 000000000000..68e8ce54ab66 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferTop.java @@ -0,0 +1,15 @@ +package org.checkerframework.checker.testchecker.ainfer.qual; + +import org.checkerframework.framework.qual.SubtypeOf; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Toy type system for testing field inference. + * + * @see AinferSibling1, AinferSibling2, AinferParent + */ +@SubtypeOf({}) +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface AinferTop {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferTreatAsSibling1.java b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferTreatAsSibling1.java new file mode 100644 index 000000000000..9cbb23f2857b --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/ainfer/qual/AinferTreatAsSibling1.java @@ -0,0 +1,15 @@ +package org.checkerframework.checker.testchecker.ainfer.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A declaration annotation used to test that the API for inferring declaration annotations on + * parameters works properly. The presence of this annotation indicates that the checker should + * treat the annotated element as if it were annotated as {@link AinferSibling1}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER, ElementType.FIELD}) +public @interface AinferTreatAsSibling1 {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseChecker.java b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseChecker.java new file mode 100644 index 000000000000..6d603c84e074 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseChecker.java @@ -0,0 +1,9 @@ +package org.checkerframework.checker.testchecker.disbaruse; + +import org.checkerframework.common.basetype.BaseTypeChecker; + +/** + * A checker that issues a "disbar.use" error at any use of fields, methods or parameters whose type + * is {@code @DisbarUse}. + */ +public class DisbarUseChecker extends BaseTypeChecker {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseTypeFactory.java b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseTypeFactory.java new file mode 100644 index 000000000000..12618d407801 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseTypeFactory.java @@ -0,0 +1,29 @@ +package org.checkerframework.checker.testchecker.disbaruse; + +import org.checkerframework.checker.testchecker.disbaruse.qual.DisbarUseBottom; +import org.checkerframework.checker.testchecker.disbaruse.qual.DisbarUseTop; +import org.checkerframework.common.basetype.BaseAnnotatedTypeFactory; +import org.checkerframework.common.basetype.BaseTypeChecker; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +/** The type factory for forbidding use of the DisbarUse type. */ +public class DisbarUseTypeFactory extends BaseAnnotatedTypeFactory { + /** + * Creates a new DisbarUseTypeFactory. + * + * @param checker the checker + */ + public DisbarUseTypeFactory(BaseTypeChecker checker) { + super(checker); + postInit(); + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new LinkedHashSet<>(Arrays.asList(DisbarUseTop.class, DisbarUseBottom.class)); + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseVisitor.java b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseVisitor.java new file mode 100644 index 000000000000..d56cd25f9f0b --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/DisbarUseVisitor.java @@ -0,0 +1,73 @@ +package org.checkerframework.checker.testchecker.disbaruse; + +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.NewClassTree; + +import org.checkerframework.checker.testchecker.disbaruse.qual.DisbarUse; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.javacutil.TreeUtils; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; + +public class DisbarUseVisitor extends BaseTypeVisitor { + public DisbarUseVisitor(BaseTypeChecker checker) { + super(checker); + } + + protected DisbarUseVisitor(BaseTypeChecker checker, DisbarUseTypeFactory typeFactory) { + super(checker, typeFactory); + } + + @Override + protected DisbarUseTypeFactory createTypeFactory() { + return new DisbarUseTypeFactory(checker); + } + + @Override + public Void visitMethodInvocation(MethodInvocationTree tree, Void p) { + ExecutableElement methodElt = TreeUtils.elementFromUse(tree); + if (methodElt != null + && atypeFactory.getDeclAnnotation(methodElt, DisbarUse.class) != null) { + checker.reportError(tree, "disbar.use"); + } + return super.visitMethodInvocation(tree, p); + } + + @Override + public Void visitNewClass(NewClassTree tree, Void p) { + ExecutableElement consElt = TreeUtils.elementFromUse(tree); + if (consElt != null && atypeFactory.getDeclAnnotation(consElt, DisbarUse.class) != null) { + checker.reportError(tree, "disbar.use"); + } + return super.visitNewClass(tree, p); + } + + @Override + public Void visitIdentifier(IdentifierTree tree, Void p) { + MemberSelectTree enclosingMemberSel = enclosingMemberSelect(); + ExpressionTree[] expressionTrees = + enclosingMemberSel == null + ? new ExpressionTree[] {tree} + : new ExpressionTree[] {enclosingMemberSel, tree}; + + for (ExpressionTree memberSel : expressionTrees) { + Element elem = TreeUtils.elementFromUse(memberSel); + + // We only issue errors for variables that are fields or parameters: + if (elem != null + && (elem.getKind().isField() || elem.getKind() == ElementKind.PARAMETER)) { + if (atypeFactory.getDeclAnnotation(elem, DisbarUse.class) != null) { + checker.reportError(memberSel, "disbar.use"); + } + } + } + + return super.visitIdentifier(tree, p); + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/messages.properties b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/messages.properties new file mode 100644 index 000000000000..7cad93edb500 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/messages.properties @@ -0,0 +1 @@ +disbar.use=Use of this element is disbarred diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUse.java b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUse.java new file mode 100644 index 000000000000..fa9e86131a71 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUse.java @@ -0,0 +1,8 @@ +package org.checkerframework.checker.testchecker.disbaruse.qual; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** The Disbar Use Checker issues a warning when an expression of this type is used. */ +@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.PARAMETER}) +public @interface DisbarUse {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUseBottom.java b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUseBottom.java new file mode 100644 index 000000000000..0bd267c75047 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUseBottom.java @@ -0,0 +1,13 @@ +package org.checkerframework.checker.testchecker.disbaruse.qual; + +import org.checkerframework.framework.qual.DefaultFor; +import org.checkerframework.framework.qual.SubtypeOf; +import org.checkerframework.framework.qual.TypeUseLocation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@DefaultFor(TypeUseLocation.LOWER_BOUND) +@SubtypeOf(DisbarUseTop.class) +@Target({ElementType.TYPE_USE}) +public @interface DisbarUseBottom {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUseTop.java b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUseTop.java new file mode 100644 index 000000000000..36e8678e16c5 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/disbaruse/qual/DisbarUseTop.java @@ -0,0 +1,12 @@ +package org.checkerframework.checker.testchecker.disbaruse.qual; + +import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; +import org.checkerframework.framework.qual.SubtypeOf; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE_USE}) +@SubtypeOf({}) +@DefaultQualifierInHierarchy +public @interface DisbarUseTop {} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/lib/Issue3105Fields.java b/checker/src/test/java/org/checkerframework/checker/testchecker/lib/Issue3105Fields.java new file mode 100644 index 000000000000..1ea34c06116f --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/lib/Issue3105Fields.java @@ -0,0 +1,13 @@ +// This class must be in a package. If a class is in the default package, clients cannot static +// import the class, or static import its members. +package org.checkerframework.framework.testchecker.lib; + +public class Issue3105Fields { + public static final String FIELD1 = "foo"; + + public static final String FIELD2; + + static { + FIELD2 = "bar"; + } +} diff --git a/framework/src/test/java/testlib/lib/README b/checker/src/test/java/org/checkerframework/checker/testchecker/lib/README similarity index 100% rename from framework/src/test/java/testlib/lib/README rename to checker/src/test/java/org/checkerframework/checker/testchecker/lib/README diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/lib/UncheckedByteCode.java b/checker/src/test/java/org/checkerframework/checker/testchecker/lib/UncheckedByteCode.java new file mode 100644 index 000000000000..1e899bafa14c --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/lib/UncheckedByteCode.java @@ -0,0 +1,42 @@ +package org.checkerframework.framework.testchecker.lib; + +public class UncheckedByteCode { + public CT classTypeVariableField; + public static Object nonFinalPublicField; + + public CT getCT() { + return classTypeVariableField; + } + + public T identity(T t) { + return t; + } + + public int getInt(int i) { + return i; + } + + public Integer getInteger(Integer i) { + return i; + } + + public String getString(CharSequence charSequence) { + return ""; + } + + public I getI(I i) { + return i; + } + + public Object getObject(Object o) { + return o; + } + + public static void unboundedWildcardParam(UncheckedByteCode param) {} + + public static void upperboundedWildcardParam(UncheckedByteCode param) {} + + public static void lowerboundedWildcardParam(UncheckedByteCode param) {} + + public static void methodWithTypeVarBoundedByNumber(F param) {} +} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/lib/VarArgMethods.java b/checker/src/test/java/org/checkerframework/checker/testchecker/lib/VarArgMethods.java new file mode 100644 index 000000000000..02b94e604fee --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/lib/VarArgMethods.java @@ -0,0 +1,33 @@ +package org.checkerframework.framework.testchecker.lib; + +import org.checkerframework.common.value.qual.StaticallyExecutable; + +/** Used by framework/tests/value/VarArgRe.java */ +public class VarArgMethods { + @StaticallyExecutable + public static int test0(Object... objects) { + if (objects == null) { + return -1; + } else { + return objects.length; + } + } + + @StaticallyExecutable + public static int test1(String s, Object... objects) { + if (objects == null) { + return -1; + } else { + return objects.length; + } + } + + @StaticallyExecutable + public static int test2(String s, String s2, Object... objects) { + if (objects == null) { + return -1; + } else { + return objects.length; + } + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/lubglb/FormatterLubGlbChecker.java b/checker/src/test/java/org/checkerframework/checker/testchecker/lubglb/FormatterLubGlbChecker.java new file mode 100644 index 000000000000..fb1a74a4cf73 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/lubglb/FormatterLubGlbChecker.java @@ -0,0 +1,543 @@ +package org.checkerframework.checker.testchecker.lubglb; + +// Test case for issues 691 and 756. +// https://github.com/typetools/checker-framework/issues/691 +// https://github.com/typetools/checker-framework/issues/756 + +import org.checkerframework.checker.formatter.FormatterAnnotatedTypeFactory; +import org.checkerframework.checker.formatter.FormatterChecker; +import org.checkerframework.checker.formatter.FormatterTreeUtil; +import org.checkerframework.checker.formatter.FormatterVisitor; +import org.checkerframework.checker.formatter.qual.ConversionCategory; +import org.checkerframework.checker.formatter.qual.Format; +import org.checkerframework.checker.formatter.qual.FormatBottom; +import org.checkerframework.checker.formatter.qual.InvalidFormat; +import org.checkerframework.checker.formatter.qual.UnknownFormat; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.type.QualifierHierarchy; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationUtils; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.util.Elements; + +/** + * This class tests the implementation of GLB computation in the Formatter Checker, but it does not + * test for the crash described in issue 691. That is done by tests/all-systems/Issue691.java. It + * also tests the implementation of LUB computation in the Formatter Checker. + */ +public class FormatterLubGlbChecker extends FormatterChecker { + + @Override + protected BaseTypeVisitor createSourceVisitor() { + return new FormatterVisitor(this) { + @Override + protected FormatterLubGlbAnnotatedTypeFactory createTypeFactory() { + return new FormatterLubGlbAnnotatedTypeFactory(checker); + } + }; + } + + /** FormatterLubGlbAnnotatedTypeFactory. */ + private static class FormatterLubGlbAnnotatedTypeFactory extends FormatterAnnotatedTypeFactory { + + /** + * Constructor. + * + * @param checker checker + */ + public FormatterLubGlbAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + postInit(); + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new HashSet<>( + Arrays.asList( + FormatBottom.class, + Format.class, + InvalidFormat.class, + UnknownFormat.class)); + } + } + + /** + * Throws an exception if glb(arg1, arg2) != result. + * + * @param arg1 the first argument + * @param arg2 the second argument + * @param expected the expected result + */ + private void glbAssert( + AnnotationMirror arg1, AnnotationMirror arg2, AnnotationMirror expected) { + QualifierHierarchy qualHierarchy = + ((BaseTypeVisitor) visitor).getTypeFactory().getQualifierHierarchy(); + AnnotationMirror result = qualHierarchy.greatestLowerBoundQualifiersOnly(arg1, arg2); + if (!AnnotationUtils.areSame(expected, result)) { + throw new AssertionError( + String.format( + "GLB of %s and %s should be %s, but is %s", + arg1, arg2, expected, result)); + } + } + + /** + * Throws an exception if lub(arg1, arg2) != result. + * + * @param arg1 the first argument + * @param arg2 the second argument + * @param expected the expected result + */ + private void lubAssert( + AnnotationMirror arg1, AnnotationMirror arg2, AnnotationMirror expected) { + QualifierHierarchy qualHierarchy = + ((BaseTypeVisitor) visitor).getTypeFactory().getQualifierHierarchy(); + AnnotationMirror result = qualHierarchy.leastUpperBoundQualifiersOnly(arg1, arg2); + if (!AnnotationUtils.areSame(expected, result)) { + throw new AssertionError( + String.format( + "LUB of %s and %s should be %s, but is %s", + arg1, arg2, expected, result)); + } + } + + @SuppressWarnings("checkstyle:localvariablename") + @Override + public void initChecker() { + super.initChecker(); + FormatterTreeUtil treeUtil = new FormatterTreeUtil(this); + + Elements elements = getElementUtils(); + AnnotationMirror UNKNOWNFORMAT = AnnotationBuilder.fromClass(elements, UnknownFormat.class); + AnnotationMirror FORMAT = + AnnotationBuilder.fromClass( + elements, + Format.class, + AnnotationBuilder.elementNamesValues("value", new ConversionCategory[0])); + AnnotationMirror INVALIDFORMAT = + AnnotationBuilder.fromClass( + elements, + InvalidFormat.class, + AnnotationBuilder.elementNamesValues("value", "dummy")); + AnnotationMirror FORMATBOTTOM = AnnotationBuilder.fromClass(elements, FormatBottom.class); + + AnnotationBuilder builder = new AnnotationBuilder(processingEnv, InvalidFormat.class); + builder.setValue("value", "Message"); + AnnotationMirror invalidFormatWithMessage = builder.build(); + + builder = new AnnotationBuilder(processingEnv, InvalidFormat.class); + builder.setValue("value", "Message2"); + AnnotationMirror invalidFormatWithMessage2 = builder.build(); + + builder = new AnnotationBuilder(processingEnv, InvalidFormat.class); + builder.setValue("value", "(\"Message\" or \"Message2\")"); + AnnotationMirror invalidFormatWithMessagesOred = builder.build(); + + builder = new AnnotationBuilder(processingEnv, InvalidFormat.class); + builder.setValue("value", "(\"Message\" and \"Message2\")"); + AnnotationMirror invalidFormatWithMessagesAnded = builder.build(); + + ConversionCategory[] cc = new ConversionCategory[1]; + + cc[0] = ConversionCategory.UNUSED; + AnnotationMirror formatUnusedAnno = treeUtil.categoriesToFormatAnnotation(cc); + cc[0] = ConversionCategory.GENERAL; + AnnotationMirror formatGeneralAnno = treeUtil.categoriesToFormatAnnotation(cc); + cc[0] = ConversionCategory.CHAR; + AnnotationMirror formatCharAnno = treeUtil.categoriesToFormatAnnotation(cc); + cc[0] = ConversionCategory.INT; + AnnotationMirror formatIntAnno = treeUtil.categoriesToFormatAnnotation(cc); + cc[0] = ConversionCategory.TIME; + AnnotationMirror formatTimeAnno = treeUtil.categoriesToFormatAnnotation(cc); + cc[0] = ConversionCategory.FLOAT; + AnnotationMirror formatFloatAnno = treeUtil.categoriesToFormatAnnotation(cc); + cc[0] = ConversionCategory.CHAR_AND_INT; + AnnotationMirror formatCharAndIntAnno = treeUtil.categoriesToFormatAnnotation(cc); + cc[0] = ConversionCategory.INT_AND_TIME; + AnnotationMirror formatIntAndTimeAnno = treeUtil.categoriesToFormatAnnotation(cc); + cc[0] = ConversionCategory.NULL; + AnnotationMirror formatNullAnno = treeUtil.categoriesToFormatAnnotation(cc); + + // ** GLB tests ** + + glbAssert(formatCharAndIntAnno, formatIntAndTimeAnno, formatIntAnno); + + // GLB of UNUSED and others + + glbAssert(formatUnusedAnno, formatUnusedAnno, formatUnusedAnno); + glbAssert(formatUnusedAnno, formatGeneralAnno, formatUnusedAnno); + glbAssert(formatUnusedAnno, formatCharAnno, formatUnusedAnno); + glbAssert(formatUnusedAnno, formatIntAnno, formatUnusedAnno); + glbAssert(formatUnusedAnno, formatTimeAnno, formatUnusedAnno); + glbAssert(formatUnusedAnno, formatFloatAnno, formatUnusedAnno); + glbAssert(formatUnusedAnno, formatCharAndIntAnno, formatUnusedAnno); + glbAssert(formatUnusedAnno, formatIntAndTimeAnno, formatUnusedAnno); + glbAssert(formatUnusedAnno, formatNullAnno, formatUnusedAnno); + + // GLB of GENERAL and others + + glbAssert(formatGeneralAnno, formatUnusedAnno, formatUnusedAnno); + glbAssert(formatGeneralAnno, formatGeneralAnno, formatGeneralAnno); + glbAssert(formatGeneralAnno, formatCharAnno, formatGeneralAnno); + glbAssert(formatGeneralAnno, formatIntAnno, formatGeneralAnno); + glbAssert(formatGeneralAnno, formatTimeAnno, formatGeneralAnno); + glbAssert(formatGeneralAnno, formatFloatAnno, formatGeneralAnno); + glbAssert(formatGeneralAnno, formatCharAndIntAnno, formatGeneralAnno); + glbAssert(formatGeneralAnno, formatIntAndTimeAnno, formatGeneralAnno); + glbAssert(formatGeneralAnno, formatNullAnno, formatGeneralAnno); + + // GLB of CHAR and others + + glbAssert(formatCharAnno, formatUnusedAnno, formatUnusedAnno); + glbAssert(formatCharAnno, formatGeneralAnno, formatGeneralAnno); + glbAssert(formatCharAnno, formatCharAnno, formatCharAnno); + glbAssert(formatCharAnno, formatIntAnno, formatGeneralAnno); + glbAssert(formatCharAnno, formatTimeAnno, formatGeneralAnno); + glbAssert(formatCharAnno, formatFloatAnno, formatGeneralAnno); + glbAssert(formatCharAnno, formatCharAndIntAnno, formatCharAnno); + glbAssert(formatCharAnno, formatIntAndTimeAnno, formatGeneralAnno); + glbAssert(formatCharAnno, formatNullAnno, formatCharAnno); + + // GLB of INT and others + + glbAssert(formatIntAnno, formatUnusedAnno, formatUnusedAnno); + glbAssert(formatIntAnno, formatGeneralAnno, formatGeneralAnno); + glbAssert(formatIntAnno, formatCharAnno, formatGeneralAnno); + glbAssert(formatIntAnno, formatIntAnno, formatIntAnno); + glbAssert(formatIntAnno, formatTimeAnno, formatGeneralAnno); + glbAssert(formatIntAnno, formatFloatAnno, formatGeneralAnno); + glbAssert(formatIntAnno, formatCharAndIntAnno, formatIntAnno); + glbAssert(formatIntAnno, formatIntAndTimeAnno, formatIntAnno); + glbAssert(formatIntAnno, formatNullAnno, formatIntAnno); + + // GLB of TIME and others + + glbAssert(formatTimeAnno, formatUnusedAnno, formatUnusedAnno); + glbAssert(formatTimeAnno, formatGeneralAnno, formatGeneralAnno); + glbAssert(formatTimeAnno, formatCharAnno, formatGeneralAnno); + glbAssert(formatTimeAnno, formatIntAnno, formatGeneralAnno); + glbAssert(formatTimeAnno, formatTimeAnno, formatTimeAnno); + glbAssert(formatTimeAnno, formatFloatAnno, formatGeneralAnno); + glbAssert(formatTimeAnno, formatCharAndIntAnno, formatGeneralAnno); + glbAssert(formatTimeAnno, formatIntAndTimeAnno, formatTimeAnno); + glbAssert(formatTimeAnno, formatNullAnno, formatTimeAnno); + + // GLB of FLOAT and others + + glbAssert(formatFloatAnno, formatUnusedAnno, formatUnusedAnno); + glbAssert(formatFloatAnno, formatGeneralAnno, formatGeneralAnno); + glbAssert(formatFloatAnno, formatCharAnno, formatGeneralAnno); + glbAssert(formatFloatAnno, formatIntAnno, formatGeneralAnno); + glbAssert(formatFloatAnno, formatTimeAnno, formatGeneralAnno); + glbAssert(formatFloatAnno, formatFloatAnno, formatFloatAnno); + glbAssert(formatFloatAnno, formatCharAndIntAnno, formatGeneralAnno); + glbAssert(formatFloatAnno, formatIntAndTimeAnno, formatGeneralAnno); + glbAssert(formatFloatAnno, formatNullAnno, formatFloatAnno); + + // GLB of CHAR_AND_INT and others + + glbAssert(formatCharAndIntAnno, formatUnusedAnno, formatUnusedAnno); + glbAssert(formatCharAndIntAnno, formatGeneralAnno, formatGeneralAnno); + glbAssert(formatCharAndIntAnno, formatCharAnno, formatCharAnno); + glbAssert(formatCharAndIntAnno, formatIntAnno, formatIntAnno); + glbAssert(formatCharAndIntAnno, formatTimeAnno, formatGeneralAnno); + glbAssert(formatCharAndIntAnno, formatFloatAnno, formatGeneralAnno); + glbAssert(formatCharAndIntAnno, formatCharAndIntAnno, formatCharAndIntAnno); + glbAssert(formatCharAndIntAnno, formatIntAndTimeAnno, formatIntAnno); + glbAssert(formatCharAndIntAnno, formatNullAnno, formatCharAndIntAnno); + + // GLB of INT_AND_TIME and others + + glbAssert(formatIntAndTimeAnno, formatUnusedAnno, formatUnusedAnno); + glbAssert(formatIntAndTimeAnno, formatGeneralAnno, formatGeneralAnno); + glbAssert(formatIntAndTimeAnno, formatCharAnno, formatGeneralAnno); + glbAssert(formatIntAndTimeAnno, formatIntAnno, formatIntAnno); + glbAssert(formatIntAndTimeAnno, formatTimeAnno, formatTimeAnno); + glbAssert(formatIntAndTimeAnno, formatFloatAnno, formatGeneralAnno); + glbAssert(formatIntAndTimeAnno, formatCharAndIntAnno, formatIntAnno); + glbAssert(formatIntAndTimeAnno, formatIntAndTimeAnno, formatIntAndTimeAnno); + glbAssert(formatIntAndTimeAnno, formatNullAnno, formatIntAndTimeAnno); + + // GLB of NULL and others + + glbAssert(formatNullAnno, formatUnusedAnno, formatUnusedAnno); + glbAssert(formatNullAnno, formatGeneralAnno, formatGeneralAnno); + glbAssert(formatNullAnno, formatCharAnno, formatCharAnno); + glbAssert(formatNullAnno, formatIntAnno, formatIntAnno); + glbAssert(formatNullAnno, formatTimeAnno, formatTimeAnno); + glbAssert(formatNullAnno, formatFloatAnno, formatFloatAnno); + glbAssert(formatNullAnno, formatCharAndIntAnno, formatCharAndIntAnno); + glbAssert(formatNullAnno, formatIntAndTimeAnno, formatIntAndTimeAnno); + glbAssert(formatNullAnno, formatNullAnno, formatNullAnno); + + // Now test with two ConversionCategory at a time: + + ConversionCategory[] cc2 = new ConversionCategory[2]; + + cc2[0] = ConversionCategory.CHAR_AND_INT; + cc2[1] = ConversionCategory.FLOAT; + AnnotationMirror formatTwoConvCat1 = treeUtil.categoriesToFormatAnnotation(cc2); + cc2[0] = ConversionCategory.INT; + cc2[1] = ConversionCategory.CHAR; + AnnotationMirror formatTwoConvCat2 = treeUtil.categoriesToFormatAnnotation(cc2); + cc2[0] = ConversionCategory.INT; + cc2[1] = ConversionCategory.GENERAL; + AnnotationMirror formatTwoConvCat3 = treeUtil.categoriesToFormatAnnotation(cc2); + + glbAssert(formatTwoConvCat1, formatTwoConvCat2, formatTwoConvCat3); + + // Test that the GLB of two ConversionCategory arrays of different sizes is an array of the + // smallest size of the two: + + glbAssert(formatGeneralAnno, formatTwoConvCat1, formatGeneralAnno); + glbAssert(formatTwoConvCat2, formatNullAnno, formatIntAnno); + + // GLB of @UnknownFormat and others + + glbAssert(UNKNOWNFORMAT, UNKNOWNFORMAT, UNKNOWNFORMAT); + glbAssert(UNKNOWNFORMAT, FORMAT, FORMAT); + glbAssert(UNKNOWNFORMAT, formatUnusedAnno, formatUnusedAnno); + glbAssert(UNKNOWNFORMAT, INVALIDFORMAT, INVALIDFORMAT); + glbAssert(UNKNOWNFORMAT, invalidFormatWithMessage, invalidFormatWithMessage); + glbAssert(UNKNOWNFORMAT, FORMATBOTTOM, FORMATBOTTOM); + + // GLB of @Format(null) and others + + glbAssert(FORMAT, UNKNOWNFORMAT, FORMAT); + // Computing the GLB of @Format(null) and @Format(null) should never occur in practice; + // skipping this case as it causes an expected crash. + // Computing the GLB of @Format(null) and @Format with a value should never occur in + // practice; skipping this case as it causes an expected crash. + glbAssert(FORMAT, INVALIDFORMAT, FORMATBOTTOM); + glbAssert(FORMAT, invalidFormatWithMessage, FORMATBOTTOM); + glbAssert(FORMAT, FORMATBOTTOM, FORMATBOTTOM); + + // GLB of @Format(UNUSED) and others + + glbAssert(formatUnusedAnno, UNKNOWNFORMAT, formatUnusedAnno); + // Computing the GLB of @Format with a value and @Format(null) should never occur in + // practice; skipping this case as it causes an expected crash. + glbAssert(formatUnusedAnno, formatUnusedAnno, formatUnusedAnno); + glbAssert(formatUnusedAnno, INVALIDFORMAT, FORMATBOTTOM); + glbAssert(formatUnusedAnno, invalidFormatWithMessage, FORMATBOTTOM); + glbAssert(formatUnusedAnno, FORMATBOTTOM, FORMATBOTTOM); + + // GLB of @InvalidFormat(null) and others + + glbAssert(INVALIDFORMAT, UNKNOWNFORMAT, INVALIDFORMAT); + glbAssert(INVALIDFORMAT, FORMAT, FORMATBOTTOM); + glbAssert(INVALIDFORMAT, formatUnusedAnno, FORMATBOTTOM); + glbAssert(INVALIDFORMAT, FORMATBOTTOM, FORMATBOTTOM); + + // GLB of @InvalidFormat("Message") and others + + glbAssert(invalidFormatWithMessage, UNKNOWNFORMAT, invalidFormatWithMessage); + glbAssert(invalidFormatWithMessage, FORMAT, FORMATBOTTOM); + glbAssert(invalidFormatWithMessage, formatUnusedAnno, FORMATBOTTOM); + glbAssert(invalidFormatWithMessage, invalidFormatWithMessage, invalidFormatWithMessage); + glbAssert( + invalidFormatWithMessage, + invalidFormatWithMessage2, + invalidFormatWithMessagesAnded); + glbAssert(invalidFormatWithMessage, FORMATBOTTOM, FORMATBOTTOM); + + // GLB of @FormatBottom and others + + glbAssert(FORMATBOTTOM, UNKNOWNFORMAT, FORMATBOTTOM); + glbAssert(FORMATBOTTOM, FORMAT, FORMATBOTTOM); + glbAssert(FORMATBOTTOM, formatUnusedAnno, FORMATBOTTOM); + glbAssert(FORMATBOTTOM, INVALIDFORMAT, FORMATBOTTOM); + glbAssert(FORMATBOTTOM, invalidFormatWithMessage, FORMATBOTTOM); + glbAssert(FORMATBOTTOM, FORMATBOTTOM, FORMATBOTTOM); + + // ** LUB tests ** + + // LUB of UNUSED and others + + lubAssert(formatUnusedAnno, formatUnusedAnno, formatUnusedAnno); + lubAssert(formatUnusedAnno, formatGeneralAnno, formatGeneralAnno); + lubAssert(formatUnusedAnno, formatCharAnno, formatCharAnno); + lubAssert(formatUnusedAnno, formatIntAnno, formatIntAnno); + lubAssert(formatUnusedAnno, formatTimeAnno, formatTimeAnno); + lubAssert(formatUnusedAnno, formatFloatAnno, formatFloatAnno); + lubAssert(formatUnusedAnno, formatCharAndIntAnno, formatCharAndIntAnno); + lubAssert(formatUnusedAnno, formatIntAndTimeAnno, formatIntAndTimeAnno); + lubAssert(formatUnusedAnno, formatNullAnno, formatNullAnno); + + // LUB of GENERAL and others + + lubAssert(formatGeneralAnno, formatUnusedAnno, formatGeneralAnno); + lubAssert(formatGeneralAnno, formatGeneralAnno, formatGeneralAnno); + lubAssert(formatGeneralAnno, formatCharAnno, formatCharAnno); + lubAssert(formatGeneralAnno, formatIntAnno, formatIntAnno); + lubAssert(formatGeneralAnno, formatTimeAnno, formatTimeAnno); + lubAssert(formatGeneralAnno, formatFloatAnno, formatFloatAnno); + lubAssert(formatGeneralAnno, formatCharAndIntAnno, formatCharAndIntAnno); + lubAssert(formatGeneralAnno, formatIntAndTimeAnno, formatIntAndTimeAnno); + lubAssert(formatGeneralAnno, formatNullAnno, formatNullAnno); + + // LUB of CHAR and others + + lubAssert(formatCharAnno, formatUnusedAnno, formatCharAnno); + lubAssert(formatCharAnno, formatGeneralAnno, formatCharAnno); + lubAssert(formatCharAnno, formatCharAnno, formatCharAnno); + lubAssert(formatCharAnno, formatIntAnno, formatCharAndIntAnno); + lubAssert(formatCharAnno, formatTimeAnno, formatNullAnno); + lubAssert(formatCharAnno, formatFloatAnno, formatNullAnno); + lubAssert(formatCharAnno, formatCharAndIntAnno, formatCharAndIntAnno); + lubAssert(formatCharAnno, formatIntAndTimeAnno, formatNullAnno); + lubAssert(formatCharAnno, formatNullAnno, formatNullAnno); + + // LUB of INT and others + + lubAssert(formatIntAnno, formatUnusedAnno, formatIntAnno); + lubAssert(formatIntAnno, formatGeneralAnno, formatIntAnno); + lubAssert(formatIntAnno, formatCharAnno, formatCharAndIntAnno); + lubAssert(formatIntAnno, formatIntAnno, formatIntAnno); + lubAssert(formatIntAnno, formatTimeAnno, formatIntAndTimeAnno); + lubAssert(formatIntAnno, formatFloatAnno, formatNullAnno); + lubAssert(formatIntAnno, formatCharAndIntAnno, formatCharAndIntAnno); + lubAssert(formatIntAnno, formatIntAndTimeAnno, formatIntAndTimeAnno); + lubAssert(formatIntAnno, formatNullAnno, formatNullAnno); + + // LUB of TIME and others + + lubAssert(formatTimeAnno, formatUnusedAnno, formatTimeAnno); + lubAssert(formatTimeAnno, formatGeneralAnno, formatTimeAnno); + lubAssert(formatTimeAnno, formatCharAnno, formatNullAnno); + lubAssert(formatTimeAnno, formatIntAnno, formatIntAndTimeAnno); + lubAssert(formatTimeAnno, formatTimeAnno, formatTimeAnno); + lubAssert(formatTimeAnno, formatFloatAnno, formatNullAnno); + lubAssert(formatTimeAnno, formatCharAndIntAnno, formatNullAnno); + lubAssert(formatTimeAnno, formatIntAndTimeAnno, formatIntAndTimeAnno); + lubAssert(formatTimeAnno, formatNullAnno, formatNullAnno); + + // LUB of FLOAT and others + + lubAssert(formatFloatAnno, formatUnusedAnno, formatFloatAnno); + lubAssert(formatFloatAnno, formatGeneralAnno, formatFloatAnno); + lubAssert(formatFloatAnno, formatCharAnno, formatNullAnno); + lubAssert(formatFloatAnno, formatIntAnno, formatNullAnno); + lubAssert(formatFloatAnno, formatTimeAnno, formatNullAnno); + lubAssert(formatFloatAnno, formatFloatAnno, formatFloatAnno); + lubAssert(formatFloatAnno, formatCharAndIntAnno, formatNullAnno); + lubAssert(formatFloatAnno, formatIntAndTimeAnno, formatNullAnno); + lubAssert(formatFloatAnno, formatNullAnno, formatNullAnno); + + // LUB of CHAR_AND_INT and others + + lubAssert(formatCharAndIntAnno, formatUnusedAnno, formatCharAndIntAnno); + lubAssert(formatCharAndIntAnno, formatGeneralAnno, formatCharAndIntAnno); + lubAssert(formatCharAndIntAnno, formatCharAnno, formatCharAndIntAnno); + lubAssert(formatCharAndIntAnno, formatIntAnno, formatCharAndIntAnno); + lubAssert(formatCharAndIntAnno, formatTimeAnno, formatNullAnno); + lubAssert(formatCharAndIntAnno, formatFloatAnno, formatNullAnno); + lubAssert(formatCharAndIntAnno, formatCharAndIntAnno, formatCharAndIntAnno); + lubAssert(formatCharAndIntAnno, formatIntAndTimeAnno, formatNullAnno); + lubAssert(formatCharAndIntAnno, formatNullAnno, formatNullAnno); + + // LUB of INT_AND_TIME and others + + lubAssert(formatIntAndTimeAnno, formatUnusedAnno, formatIntAndTimeAnno); + lubAssert(formatIntAndTimeAnno, formatGeneralAnno, formatIntAndTimeAnno); + lubAssert(formatIntAndTimeAnno, formatCharAnno, formatNullAnno); + lubAssert(formatIntAndTimeAnno, formatIntAnno, formatIntAndTimeAnno); + lubAssert(formatIntAndTimeAnno, formatTimeAnno, formatIntAndTimeAnno); + lubAssert(formatIntAndTimeAnno, formatFloatAnno, formatNullAnno); + lubAssert(formatIntAndTimeAnno, formatCharAndIntAnno, formatNullAnno); + lubAssert(formatIntAndTimeAnno, formatIntAndTimeAnno, formatIntAndTimeAnno); + lubAssert(formatIntAndTimeAnno, formatNullAnno, formatNullAnno); + + // LUB of NULL and others + + lubAssert(formatNullAnno, formatUnusedAnno, formatNullAnno); + lubAssert(formatNullAnno, formatGeneralAnno, formatNullAnno); + lubAssert(formatNullAnno, formatCharAnno, formatNullAnno); + lubAssert(formatNullAnno, formatIntAnno, formatNullAnno); + lubAssert(formatNullAnno, formatTimeAnno, formatNullAnno); + lubAssert(formatNullAnno, formatFloatAnno, formatNullAnno); + lubAssert(formatNullAnno, formatCharAndIntAnno, formatNullAnno); + lubAssert(formatNullAnno, formatIntAndTimeAnno, formatNullAnno); + lubAssert(formatNullAnno, formatNullAnno, formatNullAnno); + + // Now test with two ConversionCategory at a time: + + cc2[0] = ConversionCategory.CHAR_AND_INT; + cc2[1] = ConversionCategory.NULL; + AnnotationMirror formatTwoConvCat4 = treeUtil.categoriesToFormatAnnotation(cc2); + cc2[0] = ConversionCategory.NULL; + cc2[1] = ConversionCategory.CHAR; + AnnotationMirror formatTwoConvCat5 = treeUtil.categoriesToFormatAnnotation(cc2); + + lubAssert(formatTwoConvCat1, formatTwoConvCat2, formatTwoConvCat4); + + // Test that the LUB of two ConversionCategory arrays of different sizes is an array of the + // largest size of the two: + + lubAssert(formatGeneralAnno, formatTwoConvCat1, formatTwoConvCat1); + lubAssert(formatTwoConvCat2, formatNullAnno, formatTwoConvCat5); + + // LUB of @UnknownFormat and others + + lubAssert(UNKNOWNFORMAT, UNKNOWNFORMAT, UNKNOWNFORMAT); + lubAssert(UNKNOWNFORMAT, FORMAT, UNKNOWNFORMAT); + lubAssert(UNKNOWNFORMAT, formatUnusedAnno, UNKNOWNFORMAT); + lubAssert(UNKNOWNFORMAT, INVALIDFORMAT, UNKNOWNFORMAT); + lubAssert(UNKNOWNFORMAT, invalidFormatWithMessage, UNKNOWNFORMAT); + lubAssert(UNKNOWNFORMAT, FORMATBOTTOM, UNKNOWNFORMAT); + + // LUB of @Format(null) and others + + lubAssert(FORMAT, UNKNOWNFORMAT, UNKNOWNFORMAT); + // Computing the LUB of @Format(null) and @Format(null) should never occur in practice; + // skipping this case as it causes an expected crash. + // Computing the LUB of @Format(null) and @Format with a value should never occur in + // practice; skipping this case as it causes an expected crash. + lubAssert(FORMAT, INVALIDFORMAT, UNKNOWNFORMAT); + lubAssert(FORMAT, invalidFormatWithMessage, UNKNOWNFORMAT); + lubAssert(FORMAT, FORMATBOTTOM, FORMAT); + + // LUB of @Format(UNUSED) and others + + lubAssert(formatUnusedAnno, UNKNOWNFORMAT, UNKNOWNFORMAT); + // Computing the LUB of @Format with a value and @Format(null) should never occur in + // practice; skipping this case as it causes an expected crash. + lubAssert(formatUnusedAnno, formatUnusedAnno, formatUnusedAnno); + lubAssert(formatUnusedAnno, INVALIDFORMAT, UNKNOWNFORMAT); + lubAssert(formatUnusedAnno, invalidFormatWithMessage, UNKNOWNFORMAT); + lubAssert(formatUnusedAnno, FORMATBOTTOM, formatUnusedAnno); + + // LUB of @InvalidFormat(null) and others + + lubAssert(INVALIDFORMAT, UNKNOWNFORMAT, UNKNOWNFORMAT); + lubAssert(INVALIDFORMAT, FORMAT, UNKNOWNFORMAT); + lubAssert(INVALIDFORMAT, formatUnusedAnno, UNKNOWNFORMAT); + lubAssert(INVALIDFORMAT, FORMATBOTTOM, INVALIDFORMAT); + + // LUB of @InvalidFormat("Message") and others + + lubAssert(invalidFormatWithMessage, UNKNOWNFORMAT, UNKNOWNFORMAT); + lubAssert(invalidFormatWithMessage, FORMAT, UNKNOWNFORMAT); + lubAssert(invalidFormatWithMessage, formatUnusedAnno, UNKNOWNFORMAT); + lubAssert(invalidFormatWithMessage, invalidFormatWithMessage, invalidFormatWithMessage); + lubAssert( + invalidFormatWithMessage, invalidFormatWithMessage2, invalidFormatWithMessagesOred); + lubAssert(invalidFormatWithMessage, FORMATBOTTOM, invalidFormatWithMessage); + + // LUB of @FormatBottom and others + + lubAssert(FORMATBOTTOM, UNKNOWNFORMAT, UNKNOWNFORMAT); + lubAssert(FORMATBOTTOM, FORMAT, FORMAT); + lubAssert(FORMATBOTTOM, formatUnusedAnno, formatUnusedAnno); + lubAssert(FORMATBOTTOM, INVALIDFORMAT, INVALIDFORMAT); + lubAssert(FORMATBOTTOM, invalidFormatWithMessage, invalidFormatWithMessage); + lubAssert(FORMATBOTTOM, FORMATBOTTOM, FORMATBOTTOM); + } +} diff --git a/checker/src/test/java/org/checkerframework/checker/testchecker/lubglb/I18nFormatterLubGlbChecker.java b/checker/src/test/java/org/checkerframework/checker/testchecker/lubglb/I18nFormatterLubGlbChecker.java new file mode 100644 index 000000000000..720be2ccebb8 --- /dev/null +++ b/checker/src/test/java/org/checkerframework/checker/testchecker/lubglb/I18nFormatterLubGlbChecker.java @@ -0,0 +1,476 @@ +package org.checkerframework.checker.testchecker.lubglb; + +// Test case for issues 723 and 756. +// https://github.com/typetools/checker-framework/issues/723 +// https://github.com/typetools/checker-framework/issues/756 + +import org.checkerframework.checker.i18nformatter.I18nFormatterAnnotatedTypeFactory; +import org.checkerframework.checker.i18nformatter.I18nFormatterChecker; +import org.checkerframework.checker.i18nformatter.I18nFormatterTreeUtil; +import org.checkerframework.checker.i18nformatter.I18nFormatterVisitor; +import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory; +import org.checkerframework.checker.i18nformatter.qual.I18nFormat; +import org.checkerframework.checker.i18nformatter.qual.I18nFormatBottom; +import org.checkerframework.checker.i18nformatter.qual.I18nFormatFor; +import org.checkerframework.checker.i18nformatter.qual.I18nInvalidFormat; +import org.checkerframework.checker.i18nformatter.qual.I18nUnknownFormat; +import org.checkerframework.common.basetype.BaseTypeChecker; +import org.checkerframework.common.basetype.BaseTypeVisitor; +import org.checkerframework.framework.type.QualifierHierarchy; +import org.checkerframework.javacutil.AnnotationBuilder; +import org.checkerframework.javacutil.AnnotationUtils; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.util.Elements; + +/** + * This class tests the implementation of GLB computation in the I18n Format String Checker (see + * issue 723), but it does not test for the crash that occurs if I18nFormatterAnnotatedTypeFactory + * does not override greatestLowerBound. That is done by tests/all-systems/Issue691.java. It also + * tests the implementation of LUB computation in the I18n Format String Checker. + */ +public class I18nFormatterLubGlbChecker extends I18nFormatterChecker { + @Override + protected BaseTypeVisitor createSourceVisitor() { + return new I18nFormatterVisitor(this) { + @Override + protected I18nFormatterAnnotatedTypeFactory createTypeFactory() { + return new I18nFormatterLubGlbAnnotatedTypeFactory(checker); + } + }; + } + + /** I18nFormatterLubGlbAnnotatedTypeFactory. */ + private static class I18nFormatterLubGlbAnnotatedTypeFactory + extends I18nFormatterAnnotatedTypeFactory { + + /** + * Constructor. + * + * @param checker checker + */ + public I18nFormatterLubGlbAnnotatedTypeFactory(BaseTypeChecker checker) { + super(checker); + postInit(); + } + + @Override + protected Set> createSupportedTypeQualifiers() { + return new HashSet<>( + Arrays.asList( + I18nUnknownFormat.class, + I18nFormatBottom.class, + I18nFormat.class, + I18nInvalidFormat.class, + I18nFormatFor.class)); + } + } + + @SuppressWarnings("checkstyle:localvariablename") + @Override + public void initChecker() { + super.initChecker(); + I18nFormatterTreeUtil treeUtil = new I18nFormatterTreeUtil(this); + + Elements elements = getElementUtils(); + AnnotationMirror I18NUNKNOWNFORMAT = + AnnotationBuilder.fromClass(elements, I18nUnknownFormat.class); + AnnotationMirror I18NFORMAT = + AnnotationBuilder.fromClass( + elements, + I18nFormat.class, + AnnotationBuilder.elementNamesValues( + "value", new I18nConversionCategory[0])); + AnnotationMirror I18NINVALIDFORMAT = + AnnotationBuilder.fromClass( + elements, + I18nInvalidFormat.class, + AnnotationBuilder.elementNamesValues("value", "dummy")); + AnnotationMirror I18NFORMATFOR = + AnnotationBuilder.fromClass( + elements, + I18nFormatFor.class, + AnnotationBuilder.elementNamesValues("value", "dummy")); + AnnotationMirror I18NFORMATBOTTOM = + AnnotationBuilder.fromClass(elements, I18nFormatBottom.class); + + AnnotationBuilder builder = new AnnotationBuilder(processingEnv, I18nInvalidFormat.class); + builder.setValue("value", "Message"); + AnnotationMirror i18nInvalidFormatWithMessage = builder.build(); + + builder = new AnnotationBuilder(processingEnv, I18nInvalidFormat.class); + builder.setValue("value", "Message2"); + AnnotationMirror i18nInvalidFormatWithMessage2 = builder.build(); + + builder = new AnnotationBuilder(processingEnv, I18nInvalidFormat.class); + builder.setValue("value", "(\"Message\" or \"Message2\")"); + AnnotationMirror i18nInvalidFormatWithMessagesOred = builder.build(); + + builder = new AnnotationBuilder(processingEnv, I18nInvalidFormat.class); + builder.setValue("value", "(\"Message\" and \"Message2\")"); + AnnotationMirror i18nInvalidFormatWithMessagesAnded = builder.build(); + + builder = new AnnotationBuilder(processingEnv, I18nFormatFor.class); + builder.setValue("value", "#1"); + AnnotationMirror i18nFormatForWithValue1 = builder.build(); + + builder = new AnnotationBuilder(processingEnv, I18nFormatFor.class); + builder.setValue("value", "#2"); + AnnotationMirror i18nFormatForWithValue2 = builder.build(); + + I18nConversionCategory[] cc = new I18nConversionCategory[1]; + + cc[0] = I18nConversionCategory.UNUSED; + AnnotationMirror i18nFormatUnusedAnno = treeUtil.categoriesToFormatAnnotation(cc); + cc[0] = I18nConversionCategory.GENERAL; + AnnotationMirror i18nFormatGeneralAnno = treeUtil.categoriesToFormatAnnotation(cc); + cc[0] = I18nConversionCategory.DATE; + AnnotationMirror i18nFormatDateAnno = treeUtil.categoriesToFormatAnnotation(cc); + cc[0] = I18nConversionCategory.NUMBER; + AnnotationMirror i18nFormatNumberAnno = treeUtil.categoriesToFormatAnnotation(cc); + + QualifierHierarchy qh = + ((BaseTypeVisitor) visitor).getTypeFactory().getQualifierHierarchy(); + + // ** GLB tests ** + + // GLB of UNUSED and others + + glbAssert(i18nFormatUnusedAnno, i18nFormatUnusedAnno, i18nFormatUnusedAnno); + glbAssert(i18nFormatUnusedAnno, i18nFormatGeneralAnno, i18nFormatUnusedAnno); + glbAssert(i18nFormatUnusedAnno, i18nFormatDateAnno, i18nFormatUnusedAnno); + glbAssert(i18nFormatUnusedAnno, i18nFormatNumberAnno, i18nFormatUnusedAnno); + + // GLB of GENERAL and others + + glbAssert(i18nFormatGeneralAnno, i18nFormatUnusedAnno, i18nFormatUnusedAnno); + glbAssert(i18nFormatGeneralAnno, i18nFormatGeneralAnno, i18nFormatGeneralAnno); + glbAssert(i18nFormatGeneralAnno, i18nFormatDateAnno, i18nFormatGeneralAnno); + glbAssert(i18nFormatGeneralAnno, i18nFormatNumberAnno, i18nFormatGeneralAnno); + + // GLB of DATE and others + + glbAssert(i18nFormatDateAnno, i18nFormatUnusedAnno, i18nFormatUnusedAnno); + glbAssert(i18nFormatDateAnno, i18nFormatGeneralAnno, i18nFormatGeneralAnno); + glbAssert(i18nFormatDateAnno, i18nFormatDateAnno, i18nFormatDateAnno); + glbAssert(i18nFormatDateAnno, i18nFormatNumberAnno, i18nFormatDateAnno); + + // GLB of NUMBER and others + + glbAssert(i18nFormatNumberAnno, i18nFormatUnusedAnno, i18nFormatUnusedAnno); + glbAssert(i18nFormatNumberAnno, i18nFormatGeneralAnno, i18nFormatGeneralAnno); + glbAssert(i18nFormatNumberAnno, i18nFormatDateAnno, i18nFormatDateAnno); + glbAssert(i18nFormatNumberAnno, i18nFormatNumberAnno, i18nFormatNumberAnno); + + // Now test with two I18nConversionCategory at a time: + + I18nConversionCategory[] cc2 = new I18nConversionCategory[2]; + + cc2[0] = I18nConversionCategory.DATE; + cc2[1] = I18nConversionCategory.DATE; + AnnotationMirror formatTwoConvCat1 = treeUtil.categoriesToFormatAnnotation(cc2); + cc2[0] = I18nConversionCategory.UNUSED; + cc2[1] = I18nConversionCategory.NUMBER; + AnnotationMirror formatTwoConvCat2 = treeUtil.categoriesToFormatAnnotation(cc2); + cc2[0] = I18nConversionCategory.UNUSED; + cc2[1] = I18nConversionCategory.DATE; + AnnotationMirror formatTwoConvCat3 = treeUtil.categoriesToFormatAnnotation(cc2); + + glbAssert(formatTwoConvCat1, formatTwoConvCat2, formatTwoConvCat3); + + // Test that the GLB of two I18nConversionCategory arrays of different sizes is an array of + // the smallest size of the two: + + glbAssert(i18nFormatGeneralAnno, formatTwoConvCat1, i18nFormatGeneralAnno); + glbAssert(formatTwoConvCat2, i18nFormatDateAnno, i18nFormatUnusedAnno); + + // GLB of two distinct @I18nFormatFor(...) annotations is @I18nFormatBottom + + glbAssert(i18nFormatForWithValue1, i18nFormatForWithValue2, I18NFORMATBOTTOM); + + // GLB of @I18nUnknownFormat and others + + glbAssert(I18NUNKNOWNFORMAT, I18NUNKNOWNFORMAT, I18NUNKNOWNFORMAT); + glbAssert(I18NUNKNOWNFORMAT, I18NFORMAT, I18NFORMAT); + glbAssert(I18NUNKNOWNFORMAT, i18nFormatUnusedAnno, i18nFormatUnusedAnno); + glbAssert(I18NUNKNOWNFORMAT, I18NINVALIDFORMAT, I18NINVALIDFORMAT); + glbAssert(I18NUNKNOWNFORMAT, i18nInvalidFormatWithMessage, i18nInvalidFormatWithMessage); + glbAssert(I18NUNKNOWNFORMAT, I18NFORMATFOR, I18NFORMATFOR); + glbAssert(I18NUNKNOWNFORMAT, i18nFormatForWithValue1, i18nFormatForWithValue1); + glbAssert(I18NUNKNOWNFORMAT, I18NFORMATBOTTOM, I18NFORMATBOTTOM); + + // GLB of @I18nFormat(null) and others + + glbAssert(I18NFORMAT, I18NUNKNOWNFORMAT, I18NFORMAT); + // Computing the GLB of @I18nFormat(null) and @I18nFormat(null) should never occur in + // practice. Skipping this case as it causes an expected crash. + // Computing the GLB of @I18nFormat(null) and @I18nFormat with a value should never occur in + // practice. Skipping this case as it causes an expected crash. + glbAssert(I18NFORMAT, I18NINVALIDFORMAT, I18NFORMATBOTTOM); + glbAssert(I18NFORMAT, i18nInvalidFormatWithMessage, I18NFORMATBOTTOM); + glbAssert(I18NFORMAT, I18NFORMATFOR, I18NFORMATBOTTOM); + glbAssert(I18NFORMAT, i18nFormatForWithValue1, I18NFORMATBOTTOM); + glbAssert(I18NFORMAT, I18NFORMATBOTTOM, I18NFORMATBOTTOM); + + // GLB of @I18nFormat(UNUSED) and others + + glbAssert(i18nFormatUnusedAnno, I18NUNKNOWNFORMAT, i18nFormatUnusedAnno); + // Computing the GLB of @I18nFormat with a value and @I18nFormat(null) should never occur in + // practice. Skipping this case as it causes an expected crash. + glbAssert(i18nFormatUnusedAnno, i18nFormatUnusedAnno, i18nFormatUnusedAnno); + glbAssert(i18nFormatUnusedAnno, I18NINVALIDFORMAT, I18NFORMATBOTTOM); + glbAssert(i18nFormatUnusedAnno, i18nInvalidFormatWithMessage, I18NFORMATBOTTOM); + glbAssert(i18nFormatUnusedAnno, I18NFORMATFOR, I18NFORMATBOTTOM); + glbAssert(i18nFormatUnusedAnno, i18nFormatForWithValue1, I18NFORMATBOTTOM); + glbAssert(i18nFormatUnusedAnno, I18NFORMATBOTTOM, I18NFORMATBOTTOM); + + // GLB of @I18nInvalidFormat(null) and others + + glbAssert(I18NINVALIDFORMAT, I18NUNKNOWNFORMAT, I18NINVALIDFORMAT); + glbAssert(I18NINVALIDFORMAT, I18NFORMAT, I18NFORMATBOTTOM); + glbAssert(I18NINVALIDFORMAT, i18nFormatUnusedAnno, I18NFORMATBOTTOM); + glbAssert(I18NINVALIDFORMAT, I18NFORMATFOR, I18NFORMATBOTTOM); + glbAssert(I18NINVALIDFORMAT, i18nFormatForWithValue1, I18NFORMATBOTTOM); + glbAssert(I18NINVALIDFORMAT, I18NFORMATBOTTOM, I18NFORMATBOTTOM); + + // GLB of @I18nInvalidFormat("Message") and others + + glbAssert(i18nInvalidFormatWithMessage, I18NUNKNOWNFORMAT, i18nInvalidFormatWithMessage); + + glbAssert(i18nInvalidFormatWithMessage, I18NFORMAT, I18NFORMATBOTTOM); + glbAssert(i18nInvalidFormatWithMessage, i18nFormatUnusedAnno, I18NFORMATBOTTOM); + glbAssert( + i18nInvalidFormatWithMessage, + i18nInvalidFormatWithMessage, + i18nInvalidFormatWithMessage); + glbAssert( + i18nInvalidFormatWithMessage, + i18nInvalidFormatWithMessage2, + i18nInvalidFormatWithMessagesAnded); + glbAssert(i18nInvalidFormatWithMessage, I18NFORMATFOR, I18NFORMATBOTTOM); + glbAssert(i18nInvalidFormatWithMessage, i18nFormatForWithValue1, I18NFORMATBOTTOM); + glbAssert(i18nInvalidFormatWithMessage, I18NFORMATBOTTOM, I18NFORMATBOTTOM); + + // GLB of @I18nFormatFor(null) and others + + glbAssert(I18NFORMATFOR, I18NUNKNOWNFORMAT, I18NFORMATFOR); + glbAssert(I18NFORMATFOR, I18NFORMAT, I18NFORMATBOTTOM); + glbAssert(I18NFORMATFOR, i18nFormatUnusedAnno, I18NFORMATBOTTOM); + glbAssert(I18NFORMATFOR, I18NINVALIDFORMAT, I18NFORMATBOTTOM); + glbAssert(I18NFORMATFOR, i18nInvalidFormatWithMessage, I18NFORMATBOTTOM); + glbAssert(I18NFORMATFOR, I18NFORMATFOR, I18NFORMATFOR); + glbAssert(I18NFORMATFOR, i18nFormatForWithValue1, I18NFORMATBOTTOM); + glbAssert(I18NFORMATFOR, I18NFORMATBOTTOM, I18NFORMATBOTTOM); + + // GLB of @I18nFormatFor("#1") and others + + glbAssert(i18nFormatForWithValue1, I18NUNKNOWNFORMAT, i18nFormatForWithValue1); + glbAssert(i18nFormatForWithValue1, I18NFORMAT, I18NFORMATBOTTOM); + glbAssert(i18nFormatForWithValue1, i18nFormatUnusedAnno, I18NFORMATBOTTOM); + glbAssert(i18nFormatForWithValue1, I18NINVALIDFORMAT, I18NFORMATBOTTOM); + glbAssert(i18nFormatForWithValue1, i18nInvalidFormatWithMessage, I18NFORMATBOTTOM); + glbAssert(i18nFormatForWithValue1, I18NFORMATFOR, I18NFORMATBOTTOM); + glbAssert(i18nFormatForWithValue1, i18nFormatForWithValue1, i18nFormatForWithValue1); + glbAssert(i18nFormatForWithValue1, I18NFORMATBOTTOM, I18NFORMATBOTTOM); + + // GLB of @I18nFormatBottom and others + + glbAssert(I18NFORMATBOTTOM, I18NUNKNOWNFORMAT, I18NFORMATBOTTOM); + glbAssert(I18NFORMATBOTTOM, I18NFORMAT, I18NFORMATBOTTOM); + glbAssert(I18NFORMATBOTTOM, i18nFormatUnusedAnno, I18NFORMATBOTTOM); + glbAssert(I18NFORMATBOTTOM, I18NINVALIDFORMAT, I18NFORMATBOTTOM); + glbAssert(I18NFORMATBOTTOM, i18nInvalidFormatWithMessage, I18NFORMATBOTTOM); + glbAssert(I18NFORMATBOTTOM, I18NFORMATFOR, I18NFORMATBOTTOM); + glbAssert(I18NFORMATBOTTOM, i18nFormatForWithValue1, I18NFORMATBOTTOM); + glbAssert(I18NFORMATBOTTOM, I18NFORMATBOTTOM, I18NFORMATBOTTOM); + + // ** LUB tests ** + + // LUB of UNUSED and others + + lubAssert(i18nFormatUnusedAnno, i18nFormatUnusedAnno, i18nFormatUnusedAnno); + lubAssert(i18nFormatUnusedAnno, i18nFormatGeneralAnno, i18nFormatGeneralAnno); + lubAssert(i18nFormatUnusedAnno, i18nFormatDateAnno, i18nFormatDateAnno); + lubAssert(i18nFormatUnusedAnno, i18nFormatNumberAnno, i18nFormatNumberAnno); + + // LUB of GENERAL and others + + lubAssert(i18nFormatGeneralAnno, i18nFormatUnusedAnno, i18nFormatGeneralAnno); + lubAssert(i18nFormatGeneralAnno, i18nFormatGeneralAnno, i18nFormatGeneralAnno); + lubAssert(i18nFormatGeneralAnno, i18nFormatDateAnno, i18nFormatDateAnno); + lubAssert(i18nFormatGeneralAnno, i18nFormatNumberAnno, i18nFormatNumberAnno); + + // LUB of DATE and others + + lubAssert(i18nFormatDateAnno, i18nFormatUnusedAnno, i18nFormatDateAnno); + lubAssert(i18nFormatDateAnno, i18nFormatGeneralAnno, i18nFormatDateAnno); + lubAssert(i18nFormatDateAnno, i18nFormatDateAnno, i18nFormatDateAnno); + lubAssert(i18nFormatDateAnno, i18nFormatNumberAnno, i18nFormatNumberAnno); + + // LUB of NUMBER and others + + lubAssert(i18nFormatNumberAnno, i18nFormatUnusedAnno, i18nFormatNumberAnno); + lubAssert(i18nFormatNumberAnno, i18nFormatGeneralAnno, i18nFormatNumberAnno); + lubAssert(i18nFormatNumberAnno, i18nFormatDateAnno, i18nFormatNumberAnno); + lubAssert(i18nFormatNumberAnno, i18nFormatNumberAnno, i18nFormatNumberAnno); + + // Now test with two I18nConversionCategory at a time: + + cc2[0] = I18nConversionCategory.DATE; + cc2[1] = I18nConversionCategory.NUMBER; + AnnotationMirror formatTwoConvCat4 = treeUtil.categoriesToFormatAnnotation(cc2); + + lubAssert(formatTwoConvCat1, formatTwoConvCat2, formatTwoConvCat4); + + // Test that the LUB of two I18nConversionCategory arrays of different sizes is an array of + // the largest size of the two: + + lubAssert(i18nFormatGeneralAnno, formatTwoConvCat1, formatTwoConvCat1); + lubAssert(formatTwoConvCat2, i18nFormatDateAnno, formatTwoConvCat4); + + // LUB of two distinct @I18nFormatFor(...) annotations is @I18nUnknownFormat + + lubAssert(i18nFormatForWithValue1, i18nFormatForWithValue2, I18NUNKNOWNFORMAT); + + // LUB of @I18nUnknownFormat and others + + lubAssert(I18NUNKNOWNFORMAT, I18NUNKNOWNFORMAT, I18NUNKNOWNFORMAT); + lubAssert(I18NUNKNOWNFORMAT, I18NFORMAT, I18NUNKNOWNFORMAT); + lubAssert(I18NUNKNOWNFORMAT, i18nFormatUnusedAnno, I18NUNKNOWNFORMAT); + lubAssert(I18NUNKNOWNFORMAT, I18NINVALIDFORMAT, I18NUNKNOWNFORMAT); + lubAssert(I18NUNKNOWNFORMAT, i18nInvalidFormatWithMessage, I18NUNKNOWNFORMAT); + lubAssert(I18NUNKNOWNFORMAT, I18NFORMATFOR, I18NUNKNOWNFORMAT); + lubAssert(I18NUNKNOWNFORMAT, i18nFormatForWithValue1, I18NUNKNOWNFORMAT); + lubAssert(I18NUNKNOWNFORMAT, I18NFORMATBOTTOM, I18NUNKNOWNFORMAT); + + // LUB of @I18nFormat(null) and others + + lubAssert(I18NFORMAT, I18NUNKNOWNFORMAT, I18NUNKNOWNFORMAT); + // Computing the LUB of @I18nFormat(null) and @I18nFormat(null) should never occur in + // practice. Skipping this case as it causes an expected crash. + // Computing the LUB of @I18nFormat(null) and @I18nFormat with a value should never occur in + // practice. Skipping this case as it causes an expected crash. + lubAssert(I18NFORMAT, I18NINVALIDFORMAT, I18NUNKNOWNFORMAT); + lubAssert(I18NFORMAT, i18nInvalidFormatWithMessage, I18NUNKNOWNFORMAT); + lubAssert(I18NFORMAT, I18NFORMATFOR, I18NUNKNOWNFORMAT); + lubAssert(I18NFORMAT, i18nFormatForWithValue1, I18NUNKNOWNFORMAT); + lubAssert(I18NFORMAT, I18NFORMATBOTTOM, I18NFORMAT); + + // LUB of @I18nFormat(UNUSED) and others + + lubAssert(i18nFormatUnusedAnno, I18NUNKNOWNFORMAT, I18NUNKNOWNFORMAT); + // Computing the LUB of @I18nFormat with a value and @I18nFormat(null) should never occur in + // practice. Skipping this case as it causes an expected crash. + lubAssert(i18nFormatUnusedAnno, i18nFormatUnusedAnno, i18nFormatUnusedAnno); + lubAssert(i18nFormatUnusedAnno, I18NINVALIDFORMAT, I18NUNKNOWNFORMAT); + lubAssert(i18nFormatUnusedAnno, i18nInvalidFormatWithMessage, I18NUNKNOWNFORMAT); + lubAssert(i18nFormatUnusedAnno, I18NFORMATFOR, I18NUNKNOWNFORMAT); + lubAssert(i18nFormatUnusedAnno, i18nFormatForWithValue1, I18NUNKNOWNFORMAT); + lubAssert(i18nFormatUnusedAnno, I18NFORMATBOTTOM, i18nFormatUnusedAnno); + + // LUB of @I18nInvalidFormat(null) and others + + lubAssert(I18NINVALIDFORMAT, I18NUNKNOWNFORMAT, I18NUNKNOWNFORMAT); + lubAssert(I18NINVALIDFORMAT, I18NFORMAT, I18NUNKNOWNFORMAT); + lubAssert(I18NINVALIDFORMAT, i18nFormatUnusedAnno, I18NUNKNOWNFORMAT); + lubAssert(I18NINVALIDFORMAT, I18NFORMATFOR, I18NUNKNOWNFORMAT); + lubAssert(I18NINVALIDFORMAT, i18nFormatForWithValue1, I18NUNKNOWNFORMAT); + lubAssert(I18NINVALIDFORMAT, I18NFORMATBOTTOM, I18NINVALIDFORMAT); + + // LUB of @I18nInvalidFormat("Message") and others + + lubAssert(i18nInvalidFormatWithMessage, I18NUNKNOWNFORMAT, I18NUNKNOWNFORMAT); + + lubAssert(i18nInvalidFormatWithMessage, I18NFORMAT, I18NUNKNOWNFORMAT); + lubAssert(i18nInvalidFormatWithMessage, i18nFormatUnusedAnno, I18NUNKNOWNFORMAT); + lubAssert( + i18nInvalidFormatWithMessage, + i18nInvalidFormatWithMessage, + i18nInvalidFormatWithMessage); + lubAssert( + i18nInvalidFormatWithMessage, + i18nInvalidFormatWithMessage2, + i18nInvalidFormatWithMessagesOred); + lubAssert(i18nInvalidFormatWithMessage, I18NFORMATFOR, I18NUNKNOWNFORMAT); + lubAssert(i18nInvalidFormatWithMessage, i18nFormatForWithValue1, I18NUNKNOWNFORMAT); + lubAssert(i18nInvalidFormatWithMessage, I18NFORMATBOTTOM, i18nInvalidFormatWithMessage); + + // LUB of @I18nFormatFor(null) and others + + lubAssert(I18NFORMATFOR, I18NUNKNOWNFORMAT, I18NUNKNOWNFORMAT); + lubAssert(I18NFORMATFOR, I18NFORMAT, I18NUNKNOWNFORMAT); + lubAssert(I18NFORMATFOR, i18nFormatUnusedAnno, I18NUNKNOWNFORMAT); + lubAssert(I18NFORMATFOR, I18NINVALIDFORMAT, I18NUNKNOWNFORMAT); + lubAssert(I18NFORMATFOR, i18nInvalidFormatWithMessage, I18NUNKNOWNFORMAT); + lubAssert(I18NFORMATFOR, I18NFORMATFOR, I18NFORMATFOR); + lubAssert(I18NFORMATFOR, i18nFormatForWithValue1, I18NUNKNOWNFORMAT); + lubAssert(I18NFORMATFOR, I18NFORMATBOTTOM, I18NFORMATFOR); + + // LUB of @I18nFormatFor("#1") and others + + lubAssert(i18nFormatForWithValue1, I18NUNKNOWNFORMAT, I18NUNKNOWNFORMAT); + lubAssert(i18nFormatForWithValue1, I18NFORMAT, I18NUNKNOWNFORMAT); + lubAssert(i18nFormatForWithValue1, i18nFormatUnusedAnno, I18NUNKNOWNFORMAT); + lubAssert(i18nFormatForWithValue1, I18NINVALIDFORMAT, I18NUNKNOWNFORMAT); + lubAssert(i18nFormatForWithValue1, i18nInvalidFormatWithMessage, I18NUNKNOWNFORMAT); + lubAssert(i18nFormatForWithValue1, I18NFORMATFOR, I18NUNKNOWNFORMAT); + lubAssert(i18nFormatForWithValue1, i18nFormatForWithValue1, i18nFormatForWithValue1); + lubAssert(i18nFormatForWithValue1, I18NFORMATBOTTOM, i18nFormatForWithValue1); + + // LUB of @I18nFormatBottom and others + + lubAssert(I18NFORMATBOTTOM, I18NUNKNOWNFORMAT, I18NUNKNOWNFORMAT); + lubAssert(I18NFORMATBOTTOM, I18NFORMAT, I18NFORMAT); + lubAssert(I18NFORMATBOTTOM, i18nFormatUnusedAnno, i18nFormatUnusedAnno); + lubAssert(I18NFORMATBOTTOM, I18NINVALIDFORMAT, I18NINVALIDFORMAT); + lubAssert(I18NFORMATBOTTOM, i18nInvalidFormatWithMessage, i18nInvalidFormatWithMessage); + lubAssert(I18NFORMATBOTTOM, I18NFORMATFOR, I18NFORMATFOR); + lubAssert(I18NFORMATBOTTOM, i18nFormatForWithValue1, i18nFormatForWithValue1); + lubAssert(I18NFORMATBOTTOM, I18NFORMATBOTTOM, I18NFORMATBOTTOM); + } + + /** + * Throws an exception if glb(arg1, arg2) != result. + * + * @param arg1 the first argument + * @param arg2 the second argument + * @param expected the expected result + */ + private void glbAssert( + AnnotationMirror arg1, AnnotationMirror arg2, AnnotationMirror expected) { + QualifierHierarchy qualHierarchy = + ((BaseTypeVisitor) visitor).getTypeFactory().getQualifierHierarchy(); + AnnotationMirror result = qualHierarchy.greatestLowerBoundQualifiersOnly(arg1, arg2); + if (!AnnotationUtils.areSame(expected, result)) { + throw new AssertionError( + String.format( + "GLB of %s and %s should be %s, but is %s", + arg1, arg2, expected, result)); + } + } + + /** + * Throws an exception if lub(arg1, arg2) != result. + * + * @param arg1 the first argument + * @param arg2 the second argument + * @param expected the expected result + */ + private void lubAssert( + AnnotationMirror arg1, AnnotationMirror arg2, AnnotationMirror expected) { + QualifierHierarchy qualHierarchy = + ((BaseTypeVisitor) visitor).getTypeFactory().getQualifierHierarchy(); + AnnotationMirror result = qualHierarchy.leastUpperBoundQualifiersOnly(arg1, arg2); + if (!AnnotationUtils.areSame(expected, result)) { + throw new AssertionError( + String.format( + "LUB of %s and %s should be %s, but is %s", + arg1, arg2, expected, result)); + } + } +} diff --git a/checker/src/test/java/testlib/lib b/checker/src/test/java/testlib/lib deleted file mode 120000 index 382ebb07e999..000000000000 --- a/checker/src/test/java/testlib/lib +++ /dev/null @@ -1 +0,0 @@ -../../../../../framework/src/test/java/testlib/lib \ No newline at end of file diff --git a/checker/src/test/java/testlib/lubglb/FormatterLubGlbChecker.java b/checker/src/test/java/testlib/lubglb/FormatterLubGlbChecker.java deleted file mode 100644 index 9245dac1aa6b..000000000000 --- a/checker/src/test/java/testlib/lubglb/FormatterLubGlbChecker.java +++ /dev/null @@ -1,1006 +0,0 @@ -package testlib.lubglb; - -// Test case for issues 691 and 756. -// https://github.com/typetools/checker-framework/issues/691 -// https://github.com/typetools/checker-framework/issues/756 - -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.util.Elements; -import org.checkerframework.checker.formatter.FormatterAnnotatedTypeFactory; -import org.checkerframework.checker.formatter.FormatterChecker; -import org.checkerframework.checker.formatter.FormatterTreeUtil; -import org.checkerframework.checker.formatter.FormatterVisitor; -import org.checkerframework.checker.formatter.qual.ConversionCategory; -import org.checkerframework.checker.formatter.qual.Format; -import org.checkerframework.checker.formatter.qual.FormatBottom; -import org.checkerframework.checker.formatter.qual.InvalidFormat; -import org.checkerframework.checker.formatter.qual.UnknownFormat; -import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.common.basetype.BaseTypeVisitor; -import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationUtils; - -/** - * This class tests the implementation of GLB computation in the Formatter Checker, but it does not - * test for the crash described in issue 691. That is done by tests/all-systems/Issue691.java. It - * also tests the implementation of LUB computation in the Formatter Checker. - */ -public class FormatterLubGlbChecker extends FormatterChecker { - - @Override - protected BaseTypeVisitor createSourceVisitor() { - return new FormatterVisitor(this) { - @Override - protected FormatterLubGlbAnnotatedTypeFactory createTypeFactory() { - return new FormatterLubGlbAnnotatedTypeFactory(checker); - } - }; - } - - /** FormatterLubGlbAnnotatedTypeFactory. */ - private static class FormatterLubGlbAnnotatedTypeFactory extends FormatterAnnotatedTypeFactory { - - /** - * Constructor. - * - * @param checker checker - */ - public FormatterLubGlbAnnotatedTypeFactory(BaseTypeChecker checker) { - super(checker); - postInit(); - } - - @Override - protected Set> createSupportedTypeQualifiers() { - return new HashSet<>( - Arrays.asList( - FormatBottom.class, - Format.class, - InvalidFormat.class, - UnknownFormat.class)); - } - } - - @SuppressWarnings("checkstyle:localvariablename") - @Override - public void initChecker() { - super.initChecker(); - FormatterTreeUtil treeUtil = new FormatterTreeUtil(this); - - Elements elements = getElementUtils(); - AnnotationMirror UNKNOWNFORMAT = AnnotationBuilder.fromClass(elements, UnknownFormat.class); - AnnotationMirror FORMAT = AnnotationBuilder.fromClass(elements, Format.class); - AnnotationMirror INVALIDFORMAT = AnnotationBuilder.fromClass(elements, InvalidFormat.class); - AnnotationMirror FORMATBOTTOM = AnnotationBuilder.fromClass(elements, FormatBottom.class); - - AnnotationBuilder builder = - new AnnotationBuilder(processingEnv, InvalidFormat.class.getCanonicalName()); - builder.setValue("value", "Message"); - AnnotationMirror invalidFormatWithMessage = builder.build(); - - builder = new AnnotationBuilder(processingEnv, InvalidFormat.class.getCanonicalName()); - builder.setValue("value", "Message2"); - AnnotationMirror invalidFormatWithMessage2 = builder.build(); - - builder = new AnnotationBuilder(processingEnv, InvalidFormat.class.getCanonicalName()); - builder.setValue("value", "(\"Message\" or \"Message2\")"); - AnnotationMirror invalidFormatWithMessagesOred = builder.build(); - - builder = new AnnotationBuilder(processingEnv, InvalidFormat.class.getCanonicalName()); - builder.setValue("value", "(\"Message\" and \"Message2\")"); - AnnotationMirror invalidFormatWithMessagesAnded = builder.build(); - - ConversionCategory[] cc = new ConversionCategory[1]; - - cc[0] = ConversionCategory.UNUSED; - AnnotationMirror formatUnusedAnno = treeUtil.categoriesToFormatAnnotation(cc); - cc[0] = ConversionCategory.GENERAL; - AnnotationMirror formatGeneralAnno = treeUtil.categoriesToFormatAnnotation(cc); - cc[0] = ConversionCategory.CHAR; - AnnotationMirror formatCharAnno = treeUtil.categoriesToFormatAnnotation(cc); - cc[0] = ConversionCategory.INT; - AnnotationMirror formatIntAnno = treeUtil.categoriesToFormatAnnotation(cc); - cc[0] = ConversionCategory.TIME; - AnnotationMirror formatTimeAnno = treeUtil.categoriesToFormatAnnotation(cc); - cc[0] = ConversionCategory.FLOAT; - AnnotationMirror formatFloatAnno = treeUtil.categoriesToFormatAnnotation(cc); - cc[0] = ConversionCategory.CHAR_AND_INT; - AnnotationMirror formatCharAndIntAnno = treeUtil.categoriesToFormatAnnotation(cc); - cc[0] = ConversionCategory.INT_AND_TIME; - AnnotationMirror formatIntAndTimeAnno = treeUtil.categoriesToFormatAnnotation(cc); - cc[0] = ConversionCategory.NULL; - AnnotationMirror formatNullAnno = treeUtil.categoriesToFormatAnnotation(cc); - - QualifierHierarchy qh = - ((BaseTypeVisitor) visitor).getTypeFactory().getQualifierHierarchy(); - - // ** GLB tests ** - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAndIntAnno, formatIntAndTimeAnno), - formatIntAnno) - : "GLB of @Format(CHAR_AND_INT) and @Format(INT_AND_TIME) is not @Format(INT)!"; - - // GLB of UNUSED and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatUnusedAnno, formatUnusedAnno), formatUnusedAnno) - : "GLB of @Format(UNUSED) and @Format(UNUSED) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatUnusedAnno, formatGeneralAnno), - formatUnusedAnno) - : "GLB of @Format(UNUSED) and @Format(GENERAL) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatUnusedAnno, formatCharAnno), formatUnusedAnno) - : "GLB of @Format(UNUSED) and @Format(CHAR) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatUnusedAnno, formatIntAnno), formatUnusedAnno) - : "GLB of @Format(UNUSED) and @Format(INT) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatUnusedAnno, formatTimeAnno), formatUnusedAnno) - : "GLB of @Format(UNUSED) and @Format(TIME) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatUnusedAnno, formatFloatAnno), formatUnusedAnno) - : "GLB of @Format(UNUSED) and @Format(FLOAT) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatUnusedAnno, formatCharAndIntAnno), - formatUnusedAnno) - : "GLB of @Format(UNUSED) and @Format(CHAR_AND_INT) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatUnusedAnno, formatIntAndTimeAnno), - formatUnusedAnno) - : "GLB of @Format(UNUSED) and @Format(INT_AND_TIME) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatUnusedAnno, formatNullAnno), formatUnusedAnno) - : "GLB of @Format(UNUSED) and @Format(NULL) is not @Format(UNUSED)!"; - - // GLB of GENERAL and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatGeneralAnno, formatUnusedAnno), - formatUnusedAnno) - : "GLB of @Format(GENERAL) and @Format(UNUSED) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatGeneralAnno, formatGeneralAnno), - formatGeneralAnno) - : "GLB of @Format(GENERAL) and @Format(GENERAL) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatGeneralAnno, formatCharAnno), formatGeneralAnno) - : "GLB of @Format(GENERAL) and @Format(CHAR) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatGeneralAnno, formatIntAnno), formatGeneralAnno) - : "GLB of @Format(GENERAL) and @Format(INT) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatGeneralAnno, formatTimeAnno), formatGeneralAnno) - : "GLB of @Format(GENERAL) and @Format(TIME) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatGeneralAnno, formatFloatAnno), - formatGeneralAnno) - : "GLB of @Format(GENERAL) and @Format(FLOAT) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatGeneralAnno, formatCharAndIntAnno), - formatGeneralAnno) - : "GLB of @Format(GENERAL) and @Format(CHAR_AND_INT) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatGeneralAnno, formatIntAndTimeAnno), - formatGeneralAnno) - : "GLB of @Format(GENERAL) and @Format(INT_AND_TIME) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatGeneralAnno, formatNullAnno), formatGeneralAnno) - : "GLB of @Format(GENERAL) and @Format(NULL) is not @Format(GENERAL)!"; - - // GLB of CHAR and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAnno, formatUnusedAnno), formatUnusedAnno) - : "GLB of @Format(CHAR) and @Format(UNUSED) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAnno, formatGeneralAnno), formatGeneralAnno) - : "GLB of @Format(CHAR) and @Format(GENERAL) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAnno, formatCharAnno), formatCharAnno) - : "GLB of @Format(CHAR) and @Format(CHAR) is not @Format(CHAR)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAnno, formatIntAnno), formatGeneralAnno) - : "GLB of @Format(CHAR) and @Format(INT) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAnno, formatTimeAnno), formatGeneralAnno) - : "GLB of @Format(CHAR) and @Format(TIME) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAnno, formatFloatAnno), formatGeneralAnno) - : "GLB of @Format(CHAR) and @Format(FLOAT) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAnno, formatCharAndIntAnno), formatCharAnno) - : "GLB of @Format(CHAR) and @Format(CHAR_AND_INT) is not @Format(CHAR)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAnno, formatIntAndTimeAnno), - formatGeneralAnno) - : "GLB of @Format(CHAR) and @Format(INT_AND_TIME) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAnno, formatNullAnno), formatCharAnno) - : "GLB of @Format(CHAR) and @Format(NULL) is not @Format(CHAR)!"; - - // GLB of INT and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAnno, formatUnusedAnno), formatUnusedAnno) - : "GLB of @Format(INT) and @Format(UNUSED) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAnno, formatGeneralAnno), formatGeneralAnno) - : "GLB of @Format(INT) and @Format(GENERAL) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAnno, formatCharAnno), formatGeneralAnno) - : "GLB of @Format(INT) and @Format(CHAR) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAnno, formatIntAnno), formatIntAnno) - : "GLB of @Format(INT) and @Format(INT) is not @Format(INT)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAnno, formatTimeAnno), formatGeneralAnno) - : "GLB of @Format(INT) and @Format(TIME) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAnno, formatFloatAnno), formatGeneralAnno) - : "GLB of @Format(INT) and @Format(FLOAT) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAnno, formatCharAndIntAnno), formatIntAnno) - : "GLB of @Format(INT) and @Format(CHAR_AND_INT) is not @Format(INT)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAnno, formatIntAndTimeAnno), formatIntAnno) - : "GLB of @Format(INT) and @Format(INT_AND_TIME) is not @Format(INT)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAnno, formatNullAnno), formatIntAnno) - : "GLB of @Format(INT) and @Format(NULL) is not @Format(INT)!"; - - // GLB of TIME and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatTimeAnno, formatUnusedAnno), formatUnusedAnno) - : "GLB of @Format(TIME) and @Format(UNUSED) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatTimeAnno, formatGeneralAnno), formatGeneralAnno) - : "GLB of @Format(TIME) and @Format(GENERAL) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatTimeAnno, formatCharAnno), formatGeneralAnno) - : "GLB of @Format(TIME) and @Format(CHAR) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatTimeAnno, formatIntAnno), formatGeneralAnno) - : "GLB of @Format(TIME) and @Format(INT) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatTimeAnno, formatTimeAnno), formatTimeAnno) - : "GLB of @Format(TIME) and @Format(TIME) is not @Format(TIME)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatTimeAnno, formatFloatAnno), formatGeneralAnno) - : "GLB of @Format(TIME) and @Format(FLOAT) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatTimeAnno, formatCharAndIntAnno), - formatGeneralAnno) - : "GLB of @Format(TIME) and @Format(CHAR_AND_INT) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatTimeAnno, formatIntAndTimeAnno), formatTimeAnno) - : "GLB of @Format(TIME) and @Format(INT_AND_TIME) is not @Format(TIME)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatTimeAnno, formatNullAnno), formatTimeAnno) - : "GLB of @Format(TIME) and @Format(NULL) is not @Format(TIME)!"; - - // GLB of FLOAT and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatFloatAnno, formatUnusedAnno), formatUnusedAnno) - : "GLB of @Format(FLOAT) and @Format(UNUSED) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatFloatAnno, formatGeneralAnno), - formatGeneralAnno) - : "GLB of @Format(FLOAT) and @Format(GENERAL) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatFloatAnno, formatCharAnno), formatGeneralAnno) - : "GLB of @Format(FLOAT) and @Format(CHAR) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatFloatAnno, formatIntAnno), formatGeneralAnno) - : "GLB of @Format(FLOAT) and @Format(INT) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatFloatAnno, formatTimeAnno), formatGeneralAnno) - : "GLB of @Format(FLOAT) and @Format(TIME) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatFloatAnno, formatFloatAnno), formatFloatAnno) - : "GLB of @Format(FLOAT) and @Format(FLOAT) is not @Format(FLOAT)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatFloatAnno, formatCharAndIntAnno), - formatGeneralAnno) - : "GLB of @Format(FLOAT) and @Format(CHAR_AND_INT) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatFloatAnno, formatIntAndTimeAnno), - formatGeneralAnno) - : "GLB of @Format(FLOAT) and @Format(INT_AND_TIME) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatFloatAnno, formatNullAnno), formatFloatAnno) - : "GLB of @Format(FLOAT) and @Format(NULL) is not @Format(FLOAT)!"; - - // GLB of CHAR_AND_INT and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAndIntAnno, formatUnusedAnno), - formatUnusedAnno) - : "GLB of @Format(CHAR_AND_INT) and @Format(UNUSED) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAndIntAnno, formatGeneralAnno), - formatGeneralAnno) - : "GLB of @Format(CHAR_AND_INT) and @Format(GENERAL) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAndIntAnno, formatCharAnno), formatCharAnno) - : "GLB of @Format(CHAR_AND_INT) and @Format(CHAR) is not @Format(CHAR)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAndIntAnno, formatIntAnno), formatIntAnno) - : "GLB of @Format(CHAR_AND_INT) and @Format(INT) is not @Format(INT)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAndIntAnno, formatTimeAnno), - formatGeneralAnno) - : "GLB of @Format(CHAR_AND_INT) and @Format(TIME) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAndIntAnno, formatFloatAnno), - formatGeneralAnno) - : "GLB of @Format(CHAR_AND_INT) and @Format(FLOAT) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAndIntAnno, formatCharAndIntAnno), - formatCharAndIntAnno) - : "GLB of @Format(CHAR_AND_INT) and @Format(CHAR_AND_INT) is not @Format(CHAR_AND_INT)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAndIntAnno, formatIntAndTimeAnno), - formatIntAnno) - : "GLB of @Format(CHAR_AND_INT) and @Format(INT_AND_TIME) is not @Format(INT)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatCharAndIntAnno, formatNullAnno), - formatCharAndIntAnno) - : "GLB of @Format(CHAR_AND_INT) and @Format(NULL) is not @Format(CHAR_AND_INT)!"; - - // GLB of INT_AND_TIME and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAndTimeAnno, formatUnusedAnno), - formatUnusedAnno) - : "GLB of @Format(INT_AND_TIME) and @Format(UNUSED) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAndTimeAnno, formatGeneralAnno), - formatGeneralAnno) - : "GLB of @Format(INT_AND_TIME) and @Format(GENERAL) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAndTimeAnno, formatCharAnno), - formatGeneralAnno) - : "GLB of @Format(INT_AND_TIME) and @Format(CHAR) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAndTimeAnno, formatIntAnno), formatIntAnno) - : "GLB of @Format(INT_AND_TIME) and @Format(INT) is not @Format(INT)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAndTimeAnno, formatTimeAnno), formatTimeAnno) - : "GLB of @Format(INT_AND_TIME) and @Format(TIME) is not @Format(TIME)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAndTimeAnno, formatFloatAnno), - formatGeneralAnno) - : "GLB of @Format(INT_AND_TIME) and @Format(FLOAT) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAndTimeAnno, formatCharAndIntAnno), - formatIntAnno) - : "GLB of @Format(INT_AND_TIME) and @Format(CHAR_AND_INT) is not @Format(INT)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAndTimeAnno, formatIntAndTimeAnno), - formatIntAndTimeAnno) - : "GLB of @Format(INT_AND_TIME) and @Format(INT_AND_TIME) is not @Format(INT_AND_TIME)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatIntAndTimeAnno, formatNullAnno), - formatIntAndTimeAnno) - : "GLB of @Format(INT_AND_TIME) and @Format(NULL) is not @Format(INT_AND_TIME)!"; - - // GLB of NULL and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatNullAnno, formatUnusedAnno), formatUnusedAnno) - : "GLB of @Format(NULL) and @Format(UNUSED) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatNullAnno, formatGeneralAnno), formatGeneralAnno) - : "GLB of @Format(NULL) and @Format(GENERAL) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatNullAnno, formatCharAnno), formatCharAnno) - : "GLB of @Format(NULL) and @Format(CHAR) is not @Format(CHAR)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatNullAnno, formatIntAnno), formatIntAnno) - : "GLB of @Format(NULL) and @Format(INT) is not @Format(INT)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatNullAnno, formatTimeAnno), formatTimeAnno) - : "GLB of @Format(NULL) and @Format(TIME) is not @Format(TIME)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatNullAnno, formatFloatAnno), formatFloatAnno) - : "GLB of @Format(NULL) and @Format(FLOAT) is not @Format(FLOAT)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatNullAnno, formatCharAndIntAnno), - formatCharAndIntAnno) - : "GLB of @Format(NULL) and @Format(CHAR_AND_INT) is not @Format(CHAR_AND_INT)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatNullAnno, formatIntAndTimeAnno), - formatIntAndTimeAnno) - : "GLB of @Format(NULL) and @Format(INT_AND_TIME) is not @Format(INT_AND_TIME)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatNullAnno, formatNullAnno), formatNullAnno) - : "GLB of @Format(NULL) and @Format(NULL) is not @Format(NULL)!"; - - // Now test with two ConversionCategory at a time: - - ConversionCategory[] cc2 = new ConversionCategory[2]; - - cc2[0] = ConversionCategory.CHAR_AND_INT; - cc2[1] = ConversionCategory.FLOAT; - AnnotationMirror formatTwoConvCat1 = treeUtil.categoriesToFormatAnnotation(cc2); - cc2[0] = ConversionCategory.INT; - cc2[1] = ConversionCategory.CHAR; - AnnotationMirror formatTwoConvCat2 = treeUtil.categoriesToFormatAnnotation(cc2); - cc2[0] = ConversionCategory.INT; - cc2[1] = ConversionCategory.GENERAL; - AnnotationMirror formatTwoConvCat3 = treeUtil.categoriesToFormatAnnotation(cc2); - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatTwoConvCat1, formatTwoConvCat2), - formatTwoConvCat3) - : "GLB of @Format([CHAR_AND_INT,FLOAT]) and @Format([INT,CHAR]) is not @Format([INT,GENERAL])!"; - - // Test that the GLB of two ConversionCategory arrays of different sizes is an array of the - // smallest size of the two: - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatGeneralAnno, formatTwoConvCat1), - formatGeneralAnno) - : "GLB of @I18nFormat(GENERAL) and @I18nFormat([CHAR_AND_INT,FLOAT]) is not @I18nFormat(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatTwoConvCat2, formatNullAnno), formatIntAnno) - : "GLB of @I18nFormat([INT,CHAR]) and @I18nFormat(NULL) is not @I18nFormat(INT)!"; - - // GLB of @UnknownFormat and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(UNKNOWNFORMAT, UNKNOWNFORMAT), UNKNOWNFORMAT) - : "GLB of @UnknownFormat and @UnknownFormat is not @UnknownFormat!"; - assert AnnotationUtils.areSame(qh.greatestLowerBound(UNKNOWNFORMAT, FORMAT), FORMAT) - : "GLB of @UnknownFormat and @Format(null) is not @Format(null)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(UNKNOWNFORMAT, formatUnusedAnno), formatUnusedAnno) - : "GLB of @UnknownFormat and @Format(UNUSED) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(UNKNOWNFORMAT, INVALIDFORMAT), INVALIDFORMAT) - : "GLB of @UnknownFormat and @InvalidFormat(null) is not @InvalidFormat(null)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(UNKNOWNFORMAT, invalidFormatWithMessage), - invalidFormatWithMessage) - : "GLB of @UnknownFormat and @InvalidFormat(\"Message\") is not @InvalidFormat(\"Message\")!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(UNKNOWNFORMAT, FORMATBOTTOM), FORMATBOTTOM) - : "GLB of @UnknownFormat and @FormatBottom is not @FormatBottom!"; - - // GLB of @Format(null) and others - - assert AnnotationUtils.areSame(qh.greatestLowerBound(FORMAT, UNKNOWNFORMAT), FORMAT) - : "GLB of @Format(null) and @UnknownFormat is not @Format(null)!"; - // Computing the GLB of @Format(null) and @Format(null) should never occur in practice. - // Skipping this case as it causes an expected crash. - // Computing the GLB of @Format(null) and @Format with a value should never occur in - // practice. Skipping this case as it causes an expected crash. - assert AnnotationUtils.areSame(qh.greatestLowerBound(FORMAT, INVALIDFORMAT), FORMATBOTTOM) - : "GLB of @Format(null) and @InvalidFormat(null) is not @FormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(FORMAT, invalidFormatWithMessage), FORMATBOTTOM) - : "GLB of @Format(null) and @InvalidFormat(\"Message\") is not @FormatBottom!"; - assert AnnotationUtils.areSame(qh.greatestLowerBound(FORMAT, FORMATBOTTOM), FORMATBOTTOM) - : "GLB of @Format(null) and @FormatBottom is not @FormatBottom!"; - - // GLB of @Format(UNUSED) and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatUnusedAnno, UNKNOWNFORMAT), formatUnusedAnno) - : "GLB of @Format(UNUSED) and @UnknownFormat is not @Format(UNUSED)!"; - // Computing the GLB of @Format with a value and @Format(null) should never occur in - // practice. Skipping this case as it causes an expected crash. - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatUnusedAnno, formatUnusedAnno), formatUnusedAnno) - : "GLB of @Format(UNUSED) and @Format(UNUSED) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatUnusedAnno, INVALIDFORMAT), FORMATBOTTOM) - : "GLB of @Format(UNUSED) and @InvalidFormat(null) is not @FormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatUnusedAnno, invalidFormatWithMessage), - FORMATBOTTOM) - : "GLB of @Format(UNUSED) and @InvalidFormat(\"Message\") is not @FormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatUnusedAnno, FORMATBOTTOM), FORMATBOTTOM) - : "GLB of @Format(UNUSED) and @FormatBottom is not @FormatBottom!"; - - // GLB of @InvalidFormat(null) and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(INVALIDFORMAT, UNKNOWNFORMAT), INVALIDFORMAT) - : "GLB of @InvalidFormat(null) and @UnknownFormat is not @InvalidFormat(null)!"; - assert AnnotationUtils.areSame(qh.greatestLowerBound(INVALIDFORMAT, FORMAT), FORMATBOTTOM) - : "GLB of @InvalidFormat(null) and @Format(null) is not @FormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(INVALIDFORMAT, formatUnusedAnno), FORMATBOTTOM) - : "GLB of @InvalidFormat(null) and @Format(UNUSED) is not @FormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(INVALIDFORMAT, FORMATBOTTOM), FORMATBOTTOM) - : "GLB of @InvalidFormat(null) and @FormatBottom is not @FormatBottom!"; - - // GLB of @InvalidFormat("Message") and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(invalidFormatWithMessage, UNKNOWNFORMAT), - invalidFormatWithMessage) - : "GLB of @InvalidFormat(\"Message\") and @UnknownFormat is not @InvalidFormat(\"Message\")!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(invalidFormatWithMessage, FORMAT), FORMATBOTTOM) - : "GLB of @InvalidFormat(\"Message\") and @Format(null) is not @FormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(invalidFormatWithMessage, formatUnusedAnno), - FORMATBOTTOM) - : "GLB of @InvalidFormat(\"Message\") and @Format(UNUSED) is not @FormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(invalidFormatWithMessage, invalidFormatWithMessage), - invalidFormatWithMessage) - : "GLB of @InvalidFormat(\"Message\") and @InvalidFormat(\"Message\") is not @InvalidFormat(\"Message\")!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(invalidFormatWithMessage, invalidFormatWithMessage2), - invalidFormatWithMessagesAnded) - : "GLB of @InvalidFormat(\"Message\") and @InvalidFormat(\"Message2\") is not @InvalidFormat(\"(\"Message\" and \"Message2\")\")!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(invalidFormatWithMessage, FORMATBOTTOM), FORMATBOTTOM) - : "GLB of @InvalidFormat(\"Message\") and @FormatBottom is not @FormatBottom!"; - - // GLB of @FormatBottom and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(FORMATBOTTOM, UNKNOWNFORMAT), FORMATBOTTOM) - : "GLB of @FormatBottom and @UnknownFormat is not @FormatBottom!"; - assert AnnotationUtils.areSame(qh.greatestLowerBound(FORMATBOTTOM, FORMAT), FORMATBOTTOM) - : "GLB of @FormatBottom and @Format(null) is not @FormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(FORMATBOTTOM, formatUnusedAnno), FORMATBOTTOM) - : "GLB of @FormatBottom and @Format(UNUSED) is not @FormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(FORMATBOTTOM, INVALIDFORMAT), FORMATBOTTOM) - : "GLB of @FormatBottom and @InvalidFormat(null) is not @FormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(FORMATBOTTOM, invalidFormatWithMessage), FORMATBOTTOM) - : "GLB of @FormatBottom and @InvalidFormat(\"Message\") is not @FormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(FORMATBOTTOM, FORMATBOTTOM), FORMATBOTTOM) - : "GLB of @FormatBottom and @FormatBottom is not @FormatBottom!"; - - // ** LUB tests ** - - // LUB of UNUSED and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatUnusedAnno, formatUnusedAnno), formatUnusedAnno) - : "LUB of @Format(UNUSED) and @Format(UNUSED) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatUnusedAnno, formatGeneralAnno), formatGeneralAnno) - : "LUB of @Format(UNUSED) and @Format(GENERAL) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatUnusedAnno, formatCharAnno), formatCharAnno) - : "LUB of @Format(UNUSED) and @Format(CHAR) is not @Format(CHAR)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatUnusedAnno, formatIntAnno), formatIntAnno) - : "LUB of @Format(UNUSED) and @Format(INT) is not @Format(INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatUnusedAnno, formatTimeAnno), formatTimeAnno) - : "LUB of @Format(UNUSED) and @Format(TIME) is not @Format(TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatUnusedAnno, formatFloatAnno), formatFloatAnno) - : "LUB of @Format(UNUSED) and @Format(FLOAT) is not @Format(FLOAT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatUnusedAnno, formatCharAndIntAnno), - formatCharAndIntAnno) - : "LUB of @Format(UNUSED) and @Format(CHAR_AND_INT) is not @Format(CHAR_AND_INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatUnusedAnno, formatIntAndTimeAnno), - formatIntAndTimeAnno) - : "LUB of @Format(UNUSED) and @Format(INT_AND_TIME) is not @Format(INT_AND_TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatUnusedAnno, formatNullAnno), formatNullAnno) - : "LUB of @Format(UNUSED) and @Format(NULL) is not @Format(NULL)!"; - - // LUB of GENERAL and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatGeneralAnno, formatUnusedAnno), formatGeneralAnno) - : "LUB of @Format(GENERAL) and @Format(UNUSED) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatGeneralAnno, formatGeneralAnno), formatGeneralAnno) - : "LUB of @Format(GENERAL) and @Format(GENERAL) is not @Format(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatGeneralAnno, formatCharAnno), formatCharAnno) - : "LUB of @Format(GENERAL) and @Format(CHAR) is not @Format(CHAR)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatGeneralAnno, formatIntAnno), formatIntAnno) - : "LUB of @Format(GENERAL) and @Format(INT) is not @Format(INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatGeneralAnno, formatTimeAnno), formatTimeAnno) - : "LUB of @Format(GENERAL) and @Format(TIME) is not @Format(TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatGeneralAnno, formatFloatAnno), formatFloatAnno) - : "LUB of @Format(GENERAL) and @Format(FLOAT) is not @Format(FLOAT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatGeneralAnno, formatCharAndIntAnno), - formatCharAndIntAnno) - : "LUB of @Format(GENERAL) and @Format(CHAR_AND_INT) is not @Format(CHAR_AND_INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatGeneralAnno, formatIntAndTimeAnno), - formatIntAndTimeAnno) - : "LUB of @Format(GENERAL) and @Format(INT_AND_TIME) is not @Format(INT_AND_TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatGeneralAnno, formatNullAnno), formatNullAnno) - : "LUB of @Format(GENERAL) and @Format(NULL) is not @Format(NULL)!"; - - // LUB of CHAR and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAnno, formatUnusedAnno), formatCharAnno) - : "LUB of @Format(CHAR) and @Format(UNUSED) is not @Format(CHAR)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAnno, formatGeneralAnno), formatCharAnno) - : "LUB of @Format(CHAR) and @Format(GENERAL) is not @Format(CHAR)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAnno, formatCharAnno), formatCharAnno) - : "LUB of @Format(CHAR) and @Format(CHAR) is not @Format(CHAR)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAnno, formatIntAnno), formatCharAndIntAnno) - : "LUB of @Format(CHAR) and @Format(INT) is not @Format(CHAR_AND_INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAnno, formatTimeAnno), formatNullAnno) - : "LUB of @Format(CHAR) and @Format(TIME) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAnno, formatFloatAnno), formatNullAnno) - : "LUB of @Format(CHAR) and @Format(FLOAT) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAnno, formatCharAndIntAnno), - formatCharAndIntAnno) - : "LUB of @Format(CHAR) and @Format(CHAR_AND_INT) is not @Format(CHAR_AND_INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAnno, formatIntAndTimeAnno), formatNullAnno) - : "LUB of @Format(CHAR) and @Format(INT_AND_TIME) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAnno, formatNullAnno), formatNullAnno) - : "LUB of @Format(CHAR) and @Format(NULL) is not @Format(NULL)!"; - - // LUB of INT and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAnno, formatUnusedAnno), formatIntAnno) - : "LUB of @Format(INT) and @Format(UNUSED) is not @Format(INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAnno, formatGeneralAnno), formatIntAnno) - : "LUB of @Format(INT) and @Format(GENERAL) is not @Format(INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAnno, formatCharAnno), formatCharAndIntAnno) - : "LUB of @Format(INT) and @Format(CHAR) is not @Format(CHAR_AND_INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAnno, formatIntAnno), formatIntAnno) - : "LUB of @Format(INT) and @Format(INT) is not @Format(INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAnno, formatTimeAnno), formatIntAndTimeAnno) - : "LUB of @Format(INT) and @Format(TIME) is not @Format(INT_AND_TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAnno, formatFloatAnno), formatNullAnno) - : "LUB of @Format(INT) and @Format(FLOAT) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAnno, formatCharAndIntAnno), - formatCharAndIntAnno) - : "LUB of @Format(INT) and @Format(CHAR_AND_INT) is not @Format(CHAR_AND_INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAnno, formatIntAndTimeAnno), - formatIntAndTimeAnno) - : "LUB of @Format(INT) and @Format(INT_AND_TIME) is not @Format(INT_AND_TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAnno, formatNullAnno), formatNullAnno) - : "LUB of @Format(INT) and @Format(NULL) is not @Format(NULL)!"; - - // LUB of TIME and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatTimeAnno, formatUnusedAnno), formatTimeAnno) - : "LUB of @Format(TIME) and @Format(UNUSED) is not @Format(TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatTimeAnno, formatGeneralAnno), formatTimeAnno) - : "LUB of @Format(TIME) and @Format(GENERAL) is not @Format(TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatTimeAnno, formatCharAnno), formatNullAnno) - : "LUB of @Format(TIME) and @Format(CHAR) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatTimeAnno, formatIntAnno), formatIntAndTimeAnno) - : "LUB of @Format(TIME) and @Format(INT) is not @Format(INT_AND_TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatTimeAnno, formatTimeAnno), formatTimeAnno) - : "LUB of @Format(TIME) and @Format(TIME) is not @Format(TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatTimeAnno, formatFloatAnno), formatNullAnno) - : "LUB of @Format(TIME) and @Format(FLOAT) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatTimeAnno, formatCharAndIntAnno), formatNullAnno) - : "LUB of @Format(TIME) and @Format(CHAR_AND_INT) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatTimeAnno, formatIntAndTimeAnno), - formatIntAndTimeAnno) - : "LUB of @Format(TIME) and @Format(INT_AND_TIME) is not @Format(INT_AND_TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatTimeAnno, formatNullAnno), formatNullAnno) - : "LUB of @Format(TIME) and @Format(NULL) is not @Format(NULL)!"; - - // LUB of FLOAT and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatFloatAnno, formatUnusedAnno), formatFloatAnno) - : "LUB of @Format(FLOAT) and @Format(UNUSED) is not @Format(FLOAT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatFloatAnno, formatGeneralAnno), formatFloatAnno) - : "LUB of @Format(FLOAT) and @Format(GENERAL) is not @Format(FLOAT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatFloatAnno, formatCharAnno), formatNullAnno) - : "LUB of @Format(FLOAT) and @Format(CHAR) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatFloatAnno, formatIntAnno), formatNullAnno) - : "LUB of @Format(FLOAT) and @Format(INT) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatFloatAnno, formatTimeAnno), formatNullAnno) - : "LUB of @Format(FLOAT) and @Format(TIME) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatFloatAnno, formatFloatAnno), formatFloatAnno) - : "LUB of @Format(FLOAT) and @Format(FLOAT) is not @Format(FLOAT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatFloatAnno, formatCharAndIntAnno), formatNullAnno) - : "LUB of @Format(FLOAT) and @Format(CHAR_AND_INT) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatFloatAnno, formatIntAndTimeAnno), formatNullAnno) - : "LUB of @Format(FLOAT) and @Format(INT_AND_TIME) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatFloatAnno, formatNullAnno), formatNullAnno) - : "LUB of @Format(FLOAT) and @Format(NULL) is not @Format(NULL)!"; - - // LUB of CHAR_AND_INT and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAndIntAnno, formatUnusedAnno), - formatCharAndIntAnno) - : "LUB of @Format(CHAR_AND_INT) and @Format(UNUSED) is not @Format(CHAR_AND_INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAndIntAnno, formatGeneralAnno), - formatCharAndIntAnno) - : "LUB of @Format(CHAR_AND_INT) and @Format(GENERAL) is not @Format(CHAR_AND_INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAndIntAnno, formatCharAnno), - formatCharAndIntAnno) - : "LUB of @Format(CHAR_AND_INT) and @Format(CHAR) is not @Format(CHAR_AND_INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAndIntAnno, formatIntAnno), - formatCharAndIntAnno) - : "LUB of @Format(CHAR_AND_INT) and @Format(INT) is not @Format(CHAR_AND_INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAndIntAnno, formatTimeAnno), formatNullAnno) - : "LUB of @Format(CHAR_AND_INT) and @Format(TIME) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAndIntAnno, formatFloatAnno), formatNullAnno) - : "LUB of @Format(CHAR_AND_INT) and @Format(FLOAT) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAndIntAnno, formatCharAndIntAnno), - formatCharAndIntAnno) - : "LUB of @Format(CHAR_AND_INT) and @Format(CHAR_AND_INT) is not @Format(CHAR_AND_INT)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAndIntAnno, formatIntAndTimeAnno), - formatNullAnno) - : "LUB of @Format(CHAR_AND_INT) and @Format(INT_AND_TIME) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatCharAndIntAnno, formatNullAnno), formatNullAnno) - : "LUB of @Format(CHAR_AND_INT) and @Format(NULL) is not @Format(NULL)!"; - - // LUB of INT_AND_TIME and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAndTimeAnno, formatUnusedAnno), - formatIntAndTimeAnno) - : "LUB of @Format(INT_AND_TIME) and @Format(UNUSED) is not @Format(INT_AND_TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAndTimeAnno, formatGeneralAnno), - formatIntAndTimeAnno) - : "LUB of @Format(INT_AND_TIME) and @Format(GENERAL) is not @Format(INT_AND_TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAndTimeAnno, formatCharAnno), formatNullAnno) - : "LUB of @Format(INT_AND_TIME) and @Format(CHAR) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAndTimeAnno, formatIntAnno), - formatIntAndTimeAnno) - : "LUB of @Format(INT_AND_TIME) and @Format(INT) is not @Format(INT_AND_TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAndTimeAnno, formatTimeAnno), - formatIntAndTimeAnno) - : "LUB of @Format(INT_AND_TIME) and @Format(TIME) is not @Format(INT_AND_TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAndTimeAnno, formatFloatAnno), formatNullAnno) - : "LUB of @Format(INT_AND_TIME) and @Format(FLOAT) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAndTimeAnno, formatCharAndIntAnno), - formatNullAnno) - : "LUB of @Format(INT_AND_TIME) and @Format(CHAR_AND_INT) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAndTimeAnno, formatIntAndTimeAnno), - formatIntAndTimeAnno) - : "LUB of @Format(INT_AND_TIME) and @Format(INT_AND_TIME) is not @Format(INT_AND_TIME)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatIntAndTimeAnno, formatNullAnno), formatNullAnno) - : "LUB of @Format(INT_AND_TIME) and @Format(NULL) is not @Format(NULL)!"; - - // LUB of NULL and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatNullAnno, formatUnusedAnno), formatNullAnno) - : "LUB of @Format(NULL) and @Format(UNUSED) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatNullAnno, formatGeneralAnno), formatNullAnno) - : "LUB of @Format(NULL) and @Format(GENERAL) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatNullAnno, formatCharAnno), formatNullAnno) - : "LUB of @Format(NULL) and @Format(CHAR) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatNullAnno, formatIntAnno), formatNullAnno) - : "LUB of @Format(NULL) and @Format(INT) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatNullAnno, formatTimeAnno), formatNullAnno) - : "LUB of @Format(NULL) and @Format(TIME) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatNullAnno, formatFloatAnno), formatNullAnno) - : "LUB of @Format(NULL) and @Format(FLOAT) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatNullAnno, formatCharAndIntAnno), formatNullAnno) - : "LUB of @Format(NULL) and @Format(CHAR_AND_INT) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatNullAnno, formatIntAndTimeAnno), formatNullAnno) - : "LUB of @Format(NULL) and @Format(INT_AND_TIME) is not @Format(NULL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatNullAnno, formatNullAnno), formatNullAnno) - : "LUB of @Format(NULL) and @Format(NULL) is not @Format(NULL)!"; - - // Now test with two ConversionCategory at a time: - - cc2[0] = ConversionCategory.CHAR_AND_INT; - cc2[1] = ConversionCategory.NULL; - AnnotationMirror formatTwoConvCat4 = treeUtil.categoriesToFormatAnnotation(cc2); - cc2[0] = ConversionCategory.NULL; - cc2[1] = ConversionCategory.CHAR; - AnnotationMirror formatTwoConvCat5 = treeUtil.categoriesToFormatAnnotation(cc2); - - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatTwoConvCat1, formatTwoConvCat2), formatTwoConvCat4) - : "LUB of @Format([CHAR_AND_INT,FLOAT]) and @Format([INT,CHAR]) is not @Format([CHAR_AND_INT,NULL])!"; - - // Test that the LUB of two ConversionCategory arrays of different sizes is an array of the - // largest size of the two: - - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatGeneralAnno, formatTwoConvCat1), formatTwoConvCat1) - : "LUB of @I18nFormat(GENERAL) and @I18nFormat([CHAR_AND_INT,FLOAT]) is not @I18nFormat([CHAR_AND_INT,FLOAT])!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatTwoConvCat2, formatNullAnno), formatTwoConvCat5) - : "LUB of @I18nFormat([INT,CHAR]) and @I18nFormat(NULL) is not @I18nFormat([NULL,CHAR])!"; - - // LUB of @UnknownFormat and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(UNKNOWNFORMAT, UNKNOWNFORMAT), UNKNOWNFORMAT) - : "LUB of @UnknownFormat and @UnknownFormat is not @UnknownFormat!"; - assert AnnotationUtils.areSame(qh.leastUpperBound(UNKNOWNFORMAT, FORMAT), UNKNOWNFORMAT) - : "LUB of @UnknownFormat and @Format(null) is not @UnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(UNKNOWNFORMAT, formatUnusedAnno), UNKNOWNFORMAT) - : "LUB of @UnknownFormat and @Format(UNUSED) is not @UnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(UNKNOWNFORMAT, INVALIDFORMAT), UNKNOWNFORMAT) - : "LUB of @UnknownFormat and @InvalidFormat(null) is not @UnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(UNKNOWNFORMAT, invalidFormatWithMessage), UNKNOWNFORMAT) - : "LUB of @UnknownFormat and @InvalidFormat(\"Message\") is not @UnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(UNKNOWNFORMAT, FORMATBOTTOM), UNKNOWNFORMAT) - : "LUB of @UnknownFormat and @FormatBottom is not @UnknownFormat!"; - - // LUB of @Format(null) and others - - assert AnnotationUtils.areSame(qh.leastUpperBound(FORMAT, UNKNOWNFORMAT), UNKNOWNFORMAT) - : "LUB of @Format(null) and @UnknownFormat is not @UnknownFormat!"; - // Computing the LUB of @Format(null) and @Format(null) should never occur in practice. - // Skipping this case as it causes an expected crash. - // Computing the LUB of @Format(null) and @Format with a value should never occur in - // practice. Skipping this case as it causes an expected crash. - assert AnnotationUtils.areSame(qh.leastUpperBound(FORMAT, INVALIDFORMAT), UNKNOWNFORMAT) - : "LUB of @Format(null) and @InvalidFormat(null) is not @UnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(FORMAT, invalidFormatWithMessage), UNKNOWNFORMAT) - : "LUB of @Format(null) and @InvalidFormat(\"Message\") is not @UnknownFormat!"; - assert AnnotationUtils.areSame(qh.leastUpperBound(FORMAT, FORMATBOTTOM), FORMAT) - : "LUB of @Format(null) and @FormatBottom is not @Format(null)!"; - - // LUB of @Format(UNUSED) and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatUnusedAnno, UNKNOWNFORMAT), UNKNOWNFORMAT) - : "LUB of @Format(UNUSED) and @UnknownFormat is not @UnknownFormat!"; - // Computing the LUB of @Format with a value and @Format(null) should never occur in - // practice. Skipping this case as it causes an expected crash. - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatUnusedAnno, formatUnusedAnno), formatUnusedAnno) - : "LUB of @Format(UNUSED) and @Format(UNUSED) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatUnusedAnno, INVALIDFORMAT), UNKNOWNFORMAT) - : "LUB of @Format(UNUSED) and @InvalidFormat(null) is not @UnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatUnusedAnno, invalidFormatWithMessage), - UNKNOWNFORMAT) - : "LUB of @Format(UNUSED) and @InvalidFormat(\"Message\") is not @UnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatUnusedAnno, FORMATBOTTOM), formatUnusedAnno) - : "LUB of @Format(UNUSED) and @FormatBottom is not @Format(UNUSED)!"; - - // LUB of @InvalidFormat(null) and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(INVALIDFORMAT, UNKNOWNFORMAT), UNKNOWNFORMAT) - : "LUB of @InvalidFormat(null) and @UnknownFormat is not @UnknownFormat!"; - assert AnnotationUtils.areSame(qh.leastUpperBound(INVALIDFORMAT, FORMAT), UNKNOWNFORMAT) - : "LUB of @InvalidFormat(null) and @Format(null) is not @UnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(INVALIDFORMAT, formatUnusedAnno), UNKNOWNFORMAT) - : "LUB of @InvalidFormat(null) and @Format(UNUSED) is not @UnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(INVALIDFORMAT, FORMATBOTTOM), INVALIDFORMAT) - : "LUB of @InvalidFormat(null) and @FormatBottom is not @InvalidFormat(null)!"; - - // LUB of @InvalidFormat("Message") and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(invalidFormatWithMessage, UNKNOWNFORMAT), UNKNOWNFORMAT) - : "LUB of @InvalidFormat(\"Message\") and @UnknownFormat is not @UnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(invalidFormatWithMessage, FORMAT), UNKNOWNFORMAT) - : "LUB of @InvalidFormat(\"Message\") and @Format(null) is not @UnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(invalidFormatWithMessage, formatUnusedAnno), - UNKNOWNFORMAT) - : "LUB of @InvalidFormat(\"Message\") and @Format(UNUSED) is not @UnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(invalidFormatWithMessage, invalidFormatWithMessage), - invalidFormatWithMessage) - : "LUB of @InvalidFormat(\"Message\") and @InvalidFormat(\"Message\") is not @InvalidFormat(\"Message\")!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(invalidFormatWithMessage, invalidFormatWithMessage2), - invalidFormatWithMessagesOred) - : "LUB of @InvalidFormat(\"Message\") and @InvalidFormat(\"Message2\") is not @InvalidFormat(\"(\"Message\" or \"Message2\")\")!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(invalidFormatWithMessage, FORMATBOTTOM), - invalidFormatWithMessage) - : "LUB of @InvalidFormat(\"Message\") and @FormatBottom is not @InvalidFormat(\"Message\")!"; - - // LUB of @FormatBottom and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(FORMATBOTTOM, UNKNOWNFORMAT), UNKNOWNFORMAT) - : "LUB of @FormatBottom and @UnknownFormat is not @UnknownFormat!"; - assert AnnotationUtils.areSame(qh.leastUpperBound(FORMATBOTTOM, FORMAT), FORMAT) - : "LUB of @FormatBottom and @Format(null) is not @Format(null)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(FORMATBOTTOM, formatUnusedAnno), formatUnusedAnno) - : "LUB of @FormatBottom and @Format(UNUSED) is not @Format(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(FORMATBOTTOM, INVALIDFORMAT), INVALIDFORMAT) - : "LUB of @FormatBottom and @InvalidFormat(null) is not @InvalidFormat(null)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(FORMATBOTTOM, invalidFormatWithMessage), - invalidFormatWithMessage) - : "LUB of @FormatBottom and @InvalidFormat(\"Message\") is not @InvalidFormat(\"Message\")!"; - assert AnnotationUtils.areSame(qh.leastUpperBound(FORMATBOTTOM, FORMATBOTTOM), FORMATBOTTOM) - : "LUB of @FormatBottom and @FormatBottom is not @FormatBottom!"; - } -} diff --git a/checker/src/test/java/testlib/lubglb/I18nFormatterLubGlbChecker.java b/checker/src/test/java/testlib/lubglb/I18nFormatterLubGlbChecker.java deleted file mode 100644 index 570c06bf1755..000000000000 --- a/checker/src/test/java/testlib/lubglb/I18nFormatterLubGlbChecker.java +++ /dev/null @@ -1,843 +0,0 @@ -package testlib.lubglb; - -// Test case for issues 723 and 756. -// https://github.com/typetools/checker-framework/issues/723 -// https://github.com/typetools/checker-framework/issues/756 - -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.util.Elements; -import org.checkerframework.checker.i18nformatter.I18nFormatterAnnotatedTypeFactory; -import org.checkerframework.checker.i18nformatter.I18nFormatterChecker; -import org.checkerframework.checker.i18nformatter.I18nFormatterTreeUtil; -import org.checkerframework.checker.i18nformatter.I18nFormatterVisitor; -import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory; -import org.checkerframework.checker.i18nformatter.qual.I18nFormat; -import org.checkerframework.checker.i18nformatter.qual.I18nFormatBottom; -import org.checkerframework.checker.i18nformatter.qual.I18nFormatFor; -import org.checkerframework.checker.i18nformatter.qual.I18nInvalidFormat; -import org.checkerframework.checker.i18nformatter.qual.I18nUnknownFormat; -import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.common.basetype.BaseTypeVisitor; -import org.checkerframework.framework.type.QualifierHierarchy; -import org.checkerframework.javacutil.AnnotationBuilder; -import org.checkerframework.javacutil.AnnotationUtils; - -/** - * This class tests the implementation of GLB computation in the I18n Format String Checker (see - * issue 723), but it does not test for the crash that occurs if I18nFormatterAnnotatedTypeFactory - * does not override greatestLowerBound. That is done by tests/all-systems/Issue691.java. It also - * tests the implementation of LUB computation in the I18n Format String Checker. - */ -public class I18nFormatterLubGlbChecker extends I18nFormatterChecker { - @Override - protected BaseTypeVisitor createSourceVisitor() { - return new I18nFormatterVisitor(this) { - @Override - protected I18nFormatterAnnotatedTypeFactory createTypeFactory() { - return new I18nFormatterLubGlbAnnotatedTypeFactory(checker); - } - }; - } - - /** I18nFormatterLubGlbAnnotatedTypeFactory. */ - private static class I18nFormatterLubGlbAnnotatedTypeFactory - extends I18nFormatterAnnotatedTypeFactory { - - /** - * Constructor. - * - * @param checker checker - */ - public I18nFormatterLubGlbAnnotatedTypeFactory(BaseTypeChecker checker) { - super(checker); - postInit(); - } - - @Override - protected Set> createSupportedTypeQualifiers() { - return new HashSet<>( - Arrays.asList( - I18nUnknownFormat.class, - I18nFormatBottom.class, - I18nFormat.class, - I18nInvalidFormat.class, - I18nFormatFor.class)); - } - } - - @SuppressWarnings("checkstyle:localvariablename") - @Override - public void initChecker() { - super.initChecker(); - I18nFormatterTreeUtil treeUtil = new I18nFormatterTreeUtil(this); - - Elements elements = getElementUtils(); - AnnotationMirror I18NUNKNOWNFORMAT = - AnnotationBuilder.fromClass(elements, I18nUnknownFormat.class); - AnnotationMirror I18NFORMAT = AnnotationBuilder.fromClass(elements, I18nFormat.class); - AnnotationMirror I18NINVALIDFORMAT = - AnnotationBuilder.fromClass(elements, I18nInvalidFormat.class); - AnnotationMirror I18NFORMATFOR = AnnotationBuilder.fromClass(elements, I18nFormatFor.class); - AnnotationMirror I18NFORMATBOTTOM = - AnnotationBuilder.fromClass(elements, I18nFormatBottom.class); - - AnnotationBuilder builder = - new AnnotationBuilder(processingEnv, I18nInvalidFormat.class.getCanonicalName()); - builder.setValue("value", "Message"); - AnnotationMirror i18nInvalidFormatWithMessage = builder.build(); - - builder = new AnnotationBuilder(processingEnv, I18nInvalidFormat.class.getCanonicalName()); - builder.setValue("value", "Message2"); - AnnotationMirror i18nInvalidFormatWithMessage2 = builder.build(); - - builder = new AnnotationBuilder(processingEnv, I18nInvalidFormat.class.getCanonicalName()); - builder.setValue("value", "(\"Message\" or \"Message2\")"); - AnnotationMirror i18nInvalidFormatWithMessagesOred = builder.build(); - - builder = new AnnotationBuilder(processingEnv, I18nInvalidFormat.class.getCanonicalName()); - builder.setValue("value", "(\"Message\" and \"Message2\")"); - AnnotationMirror i18nInvalidFormatWithMessagesAnded = builder.build(); - - builder = new AnnotationBuilder(processingEnv, I18nFormatFor.class.getCanonicalName()); - builder.setValue("value", "#1"); - AnnotationMirror i18nFormatForWithValue1 = builder.build(); - - builder = new AnnotationBuilder(processingEnv, I18nFormatFor.class.getCanonicalName()); - builder.setValue("value", "#2"); - AnnotationMirror i18nFormatForWithValue2 = builder.build(); - - I18nConversionCategory[] cc = new I18nConversionCategory[1]; - - cc[0] = I18nConversionCategory.UNUSED; - AnnotationMirror i18nFormatUnusedAnno = treeUtil.categoriesToFormatAnnotation(cc); - cc[0] = I18nConversionCategory.GENERAL; - AnnotationMirror i18nFormatGeneralAnno = treeUtil.categoriesToFormatAnnotation(cc); - cc[0] = I18nConversionCategory.DATE; - AnnotationMirror i18nFormatDateAnno = treeUtil.categoriesToFormatAnnotation(cc); - cc[0] = I18nConversionCategory.NUMBER; - AnnotationMirror i18nFormatNumberAnno = treeUtil.categoriesToFormatAnnotation(cc); - - QualifierHierarchy qh = - ((BaseTypeVisitor) visitor).getTypeFactory().getQualifierHierarchy(); - - // ** GLB tests ** - - // GLB of UNUSED and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatUnusedAnno, i18nFormatUnusedAnno), - i18nFormatUnusedAnno) - : "GLB of @I18nFormat(UNUSED) and @I18nFormat(UNUSED) is not @I18nFormat(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatUnusedAnno, i18nFormatGeneralAnno), - i18nFormatUnusedAnno) - : "GLB of @I18nFormat(UNUSED) and @I18nFormat(GENERAL) is not @I18nFormat(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatUnusedAnno, i18nFormatDateAnno), - i18nFormatUnusedAnno) - : "GLB of @I18nFormat(UNUSED) and @I18nFormat(DATE) is not @I18nFormat(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatUnusedAnno, i18nFormatNumberAnno), - i18nFormatUnusedAnno) - : "GLB of @I18nFormat(UNUSED) and @I18nFormat(NUMBER) is not @I18nFormat(UNUSED)!"; - - // GLB of GENERAL and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatGeneralAnno, i18nFormatUnusedAnno), - i18nFormatUnusedAnno) - : "GLB of @I18nFormat(GENERAL) and @I18nFormat(UNUSED) is not @I18nFormat(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatGeneralAnno, i18nFormatGeneralAnno), - i18nFormatGeneralAnno) - : "GLB of @I18nFormat(GENERAL) and @I18nFormat(GENERAL) is not @I18nFormat(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatGeneralAnno, i18nFormatDateAnno), - i18nFormatGeneralAnno) - : "GLB of @I18nFormat(GENERAL) and @I18nFormat(DATE) is not @I18nFormat(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatGeneralAnno, i18nFormatNumberAnno), - i18nFormatGeneralAnno) - : "GLB of @I18nFormat(GENERAL) and @I18nFormat(NUMBER) is not @I18nFormat(GENERAL)!"; - - // GLB of DATE and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatDateAnno, i18nFormatUnusedAnno), - i18nFormatUnusedAnno) - : "GLB of @I18nFormat(DATE) and @I18nFormat(UNUSED) is not @I18nFormat(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatDateAnno, i18nFormatGeneralAnno), - i18nFormatGeneralAnno) - : "GLB of @I18nFormat(DATE) and @I18nFormat(GENERAL) is not @I18nFormat(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatDateAnno, i18nFormatDateAnno), - i18nFormatDateAnno) - : "GLB of @I18nFormat(DATE) and @I18nFormat(DATE) is not @I18nFormat(DATE)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatDateAnno, i18nFormatNumberAnno), - i18nFormatDateAnno) - : "GLB of @I18nFormat(DATE) and @I18nFormat(NUMBER) is not @I18nFormat(DATE)!"; - - // GLB of NUMBER and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatNumberAnno, i18nFormatUnusedAnno), - i18nFormatUnusedAnno) - : "GLB of @I18nFormat(NUMBER) and @I18nFormat(UNUSED) is not @I18nFormat(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatNumberAnno, i18nFormatGeneralAnno), - i18nFormatGeneralAnno) - : "GLB of @I18nFormat(NUMBER) and @I18nFormat(GENERAL) is not @I18nFormat(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatNumberAnno, i18nFormatDateAnno), - i18nFormatDateAnno) - : "GLB of @I18nFormat(NUMBER) and @I18nFormat(DATE) is not @I18nFormat(DATE)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatNumberAnno, i18nFormatNumberAnno), - i18nFormatNumberAnno) - : "GLB of @I18nFormat(NUMBER) and @I18nFormat(NUMBER) is not @I18nFormat(NUMBER)!"; - - // Now test with two I18nConversionCategory at a time: - - I18nConversionCategory[] cc2 = new I18nConversionCategory[2]; - - cc2[0] = I18nConversionCategory.DATE; - cc2[1] = I18nConversionCategory.DATE; - AnnotationMirror formatTwoConvCat1 = treeUtil.categoriesToFormatAnnotation(cc2); - cc2[0] = I18nConversionCategory.UNUSED; - cc2[1] = I18nConversionCategory.NUMBER; - AnnotationMirror formatTwoConvCat2 = treeUtil.categoriesToFormatAnnotation(cc2); - cc2[0] = I18nConversionCategory.UNUSED; - cc2[1] = I18nConversionCategory.DATE; - AnnotationMirror formatTwoConvCat3 = treeUtil.categoriesToFormatAnnotation(cc2); - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatTwoConvCat1, formatTwoConvCat2), - formatTwoConvCat3) - : "GLB of @I18nFormat([DATE,DATE]) and @I18nFormat([UNUSED,NUMBER]) is not @I18nFormat([UNUSED,DATE])!"; - - // Test that the GLB of two I18nConversionCategory arrays of different sizes is an array of - // the smallest size of the two: - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatGeneralAnno, formatTwoConvCat1), - i18nFormatGeneralAnno) - : "GLB of @I18nFormat(GENERAL) and @I18nFormat([DATE,DATE]) is not @I18nFormat(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(formatTwoConvCat2, i18nFormatDateAnno), - i18nFormatUnusedAnno) - : "GLB of @I18nFormat([UNUSED,NUMBER]) and @I18nFormat(DATE) is not @I18nFormat(UNUSED)!"; - - // GLB of two distinct @I18nFormatFor(...) annotations is @I18nFormatBottom - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatForWithValue1, i18nFormatForWithValue2), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatFor(\"#1\") and @I18nFormatFor(\"#2\") is not @I18nFormatBottom!"; - - // GLB of @I18nUnknownFormat and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NUNKNOWNFORMAT, I18NUNKNOWNFORMAT), - I18NUNKNOWNFORMAT) - : "GLB of @I18nUnknownFormat and @I18nUnknownFormat is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NUNKNOWNFORMAT, I18NFORMAT), I18NFORMAT) - : "GLB of @I18nUnknownFormat and @I18nFormat(null) is not @I18nFormat(null)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NUNKNOWNFORMAT, i18nFormatUnusedAnno), - i18nFormatUnusedAnno) - : "GLB of @I18nUnknownFormat and @I18nFormat(UNUSED) is not @I18nFormat(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NUNKNOWNFORMAT, I18NINVALIDFORMAT), - I18NINVALIDFORMAT) - : "GLB of @I18nUnknownFormat and @I18nInvalidFormat(null) is not @I18nInvalidFormat(null)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NUNKNOWNFORMAT, i18nInvalidFormatWithMessage), - i18nInvalidFormatWithMessage) - : "GLB of @I18nUnknownFormat and @I18nInvalidFormat(\"Message\") is not @I18nInvalidFormat(\"Message\")!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NUNKNOWNFORMAT, I18NFORMATFOR), I18NFORMATFOR) - : "GLB of @I18nUnknownFormat and @I18nFormatFor(null) is not @I18nFormatFor(null)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NUNKNOWNFORMAT, i18nFormatForWithValue1), - i18nFormatForWithValue1) - : "GLB of @I18nUnknownFormat and @I18nFormatFor(\"#1\") is not @I18nFormatFor(\"#1\")!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NUNKNOWNFORMAT, I18NFORMATBOTTOM), - I18NFORMATBOTTOM) - : "GLB of @I18nUnknownFormat and @I18nFormatBottom is not @I18nFormatBottom!"; - - // GLB of @I18nFormat(null) and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMAT, I18NUNKNOWNFORMAT), I18NFORMAT) - : "GLB of @I18nFormat(null) and @I18nUnknownFormat is not @I18nFormat(null)!"; - // Computing the GLB of @I18nFormat(null) and @I18nFormat(null) should never occur in - // practice. Skipping this case as it causes an expected crash. - // Computing the GLB of @I18nFormat(null) and @I18nFormat with a value should never occur in - // practice. Skipping this case as it causes an expected crash. - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMAT, I18NINVALIDFORMAT), I18NFORMATBOTTOM) - : "GLB of @I18nFormat(null) and @I18nInvalidFormat(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMAT, i18nInvalidFormatWithMessage), - I18NFORMATBOTTOM) - : "GLB of @I18nFormat(null) and @I18nInvalidFormat(\"Message\") is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMAT, I18NFORMATFOR), I18NFORMATBOTTOM) - : "GLB of @I18nFormat(null) and @I18nFormatFor(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMAT, i18nFormatForWithValue1), - I18NFORMATBOTTOM) - : "GLB of @I18nFormat(null) and @I18nFormatFor(\"#1\") is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMAT, I18NFORMATBOTTOM), I18NFORMATBOTTOM) - : "GLB of @I18nFormat(null) and @I18nFormatBottom is not @I18nFormatBottom!"; - - // GLB of @I18nFormat(UNUSED) and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatUnusedAnno, I18NUNKNOWNFORMAT), - i18nFormatUnusedAnno) - : "GLB of @I18nFormat(UNUSED) and @I18nUnknownFormat is not @I18nFormat(UNUSED)!"; - // Computing the GLB of @I18nFormat with a value and @I18nFormat(null) should never occur in - // practice. Skipping this case as it causes an expected crash. - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatUnusedAnno, i18nFormatUnusedAnno), - i18nFormatUnusedAnno) - : "GLB of @I18nFormat(UNUSED) and @I18nFormat(UNUSED) is not @I18nFormat(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatUnusedAnno, I18NINVALIDFORMAT), - I18NFORMATBOTTOM) - : "GLB of @I18nFormat(UNUSED) and @I18nInvalidFormat(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatUnusedAnno, i18nInvalidFormatWithMessage), - I18NFORMATBOTTOM) - : "GLB of @I18nFormat(UNUSED) and @I18nInvalidFormat(\"Message\") is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatUnusedAnno, I18NFORMATFOR), - I18NFORMATBOTTOM) - : "GLB of @I18nFormat(UNUSED) and @I18nFormatFor(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatUnusedAnno, i18nFormatForWithValue1), - I18NFORMATBOTTOM) - : "GLB of @I18nFormat(UNUSED) and @I18nFormatFor(\"#1\") is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatUnusedAnno, I18NFORMATBOTTOM), - I18NFORMATBOTTOM) - : "GLB of @I18nFormat(UNUSED) and @I18nFormatBottom is not @I18nFormatBottom!"; - - // GLB of @I18nInvalidFormat(null) and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NINVALIDFORMAT, I18NUNKNOWNFORMAT), - I18NINVALIDFORMAT) - : "GLB of @I18nInvalidFormat(null) and @I18nUnknownFormat is not @I18nInvalidFormat(null)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NINVALIDFORMAT, I18NFORMAT), I18NFORMATBOTTOM) - : "GLB of @I18nInvalidFormat(null) and @I18nFormat(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NINVALIDFORMAT, i18nFormatUnusedAnno), - I18NFORMATBOTTOM) - : "GLB of @I18nInvalidFormat(null) and @I18nFormat(UNUSED) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NINVALIDFORMAT, I18NFORMATFOR), I18NFORMATBOTTOM) - : "GLB of @I18nInvalidFormat(null) and @I18nFormatFor(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NINVALIDFORMAT, i18nFormatForWithValue1), - I18NFORMATBOTTOM) - : "GLB of @I18nInvalidFormat(null) and @I18nFormatFor(\"#1\") is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NINVALIDFORMAT, I18NFORMATBOTTOM), - I18NFORMATBOTTOM) - : "GLB of @I18nInvalidFormat(null) and @I18nFormatBottom is not @I18nFormatBottom!"; - - // GLB of @I18nInvalidFormat("Message") and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nInvalidFormatWithMessage, I18NUNKNOWNFORMAT), - i18nInvalidFormatWithMessage) - : "GLB of @I18nInvalidFormat(\"Message\") and @I18nUnknownFormat is not @I18nInvalidFormat(\"Message\")!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nInvalidFormatWithMessage, I18NFORMAT), - I18NFORMATBOTTOM) - : "GLB of @I18nInvalidFormat(\"Message\") and @I18nFormat(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nInvalidFormatWithMessage, i18nFormatUnusedAnno), - I18NFORMATBOTTOM) - : "GLB of @I18nInvalidFormat(\"Message\") and @I18nFormat(UNUSED) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound( - i18nInvalidFormatWithMessage, i18nInvalidFormatWithMessage), - i18nInvalidFormatWithMessage) - : "GLB of @I18nInvalidFormat(\"Message\") and @I18nInvalidFormat(\"Message\") is not @I18nInvalidFormat(\"Message\")!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound( - i18nInvalidFormatWithMessage, i18nInvalidFormatWithMessage2), - i18nInvalidFormatWithMessagesAnded) - : "GLB of @I18nInvalidFormat(\"Message\") and @I18nInvalidFormat(\"Message2\") is not @I18nInvalidFormat(\"(\"Message\" and \"Message2\")\")!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nInvalidFormatWithMessage, I18NFORMATFOR), - I18NFORMATBOTTOM) - : "GLB of @I18nInvalidFormat(\"Message\") and @I18nFormatFor(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound( - i18nInvalidFormatWithMessage, i18nFormatForWithValue1), - I18NFORMATBOTTOM) - : "GLB of @I18nInvalidFormat(\"Message\") and @I18nFormatFor(\"#1\") is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nInvalidFormatWithMessage, I18NFORMATBOTTOM), - I18NFORMATBOTTOM) - : "GLB of @I18nInvalidFormat(\"Message\") and @I18nFormatBottom is not @I18nFormatBottom!"; - - // GLB of @I18nFormatFor(null) and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATFOR, I18NUNKNOWNFORMAT), I18NFORMATFOR) - : "GLB of @I18nFormatFor(null) and @I18nUnknownFormat is not @I18nFormatFor(null)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATFOR, I18NFORMAT), I18NFORMATBOTTOM) - : "GLB of @I18nFormatFor(null) and @I18nFormat(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATFOR, i18nFormatUnusedAnno), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatFor(null) and @I18nFormat(UNUSED) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATFOR, I18NINVALIDFORMAT), I18NFORMATBOTTOM) - : "GLB of @I18nFormatFor(null) and @I18nInvalidFormat(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATFOR, i18nInvalidFormatWithMessage), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatFor(null) and @I18nInvalidFormat(\"Message\") is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATFOR, I18NFORMATFOR), I18NFORMATFOR) - : "GLB of @I18nFormatFor(null) and @I18nFormatFor(null) is not @I18nFormatFor(null)!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATFOR, i18nFormatForWithValue1), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatFor(null) and @I18nFormatFor(\"#1\") is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATFOR, I18NFORMATBOTTOM), I18NFORMATBOTTOM) - : "GLB of @I18nFormatFor(null) and @I18nFormatBottom is not @I18nFormatBottom!"; - - // GLB of @I18nFormatFor("#1") and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatForWithValue1, I18NUNKNOWNFORMAT), - i18nFormatForWithValue1) - : "GLB of @I18nFormatFor(\"#1\") and @I18nUnknownFormat is not @I18nFormatFor(\"#1\")!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatForWithValue1, I18NFORMAT), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatFor(\"#1\") and @I18nFormat(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatForWithValue1, i18nFormatUnusedAnno), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatFor(\"#1\") and @I18nFormat(UNUSED) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatForWithValue1, I18NINVALIDFORMAT), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatFor(\"#1\") and @I18nInvalidFormat(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound( - i18nFormatForWithValue1, i18nInvalidFormatWithMessage), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatFor(\"#1\") and @I18nInvalidFormat(\"Message\") is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatForWithValue1, I18NFORMATFOR), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatFor(\"#1\") and @I18nFormatFor(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatForWithValue1, i18nFormatForWithValue1), - i18nFormatForWithValue1) - : "GLB of @I18nFormatFor(\"#1\") and @I18nFormatFor(\"#1\") is not @I18nFormatFor(\"#1\")!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(i18nFormatForWithValue1, I18NFORMATBOTTOM), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatFor(\"#1\") and @I18nFormatBottom is not @I18nFormatBottom!"; - - // GLB of @I18nFormatBottom and others - - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATBOTTOM, I18NUNKNOWNFORMAT), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatBottom and @I18nUnknownFormat is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATBOTTOM, I18NFORMAT), I18NFORMATBOTTOM) - : "GLB of @I18nFormatBottom and @I18nFormat(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATBOTTOM, i18nFormatUnusedAnno), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatBottom and @I18nFormat(UNUSED) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATBOTTOM, I18NINVALIDFORMAT), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatBottom and @I18nInvalidFormat(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATBOTTOM, i18nInvalidFormatWithMessage), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatBottom and @I18nInvalidFormat(\"Message\") is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATBOTTOM, I18NFORMATFOR), I18NFORMATBOTTOM) - : "GLB of @I18nFormatBottom and @I18nFormatFor(null) is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATBOTTOM, i18nFormatForWithValue1), - I18NFORMATBOTTOM) - : "GLB of @I18nFormatBottom and @I18nFormatFor(\"#1\") is not @I18nFormatBottom!"; - assert AnnotationUtils.areSame( - qh.greatestLowerBound(I18NFORMATBOTTOM, I18NFORMATBOTTOM), I18NFORMATBOTTOM) - : "GLB of @I18nFormatBottom and @I18nFormatBottom is not @I18nFormatBottom!"; - - // ** LUB tests ** - - // LUB of UNUSED and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatUnusedAnno, i18nFormatUnusedAnno), - i18nFormatUnusedAnno) - : "LUB of @I18nFormat(UNUSED) and @I18nFormat(UNUSED) is not @I18nFormat(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatUnusedAnno, i18nFormatGeneralAnno), - i18nFormatGeneralAnno) - : "LUB of @I18nFormat(UNUSED) and @I18nFormat(GENERAL) is not @I18nFormat(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatUnusedAnno, i18nFormatDateAnno), - i18nFormatDateAnno) - : "LUB of @I18nFormat(UNUSED) and @I18nFormat(DATE) is not @I18nFormat(DATE)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatUnusedAnno, i18nFormatNumberAnno), - i18nFormatNumberAnno) - : "LUB of @I18nFormat(UNUSED) and @I18nFormat(NUMBER) is not @I18nFormat(NUMBER)!"; - - // LUB of GENERAL and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatGeneralAnno, i18nFormatUnusedAnno), - i18nFormatGeneralAnno) - : "LUB of @I18nFormat(GENERAL) and @I18nFormat(UNUSED) is not @I18nFormat(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatGeneralAnno, i18nFormatGeneralAnno), - i18nFormatGeneralAnno) - : "LUB of @I18nFormat(GENERAL) and @I18nFormat(GENERAL) is not @I18nFormat(GENERAL)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatGeneralAnno, i18nFormatDateAnno), - i18nFormatDateAnno) - : "LUB of @I18nFormat(GENERAL) and @I18nFormat(DATE) is not @I18nFormat(DATE)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatGeneralAnno, i18nFormatNumberAnno), - i18nFormatNumberAnno) - : "LUB of @I18nFormat(GENERAL) and @I18nFormat(NUMBER) is not @I18nFormat(NUMBER)!"; - - // LUB of DATE and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatDateAnno, i18nFormatUnusedAnno), - i18nFormatDateAnno) - : "LUB of @I18nFormat(DATE) and @I18nFormat(UNUSED) is not @I18nFormat(DATE)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatDateAnno, i18nFormatGeneralAnno), - i18nFormatDateAnno) - : "LUB of @I18nFormat(DATE) and @I18nFormat(GENERAL) is not @I18nFormat(DATE)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatDateAnno, i18nFormatDateAnno), - i18nFormatDateAnno) - : "LUB of @I18nFormat(DATE) and @I18nFormat(DATE) is not @I18nFormat(DATE)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatDateAnno, i18nFormatNumberAnno), - i18nFormatNumberAnno) - : "LUB of @I18nFormat(DATE) and @I18nFormat(NUMBER) is not @I18nFormat(NUMBER)!"; - - // LUB of NUMBER and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatNumberAnno, i18nFormatUnusedAnno), - i18nFormatNumberAnno) - : "LUB of @I18nFormat(NUMBER) and @I18nFormat(UNUSED) is not @I18nFormat(NUMBER)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatNumberAnno, i18nFormatGeneralAnno), - i18nFormatNumberAnno) - : "LUB of @I18nFormat(NUMBER) and @I18nFormat(GENERAL) is not @I18nFormat(NUMBER)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatNumberAnno, i18nFormatDateAnno), - i18nFormatNumberAnno) - : "LUB of @I18nFormat(NUMBER) and @I18nFormat(DATE) is not @I18nFormat(NUMBER)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatNumberAnno, i18nFormatNumberAnno), - i18nFormatNumberAnno) - : "LUB of @I18nFormat(NUMBER) and @I18nFormat(NUMBER) is not @I18nFormat(NUMBER)!"; - - // Now test with two I18nConversionCategory at a time: - - cc2[0] = I18nConversionCategory.DATE; - cc2[1] = I18nConversionCategory.NUMBER; - AnnotationMirror formatTwoConvCat4 = treeUtil.categoriesToFormatAnnotation(cc2); - - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatTwoConvCat1, formatTwoConvCat2), formatTwoConvCat4) - : "LUB of @I18nFormat([DATE,DATE]) and @I18nFormat([UNUSED,NUMBER]) is not @I18nFormat([DATE,NUMBER])!"; - - // Test that the LUB of two I18nConversionCategory arrays of different sizes is an array of - // the largest size of the two: - - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatGeneralAnno, formatTwoConvCat1), - formatTwoConvCat1) - : "LUB of @I18nFormat(GENERAL) and @I18nFormat([DATE,DATE]) is not @I18nFormat([DATE,DATE])!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(formatTwoConvCat2, i18nFormatDateAnno), - formatTwoConvCat4) - : "LUB of @I18nFormat([UNUSED,NUMBER]) and @I18nFormat(DATE) is not @I18nFormat([DATE,NUMBER])!"; - - // LUB of two distinct @I18nFormatFor(...) annotations is @I18nUnknownFormat - - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatForWithValue1, i18nFormatForWithValue2), - I18NUNKNOWNFORMAT) - : "LUB of @I18nFormatFor(\"#1\") and @I18nFormatFor(\"#2\") is not @I18nUnknownFormat!"; - - // LUB of @I18nUnknownFormat and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NUNKNOWNFORMAT, I18NUNKNOWNFORMAT), I18NUNKNOWNFORMAT) - : "LUB of @I18nUnknownFormat and @I18nUnknownFormat is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NUNKNOWNFORMAT, I18NFORMAT), I18NUNKNOWNFORMAT) - : "LUB of @I18nUnknownFormat and @I18nFormat(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NUNKNOWNFORMAT, i18nFormatUnusedAnno), - I18NUNKNOWNFORMAT) - : "LUB of @I18nUnknownFormat and @I18nFormat(UNUSED) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NUNKNOWNFORMAT, I18NINVALIDFORMAT), I18NUNKNOWNFORMAT) - : "LUB of @I18nUnknownFormat and @I18nInvalidFormat(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NUNKNOWNFORMAT, i18nInvalidFormatWithMessage), - I18NUNKNOWNFORMAT) - : "LUB of @I18nUnknownFormat and @I18nInvalidFormat(\"Message\") is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NUNKNOWNFORMAT, I18NFORMATFOR), I18NUNKNOWNFORMAT) - : "LUB of @I18nUnknownFormat and @I18nFormatFor(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NUNKNOWNFORMAT, i18nFormatForWithValue1), - I18NUNKNOWNFORMAT) - : "LUB of @I18nUnknownFormat and @I18nFormatFor(\"#1\") is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NUNKNOWNFORMAT, I18NFORMATBOTTOM), I18NUNKNOWNFORMAT) - : "LUB of @I18nUnknownFormat and @I18nFormatBottom is not @I18nUnknownFormat!"; - - // LUB of @I18nFormat(null) and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMAT, I18NUNKNOWNFORMAT), I18NUNKNOWNFORMAT) - : "LUB of @I18nFormat(null) and @I18nUnknownFormat is not @I18nUnknownFormat!"; - // Computing the LUB of @I18nFormat(null) and @I18nFormat(null) should never occur in - // practice. Skipping this case as it causes an expected crash. - // Computing the LUB of @I18nFormat(null) and @I18nFormat with a value should never occur in - // practice. Skipping this case as it causes an expected crash. - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMAT, I18NINVALIDFORMAT), I18NUNKNOWNFORMAT) - : "LUB of @I18nFormat(null) and @I18nInvalidFormat(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMAT, i18nInvalidFormatWithMessage), - I18NUNKNOWNFORMAT) - : "LUB of @I18nFormat(null) and @I18nInvalidFormat(\"Message\") is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMAT, I18NFORMATFOR), I18NUNKNOWNFORMAT) - : "LUB of @I18nFormat(null) and @I18nFormatFor(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMAT, i18nFormatForWithValue1), I18NUNKNOWNFORMAT) - : "LUB of @I18nFormat(null) and @I18nFormatFor(\"#1\") is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame(qh.leastUpperBound(I18NFORMAT, I18NFORMATBOTTOM), I18NFORMAT) - : "LUB of @I18nFormat(null) and @I18nFormatBottom is not @I18nFormat(null)!"; - - // LUB of @I18nFormat(UNUSED) and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatUnusedAnno, I18NUNKNOWNFORMAT), - I18NUNKNOWNFORMAT) - : "LUB of @I18nFormat(UNUSED) and @I18nUnknownFormat is not @I18nUnknownFormat!"; - // Computing the LUB of @I18nFormat with a value and @I18nFormat(null) should never occur in - // practice. Skipping this case as it causes an expected crash. - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatUnusedAnno, i18nFormatUnusedAnno), - i18nFormatUnusedAnno) - : "LUB of @I18nFormat(UNUSED) and @I18nFormat(UNUSED) is not @I18nFormat(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatUnusedAnno, I18NINVALIDFORMAT), - I18NUNKNOWNFORMAT) - : "LUB of @I18nFormat(UNUSED) and @I18nInvalidFormat(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatUnusedAnno, i18nInvalidFormatWithMessage), - I18NUNKNOWNFORMAT) - : "LUB of @I18nFormat(UNUSED) and @I18nInvalidFormat(\"Message\") is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatUnusedAnno, I18NFORMATFOR), I18NUNKNOWNFORMAT) - : "LUB of @I18nFormat(UNUSED) and @I18nFormatFor(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatUnusedAnno, i18nFormatForWithValue1), - I18NUNKNOWNFORMAT) - : "LUB of @I18nFormat(UNUSED) and @I18nFormatFor(\"#1\") is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatUnusedAnno, I18NFORMATBOTTOM), - i18nFormatUnusedAnno) - : "LUB of @I18nFormat(UNUSED) and @I18nFormatBottom is not @I18nFormat(UNUSED)!"; - - // LUB of @I18nInvalidFormat(null) and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NINVALIDFORMAT, I18NUNKNOWNFORMAT), I18NUNKNOWNFORMAT) - : "LUB of @I18nInvalidFormat(null) and @I18nUnknownFormat is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NINVALIDFORMAT, I18NFORMAT), I18NUNKNOWNFORMAT) - : "LUB of @I18nInvalidFormat(null) and @I18nFormat(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NINVALIDFORMAT, i18nFormatUnusedAnno), - I18NUNKNOWNFORMAT) - : "LUB of @I18nInvalidFormat(null) and @I18nFormat(UNUSED) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NINVALIDFORMAT, I18NFORMATFOR), I18NUNKNOWNFORMAT) - : "LUB of @I18nInvalidFormat(null) and @I18nFormatFor(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NINVALIDFORMAT, i18nFormatForWithValue1), - I18NUNKNOWNFORMAT) - : "LUB of @I18nInvalidFormat(null) and @I18nFormatFor(\"#1\") is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NINVALIDFORMAT, I18NFORMATBOTTOM), I18NINVALIDFORMAT) - : "LUB of @I18nInvalidFormat(null) and @I18nFormatBottom is not @I18nInvalidFormat(null)!"; - - // LUB of @I18nInvalidFormat("Message") and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nInvalidFormatWithMessage, I18NUNKNOWNFORMAT), - I18NUNKNOWNFORMAT) - : "LUB of @I18nInvalidFormat(\"Message\") and @I18nUnknownFormat is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nInvalidFormatWithMessage, I18NFORMAT), - I18NUNKNOWNFORMAT) - : "LUB of @I18nInvalidFormat(\"Message\") and @I18nFormat(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nInvalidFormatWithMessage, i18nFormatUnusedAnno), - I18NUNKNOWNFORMAT) - : "LUB of @I18nInvalidFormat(\"Message\") and @I18nFormat(UNUSED) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound( - i18nInvalidFormatWithMessage, i18nInvalidFormatWithMessage), - i18nInvalidFormatWithMessage) - : "LUB of @I18nInvalidFormat(\"Message\") and @I18nInvalidFormat(\"Message\") is not @I18nInvalidFormat(\"Message\")!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound( - i18nInvalidFormatWithMessage, i18nInvalidFormatWithMessage2), - i18nInvalidFormatWithMessagesOred) - : "LUB of @I18nInvalidFormat(\"Message\") and @I18nInvalidFormat(\"Message2\") is not @I18nInvalidFormat(\"(\"Message\" or \"Message2\")\")!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nInvalidFormatWithMessage, I18NFORMATFOR), - I18NUNKNOWNFORMAT) - : "LUB of @I18nInvalidFormat(\"Message\") and @I18nFormatFor(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nInvalidFormatWithMessage, i18nFormatForWithValue1), - I18NUNKNOWNFORMAT) - : "LUB of @I18nInvalidFormat(\"Message\") and @I18nFormatFor(\"#1\") is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nInvalidFormatWithMessage, I18NFORMATBOTTOM), - i18nInvalidFormatWithMessage) - : "LUB of @I18nInvalidFormat(\"Message\") and @I18nFormatBottom is not @I18nInvalidFormat(\"Message\")!"; - - // LUB of @I18nFormatFor(null) and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATFOR, I18NUNKNOWNFORMAT), I18NUNKNOWNFORMAT) - : "LUB of @I18nFormatFor(null) and @I18nUnknownFormat is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATFOR, I18NFORMAT), I18NUNKNOWNFORMAT) - : "LUB of @I18nFormatFor(null) and @I18nFormat(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATFOR, i18nFormatUnusedAnno), I18NUNKNOWNFORMAT) - : "LUB of @I18nFormatFor(null) and @I18nFormat(UNUSED) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATFOR, I18NINVALIDFORMAT), I18NUNKNOWNFORMAT) - : "LUB of @I18nFormatFor(null) and @I18nInvalidFormat(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATFOR, i18nInvalidFormatWithMessage), - I18NUNKNOWNFORMAT) - : "LUB of @I18nFormatFor(null) and @I18nInvalidFormat(\"Message\") is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATFOR, I18NFORMATFOR), I18NFORMATFOR) - : "LUB of @I18nFormatFor(null) and @I18nFormatFor(null) is not @I18nFormatFor(null)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATFOR, i18nFormatForWithValue1), - I18NUNKNOWNFORMAT) - : "LUB of @I18nFormatFor(null) and @I18nFormatFor(\"#1\") is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATFOR, I18NFORMATBOTTOM), I18NFORMATFOR) - : "LUB of @I18nFormatFor(null) and @I18nFormatBottom is not @I18nFormatFor(null)!"; - - // LUB of @I18nFormatFor("#1") and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatForWithValue1, I18NUNKNOWNFORMAT), - I18NUNKNOWNFORMAT) - : "LUB of @I18nFormatFor(\"#1\") and @I18nUnknownFormat is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatForWithValue1, I18NFORMAT), I18NUNKNOWNFORMAT) - : "LUB of @I18nFormatFor(\"#1\") and @I18nFormat(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatForWithValue1, i18nFormatUnusedAnno), - I18NUNKNOWNFORMAT) - : "LUB of @I18nFormatFor(\"#1\") and @I18nFormat(UNUSED) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatForWithValue1, I18NINVALIDFORMAT), - I18NUNKNOWNFORMAT) - : "LUB of @I18nFormatFor(\"#1\") and @I18nInvalidFormat(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatForWithValue1, i18nInvalidFormatWithMessage), - I18NUNKNOWNFORMAT) - : "LUB of @I18nFormatFor(\"#1\") and @I18nInvalidFormat(\"Message\") is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatForWithValue1, I18NFORMATFOR), - I18NUNKNOWNFORMAT) - : "LUB of @I18nFormatFor(\"#1\") and @I18nFormatFor(null) is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatForWithValue1, i18nFormatForWithValue1), - i18nFormatForWithValue1) - : "LUB of @I18nFormatFor(\"#1\") and @I18nFormatFor(\"#1\") is not @I18nFormatFor(\"#1\")!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(i18nFormatForWithValue1, I18NFORMATBOTTOM), - i18nFormatForWithValue1) - : "LUB of @I18nFormatFor(\"#1\") and @I18nFormatBottom is not @I18nFormatFor(\"#1\")!"; - - // LUB of @I18nFormatBottom and others - - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATBOTTOM, I18NUNKNOWNFORMAT), I18NUNKNOWNFORMAT) - : "LUB of @I18nFormatBottom and @I18nUnknownFormat is not @I18nUnknownFormat!"; - assert AnnotationUtils.areSame(qh.leastUpperBound(I18NFORMATBOTTOM, I18NFORMAT), I18NFORMAT) - : "LUB of @I18nFormatBottom and @I18nFormat(null) is not @I18nFormat(null)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATBOTTOM, i18nFormatUnusedAnno), - i18nFormatUnusedAnno) - : "LUB of @I18nFormatBottom and @I18nFormat(UNUSED) is not @I18nFormat(UNUSED)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATBOTTOM, I18NINVALIDFORMAT), I18NINVALIDFORMAT) - : "LUB of @I18nFormatBottom and @I18nInvalidFormat(null) is not @I18nInvalidFormat(null)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATBOTTOM, i18nInvalidFormatWithMessage), - i18nInvalidFormatWithMessage) - : "LUB of @I18nFormatBottom and @I18nInvalidFormat(\"Message\") is not @I18nInvalidFormat(\"Message\")!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATBOTTOM, I18NFORMATFOR), I18NFORMATFOR) - : "LUB of @I18nFormatBottom and @I18nFormatFor(null) is not @I18nFormatFor(null)!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATBOTTOM, i18nFormatForWithValue1), - i18nFormatForWithValue1) - : "LUB of @I18nFormatBottom and @I18nFormatFor(\"#1\") is not @I18nFormatFor(\"#1\")!"; - assert AnnotationUtils.areSame( - qh.leastUpperBound(I18NFORMATBOTTOM, I18NFORMATBOTTOM), I18NFORMATBOTTOM) - : "LUB of @I18nFormatBottom and @I18nFormatBottom is not @I18nFormatBottom!"; - } -} diff --git a/checker/src/test/java/tests/FenumTest.java b/checker/src/test/java/tests/FenumTest.java deleted file mode 100644 index c8f6be5e1efc..000000000000 --- a/checker/src/test/java/tests/FenumTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package tests; - -import java.io.File; -import java.util.List; -import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; -import org.junit.runners.Parameterized.Parameters; - -public class FenumTest extends CheckerFrameworkPerDirectoryTest { - - /** - * Create a FenumTest. - * - * @param testFiles the files containing test code, which will be type-checked - */ - public FenumTest(List testFiles) { - super( - testFiles, - org.checkerframework.checker.fenum.FenumChecker.class, - "fenum", - "-Anomsgtext"); - } - - @Parameters - public static String[] getTestDirs() { - return new String[] {"fenum", "all-systems"}; - } -} diff --git a/checker/src/test/java/tests/FormatterLubGlbCheckerTest.java b/checker/src/test/java/tests/FormatterLubGlbCheckerTest.java deleted file mode 100644 index e370ee837a87..000000000000 --- a/checker/src/test/java/tests/FormatterLubGlbCheckerTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package tests; - -// Test case for issue 691. -// https://github.com/typetools/checker-framework/issues/691 -// This exists to just run the FormatterLubGlbChecker - -import java.io.File; -import java.util.List; -import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; -import org.junit.runners.Parameterized.Parameters; -import testlib.lubglb.FormatterLubGlbChecker; - -public class FormatterLubGlbCheckerTest extends CheckerFrameworkPerDirectoryTest { - - /** - * Create a FormatterLubGlbCheckerTest. - * - * @param testFiles the files containing test code, which will be type-checked - */ - public FormatterLubGlbCheckerTest(List testFiles) { - super( - testFiles, - FormatterLubGlbChecker.class, - "", - "-Anomsgtext", - "-AcheckPurityAnnotations"); - } - - @Parameters - public static String[] getTestDirs() { - return new String[] {"formatter-lubglb"}; - } -} diff --git a/checker/src/test/java/tests/I18nFormatterLubGlbCheckerTest.java b/checker/src/test/java/tests/I18nFormatterLubGlbCheckerTest.java deleted file mode 100644 index ea2697f404a5..000000000000 --- a/checker/src/test/java/tests/I18nFormatterLubGlbCheckerTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package tests; - -// Test case for issue 723. -// https://github.com/typetools/checker-framework/issues/723 -// This exists to just run the I18nFormatterLubGlbChecker - -import java.io.File; -import java.util.List; -import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; -import org.junit.runners.Parameterized.Parameters; -import testlib.lubglb.I18nFormatterLubGlbChecker; - -public class I18nFormatterLubGlbCheckerTest extends CheckerFrameworkPerDirectoryTest { - - /** - * Create an I18nFormatterLubGlbCheckerTest. - * - * @param testFiles the files containing test code, which will be type-checked - */ - public I18nFormatterLubGlbCheckerTest(List testFiles) { - super( - testFiles, - I18nFormatterLubGlbChecker.class, - "", - "-Anomsgtext", - "-AcheckPurityAnnotations"); - } - - @Parameters - public static String[] getTestDirs() { - return new String[] {"i18n-formatter-lubglb"}; - } -} diff --git a/checker/src/test/java/tests/LockTest.java b/checker/src/test/java/tests/LockTest.java deleted file mode 100644 index ca1ff35f9c14..000000000000 --- a/checker/src/test/java/tests/LockTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package tests; - -import java.io.File; -import java.util.List; -import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; -import org.junit.runners.Parameterized.Parameters; - -public class LockTest extends CheckerFrameworkPerDirectoryTest { - - /** - * Create a LockTest. - * - * @param testFiles the files containing test code, which will be type-checked - */ - public LockTest(List testFiles) { - super( - testFiles, - org.checkerframework.checker.lock.LockChecker.class, - "lock", - "-Anomsgtext"); - } - - @Parameters - public static String[] getTestDirs() { - return new String[] {"lock", "all-systems"}; - } -} diff --git a/checker/src/test/java/tests/NestedAggregateCheckerTest.java b/checker/src/test/java/tests/NestedAggregateCheckerTest.java deleted file mode 100644 index dd1305aff4c4..000000000000 --- a/checker/src/test/java/tests/NestedAggregateCheckerTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package tests; - -// Test case for issue 343. -// https://github.com/typetools/checker-framework/issues/343 -// This exists to just run the NestedAggregateChecker - -import java.io.File; -import java.util.List; -import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; -import org.junit.runners.Parameterized.Parameters; -import testlib.NestedAggregateChecker; - -public class NestedAggregateCheckerTest extends CheckerFrameworkPerDirectoryTest { - - /** - * Create a NestedAggregateCheckerTest. - * - * @param testFiles the files containing test code, which will be type-checked - */ - public NestedAggregateCheckerTest(List testFiles) { - super( - testFiles, - NestedAggregateChecker.class, - "", - "-Anomsgtext", - "-AcheckPurityAnnotations"); - } - - @Parameters - public static String[] getTestDirs() { - return new String[] {"aggregate", "all-systems"}; - } -} diff --git a/checker/src/test/java/tests/NullnessFbcJavacErrorsTest.java b/checker/src/test/java/tests/NullnessFbcJavacErrorsTest.java deleted file mode 100644 index 2fb6aa78a186..000000000000 --- a/checker/src/test/java/tests/NullnessFbcJavacErrorsTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package tests; - -import java.io.File; -import org.checkerframework.checker.nullness.NullnessChecker; -import org.checkerframework.framework.test.CheckerFrameworkPerFileTest; -import org.junit.runners.Parameterized.Parameters; - -/** JUnit tests for the Nullness checker that issue javac errors. */ -public class NullnessFbcJavacErrorsTest extends CheckerFrameworkPerFileTest { - - public NullnessFbcJavacErrorsTest(File testFile) { - // TODO: remove soundArrayCreationNullness option once it's no - // longer needed. See issue #986: - // https://github.com/typetools/checker-framework/issues/986 - super( - testFile, - org.checkerframework.checker.nullness.NullnessChecker.class, - "nullness", - "-AcheckPurityAnnotations", - "-Anomsgtext", - "-Xlint:deprecation", - "-Alint=soundArrayCreationNullness," - + NullnessChecker.LINT_REDUNDANTNULLCOMPARISON); - } - - @Parameters - public static String[] getTestDirs() { - return new String[] {"nullness-javac-errors"}; - } -} diff --git a/checker/src/test/java/tests/NullnessFbcTest.java b/checker/src/test/java/tests/NullnessFbcTest.java deleted file mode 100644 index fa2e6f7427e0..000000000000 --- a/checker/src/test/java/tests/NullnessFbcTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package tests; - -import java.io.File; -import java.util.List; -import org.checkerframework.checker.nullness.NullnessChecker; -import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; -import org.junit.runners.Parameterized.Parameters; - -/** JUnit tests for the Nullness checker. */ -public class NullnessFbcTest extends CheckerFrameworkPerDirectoryTest { - - /** - * Create a NullnessFbcTest. - * - * @param testFiles the files containing test code, which will be type-checked - */ - public NullnessFbcTest(List testFiles) { - // TODO: remove soundArrayCreationNullness option once it's no - // longer needed. See issue #986: - // https://github.com/typetools/checker-framework/issues/986 - super( - testFiles, - org.checkerframework.checker.nullness.NullnessChecker.class, - "nullness", - "-AcheckPurityAnnotations", - "-Anomsgtext", - "-Xlint:deprecation", - "-Alint=soundArrayCreationNullness," - + NullnessChecker.LINT_REDUNDANTNULLCOMPARISON); - } - - @Parameters - public static String[] getTestDirs() { - return new String[] {"nullness", "initialization/fbc", "all-systems"}; - } -} diff --git a/checker/src/test/java/tests/NullnessGenericWildcardTest.java b/checker/src/test/java/tests/NullnessGenericWildcardTest.java deleted file mode 100644 index 63d5e51a9575..000000000000 --- a/checker/src/test/java/tests/NullnessGenericWildcardTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package tests; - -import java.io.File; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.checkerframework.framework.test.*; -import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; -import org.junit.runners.Parameterized.Parameters; - -/** JUnit tests for the Nullness checker for issue #511. */ -public class NullnessGenericWildcardTest extends CheckerFrameworkPerDirectoryTest { - - /** - * Create a NullnessGenericWildcardTest. - * - * @param testFiles the files containing test code, which will be type-checked - */ - public NullnessGenericWildcardTest(List testFiles) { - super( - testFiles, - org.checkerframework.checker.nullness.NullnessChecker.class, - "nullness", - // This test reads bytecode .class files created by NullnessGenericWildcardLibTest - "-cp", - "dist/checker.jar:tests/build/testclasses/", - "-Anomsgtext"); - } - - @Parameters - public static String[] getTestDirs() { - return new String[] {"nullness-genericwildcard"}; - } - - @Override - public void run() { - boolean shouldEmitDebugInfo = TestUtilities.getShouldEmitDebugInfo(); - List customizedOptions1 = customizeOptions(Arrays.asList("-Anomsgtext")); - TestConfiguration config1 = - TestConfigurationBuilder.buildDefaultConfiguration( - "tests/nullness-genericwildcardlib", - new File("tests/nullness-genericwildcardlib", "GwiParent.java"), - checkerName, - customizedOptions1, - shouldEmitDebugInfo); - TypecheckResult testResult1 = new TypecheckExecutor().runTest(config1); - TestUtilities.assertResultsAreValid(testResult1); - - List customizedOptions2 = - customizeOptions(Collections.unmodifiableList(checkerOptions)); - TestConfiguration config2 = - TestConfigurationBuilder.buildDefaultConfiguration( - testDir, - testFiles, - Collections.singleton(checkerName), - customizedOptions2, - shouldEmitDebugInfo); - TypecheckResult testResult2 = new TypecheckExecutor().runTest(config2); - TestUtilities.assertResultsAreValid(testResult2); - } -} diff --git a/checker/src/test/java/tests/NullnessSafeDefaultsSourceCodeTest.java b/checker/src/test/java/tests/NullnessSafeDefaultsSourceCodeTest.java deleted file mode 100644 index 813a44432306..000000000000 --- a/checker/src/test/java/tests/NullnessSafeDefaultsSourceCodeTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package tests; - -import java.io.File; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import org.checkerframework.framework.test.*; -import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; -import org.junit.runners.Parameterized.Parameters; - -/** JUnit tests for the Nullness checker when using safe defaults for unannotated source code. */ -public class NullnessSafeDefaultsSourceCodeTest extends CheckerFrameworkPerDirectoryTest { - - /** - * Create a NullnessSafeDefaultsSourceCodeTest. - * - * @param testFiles the files containing test code, which will be type-checked - */ - public NullnessSafeDefaultsSourceCodeTest(List testFiles) { - super( - testFiles, - org.checkerframework.checker.nullness.NullnessChecker.class, - "nullness", - "-AuseConservativeDefaultsForUncheckedCode=source", - "-cp", - "dist/checker.jar:tests/build/testclasses/", - "-Anomsgtext"); - } - - @Parameters - public static String[] getTestDirs() { - return new String[] {"nullness-safedefaultssourcecode"}; - } - - @Override - public void run() { - boolean shouldEmitDebugInfo = TestUtilities.getShouldEmitDebugInfo(); - List customizedOptions1 = - customizeOptions( - Arrays.asList( - "-AuseConservativeDefaultsForUncheckedCode=source,bytecode", - "-Anomsgtext")); - TestConfiguration config1 = - TestConfigurationBuilder.buildDefaultConfiguration( - "tests/nullness-safedefaultssourcecodelib", - new File("tests/nullness-safedefaultssourcecodelib", "Lib.java"), - checkerName, - customizedOptions1, - shouldEmitDebugInfo); - TypecheckResult testResult1 = new TypecheckExecutor().runTest(config1); - TestUtilities.assertResultsAreValid(testResult1); - - List customizedOptions2 = - customizeOptions(Collections.unmodifiableList(checkerOptions)); - TestConfiguration config2 = - TestConfigurationBuilder.buildDefaultConfiguration( - testDir, - testFiles, - Collections.singleton(checkerName), - customizedOptions2, - shouldEmitDebugInfo); - TypecheckResult testResult2 = new TypecheckExecutor().runTest(config2); - TestUtilities.assertResultsAreValid(testResult2); - } -} diff --git a/checker/src/test/java/tests/NullnessStubfileTest.java b/checker/src/test/java/tests/NullnessStubfileTest.java deleted file mode 100644 index 33cc78353a33..000000000000 --- a/checker/src/test/java/tests/NullnessStubfileTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package tests; - -import java.io.File; -import java.util.List; -import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; -import org.junit.runners.Parameterized.Parameters; - -public class NullnessStubfileTest extends CheckerFrameworkPerDirectoryTest { - - /** - * Create a NullnessStubfileTest. - * - * @param testFiles the files containing test code, which will be type-checked - */ - public NullnessStubfileTest(List testFiles) { - super( - testFiles, - org.checkerframework.checker.nullness.NullnessChecker.class, - "nullness", - "-Anomsgtext", - "-AstubWarnIfNotFound", - "-Astubs=" - + "tests/nullness-stubfile/stubfile1.astub:" - + "tests/nullness-stubfile/stubfile2.astub"); - } - - @Parameters - public static String[] getTestDirs() { - return new String[] {"nullness-stubfile"}; - } -} diff --git a/checker/src/test/java/tests/StubparserTest.java b/checker/src/test/java/tests/StubparserTest.java deleted file mode 100644 index 7ec03ee0aaf9..000000000000 --- a/checker/src/test/java/tests/StubparserTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package tests; - -import java.io.File; -import java.util.List; -import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; -import org.junit.runners.Parameterized; - -public class StubparserTest extends CheckerFrameworkPerDirectoryTest { - - /** - * Create a StubparserTest. - * - * @param testFiles the files containing test code, which will be type-checked - */ - public StubparserTest(List testFiles) { - super( - testFiles, - org.checkerframework.checker.nullness.NullnessChecker.class, - "nullness", - "-Anomsgtext", - "-AstubWarnIfNotFound"); - } - - @Parameterized.Parameters - public static String[] getTestDirs() { - return new String[] {"stubparser-tests"}; - } -} diff --git a/checker/src/test/java/tests/UnitsTest.java b/checker/src/test/java/tests/UnitsTest.java deleted file mode 100644 index 9df2723e93ff..000000000000 --- a/checker/src/test/java/tests/UnitsTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package tests; - -import java.io.File; -import java.util.List; -import org.checkerframework.framework.test.CheckerFrameworkPerDirectoryTest; -import org.junit.runners.Parameterized.Parameters; - -public class UnitsTest extends CheckerFrameworkPerDirectoryTest { - - /** - * Create a UnitsTest. - * - * @param testFiles the files containing test code, which will be type-checked - */ - public UnitsTest(List testFiles) { - super( - testFiles, - org.checkerframework.checker.units.UnitsChecker.class, - "units", - "-Anomsgtext"); - } - - @Parameters - public static String[] getTestDirs() { - return new String[] {"units", "all-systems"}; - } -} diff --git a/checker/src/testannotations/java/android/annotation/NonNull.java b/checker/src/testannotations/java/android/annotation/NonNull.java index 37555123aba5..e3c99016f357 100644 --- a/checker/src/testannotations/java/android/annotation/NonNull.java +++ b/checker/src/testannotations/java/android/annotation/NonNull.java @@ -1,4 +1,4 @@ -// Upstream version: +// Upstream version (this is a clean-room reimplementation of its interface): // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/annotation/NonNull.java package android.annotation; diff --git a/checker/src/testannotations/java/android/annotation/Nullable.java b/checker/src/testannotations/java/android/annotation/Nullable.java index 8ea1b345f026..fb1d353d2aa4 100644 --- a/checker/src/testannotations/java/android/annotation/Nullable.java +++ b/checker/src/testannotations/java/android/annotation/Nullable.java @@ -1,4 +1,4 @@ -// Upstream version: +// Upstream version (this is a clean-room reimplementation of its interface): // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/annotation/Nullable.java package android.annotation; diff --git a/checker/src/testannotations/java/android/support/annotation/NonNull.java b/checker/src/testannotations/java/android/support/annotation/NonNull.java index b7b66c2b72a6..24e84cfa2fca 100644 --- a/checker/src/testannotations/java/android/support/annotation/NonNull.java +++ b/checker/src/testannotations/java/android/support/annotation/NonNull.java @@ -1,4 +1,4 @@ -// Upstream version: +// Upstream version (this is a clean-room reimplementation of its interface): // https://android.googlesource.com/platform/frameworks/support/+/master/annotations/src/main/java/android/support/annotation/NonNull.java package android.support.annotation; diff --git a/checker/src/testannotations/java/android/support/annotation/Nullable.java b/checker/src/testannotations/java/android/support/annotation/Nullable.java index fce430028513..b6472495a34f 100644 --- a/checker/src/testannotations/java/android/support/annotation/Nullable.java +++ b/checker/src/testannotations/java/android/support/annotation/Nullable.java @@ -1,4 +1,4 @@ -// Upstream version: +// Upstream version (this is a clean-room reimplementation of its interface): // https://android.googlesource.com/platform/frameworks/support/+/master/annotations/src/main/java/android/support/annotation/Nullable.java package android.support.annotation; diff --git a/checker/src/testannotations/java/com/sun/istack/internal/Interned.java b/checker/src/testannotations/java/com/sun/istack/internal/Interned.java index 44e21dfa5bb3..f8a8f618a8ae 100644 --- a/checker/src/testannotations/java/com/sun/istack/internal/Interned.java +++ b/checker/src/testannotations/java/com/sun/istack/internal/Interned.java @@ -1,11 +1,11 @@ -// Upstream version: in the OpenJDK repository, file +// Upstream version (this is a clean-room reimplementation of its interface): +// in the OpenJDK repository, file // jaxws/src/share/jaxws_classes/com/sun/istack/internal/Interned.java package com.sun.istack.internal; 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; @@ -13,5 +13,4 @@ @Documented @Retention(RetentionPolicy.CLASS) @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE}) -@Inherited public @interface Interned {} diff --git a/checker/src/testannotations/java/com/sun/istack/internal/NotNull.java b/checker/src/testannotations/java/com/sun/istack/internal/NotNull.java index c8e29488eff3..e87ccb6d717c 100644 --- a/checker/src/testannotations/java/com/sun/istack/internal/NotNull.java +++ b/checker/src/testannotations/java/com/sun/istack/internal/NotNull.java @@ -1,4 +1,5 @@ -// Upstream version: in the OpenJDK repository, file +// Upstream version (this is a clean-room reimplementation of its interface): +// in the OpenJDK repository, file // jaxws/src/share/jaxws_classes/com/sun/istack/internal/NotNull.java package com.sun.istack.internal; diff --git a/checker/src/testannotations/java/com/sun/istack/internal/Nullable.java b/checker/src/testannotations/java/com/sun/istack/internal/Nullable.java index 5b2c00400d9b..958d417e4e73 100644 --- a/checker/src/testannotations/java/com/sun/istack/internal/Nullable.java +++ b/checker/src/testannotations/java/com/sun/istack/internal/Nullable.java @@ -1,4 +1,5 @@ -// Upstream version: in the OpenJDK repository, file +// Upstream version (this is a clean-room reimplementation of its interface): +// in the OpenJDK repository, file // jaxws/src/share/jaxws_classes/com/sun/istack/internal/Nullable.java package com.sun.istack.internal; diff --git a/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/CheckForNull.java b/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/CheckForNull.java index df13d73176c5..7c716752dba7 100644 --- a/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/CheckForNull.java +++ b/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/CheckForNull.java @@ -1,5 +1,5 @@ -// Upstream version: -// http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/CheckForNull.html +// Upstream version (this is a clean-room reimplementation of its interface): +// https://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/CheckForNull.html package edu.umd.cs.findbugs.annotations; diff --git a/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/NonNull.java b/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/NonNull.java index e61e44da9120..a952b34a3bee 100644 --- a/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/NonNull.java +++ b/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/NonNull.java @@ -1,5 +1,5 @@ -// Upstream version: -// http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/NonNull.html +// Upstream version (this is a clean-room reimplementation of its interface): +// https://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/NonNull.html package edu.umd.cs.findbugs.annotations; diff --git a/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/Nullable.java b/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/Nullable.java index cac20cfbdd80..4a03f204c67d 100644 --- a/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/Nullable.java +++ b/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/Nullable.java @@ -1,5 +1,5 @@ -// Upstream version: -// http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/Nullable.html +// Upstream version (this is a clean-room reimplementation of its interface): +// https://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/Nullable.html package edu.umd.cs.findbugs.annotations; diff --git a/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/PossiblyNull.java b/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/PossiblyNull.java index b24b9c2a2e75..2cbc4ccf592a 100644 --- a/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/PossiblyNull.java +++ b/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/PossiblyNull.java @@ -1,5 +1,5 @@ -// Upstream version: -// http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/PossiblyNull.html +// Upstream version (this is a clean-room reimplementation of its interface): +// https://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/PossiblyNull.html package edu.umd.cs.findbugs.annotations; diff --git a/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/UnknownNullness.java b/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/UnknownNullness.java index 0c945535c6da..cf7dfb91c811 100644 --- a/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/UnknownNullness.java +++ b/checker/src/testannotations/java/edu/umd/cs/findbugs/annotations/UnknownNullness.java @@ -1,5 +1,5 @@ -// Upstream version: -// http://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/UnknownNullness.html +// Upstream version (this is a clean-room reimplementation of its interface): +// https://findbugs.sourceforge.net/api/edu/umd/cs/findbugs/annotations/UnknownNullness.html package edu.umd.cs.findbugs.annotations; diff --git a/checker/src/testannotations/java/javax/annotation/Nonnull.java b/checker/src/testannotations/java/javax/annotation/Nonnull.java index 1b70811ca99d..a6f2e3bc1272 100644 --- a/checker/src/testannotations/java/javax/annotation/Nonnull.java +++ b/checker/src/testannotations/java/javax/annotation/Nonnull.java @@ -3,6 +3,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; + import javax.annotation.meta.When; @Documented diff --git a/checker/src/testannotations/java/javax/annotation/concurrent/GuardedBy.java b/checker/src/testannotations/java/javax/annotation/concurrent/GuardedBy.java index 26e5143bb446..a34b6f2e8811 100644 --- a/checker/src/testannotations/java/javax/annotation/concurrent/GuardedBy.java +++ b/checker/src/testannotations/java/javax/annotation/concurrent/GuardedBy.java @@ -1,17 +1,17 @@ package javax.annotation.concurrent; +import org.checkerframework.checker.lock.qual.LockHeld; +import org.checkerframework.framework.qual.PreconditionAnnotation; + 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.checker.lock.qual.LockHeld; -import org.checkerframework.framework.qual.PreconditionAnnotation; -// The JCIP annotation can be used on a field (in which case it corresponds -// to the Lock Checker's @GuardedBy annotation) or on a method (in which case -// it is a declaration annotation corresponding to the Lock Checker's @Holding -// annotation). +// The JCIP annotation can be used on a field (in which case it corresponds to the Lock Checker's +// @GuardedBy annotation) or on a method (in which case it is a declaration annotation corresponding +// to the Lock Checker's @Holding annotation). // It is preferred to use these Checker Framework annotations instead: // org.checkerframework.checker.lock.qual.GuardedBy // org.checkerframework.checker.lock.qual.Holding @@ -24,7 +24,7 @@ /** * The Java expressions that need to be held. * - * @see Syntax of + * @see Syntax of * Java expressions */ String[] value() default {}; diff --git a/checker/src/testannotations/java/javax/validation/constraints/NotNull.java b/checker/src/testannotations/java/javax/validation/constraints/NotNull.java deleted file mode 100644 index 585946160e87..000000000000 --- a/checker/src/testannotations/java/javax/validation/constraints/NotNull.java +++ /dev/null @@ -1,31 +0,0 @@ -// Upstream version: -// https://javaee.github.io/javaee-spec/javadocs/javax/validation/constraints/NotNull.html - -package javax.validation.constraints; - -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; - -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ - ElementType.METHOD, - ElementType.FIELD, - ElementType.ANNOTATION_TYPE, - ElementType.CONSTRUCTOR, - ElementType.PARAMETER, - ElementType.TYPE_USE -}) -public @interface NotNull { - // The following annotation attributes are allowed in source code, - // but are ignored by the Nullness Checker. - - Class[] groups() default {}; - - String message() default "{javax.validation.constraints.NotNull.message}"; - // To not depend on the Payload class, let us use a more flexible bound. - Class[] payload() default {}; -} diff --git a/checker/src/testannotations/java/lombok/NonNull.java b/checker/src/testannotations/java/lombok/NonNull.java index 5ad2d10a8ce5..fac1e99c1e93 100644 --- a/checker/src/testannotations/java/lombok/NonNull.java +++ b/checker/src/testannotations/java/lombok/NonNull.java @@ -1,5 +1,5 @@ -// Upstream version: -// https://github.com/rzwitserloot/lombok/blob/master/src/core/lombok/NonNull.java +// Upstream version (this is a clean-room reimplementation of its interface): +// https://github.com/projectlombok/lombok/blob/master/src/core/lombok/NonNull.java package lombok; @@ -11,5 +11,11 @@ @Documented @Retention(RetentionPolicy.CLASS) -@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE}) +@Target({ + ElementType.FIELD, + ElementType.METHOD, + ElementType.PARAMETER, + ElementType.LOCAL_VARIABLE, + ElementType.TYPE_USE +}) public @interface NonNull {} diff --git a/checker/src/testannotations/java/net/jcip/annotations/GuardedBy.java b/checker/src/testannotations/java/net/jcip/annotations/GuardedBy.java index 27e097d0007d..e88cc22622aa 100644 --- a/checker/src/testannotations/java/net/jcip/annotations/GuardedBy.java +++ b/checker/src/testannotations/java/net/jcip/annotations/GuardedBy.java @@ -1,20 +1,20 @@ -// Upstream version: -// http://jcip.net/annotations/doc/net/jcip/annotations/GuardedBy.html +// Upstream version (this is a clean-room reimplementation of its interface): +// https://jcip.net/annotations/doc/net/jcip/annotations/GuardedBy.html package net.jcip.annotations; +import org.checkerframework.checker.lock.qual.LockHeld; +import org.checkerframework.framework.qual.PreconditionAnnotation; + 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.checker.lock.qual.LockHeld; -import org.checkerframework.framework.qual.PreconditionAnnotation; -// The JCIP annotation can be used on a field (in which case it corresponds -// to the Lock Checker's @GuardedBy annotation) or on a method (in which case -// it is a declaration annotation corresponding to the Lock Checker's @Holding -// annotation). +// The JCIP annotation can be used on a field (in which case it corresponds to the Lock Checker's +// @GuardedBy annotation) or on a method (in which case it is a declaration annotation corresponding +// to the Lock Checker's @Holding annotation). // It is preferred to use these Checker Framework annotations instead: // org.checkerframework.checker.lock.qual.GuardedBy // org.checkerframework.checker.lock.qual.Holding @@ -27,7 +27,7 @@ /** * The Java expressions that need to be held. * - * @see Syntax of + * @see Syntax of * Java expressions */ String[] value() default {}; diff --git a/checker/src/testannotations/java/org/eclipse/jdt/annotation/NonNull.java b/checker/src/testannotations/java/org/eclipse/jdt/annotation/NonNull.java index bbefedd83a7c..26b317bb9c3b 100644 --- a/checker/src/testannotations/java/org/eclipse/jdt/annotation/NonNull.java +++ b/checker/src/testannotations/java/org/eclipse/jdt/annotation/NonNull.java @@ -1,4 +1,4 @@ -// Upstream version: +// Upstream version (this is a clean-room reimplementation of its interface): // https://help.eclipse.org/neon/index.jsp?topic=/org.eclipse.jdt.doc.isv/reference/api/org/eclipse/jdt/annotation/NonNull.html package org.eclipse.jdt.annotation; diff --git a/checker/src/testannotations/java/org/eclipse/jdt/annotation/Nullable.java b/checker/src/testannotations/java/org/eclipse/jdt/annotation/Nullable.java index 3d95cd51863e..78651ad67adf 100644 --- a/checker/src/testannotations/java/org/eclipse/jdt/annotation/Nullable.java +++ b/checker/src/testannotations/java/org/eclipse/jdt/annotation/Nullable.java @@ -1,4 +1,4 @@ -// Upstream version: +// Upstream version (this is a clean-room reimplementation of its interface): // https://help.eclipse.org/neon/index.jsp?topic=/org.eclipse.jdt.doc.isv/reference/api/org/eclipse/jdt/annotation/Nullable.html package org.eclipse.jdt.annotation; diff --git a/checker/src/testannotations/java/org/eclipse/jgit/annotations/NonNull.java b/checker/src/testannotations/java/org/eclipse/jgit/annotations/NonNull.java index b88b111ca4d7..7258192661d7 100644 --- a/checker/src/testannotations/java/org/eclipse/jgit/annotations/NonNull.java +++ b/checker/src/testannotations/java/org/eclipse/jgit/annotations/NonNull.java @@ -1,4 +1,4 @@ -// Upstream version: +// Upstream version (this is a clean-room reimplementation of its interface): // https://github.com/eclipse/jgit/blob/master/org.eclipse.jgit/src/org/eclipse/jgit/annotations/NonNull.java package org.eclipse.jgit.annotations; diff --git a/checker/src/testannotations/java/org/eclipse/jgit/annotations/Nullable.java b/checker/src/testannotations/java/org/eclipse/jgit/annotations/Nullable.java index bd2ff2e6bb2a..b134f5c7e0b5 100644 --- a/checker/src/testannotations/java/org/eclipse/jgit/annotations/Nullable.java +++ b/checker/src/testannotations/java/org/eclipse/jgit/annotations/Nullable.java @@ -1,4 +1,4 @@ -// Upstream version: +// Upstream version (this is a clean-room reimplementation of its interface): // https://github.com/eclipse/jgit/blob/master/org.eclipse.jgit/src/org/eclipse/jgit/annotations/Nullable.java package org.eclipse.jgit.annotations; diff --git a/checker/src/testannotations/java/org/jetbrains/annotations/NotNull.java b/checker/src/testannotations/java/org/jetbrains/annotations/NotNull.java index fbb85ae06818..d193e4c03d85 100644 --- a/checker/src/testannotations/java/org/jetbrains/annotations/NotNull.java +++ b/checker/src/testannotations/java/org/jetbrains/annotations/NotNull.java @@ -1,4 +1,4 @@ -// Upstream version: +// Upstream version (this is a clean-room reimplementation of its interface): // https://github.com/JetBrains/intellij-community/blob/master/platform/annotations/java8/src/org/jetbrains/annotations/NotNull.java package org.jetbrains.annotations; diff --git a/checker/src/testannotations/java/org/jetbrains/annotations/Nullable.java b/checker/src/testannotations/java/org/jetbrains/annotations/Nullable.java index 82c3101ba112..d189bf6ce7a7 100644 --- a/checker/src/testannotations/java/org/jetbrains/annotations/Nullable.java +++ b/checker/src/testannotations/java/org/jetbrains/annotations/Nullable.java @@ -1,4 +1,4 @@ -// Upstream version: +// Upstream version (this is a clean-room reimplementation of its interface): // https://github.com/JetBrains/intellij-community/blob/master/platform/annotations/java8/src/org/jetbrains/annotations/Nullable.java package org.jetbrains.annotations; diff --git a/checker/src/testannotations/java/org/jmlspecs/annotation/NonNull.java b/checker/src/testannotations/java/org/jmlspecs/annotation/NonNull.java index 241a76c5417d..ee8eb707460f 100644 --- a/checker/src/testannotations/java/org/jmlspecs/annotation/NonNull.java +++ b/checker/src/testannotations/java/org/jmlspecs/annotation/NonNull.java @@ -1,4 +1,4 @@ -// Upstream version: +// Upstream version (this is a clean-room reimplementation of its interface): // http://svn.code.sf.net/p/jmlspecs/code/JMLAnnotations/trunk/src/org/jmlspecs/annotation/NonNull.java package org.jmlspecs.annotation; diff --git a/checker/src/testannotations/java/org/jmlspecs/annotation/Nullable.java b/checker/src/testannotations/java/org/jmlspecs/annotation/Nullable.java index 183b30f8e167..a4089fafc42d 100644 --- a/checker/src/testannotations/java/org/jmlspecs/annotation/Nullable.java +++ b/checker/src/testannotations/java/org/jmlspecs/annotation/Nullable.java @@ -1,4 +1,4 @@ -// Upstream version: +// Upstream version (this is a clean-room reimplementation of its interface): // http://svn.code.sf.net/p/jmlspecs/code/JMLAnnotations/trunk/src/org/jmlspecs/annotation/Nullable.java package org.jmlspecs.annotation; diff --git a/checker/src/testannotations/java/org/netbeans/api/annotations/common/CheckForNull.java b/checker/src/testannotations/java/org/netbeans/api/annotations/common/CheckForNull.java index 39fe769966df..e71b4f7cf596 100644 --- a/checker/src/testannotations/java/org/netbeans/api/annotations/common/CheckForNull.java +++ b/checker/src/testannotations/java/org/netbeans/api/annotations/common/CheckForNull.java @@ -1,4 +1,4 @@ -// Upstream version (update version number when a new NetBeans release appears): +// Upstream version (this is a clean-room reimplementation of its interface): // http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/CheckForNull.html package org.netbeans.api.annotations.common; diff --git a/checker/src/testannotations/java/org/netbeans/api/annotations/common/NonNull.java b/checker/src/testannotations/java/org/netbeans/api/annotations/common/NonNull.java index 12f801ab1f67..96f262dd3e84 100644 --- a/checker/src/testannotations/java/org/netbeans/api/annotations/common/NonNull.java +++ b/checker/src/testannotations/java/org/netbeans/api/annotations/common/NonNull.java @@ -1,4 +1,4 @@ -// Upstream version (update version number when a new NetBeans release appears): +// Upstream version (this is a clean-room reimplementation of its interface): // http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NonNull.html package org.netbeans.api.annotations.common; diff --git a/checker/src/testannotations/java/org/netbeans/api/annotations/common/NullAllowed.java b/checker/src/testannotations/java/org/netbeans/api/annotations/common/NullAllowed.java index 6796dbd5030b..453d951a505f 100644 --- a/checker/src/testannotations/java/org/netbeans/api/annotations/common/NullAllowed.java +++ b/checker/src/testannotations/java/org/netbeans/api/annotations/common/NullAllowed.java @@ -1,4 +1,4 @@ -// Upstream version (update version number when a new NetBeans release appears): +// Upstream version (this is a clean-room reimplementation of its interface): // http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NullAllowed.html package org.netbeans.api.annotations.common; diff --git a/checker/src/testannotations/java/org/netbeans/api/annotations/common/NullUnknown.java b/checker/src/testannotations/java/org/netbeans/api/annotations/common/NullUnknown.java index ffb071295f39..dd524523fe74 100644 --- a/checker/src/testannotations/java/org/netbeans/api/annotations/common/NullUnknown.java +++ b/checker/src/testannotations/java/org/netbeans/api/annotations/common/NullUnknown.java @@ -1,4 +1,4 @@ -// Upstream version (update version number when a new NetBeans release appears): +// Upstream version (this is a clean-room reimplementation of its interface): // http://bits.netbeans.org/8.2/javadoc/org-netbeans-api-annotations-common/org/netbeans/api/annotations/common/NullUnknown.html package org.netbeans.api.annotations.common; diff --git a/checker/src/testannotations/java/org/springframework/lang/NonNull.java b/checker/src/testannotations/java/org/springframework/lang/NonNull.java index 4fc4de43a875..72df0daeb8ba 100644 --- a/checker/src/testannotations/java/org/springframework/lang/NonNull.java +++ b/checker/src/testannotations/java/org/springframework/lang/NonNull.java @@ -1,4 +1,4 @@ -// Upstream version: +// Upstream version (this is a clean-room reimplementation of its interface): // https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/lang/NonNull.java package org.springframework.lang; diff --git a/checker/src/testannotations/java/org/springframework/lang/Nullable.java b/checker/src/testannotations/java/org/springframework/lang/Nullable.java index 2ff583ae9837..393f201b4116 100644 --- a/checker/src/testannotations/java/org/springframework/lang/Nullable.java +++ b/checker/src/testannotations/java/org/springframework/lang/Nullable.java @@ -1,4 +1,4 @@ -// Upstream version: +// Upstream version (this is a clean-room reimplementation of its interface): // https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/lang/Nullable.java package org.springframework.lang; diff --git a/checker/tests/README b/checker/tests/README index a3b95b229d13..c634f569f03f 100644 --- a/checker/tests/README +++ b/checker/tests/README @@ -13,9 +13,9 @@ How to run all the tests for the Checker Framework Other Gradle tasks also exist to run a subset of the tests; for example, # Run from the checker-framework directory: - ./gradlew :checker:NullnessFbcTest + ./gradlew :checker:NullnessTest # Run from the checker directory: - ../gradlew NullnessFbcTest + ../gradlew NullnessTest # To see all tasks ./gradlew tasks @@ -23,19 +23,21 @@ Other Gradle tasks also exist to run a subset of the tests; for example, How to run just one test for the Checker Framework ================================================== -To run an individual test (check one source code file), +To run a subset of the JUnit tests, use Gradle's --tests command-line +argument, from a project directory such as checker/, dataflow/, or +framework/: + + cd $CHECKERFRAMEWORK/checker + ../gradlew test --tests '*ParseAllJdkTest' + +To check one source code file, do something like the following, assuming that the source code file to be checked is AssertNonNullTest.java in directory $CHECKERFRAMEWORK/checker/tests/nullness/ and the checker is org.checkerframework.checker.nullness.NullnessChecker. cd $CHECKERFRAMEWORK - ./gradlew compileJava && checker/bin-devel/javac -processor org.checkerframework.checker.nullness.NullnessChecker -implicit:class checker/tests/nullness/AssertNonNullTest.java - -or (to build distribution files) - - cd $CHECKERFRAMEWORK - ./gradlew assemble && checker/bin/javac -processor org.checkerframework.checker.nullness.NullnessChecker -implicit:class checker/tests/nullness/AssertNonNullTest.java + ./gradlew assembleForJavac && checker/bin/javac -processor org.checkerframework.checker.nullness.NullnessChecker -implicit:class checker/tests/nullness/AssertNonNullTest.java where the specific checker and command-line arguments are often clear from the directory name but can also be determined from a file such as @@ -85,7 +87,7 @@ error messages. Suppose that you want to test the Nullness Checker's behavior when type-checking the following Java class: -class MyNullnessTest { +public class MyNullnessTest { void method() { Object nullable = null; nullable.toString(); // should emit error @@ -104,7 +106,7 @@ directly preceding the expected error line. If a warning rather than an error is expected, insert the line // :: warning: () If a stub parser warning is expected, insert the line - //warning: StubParser: + //warning: AnnotationFileParser: If multiple errors are expected on a single line, duplicate everthing except the "//" comment characters, as in // :: error: () :: error: () @@ -115,7 +117,7 @@ a line, to indicate a warning on the line immediately after the "{". So the final test case would be: -class MyNullnessTest { +public class MyNullnessTest { void method() { Object nullable = null; // :: error: (dereference.of.nullable) @@ -133,7 +135,7 @@ checker-framework/checker/tests/regex/, etc. You can indicate an expected warning (as opposed to error) by using "warning:" instead of "error:", as in - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) assert val != null; Multiple expected messages can be given on the same line using the @@ -177,8 +179,8 @@ Different test cases may need to pass different command-line arguments argument that should not be enabled for every test. Follow the same instructions as for writing tests for a new checker. -A Gradle task is created for each Junit test class. The task is named the same -as the Junit test classname, for example, ./gradlew RegexTest runs the Regex +A Gradle task is created for each JUnit test class. The task is named the same +as the JUnit test classname, for example, ./gradlew RegexTest runs the Regex Checker tests. @@ -187,14 +189,20 @@ Disabling a test case Write @skip-test anywhere in a test file to disable that test. -Write @below-java9-jdk-skip-test anywhere in a test file to disable that -test if the executing JDK version is lower than 9. -This is useful when the test depends on Java 9 language features that -need runtime support or depends on Java 9 APIs. -Tests that only exercise the static type annotation API do not need -this marker. +Write @below-java17-jdk-skip-test anywhere in a test file to disable that +test if the executing JDK version is lower than 17. +This is useful when the test depends on Java 17 language features that +need runtime support or depends on Java 17 APIs. If the test contains a record, +then it must also be in a directory named java17 so that it is only formatted +when using JDK 17. + +Write @infer-ajava-skip-test, @infer-jaifs-skip-test, or @infer-stubs-skip-test +to skip running inference using a particular output format on a test. If you +want to run inference on the test case but not validate the results of +inference, then delete the test file in the inference Gradle task that tests +it. -To disable all tests for a given type system, annotated the Junit test class +To disable all tests for a given type system, annotate the JUnit test class with @Ignore. diff --git a/checker/tests/aggregate/NullnessAndRegex.java b/checker/tests/aggregate/NullnessAndRegex.java index f1c1107217a2..43f77b960883 100644 --- a/checker/tests/aggregate/NullnessAndRegex.java +++ b/checker/tests/aggregate/NullnessAndRegex.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.i18n.qual.Localized; import org.checkerframework.checker.regex.qual.Regex; -class NullnessAndRegex { +public class NullnessAndRegex { // :: error: (assignment.type.incompatible) @Regex String s1 = "De(mo"; // :: error: (assignment.type.incompatible) diff --git a/checker/tests/aggregate/Placeholder.java b/checker/tests/aggregate/Placeholder.java index 79887ccf8213..33a99cf3c5e0 100644 --- a/checker/tests/aggregate/Placeholder.java +++ b/checker/tests/aggregate/Placeholder.java @@ -1,5 +1,4 @@ // We need a file to start the checker. -// We should add more files to test -// aggregate checker functionality. +// We should add more files to test aggregate checker functionality. -class Placeholder {} +public class Placeholder {} diff --git a/checker/tests/ainfer-README b/checker/tests/ainfer-README new file mode 100644 index 000000000000..6fc34a8cb64a --- /dev/null +++ b/checker/tests/ainfer-README @@ -0,0 +1,30 @@ +All tests for the -Ainfer command-line argument must be added to the +"non-annotated" folder. These tests have expected error comments +(// :: error...) in places where the type-checker issues an error before +inference but not after inference, and no other "// :: error". + +The task ainferTest tests the -Ainfer command-line argument in three +steps: + +1. The TestChecker will type-check all files in the "non-annotated" folder +using the -Ainfer command-liner argument, which write the inferred types of +some elements into annotation files. The inferred types are written into +annotation files, but are not considered during this type-check -- for that +reason the expected error comments are necessary. + +2. All tests in "non-annotated" are copied to a temporary directory, named +"annotated". All expected error comments are removed from the files in +"annotated". When testing `.jaif` files, the insert-annotations-to-source +tool inserts the annotations that were inferred in the previous step into +the files of the temporary directory. + +3. The TestChecker will type-check all files in the temporary "annotated" +folder. The expected error comments were removed, but the inferred types +(for `.jaif` files, they were inserted; for other annotation file tests, +they are used as stub files) should ensure that type-checking completes +without errors. + +If an error should persist even with the annotations produced by -Ainfer, +add the test to the "non-annotated/ExpectedErrors.java" file. This is the +only file where the expected error comments are not removed when copied to +"annotated/ExpectedErrors.java". diff --git a/checker/tests/ainfer-index/README b/checker/tests/ainfer-index/README new file mode 100644 index 000000000000..6fc34a8cb64a --- /dev/null +++ b/checker/tests/ainfer-index/README @@ -0,0 +1,30 @@ +All tests for the -Ainfer command-line argument must be added to the +"non-annotated" folder. These tests have expected error comments +(// :: error...) in places where the type-checker issues an error before +inference but not after inference, and no other "// :: error". + +The task ainferTest tests the -Ainfer command-line argument in three +steps: + +1. The TestChecker will type-check all files in the "non-annotated" folder +using the -Ainfer command-liner argument, which write the inferred types of +some elements into annotation files. The inferred types are written into +annotation files, but are not considered during this type-check -- for that +reason the expected error comments are necessary. + +2. All tests in "non-annotated" are copied to a temporary directory, named +"annotated". All expected error comments are removed from the files in +"annotated". When testing `.jaif` files, the insert-annotations-to-source +tool inserts the annotations that were inferred in the previous step into +the files of the temporary directory. + +3. The TestChecker will type-check all files in the temporary "annotated" +folder. The expected error comments were removed, but the inferred types +(for `.jaif` files, they were inserted; for other annotation file tests, +they are used as stub files) should ensure that type-checking completes +without errors. + +If an error should persist even with the annotations produced by -Ainfer, +add the test to the "non-annotated/ExpectedErrors.java" file. This is the +only file where the expected error comments are not removed when copied to +"annotated/ExpectedErrors.java". diff --git a/checker/tests/ainfer-index/non-annotated/Dataset6Crash.java b/checker/tests/ainfer-index/non-annotated/Dataset6Crash.java new file mode 100644 index 000000000000..e62a641d31a6 --- /dev/null +++ b/checker/tests/ainfer-index/non-annotated/Dataset6Crash.java @@ -0,0 +1,56 @@ +// Test case for a WPI crash caused by mismatches between captured type variables +// and the declared type of a field: in particular, the issue is that base.next() +// the next field actually have slightly different types: base.next()'s type is +// a capture that extends T. + +import java.util.Iterator; + +public class Dataset6Crash { + + public static Iterator limit( + final Iterator base, final CountingPredicate filter) { + return new Iterator() { + + private T next; + + private boolean end; + + private int index = 0; + + public boolean hasNext() { + return true; + } + + public T next() { + fetch(); + T r = next; + next = null; + return r; + } + + private void fetch() { + if (next == null && !end) { + if (base.hasNext()) { + next = base.next(); + if (!filter.apply(index++, next)) { + next = null; + end = true; + } + } else { + end = true; + } + } + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + private static class CountingPredicate { + public boolean apply(int i, T next) { + return false; + } + } +} diff --git a/checker/tests/ainfer-index/non-annotated/Dataset9Crash.java b/checker/tests/ainfer-index/non-annotated/Dataset9Crash.java new file mode 100644 index 000000000000..a1f7a86df851 --- /dev/null +++ b/checker/tests/ainfer-index/non-annotated/Dataset9Crash.java @@ -0,0 +1,84 @@ +// Minimized test that demonstrates a crash that occurs if one Index Checker +// checker attempts to query one of its subcheckers while parsing ajava files. + +import java.util.Collection; +import java.util.Iterator; + +public class Dataset9Crash { + + @SuppressWarnings("all") + private class NotificationList implements Collection { + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean contains(Object o) { + return false; + } + + @Override + public Iterator iterator() { + return null; + } + + @Override + public Object[] toArray() { + return new Object[0]; + } + + @Override + public boolean add(Object o) { + return false; + } + + @Override + public boolean remove(Object o) { + return false; + } + + @Override + public boolean addAll(Collection c) { + return false; + } + + @Override + public void clear() {} + + @Override + public boolean equals(Object o) { + return false; + } + + @Override + public int hashCode() { + return 0; + } + + @Override + public boolean retainAll(Collection c) { + return false; + } + + @Override + public boolean removeAll(Collection c) { + return false; + } + + @Override + public boolean containsAll(Collection c) { + return false; + } + + @Override + public Object[] toArray(Object[] a) { + return new Object[0]; + } + } +} diff --git a/checker/tests/ainfer-index/non-annotated/DelocalizeAtCallsites.java b/checker/tests/ainfer-index/non-annotated/DelocalizeAtCallsites.java new file mode 100644 index 000000000000..5df8af294a4c --- /dev/null +++ b/checker/tests/ainfer-index/non-annotated/DelocalizeAtCallsites.java @@ -0,0 +1,58 @@ +// test for crashes when delocalizing array variables, indices, array creation expressions, etc +// in annotations + +import org.checkerframework.checker.index.qual.IndexFor; +import org.checkerframework.checker.index.qual.LessThan; + +public class DelocalizeAtCallsites { + + void a1(int[][] a, @IndexFor("#1") int y) { + int[] z = a[y]; + } + + void a2(int y, int x) {} + + void testArrayAccess() { + int[][] arr = new int[5][5]; + int x1 = 3; + @SuppressWarnings("all") + @IndexFor(value = {"arr[x1]", "arr"}) int y1 = 2; + // test that out-of-scope indices are handled properly + a1(arr, y1); + // test that out-of-scope arrays are handled properly + a2(y1, x1); + } + + void a3(int x) {} + + void testArrayCreation() { + int x = 10; + @SuppressWarnings("all") + @IndexFor("new int[x]") int y = 0; + a3(y); + @SuppressWarnings("all") + @IndexFor("new int[x]{x, x, x, x, x, x, x, x, x, x}") int z = 0; + a3(z); + } + + void testBinary() { + int x1 = 5; + @LessThan("x1 + 1") int y = 4; + a3(y); + } + + void testUnary() { + int x1 = 5; + @LessThan("x1++") int y = 4; + a3(y); + } + + public int f; + + void testFieldAccess() { + DelocalizeAtCallsites d = new DelocalizeAtCallsites(); + d.f = 3; + @LessThan("d.f") int y = 2; + a3(y); + } +} diff --git a/checker/tests/ainfer-index/non-annotated/DependentTypesViewpointAdaptationTest.java b/checker/tests/ainfer-index/non-annotated/DependentTypesViewpointAdaptationTest.java new file mode 100644 index 000000000000..2e7b84af5423 --- /dev/null +++ b/checker/tests/ainfer-index/non-annotated/DependentTypesViewpointAdaptationTest.java @@ -0,0 +1,73 @@ +// Based on a snippet of code that caused a malformed ajava file to be +// produced by WPI. The offending ajava file contained an @SameLen annotation +// whose argument was a constant String, e.g., @SameLen({ ""Hamburg"", "word1" }) + +import org.checkerframework.checker.index.qual.SameLen; + +public class DependentTypesViewpointAdaptationTest { + + public static void run() { + String word1 = "\"Hamburg\""; + String word2 = "burg"; + // The existence of word3 here forces the inferred @SameLen + // annotation to include a local variable that isn't a parameter + // to compute(), to test that such local variables are viewpoint-adapted + // correctly. + String word3 = word1; + compute(word1, word2); + } + + public static void compute(String word1, String otherWord) { + // content doesn't matter + } + + public static void receiverTest( + @SameLen("#2") DependentTypesViewpointAdaptationTest t1, + @SameLen("#1") DependentTypesViewpointAdaptationTest t2) { + t1.compute2(t2); + } + + public void compute2( + DependentTypesViewpointAdaptationTest this, + DependentTypesViewpointAdaptationTest other) { + // content doesn't matter + } + + public void thisTest(@SameLen("this") DependentTypesViewpointAdaptationTest t1) { + compute3(this, t1); + } + + public static void compute3( + DependentTypesViewpointAdaptationTest t1, DependentTypesViewpointAdaptationTest t2) { + // content doesn't matter + } + + public void thisTestNoUse(@SameLen("this") DependentTypesViewpointAdaptationTest t1) { + compute4(t1, t1); + } + + public static void compute4( + DependentTypesViewpointAdaptationTest t1, DependentTypesViewpointAdaptationTest t2) { + // content doesn't matter + } + + public static void testThisInference( + DependentTypesViewpointAdaptationTest t1, + @SameLen("#1") DependentTypesViewpointAdaptationTest t2) { + t1.compute5(t2); + t1.compute6(t2); + } + + public void compute5( + DependentTypesViewpointAdaptationTest this, + DependentTypesViewpointAdaptationTest other) { + // :: warning: (assignment.type.incompatible) + @SameLen("this") DependentTypesViewpointAdaptationTest myOther = other; + } + + // Same as compute5, but without an explicit this parameter. + public void compute6(DependentTypesViewpointAdaptationTest other) { + // :: warning: (assignment.type.incompatible) + @SameLen("this") DependentTypesViewpointAdaptationTest myOther = other; + } +} diff --git a/checker/tests/ainfer-index/non-annotated/InternCrash.java b/checker/tests/ainfer-index/non-annotated/InternCrash.java new file mode 100644 index 000000000000..f1daf6b5bb5f --- /dev/null +++ b/checker/tests/ainfer-index/non-annotated/InternCrash.java @@ -0,0 +1,19 @@ +// Test case based on a crash encountered when running WPI on plume-util's Intern.java. +// The crash was caused by an AnnotatedDeclaredType being unsafely cast to an AnnotatedArrayType +// during WPI. + +public final class InternCrash { + + public static String[] intern(String[] a) { + return a; + } + + public static Object intern(Object a) { + if (a instanceof String[]) { + String[] asArray = (String[]) a; + return intern(asArray); + } else { + return null; + } + } +} diff --git a/checker/tests/ainfer-index/non-annotated/LongMathTest.java b/checker/tests/ainfer-index/non-annotated/LongMathTest.java new file mode 100644 index 000000000000..ff5e2e9f0588 --- /dev/null +++ b/checker/tests/ainfer-index/non-annotated/LongMathTest.java @@ -0,0 +1,5 @@ +public class LongMathTest { + public static long mod(long x, long y) { + return x % y; + } +} diff --git a/checker/tests/ainfer-index/non-annotated/RequireJavadocCrash.java b/checker/tests/ainfer-index/non-annotated/RequireJavadocCrash.java new file mode 100644 index 000000000000..284b04c2fbad --- /dev/null +++ b/checker/tests/ainfer-index/non-annotated/RequireJavadocCrash.java @@ -0,0 +1,21 @@ +// Based on a crash encountered when runnnig WPI on plume-lib's RequireJavadoc. + +import org.plumelib.options.Options; + +public class RequireJavadocCrash { + + public static void main(String[] args) { + RequireJavadocCrash rj = new RequireJavadocCrash(); + Options options = + new Options( + "java org.plumelib.javadoc.RequireJavadoc [options] [directory-or-file ...]", + rj); + String[] remainingArgs = options.parse(true, args); + + rj.setJavaFiles(remainingArgs); + } + + private RequireJavadocCrash() {} + + private void setJavaFiles(String[] args) {} +} diff --git a/checker/tests/ainfer-index/non-annotated/TypeVarAssignment.java b/checker/tests/ainfer-index/non-annotated/TypeVarAssignment.java new file mode 100644 index 000000000000..2dfe2ba6a7d8 --- /dev/null +++ b/checker/tests/ainfer-index/non-annotated/TypeVarAssignment.java @@ -0,0 +1,7 @@ +public class TypeVarAssignment { + T t; + + void foo(S s) { + t = s; + } +} diff --git a/checker/tests/ainfer-nullness/README b/checker/tests/ainfer-nullness/README new file mode 100644 index 000000000000..6fc34a8cb64a --- /dev/null +++ b/checker/tests/ainfer-nullness/README @@ -0,0 +1,30 @@ +All tests for the -Ainfer command-line argument must be added to the +"non-annotated" folder. These tests have expected error comments +(// :: error...) in places where the type-checker issues an error before +inference but not after inference, and no other "// :: error". + +The task ainferTest tests the -Ainfer command-line argument in three +steps: + +1. The TestChecker will type-check all files in the "non-annotated" folder +using the -Ainfer command-liner argument, which write the inferred types of +some elements into annotation files. The inferred types are written into +annotation files, but are not considered during this type-check -- for that +reason the expected error comments are necessary. + +2. All tests in "non-annotated" are copied to a temporary directory, named +"annotated". All expected error comments are removed from the files in +"annotated". When testing `.jaif` files, the insert-annotations-to-source +tool inserts the annotations that were inferred in the previous step into +the files of the temporary directory. + +3. The TestChecker will type-check all files in the temporary "annotated" +folder. The expected error comments were removed, but the inferred types +(for `.jaif` files, they were inserted; for other annotation file tests, +they are used as stub files) should ensure that type-checking completes +without errors. + +If an error should persist even with the annotations produced by -Ainfer, +add the test to the "non-annotated/ExpectedErrors.java" file. This is the +only file where the expected error comments are not removed when copied to +"annotated/ExpectedErrors.java". diff --git a/checker/tests/ainfer-nullness/input-annotation-files/TypeVarReturnAnnotated-org.checkerframework.checker.nullness.NullnessChecker.ajava b/checker/tests/ainfer-nullness/input-annotation-files/TypeVarReturnAnnotated-org.checkerframework.checker.nullness.NullnessChecker.ajava new file mode 100644 index 000000000000..25145411d1fd --- /dev/null +++ b/checker/tests/ainfer-nullness/input-annotation-files/TypeVarReturnAnnotated-org.checkerframework.checker.nullness.NullnessChecker.ajava @@ -0,0 +1,12 @@ +// Code like this caused WPI to loop infinitely, because the annotation on the return type +// was only sometimes inferred. Based on an example from +// https://github.com/dd482IT/cache2k-wpi/blob/0eaa156bdecd617b2aa4c745d0f8844a32609697/cache2k-api/src/main/java/org/cache2k/config/ToggleFeature.java#L73 +@org.checkerframework.framework.qual.AnnotatedFor( + "org.checkerframework.checker.nullness.NullnessChecker") +public class TypeVarReturnAnnotated { + + public static + @org.checkerframework.checker.initialization.qual.FBCBottom @org.checkerframework.checker.nullness.qual.Nullable T extract() { + return null; + } +} diff --git a/checker/tests/ainfer-nullness/non-annotated/Bug.java b/checker/tests/ainfer-nullness/non-annotated/Bug.java new file mode 100644 index 000000000000..b90b483496cd --- /dev/null +++ b/checker/tests/ainfer-nullness/non-annotated/Bug.java @@ -0,0 +1,69 @@ +import org.checkerframework.checker.initialization.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +public class Bug { + /** Actions that MultiVersionControl can perform. */ + static enum Action { + /** Clone a repository. */ + CLONE, + /** Show the working tree status. */ + STATUS, + /** Pull changes from upstream. */ + PULL, + /** List the known repositories. */ + LIST + } + + private @MonotonicNonNull Action action; + public static String home = System.getProperty("user.home"); + + void other() { + this.action = Action.LIST; + expandTilde(""); + } + + /** + * Replace "~" by the expansion of "$HOME". + * + * @param path the input path, which might contain "~" + * @return path with "~" expanded + */ + private static String expandTilde(String path) { + return path.replaceFirst("^~", home); + } + + public static void main(String[] args) { + Bug b = new Bug(); + Bug.expandTilde(""); + } + + Bug() { + parseArgs(new String[0]); + } + + @EnsuresNonNull("action") + public void parseArgs(@UnknownInitialization Bug this, String[] args) { + String actionString = args[0]; + if ("checkout".startsWith(actionString)) { + action = Action.CLONE; + } else if ("clone".startsWith(actionString)) { + action = Action.CLONE; + } else if ("list".startsWith(actionString)) { + action = Action.LIST; + } else if ("pull".startsWith(actionString)) { + action = Action.PULL; + } else if ("status".startsWith(actionString)) { + action = Action.STATUS; + } else if ("update".startsWith(actionString)) { + action = Action.PULL; + } else { + System.out.printf("Unrecognized action \"%s\"", actionString); + System.exit(1); + } + } + + public static final @Nullable String VERSION = + Bug.class.getPackage() != null + ? Bug.class.getPackage().getImplementationVersion() + : null; +} diff --git a/checker/tests/ainfer-nullness/non-annotated/MonotonicNonNullInferenceTest.java b/checker/tests/ainfer-nullness/non-annotated/MonotonicNonNullInferenceTest.java new file mode 100644 index 000000000000..f0a9b6768a85 --- /dev/null +++ b/checker/tests/ainfer-nullness/non-annotated/MonotonicNonNullInferenceTest.java @@ -0,0 +1,71 @@ +import org.checkerframework.checker.nullness.qual.NonNull; + +public class MonotonicNonNullInferenceTest { + + // :: warning: (initialization.static.field.uninitialized) + static String staticString1; + + // :: warning: (assignment.type.incompatible) + static String staticString2 = null; + + static String staticString3; + + String instanceString1; + + // :: warning: (assignment.type.incompatible) + String instanceString2 = null; + + String instanceString3; + + static { + // :: warning: (assignment.type.incompatible) + staticString3 = null; + } + + // :: warning: (initialization.fields.uninitialized) + MonotonicNonNullInferenceTest() { + String instanceString3 = "hello"; + } + + static void m1(String arg) { + staticString1 = arg; + staticString2 = arg; + staticString3 = arg; + } + + void m2(String arg) { + instanceString1 = arg; + instanceString2 = arg; + instanceString3 = arg; + } + + void hasSideEffect() {} + + void testMonotonicNonNull() { + @NonNull String s; + if (staticString1 != null) { + hasSideEffect(); + s = staticString1; + } + if (staticString2 != null) { + hasSideEffect(); + s = staticString2; + } + if (staticString3 != null) { + hasSideEffect(); + s = staticString3; + } + if (instanceString1 != null) { + hasSideEffect(); + s = instanceString1; + } + if (instanceString2 != null) { + hasSideEffect(); + s = instanceString2; + } + if (instanceString3 != null) { + hasSideEffect(); + s = instanceString3; + } + } +} diff --git a/checker/tests/ainfer-nullness/non-annotated/NullTypeVarTest.java b/checker/tests/ainfer-nullness/non-annotated/NullTypeVarTest.java new file mode 100644 index 000000000000..16e6d27802d4 --- /dev/null +++ b/checker/tests/ainfer-nullness/non-annotated/NullTypeVarTest.java @@ -0,0 +1,32 @@ +// A test that the interaction between type variables and null types +// is handled correctly in WPI, based on the indentString variable +// in +// https://github.com/plume-lib/bcel-util/blob/master/src/main/java/org/plumelib/bcelutil/SimpleLog.java + +import java.util.ArrayList; +import java.util.List; + +public class NullTypeVarTest { + + // :: warning: assignment.type.incompatible + private String indentString = null; + + private List indentStrings; + + private final String INDENT_STR_ONE_LEVEL = " "; + + public NullTypeVarTest() { + indentStrings = new ArrayList(); + indentStrings.add(""); + } + + private String getIndentString(int indentLevel) { + if (indentString == null) { + for (int i = indentStrings.size(); i <= indentLevel; i++) { + indentStrings.add(indentStrings.get(i - 1) + INDENT_STR_ONE_LEVEL); + } + indentString = indentStrings.get(indentLevel); + } + return indentString; + } +} diff --git a/checker/tests/ainfer-nullness/non-annotated/TwoCtorGenericAbstract.java b/checker/tests/ainfer-nullness/non-annotated/TwoCtorGenericAbstract.java new file mode 100644 index 000000000000..cf1bfb52eb79 --- /dev/null +++ b/checker/tests/ainfer-nullness/non-annotated/TwoCtorGenericAbstract.java @@ -0,0 +1,16 @@ +// test case for https://github.com/typetools/checker-framework/issues/5524 + +import java.util.Set; + +public abstract class TwoCtorGenericAbstract implements Set { + protected T value; + + protected TwoCtorGenericAbstract() { + // :: warning: (assignment.type.incompatible) + this.value = null; + } + + protected TwoCtorGenericAbstract(T v) { + this.value = v; + } +} diff --git a/checker/tests/ainfer-nullness/non-annotated/TypeVarPlumeUtil.java b/checker/tests/ainfer-nullness/non-annotated/TypeVarPlumeUtil.java new file mode 100644 index 000000000000..133c4a1938d3 --- /dev/null +++ b/checker/tests/ainfer-nullness/non-annotated/TypeVarPlumeUtil.java @@ -0,0 +1,19 @@ +// test case for https://github.com/typetools/checker-framework/issues/5708 + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class TypeVarPlumeUtil { + @SuppressWarnings({ + "nullness" // only check for crashes. Also, was present in the original source file (so the + // annotations in this code were preserved by RemoveAnnotationsForInference). + }) + public V merge(@NonNull V value) { + return value; + } + + public V mergeNullable(@Nullable V value) { + // :: warning: (return) + return value; + } +} diff --git a/checker/tests/ainfer-nullness/non-annotated/TypeVarReturnAnnotated.java b/checker/tests/ainfer-nullness/non-annotated/TypeVarReturnAnnotated.java new file mode 100644 index 000000000000..da50c30866f7 --- /dev/null +++ b/checker/tests/ainfer-nullness/non-annotated/TypeVarReturnAnnotated.java @@ -0,0 +1,9 @@ +// Code like this caused WPI to loop infinitely, because the annotation on the return type +// was only sometimes inferred. Based on an example from +// https://github.com/dd482IT/cache2k-wpi/blob/0eaa156bdecd617b2aa4c745d0f8844a32609697/cache2k-api/src/main/java/org/cache2k/config/ToggleFeature.java#L73 + +public class TypeVarReturnAnnotated { + public static T extract() { + return null; + } +} diff --git a/checker/tests/ainfer-resourceleak/README b/checker/tests/ainfer-resourceleak/README new file mode 100644 index 000000000000..6fc34a8cb64a --- /dev/null +++ b/checker/tests/ainfer-resourceleak/README @@ -0,0 +1,30 @@ +All tests for the -Ainfer command-line argument must be added to the +"non-annotated" folder. These tests have expected error comments +(// :: error...) in places where the type-checker issues an error before +inference but not after inference, and no other "// :: error". + +The task ainferTest tests the -Ainfer command-line argument in three +steps: + +1. The TestChecker will type-check all files in the "non-annotated" folder +using the -Ainfer command-liner argument, which write the inferred types of +some elements into annotation files. The inferred types are written into +annotation files, but are not considered during this type-check -- for that +reason the expected error comments are necessary. + +2. All tests in "non-annotated" are copied to a temporary directory, named +"annotated". All expected error comments are removed from the files in +"annotated". When testing `.jaif` files, the insert-annotations-to-source +tool inserts the annotations that were inferred in the previous step into +the files of the temporary directory. + +3. The TestChecker will type-check all files in the temporary "annotated" +folder. The expected error comments were removed, but the inferred types +(for `.jaif` files, they were inserted; for other annotation file tests, +they are used as stub files) should ensure that type-checking completes +without errors. + +If an error should persist even with the annotations produced by -Ainfer, +add the test to the "non-annotated/ExpectedErrors.java" file. This is the +only file where the expected error comments are not removed when copied to +"annotated/ExpectedErrors.java". diff --git a/checker/tests/ainfer-resourceleak/non-annotated/AddNotOwning.java b/checker/tests/ainfer-resourceleak/non-annotated/AddNotOwning.java new file mode 100644 index 000000000000..393e86e6ca43 --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/AddNotOwning.java @@ -0,0 +1,92 @@ +// This test ensures that the @NotOwning annotation is inferred for the return type of a method if +// it returns a field. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class AddNotOwning { + + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + static class NonEmptyMustCallFinalField { + final Foo f; // expect owning annotation for this field + + NonEmptyMustCallFinalField() { + // :: warning: (required.method.not.called) + f = new Foo(); + } + + Foo getField() { + return f; + } + + Foo getFieldOnSomePath() { + if (true) { + return null; + } else { + return f; + } + } + + void testNotOwningOnFinal() { + // :: warning: (required.method.not.called) + Foo f = getField(); + } + + void testNotOwningOnGetFieldOnSomePath() { + // :: warning: (required.method.not.called) + Foo f = getFieldOnSomePath(); + } + + @EnsuresCalledMethods( + value = {"this.f"}, + methods = {"a"}) + void dispose() { + f.a(); + } + } + + @InheritableMustCall("dispose") + static class NonEmptyMustCallNonFinalField { + Foo f; // expect owning annotation for this field + + @SuppressWarnings("missing.creates.mustcall.for") + void initialyzeFoo() { + f.a(); + // :: warning: (required.method.not.called) + f = new Foo(); + } + + Foo getField() { + return f; + } + + Foo getFieldOnSomePath() { + if (true) { + return null; + } else { + return f; + } + } + + void testNotOwningOnNonFinal() { + // :: warning: (required.method.not.called) + Foo f = getField(); + } + + void testNotOwningOnGetFieldOnSomePath() { + // :: warning: (required.method.not.called) + Foo f = getFieldOnSomePath(); + } + + @EnsuresCalledMethods( + value = {"this.f"}, + methods = {"a"}) + void dispose() { + f.a(); + } + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/ClassWithTwoOwningFieldsTest.java b/checker/tests/ainfer-resourceleak/non-annotated/ClassWithTwoOwningFieldsTest.java new file mode 100644 index 000000000000..e19b06ccf956 --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/ClassWithTwoOwningFieldsTest.java @@ -0,0 +1,40 @@ +// This test ensures that the @MustCallAlias annotation is not inferred when the enclosing class has +// more than one owning field. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class ClassWithTwoOwningFieldsTest { + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + @InheritableMustCall("close") + private class ClassWithTwoOwningFields { + // :: warning: (required.method.not.called) + final @Owning Foo foo1; + // :: warning: (required.method.not.called) + final @Owning Foo foo2; + + public ClassWithTwoOwningFields(Foo f1, Foo f2) { + foo1 = f1; + foo2 = f2; + } + + void close() { + foo1.a(); + foo2.a(); + } + } + + void testTwoOwning() { + // :: warning: (required.method.not.called) + Foo f1 = new Foo(); + // :: warning: (required.method.not.called) + Foo f2 = new Foo(); + + ClassWithTwoOwningFields ff = new ClassWithTwoOwningFields(f1, f2); + ff.close(); + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/DatadirCleanupManager.java b/checker/tests/ainfer-resourceleak/non-annotated/DatadirCleanupManager.java new file mode 100644 index 000000000000..0db8cca1c225 --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/DatadirCleanupManager.java @@ -0,0 +1,16 @@ +import java.io.*; +import java.util.*; + +public class DatadirCleanupManager { + static class PurgeTask extends TimerTask { + + @Override + public void run() { + try { + PurgeTxnLog.purge(); + } catch (Exception e) { + + } + } + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/ECMInference.java b/checker/tests/ainfer-resourceleak/non-annotated/ECMInference.java new file mode 100644 index 000000000000..5bde4f629b41 --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/ECMInference.java @@ -0,0 +1,58 @@ +// @skip-test the test contains no resource types to infer. +// To pass this test, RLC's inference needs to infer CalledMethods annotations for empty must-call +// types, which requires the -AenableWpirForRLC flag. +import org.checkerframework.checker.calledmethods.qual.CalledMethods; + +public class ECMInference { + + class A1 { + void doStuff() { + toString(); + } + + void clientA1() { + doStuff(); + // :: warning: (assignment) + @CalledMethods("toString") A1 a1 = this; + } + } + + class B1 extends A1 { + @Override + void doStuff() { + toString(); + } + + void clientB1() { + doStuff(); + // :: warning: (assignment) + @CalledMethods("toString") B1 b1 = this; + } + } + + class A2 { + void doStuff() { + toString(); + } + + void clientA2() { + doStuff(); + // :: warning: (assignment) + @CalledMethods("toString") A2 a2 = this; + } + } + + class B2 extends A2 { + @Override + void doStuff() { + toString(); + hashCode(); + } + + void clientB2() { + doStuff(); + // :: warning: (assignment) + @CalledMethods({"toString", "hashCode"}) B2 b2 = this; + } + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/EnsuresCalledMethodsTest.java b/checker/tests/ainfer-resourceleak/non-annotated/EnsuresCalledMethodsTest.java new file mode 100644 index 000000000000..269918e0d53b --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/EnsuresCalledMethodsTest.java @@ -0,0 +1,29 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class EnsuresCalledMethodsTest { + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + @InheritableMustCall("close") + class ECM { + // :: warning: (required.method.not.called) + @Owning Foo foo; + + private void closePrivate() { + if (foo != null) { + foo.a(); + foo = null; + } + } + + void close() { + if (foo != null) { + foo.a(); + foo = null; + } + } + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/EnsuresCalledMethodsVarArgsTest.java b/checker/tests/ainfer-resourceleak/non-annotated/EnsuresCalledMethodsVarArgsTest.java new file mode 100644 index 000000000000..005ad4d531f9 --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/EnsuresCalledMethodsVarArgsTest.java @@ -0,0 +1,60 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class EnsuresCalledMethodsVarArgsTest { + + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + static class Utils { + @SuppressWarnings("ensuresvarargs.unverified") + @EnsuresCalledMethodsVarArgs("a") + public static void close(Foo... foos) { + for (Foo f : foos) { + if (f != null) { + f.a(); + } + } + } + } + + private class ECMVA { + final Foo foo; + + ECMVA() { + // :: warning: (required.method.not.called) + foo = new Foo(); + } + + void finalyzer() { + Utils.close(foo); + } + + @EnsuresCalledMethods( + value = {"#1"}, + methods = {"a"}) + void closef(Foo f) { + if (f != null) { + Utils.close(f); + } + } + + void owningParam(Foo f) { + Foo foo = f; + Utils.close(foo); + } + + void testOwningParamOnOwningParam() { + // :: warning: (required.method.not.called) + Foo f = new Foo(); + owningParam(f); + } + } + + void testCorrect() { + ECMVA e = new ECMVA(); + e.finalyzer(); + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/HadoopCrash.java b/checker/tests/ainfer-resourceleak/non-annotated/HadoopCrash.java new file mode 100644 index 000000000000..527b185c1753 --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/HadoopCrash.java @@ -0,0 +1,9 @@ +class NullReceiverTest { + public static void testReceiver(NullReceiverTest nrt) { + nrt.nullReceiver(); + } + + public static NullReceiverTest nullReceiver() { + return new NullReceiverTest(); + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/MustCallAliasOnReceiver.java b/checker/tests/ainfer-resourceleak/non-annotated/MustCallAliasOnReceiver.java new file mode 100644 index 000000000000..20d0f9b5f8ce --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/MustCallAliasOnReceiver.java @@ -0,0 +1,41 @@ +// @skip-test until we have support for adding annotation on the receiver parameter. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +@InheritableMustCall("close") +public class MustCallAliasOnReceiver { + + final @Owning InputStream is; + + @MustCallAlias MustCallAliasOnReceiver(@MustCallAlias InputStream p, boolean b) { + this.is = p; + } + + MustCallAliasOnReceiver returnReceiver(MustCallAliasOnReceiver this) { + return this; + } + + // :: warning: (required.method.not.called) + void testReceiverMCAAnnotation(@Owning InputStream inputStream) throws IOException { + MustCallAliasOnReceiver mcar = new MustCallAliasOnReceiver(is, false); + mcar.returnReceiver().close(); + } + + @EnsuresCalledMethods(value = "this.is", methods = "close") + public void close() throws IOException { + this.is.close(); + } + + public static MustCallAliasOnReceiver mcaneFactory(InputStream is) { + return new MustCallAliasOnReceiver(is, false); + } + + // :: warning: (required.method.not.called) + public static void testUse(@Owning InputStream inputStream) throws Exception { + MustCallAliasOnReceiver mcane = mcaneFactory(inputStream); + mcane.close(); + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/MustCallAliasOnRegularExits.java b/checker/tests/ainfer-resourceleak/non-annotated/MustCallAliasOnRegularExits.java new file mode 100644 index 000000000000..2199bc046045 --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/MustCallAliasOnRegularExits.java @@ -0,0 +1,53 @@ +// This test ensures that the all-paths condition for inferring the @MustCallAlias annotation is +// restricted to the examination of 'Regular' paths. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.IOException; + +class MustCallAliasOnRegularExits { + + @InheritableMustCall("a") + static class Foo { + void a() {} + + int b() throws IOException { + return 0; + } + } + + private class MCAConstructor extends Foo { + + protected final @Owning Foo f; + protected long s = 0L; + + // The Must Call Checker for assigning @MustCallAlias parameters to @Owning fields reports a + // false positive. + @SuppressWarnings("assignment") + protected MCAConstructor(Foo foo) throws IOException { + if (foo == null) { + this.s = foo.b(); + } + this.f = foo; + } + + @EnsuresCalledMethods( + value = {"this.f"}, + methods = {"a"}) + public void a() { + f.a(); + } + } + + void testMCAOnMCAConstructor() { + Foo f = new Foo(); + try { + // :: warning: (required.method.not.called) + MCAConstructor mcaf = new MCAConstructor(f); + } catch (IOException e) { + } finally { + f.a(); + } + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/MustCallAliasParams.java b/checker/tests/ainfer-resourceleak/non-annotated/MustCallAliasParams.java new file mode 100644 index 000000000000..295183bc20db --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/MustCallAliasParams.java @@ -0,0 +1,159 @@ +// This test examines all scenarios for inferring the @MustCallAlias annotation. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class MustCallAliasParams { + + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + @InheritableMustCall("a") + private class MCAConstructor { + + final @Owning Foo f; + + // The Must Call Checker for assigning @MustCallAlias parameters to @Owning fields reports a + // false positive. + @SuppressWarnings("assignment") + MCAConstructor(Foo foo) { + f = foo; + } + + @EnsuresCalledMethods( + value = {"this.f"}, + methods = {"a"}) + public void a() { + f.a(); + } + } + + void testMCAConstructor() { + // :: warning: (required.method.not.called) + Foo f = new Foo(); + MCAConstructor mcac = new MCAConstructor(f); + mcac.a(); + } + + @InheritableMustCall("a") + private class MCASuperClass { + int i; + final @Owning Foo f; + + // The Must Call Checker for assigning @MustCallAlias parameters to @Owning fields reports a + // false positive. + @SuppressWarnings("assignment") + @MustCallAlias MCASuperClass(@MustCallAlias Foo foo, int i) { + f = foo; + i = i; + } + + @EnsuresCalledMethods( + value = {"this.f"}, + methods = {"a"}) + public void a() { + f.a(); + } + } + + private class MCASuperCall extends MCASuperClass { + MCASuperCall(Foo foo) { + super(foo, 1); + } + } + + void mCASuperCallTest() { + // :: warning: (required.method.not.called) + Foo f = new Foo(); + MCASuperCall mcaSuperCall = new MCASuperCall(f); + mcaSuperCall.a(); + } + + private class MCAThisCall extends MCASuperClass { + @MustCallAlias MCAThisCall(@MustCallAlias Foo foo) { + super(foo, 1); + } + + MCAThisCall(Foo foo, boolean b) { + this(foo); + } + } + + void mCAThisCallTest() { + // :: warning: (required.method.not.called) + Foo f = new Foo(); + MCAThisCall mcaThisCall = new MCAThisCall(f, true); + mcaThisCall.a(); + } + + private class MCAMethod { + Foo returnFoo(Foo foo) { + return foo; + } + + void returnFooTest() { + // :: warning: (required.method.not.called) + Foo f = new Foo(); + Foo foo = returnFoo(f); + foo.a(); + } + + Foo returnAliasFoo(Foo foo) { + Foo f = foo; + return f; + } + + MCASuperClass returnAliasFoo2(Foo foo, int i) { + MCASuperClass f = new MCASuperClass(foo, i); + return f; + } + + void testReturnAliasFoo2() { + // :: warning: (required.method.not.called) + Foo foo = new Foo(); + MCASuperClass f = returnAliasFoo2(foo, 0); + f.a(); + } + + void returnAliasFooTest() { + // :: warning: (required.method.not.called) + Foo f = new Foo(); + Foo foo = returnAliasFoo(f); + foo.a(); + } + + Foo returnFooAllPaths(Foo foo) { + if (true) { + Foo f = foo; + return f; + } else { + return foo; + } + } + + void returnFooAllPathsTest() { + // :: warning: (required.method.not.called) + Foo f = new Foo(); + Foo foo = returnFooAllPaths(f); + foo.a(); + } + + Foo returnFooSomePath(Foo foo) { + if (true) { + Foo f = new Foo(); + return f; + } else { + return foo; + } + } + + void returnFooSomePathTest() { + Foo f = new Foo(); + Foo foo = returnFooSomePath(f); + f.a(); + foo.a(); + } + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/OwnershipTransferOnConstructor.java b/checker/tests/ainfer-resourceleak/non-annotated/OwnershipTransferOnConstructor.java new file mode 100644 index 000000000000..267624cebc52 --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/OwnershipTransferOnConstructor.java @@ -0,0 +1,29 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.IOException; +import java.net.*; + +class OwnershipTransferOnConstructor { + static class Foo { + Foo(@Owning Socket s) { + try { + s.close(); + } catch (IOException e) { + + } + } + } + + private class Bar { + void baz(Socket s) { + Foo f = new Foo(s); + } + + // :: warning: (required.method.not.called) + void testOwningOnBaz(@Owning Socket s) { + Socket s2 = s; + baz(s2); + } + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/OwningField.java b/checker/tests/ainfer-resourceleak/non-annotated/OwningField.java new file mode 100644 index 000000000000..614266af4b02 --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/OwningField.java @@ -0,0 +1,23 @@ +import org.checkerframework.checker.mustcall.qual.*; + +class OwningField { + + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + @InheritableMustCall("dispose") + static class FinalOwningField { + final Foo f; + + FinalOwningField() { + // :: warning: (required.method.not.called) + f = new Foo(); + } + + void dispose() { + f.a(); + } + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/OwningFieldIndirectCall.java b/checker/tests/ainfer-resourceleak/non-annotated/OwningFieldIndirectCall.java new file mode 100644 index 000000000000..edf4117ed2d4 --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/OwningFieldIndirectCall.java @@ -0,0 +1,37 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class OwningFieldIndirectCall { + + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + static class Utility { + @EnsuresCalledMethods(value = "#1", methods = "a") + public static void closeStream(Foo f) { + if (f != null) { + f.a(); + } + } + } + + static class DisposeFieldUsingECM { + final Foo f; // expect owning annotation for this field + + DisposeFieldUsingECM() { + // :: warning: (required.method.not.called) + f = new Foo(); + } + + void dispose() { + Utility.closeStream(f); + } + } + + void testCorrect() { + DisposeFieldUsingECM d = new DisposeFieldUsingECM(); + d.dispose(); + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/OwningParams.java b/checker/tests/ainfer-resourceleak/non-annotated/OwningParams.java new file mode 100644 index 000000000000..97f7040595f1 --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/OwningParams.java @@ -0,0 +1,60 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class OwningParams { + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + private class OwningParamsDirectCall { + void passOwnership(Foo f) { + f.a(); + } + + void passOwnershipTest() { + // :: warning: (required.method.not.called) + Foo f = new Foo(); + passOwnership(f); + } + } + + private class OwningParamsIndirectCall { + @EnsuresCalledMethods( + value = {"#1"}, + methods = {"a"}) + void hasECM(Foo f) { + f.a(); + } + + void owningFoo(@Owning Foo f) { + f.a(); + } + + void passOwnership(Foo f1, Foo f2) { + Foo f11 = f1; + hasECM(f11); + Foo f22 = f2; + owningFoo(f22); + } + + void checkAlias(Foo f1) { + Foo f2 = f1; + f2.a(); + } + + void checkAliasTest() { + // :: warning: (required.method.not.called) + Foo f = new Foo(); + checkAlias(f); + } + + void passOwnershipTest() { + // :: warning: (required.method.not.called) + Foo f1 = new Foo(); + // :: warning: (required.method.not.called) + Foo f2 = new Foo(); + passOwnership(f1, f2); + } + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/PurgeTxnLog.java b/checker/tests/ainfer-resourceleak/non-annotated/PurgeTxnLog.java new file mode 100644 index 000000000000..13dc3762f9e4 --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/PurgeTxnLog.java @@ -0,0 +1,22 @@ +import java.io.*; +import java.util.*; + +public class PurgeTxnLog { + + public static void purge() throws IOException { + staticMethod(); + } + + static void staticMethod() { + + class MyFileFilter implements FileFilter { + MyFileFilter() {} + + public boolean accept(File f) { + return true; + } + } + + MyFileFilter m = new MyFileFilter(); + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/ReplaceMustCallAliasAnnotation.java b/checker/tests/ainfer-resourceleak/non-annotated/ReplaceMustCallAliasAnnotation.java new file mode 100644 index 000000000000..e1ce8cf4fd09 --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/ReplaceMustCallAliasAnnotation.java @@ -0,0 +1,42 @@ +// This test corrects the wrong @MustCallAlias annotation written on the constrcutor when there are +// more than one owning field. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class ReplaceMustCallAliasAnnotation { + + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + @InheritableMustCall("a") + private class TwoOwningFields { + + final @Owning Foo f1; + final @Owning Foo f2; + + @SuppressWarnings({"assignment", "mustcallalias.out.of.scope"}) + @MustCallAlias TwoOwningFields(@MustCallAlias Foo foo1, Foo foo2) { + f1 = foo1; + f2 = foo2; + } + + @EnsuresCalledMethods( + value = {"this.f1", "this.f2"}, + methods = {"a"}) + public void a() { + f1.a(); + f2.a(); + } + } + + void testOwningAnnotations() { + Foo f1 = new Foo(); + // :: warning: (required.method.not.called) + Foo f2 = new Foo(); + TwoOwningFields t = new TwoOwningFields(f1, f2); + t.a(); + } +} diff --git a/checker/tests/ainfer-resourceleak/non-annotated/UnwantedECMInference.java b/checker/tests/ainfer-resourceleak/non-annotated/UnwantedECMInference.java new file mode 100644 index 000000000000..adac77e685fb --- /dev/null +++ b/checker/tests/ainfer-resourceleak/non-annotated/UnwantedECMInference.java @@ -0,0 +1,19 @@ +public class UnwantedECMInference { + + class Bar { + Object field; + + void doStuff() { + field.toString(); + } + } + + class Baz extends Bar { + @Override + void doStuff() { + // This method does not call toString(), so an @EnsuresCalledMethods("toString") + // annotation on either this method or on the overridden method is an error! + field.hashCode(); + } + } +} diff --git a/checker/tests/ainfer-testchecker/README b/checker/tests/ainfer-testchecker/README new file mode 100644 index 000000000000..6fc34a8cb64a --- /dev/null +++ b/checker/tests/ainfer-testchecker/README @@ -0,0 +1,30 @@ +All tests for the -Ainfer command-line argument must be added to the +"non-annotated" folder. These tests have expected error comments +(// :: error...) in places where the type-checker issues an error before +inference but not after inference, and no other "// :: error". + +The task ainferTest tests the -Ainfer command-line argument in three +steps: + +1. The TestChecker will type-check all files in the "non-annotated" folder +using the -Ainfer command-liner argument, which write the inferred types of +some elements into annotation files. The inferred types are written into +annotation files, but are not considered during this type-check -- for that +reason the expected error comments are necessary. + +2. All tests in "non-annotated" are copied to a temporary directory, named +"annotated". All expected error comments are removed from the files in +"annotated". When testing `.jaif` files, the insert-annotations-to-source +tool inserts the annotations that were inferred in the previous step into +the files of the temporary directory. + +3. The TestChecker will type-check all files in the temporary "annotated" +folder. The expected error comments were removed, but the inferred types +(for `.jaif` files, they were inserted; for other annotation file tests, +they are used as stub files) should ensure that type-checking completes +without errors. + +If an error should persist even with the annotations produced by -Ainfer, +add the test to the "non-annotated/ExpectedErrors.java" file. This is the +only file where the expected error comments are not removed when copied to +"annotated/ExpectedErrors.java". diff --git a/checker/tests/ainfer-testchecker/input-annotation-files/ExistingPurityAnnotations-org.checkerframework.checker.testchecker.ainfer.AinferTestChecker.ajava b/checker/tests/ainfer-testchecker/input-annotation-files/ExistingPurityAnnotations-org.checkerframework.checker.testchecker.ainfer.AinferTestChecker.ajava new file mode 100644 index 000000000000..995fe6b92e1e --- /dev/null +++ b/checker/tests/ainfer-testchecker/input-annotation-files/ExistingPurityAnnotations-org.checkerframework.checker.testchecker.ainfer.AinferTestChecker.ajava @@ -0,0 +1,36 @@ +// This test checks that purity annotations are emitted, as expected, +// when an astub/ajava/jaif file with existing purity annotations is +// supplied. + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.framework.qual.EnsuresQualifierIf; + +@org.checkerframework.framework.qual.AnnotatedFor( + "org.checkerframework.checker.testchecker.ainfer.AinferTestChecker") +public class ExistingPurityAnnotations { + + Object obj; + + @org.checkerframework.dataflow.qual.Pure + public Object pureMethod(Object object) { + return null; + } + + @SuppressWarnings("ainfertest") + @EnsuresQualifierIf(expression = "#1", result = true, qualifier = AinferSibling1.class) + public boolean checkAinferSibling1(Object obj1) { + return true; + } + + public @AinferSibling1 Object usePureMethod() { + + if (checkAinferSibling1(obj)) { + // If pureMethod doesn't have (and can't infer) an @Pure annotation, this call should + // unrefine the type of obj, and an error will be issued when + // we try to return obj on the next line. + pureMethod(obj); + return obj; + } + return null; + } +} diff --git a/checker/tests/ainfer-testchecker/input-annotation-files/ExistingPurityAnnotations-org.checkerframework.checker.testchecker.ainfer.AinferTestChecker.astub b/checker/tests/ainfer-testchecker/input-annotation-files/ExistingPurityAnnotations-org.checkerframework.checker.testchecker.ainfer.AinferTestChecker.astub new file mode 100644 index 000000000000..0ada5af65331 --- /dev/null +++ b/checker/tests/ainfer-testchecker/input-annotation-files/ExistingPurityAnnotations-org.checkerframework.checker.testchecker.ainfer.AinferTestChecker.astub @@ -0,0 +1,14 @@ +import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferDefaultType; +import org.checkerframework.framework.qual.AnnotatedFor; + +@AnnotatedFor("org.checkerframework.checker.testchecker.ainfer.AinferTestChecker") +class ExistingPurityAnnotations { + + // methods: + + @Pure + java.lang.Object pureMethod(@AinferSibling1 java.lang.Object object); + +} diff --git a/checker/tests/ainfer-testchecker/input-annotation-files/README.md b/checker/tests/ainfer-testchecker/input-annotation-files/README.md new file mode 100644 index 000000000000..b9d3f9af9d2f --- /dev/null +++ b/checker/tests/ainfer-testchecker/input-annotation-files/README.md @@ -0,0 +1,3 @@ +This directory contains annotations files to use as input to WPI while running +the -Ainfer test checker tests. You must modify the relevant test to pass an annotation +file: files in this directory are not passed automatically. diff --git a/checker/tests/ainfer-testchecker/non-annotated/AddAnnosToFormalParameterTest.java b/checker/tests/ainfer-testchecker/non-annotated/AddAnnosToFormalParameterTest.java new file mode 100644 index 000000000000..d4d6eff23394 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/AddAnnosToFormalParameterTest.java @@ -0,0 +1,28 @@ +// Reproduces a crash that occurred when running WPI on Apache Hadoop. + +import java.io.*; + +class JavaSerialization { + private interface Serializer {} + + static class JavaSerializationSerializer implements Serializer { + + private ObjectOutputStream oos; + + // Note that it is important to reproduce the crash that the name of this parameter not + // be changed: if it is e.g., "iShouldBeTreatedAsSibling1", no crash occurs. + public JavaSerializationSerializer(OutputStream out) throws IOException { + oos = + new ObjectOutputStream(out) { + @Override + protected void writeStreamHeader() { + // no header + } + }; + } + + public void close() throws IOException { + oos.close(); + } + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/AinferSibling1.java b/checker/tests/ainfer-testchecker/non-annotated/AinferSibling1.java new file mode 100644 index 000000000000..3f7886f5a60e --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/AinferSibling1.java @@ -0,0 +1,13 @@ +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * Copy of the AinferSibling1 annotation, to test how WPI handles annotations with the same simple + * name. + */ +@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) +public @interface AinferSibling1 { + String value() default "Sibling1"; + + String anotherValue() default "foo"; +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/AnnotationWithFieldTest.java b/checker/tests/ainfer-testchecker/non-annotated/AnnotationWithFieldTest.java new file mode 100644 index 000000000000..72ac6ada89fb --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/AnnotationWithFieldTest.java @@ -0,0 +1,50 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSiblingWithFields; + +public class AnnotationWithFieldTest { + + private String fields; + + private String emptyFields; + + void testAnnotationWithFields() { + fields = getAinferSiblingWithFields(); + // :: warning: (argument.type.incompatible) + expectsAinferSiblingWithFields(fields); + } + + void testAnnotationWithEmptyFields() { + emptyFields = getAinferSiblingWithFieldsEmpty(); + // :: warning: (argument.type.incompatible) + expectsAinferSiblingWithEmptyFields(emptyFields); + } + + void expectsAinferSiblingWithFields( + @AinferSiblingWithFields( + value = {"test", "test2"}, + value2 = "test3") + String s) {} + + void expectsAinferSiblingWithEmptyFields( + @AinferSiblingWithFields( + value = {}, + value2 = "") + String s) {} + + @SuppressWarnings("cast.unsafe") + String getAinferSiblingWithFields() { + return (@AinferSiblingWithFields( + value = {"test", "test2"}, + value2 = "test3") + String) + ""; + } + + @SuppressWarnings("cast.unsafe") + String getAinferSiblingWithFieldsEmpty() { + return (@AinferSiblingWithFields( + value = {}, + value2 = "") + String) + ""; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/Anonymous.java b/checker/tests/ainfer-testchecker/non-annotated/Anonymous.java new file mode 100644 index 000000000000..38518230da5d --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/Anonymous.java @@ -0,0 +1,37 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferBottom; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferParent; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferTop; + +public class Anonymous { + public static int field1; // parent + public static int field2; // sib2 + + public Anonymous() { + field1 = getAinferSibling1(); + } + + void testPublicInference() { + // :: warning: (argument.type.incompatible) + expectsAinferSibling2(field2); + // :: warning: (argument.type.incompatible) + expectsParent(field1); + // :: warning: (argument.type.incompatible) + expectsParent(field2); + } + + void expectsBottom(@AinferBottom int t) {} + + void expectsAinferSibling1(@AinferSibling1 int t) {} + + void expectsAinferSibling2(@AinferSibling2 int t) {} + + void expectsAinferTop(@AinferTop int t) {} + + void expectsParent(@AinferParent int t) {} + + @AinferSibling1 int getAinferSibling1() { + return (@AinferSibling1 int) 0; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/AnonymousAndInnerClass.java b/checker/tests/ainfer-testchecker/non-annotated/AnonymousAndInnerClass.java new file mode 100644 index 000000000000..030f88cbabd2 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/AnonymousAndInnerClass.java @@ -0,0 +1,41 @@ +// This test is copied from the all-systems tests, and can be removed from the +// AinferTestChecker test suite once the all-systems tests pass. +// For WPI, this test is actually useful for testing that varargs are handled properly. + +public class AnonymousAndInnerClass { + class MyInnerClass { + public MyInnerClass() {} + + public MyInnerClass(String s) {} + + public MyInnerClass(int... i) {} + } + + static class MyClass { + public MyClass() {} + + public MyClass(String s) {} + + public MyClass(int... i) {} + } + + void test(AnonymousAndInnerClass outer, String tainted) { + new MyClass() {}; + new MyClass(tainted) {}; + new MyClass(1, 2, 3) {}; + new MyClass(1) {}; + new MyInnerClass() {}; + new MyInnerClass(tainted) {}; + new MyInnerClass(1) {}; + new MyInnerClass(1, 2, 3) {}; + this.new MyInnerClass() {}; + this.new MyInnerClass(tainted) {}; + this.new MyInnerClass(1) {}; + this.new MyInnerClass(1, 2, 3) {}; + outer.new MyInnerClass() {}; + outer.new MyInnerClass(tainted) {}; + outer.new MyInnerClass(tainted) {}; + outer.new MyInnerClass(1) {}; + outer.new MyInnerClass(1, 2, 3) {}; + } +} diff --git a/framework/tests/whole-program-inference/non-annotated/AnonymousClassField.java b/checker/tests/ainfer-testchecker/non-annotated/AnonymousClassField.java similarity index 85% rename from framework/tests/whole-program-inference/non-annotated/AnonymousClassField.java rename to checker/tests/ainfer-testchecker/non-annotated/AnonymousClassField.java index 591d693c9f00..e97b9ff0d9fb 100644 --- a/framework/tests/whole-program-inference/non-annotated/AnonymousClassField.java +++ b/checker/tests/ainfer-testchecker/non-annotated/AnonymousClassField.java @@ -3,6 +3,6 @@ import java.util.*; -class AnonymousClassField { +public class AnonymousClassField { public static final List foo = new ArrayList() {}; } diff --git a/checker/tests/ainfer-testchecker/non-annotated/AnonymousClassWithField.java b/checker/tests/ainfer-testchecker/non-annotated/AnonymousClassWithField.java new file mode 100644 index 000000000000..49aa669c4bc8 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/AnonymousClassWithField.java @@ -0,0 +1,27 @@ +// This test checks that an anonymous class with a field +// doesn't cause a crash. + +public class AnonymousClassWithField { + + public void scan(InterfaceTest foo) { + // do nothing + } + + public void test() { + this.scan( + new InterfaceTestExtension() { + private String s1 = InterfaceTest.getAinferSibling1(); + + @Override + public void testX() { + // :: warning: (argument) + requireAinferSibling1(s1); + } + + public void testY() { + // :: warning: (argument) + requireAinferSibling1(toaster); + } + }); + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/AnonymousOverride.java b/checker/tests/ainfer-testchecker/non-annotated/AnonymousOverride.java new file mode 100644 index 000000000000..4426b2fc58b2 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/AnonymousOverride.java @@ -0,0 +1,18 @@ +// A test that inference does not crash on code that contains an +// anonymous class that overrides a method. + +class AnonymousOverride { + public static void main(String[] args) { + (new SpecialThread() { + @Override + public void run() { + System.out.println("starting a thread!"); + } + }) + .start(); + } + + private static class SpecialThread extends Thread { + public T t; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/ArrayAndTypevar.java b/checker/tests/ainfer-testchecker/non-annotated/ArrayAndTypevar.java new file mode 100644 index 000000000000..b7488365d561 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/ArrayAndTypevar.java @@ -0,0 +1,17 @@ +// A test that makes sure that inference doesn't crash when an array is +// used as a type parameter. + +class ArrayAndTypevar { + + private class MyClass { + private T t; + + public MyClass(T t) { + this.t = t; + } + } + + public void test() { + MyClass m = new MyClass(new String[] {"foo", "bar"}); + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/CompoundTypeTest.java b/checker/tests/ainfer-testchecker/non-annotated/CompoundTypeTest.java new file mode 100644 index 000000000000..07f75d63e10b --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/CompoundTypeTest.java @@ -0,0 +1,24 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; + +public class CompoundTypeTest { + // The default type for fields is @AinferDefaultType. + Object[] field; + + void assign() { + field = getCompoundType(); + } + + void test() { + // :: warning: (argument.type.incompatible) + expectsCompoundType(field); + } + + void expectsCompoundType(@AinferSibling1 Object @AinferSibling2 [] obj) {} + + @AinferSibling1 Object @AinferSibling2 [] getCompoundType() { + @SuppressWarnings("cast.unsafe") + @AinferSibling1 Object @AinferSibling2 [] out = (@AinferSibling1 Object @AinferSibling2 []) new Object[1]; + return out; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/CompoundTypeTest2.java b/checker/tests/ainfer-testchecker/non-annotated/CompoundTypeTest2.java new file mode 100644 index 000000000000..91fb7315d3c0 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/CompoundTypeTest2.java @@ -0,0 +1,4 @@ +public class CompoundTypeTest2 { + private static int[] foo = new int[10]; + private static String[] bar = new String[10]; +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/ConflictingAnnotationsTest.java b/checker/tests/ainfer-testchecker/non-annotated/ConflictingAnnotationsTest.java new file mode 100644 index 000000000000..6ff64312e7f0 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/ConflictingAnnotationsTest.java @@ -0,0 +1,31 @@ +// Tests whether inferring an @AinferSibling1 annotation when another @AinferSibling1 annotation in +// the default +// package is present causes problems. Conflicting annotations that are not in the default package +// are not a problem, because TypeMirror#toString prints their fully-qualified names, making +// namespace collisions impossible. + +public class ConflictingAnnotationsTest { + + int getWPINamespaceAinferSibling1() { + return getAinferSibling1(); + } + + // This version of AinferSibling1 is not typechecked - it doesn't belong to the checker and + // instead is + // defined in the AinferSibling1.java file in this directory. + @AinferSibling1 Object getLocalAinferSibling1(Object o) { + return o; + } + + void test() { + // :: warning: argument.type.incompatible + expectsAinferSibling1(getWPINamespaceAinferSibling1()); + } + + @org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1 int getAinferSibling1() { + return 1; + } + + void expectsAinferSibling1( + @org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1 int i) {} +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/ConstructorTest.java b/checker/tests/ainfer-testchecker/non-annotated/ConstructorTest.java new file mode 100644 index 000000000000..8d3cdddb66b1 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/ConstructorTest.java @@ -0,0 +1,12 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferTop; + +public class ConstructorTest { + + public ConstructorTest(int top) {} + + void test() { + @AinferTop int top = (@AinferTop int) 0; + // :: warning: (argument.type.incompatible) + new ConstructorTest(top); + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/CrazyEnum.java b/checker/tests/ainfer-testchecker/non-annotated/CrazyEnum.java new file mode 100644 index 000000000000..fd9a9064b0d4 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/CrazyEnum.java @@ -0,0 +1,20 @@ +@SuppressWarnings("all") // Check for crashes. +public class CrazyEnum { + private enum MyEnum { + ENUM_CONST1 { + private final String s = method(); + + private String method() { + return "hello"; + } + }, + + ENUM_CONST2 { + private final String s = this.method(); + + private String method() { + return "hello"; + } + } + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/DefaultsTest.java b/checker/tests/ainfer-testchecker/non-annotated/DefaultsTest.java new file mode 100644 index 000000000000..c0d466cbbf81 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/DefaultsTest.java @@ -0,0 +1,29 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferBottom; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferDefaultType; + +// The @AinferDefaultType annotation, which is the default for every location, is forbidden to be +// written anywhere. This class attempts to infer @AinferDefaultType in several locations, and the +// annotated version of this class (in the annotated folder) should have no explicit +// @AinferDefaultType annotations. +public class DefaultsTest { + String defaultField = ""; + String defaultField2; + + void test() { + @SuppressWarnings("all") // To allow the use of the explicit @AinferDefaultType. + @AinferDefaultType String explicitDefault = ""; + defaultField2 = explicitDefault; + } + + // This method's return type should not be updated by the whole-program inference + // since it is the default. + String lubTest() { + if (Math.random() > 0.5) { + return ""; // @AinferDefaultType + } else { + @SuppressWarnings("cast.unsafe") + @AinferBottom String s = (@AinferBottom String) ""; + return s; + } + } +} diff --git a/framework/tests/whole-program-inference/non-annotated/DeviceTypeTest.java b/checker/tests/ainfer-testchecker/non-annotated/DeviceTypeTest.java similarity index 100% rename from framework/tests/whole-program-inference/non-annotated/DeviceTypeTest.java rename to checker/tests/ainfer-testchecker/non-annotated/DeviceTypeTest.java diff --git a/framework/tests/whole-program-inference/non-annotated/DoubleGeneric.java b/checker/tests/ainfer-testchecker/non-annotated/DoubleGeneric.java similarity index 89% rename from framework/tests/whole-program-inference/non-annotated/DoubleGeneric.java rename to checker/tests/ainfer-testchecker/non-annotated/DoubleGeneric.java index 8ec1eefe62f7..6844994e7fab 100644 --- a/framework/tests/whole-program-inference/non-annotated/DoubleGeneric.java +++ b/checker/tests/ainfer-testchecker/non-annotated/DoubleGeneric.java @@ -3,6 +3,6 @@ import java.util.HashMap; import java.util.Map; -class DoubleGeneric { +public class DoubleGeneric { static Map> map10 = new HashMap>(); } diff --git a/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierFieldDecl.java b/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierFieldDecl.java new file mode 100644 index 000000000000..5e16ac6eaba3 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierFieldDecl.java @@ -0,0 +1,14 @@ +// This test ensures that a field having a non-default inferred type +// does not cause inference to issue an @EnsuresQualifier annotation +// stating that fact on every method in the class, even those in which +// the field is not mentioned (and therefore not in the store, making +// them unverifiable). + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +class EnsuresQualifierFieldDecl { + @AinferSibling1 Object bar; + + // No annotation should be inferred here. + void test() {} +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierParamsTest.java b/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierParamsTest.java new file mode 100644 index 000000000000..079c061bbb6a --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierParamsTest.java @@ -0,0 +1,184 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferBottom; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferParent; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; +import org.checkerframework.framework.qual.EnsuresQualifier; + +class EnsuresQualifierParamsTest { + + // these methods are used to infer types + + @SuppressWarnings("contracts.postcondition") // establish ground truth + @EnsuresQualifier(expression = "#1", qualifier = AinferParent.class) + void becomeParent(Object arg) {} + + @SuppressWarnings("contracts.postcondition") // establish ground truth + @EnsuresQualifier(expression = "#1", qualifier = AinferSibling1.class) + void becomeAinferSibling1(Object arg) {} + + @SuppressWarnings("contracts.postcondition") // establish ground truth + @EnsuresQualifier(expression = "#1", qualifier = AinferSibling2.class) + void becomeAinferSibling2(Object arg) {} + + @SuppressWarnings("contracts.postcondition") // establish ground truth + @EnsuresQualifier(expression = "#1", qualifier = AinferBottom.class) + void becomeBottom(Object arg) {} + + // these methods should have types inferred for them + + void argIsParent(Object arg) { + becomeParent(arg); + } + + void argIsParent_2(Object arg, boolean b) { + if (b) { + becomeAinferSibling1(arg); + } else { + becomeAinferSibling2(arg); + } + } + + void argIsAinferSibling2(Object arg) { + becomeAinferSibling2(arg); + } + + void argIsAinferSibling2_2(Object arg, boolean b) { + if (b) { + becomeAinferSibling2(arg); + } else { + becomeBottom(arg); + } + } + + void thisIsParent() { + becomeParent(this); + } + + void thisIsParent_2(boolean b) { + if (b) { + becomeAinferSibling1(this); + } else { + becomeAinferSibling2(this); + } + } + + void thisIsParent_2_2(boolean b) { + if (b) { + becomeAinferSibling2(this); + } else { + becomeAinferSibling1(this); + } + } + + void thisIsParent_3(boolean b) { + if (b) { + becomeAinferSibling1(this); + } else { + becomeAinferSibling2(this); + } + noEnsures(); + } + + void thisIsEmpty(boolean b) { + if (b) { + // do nothing + this.noEnsures(); + } else { + becomeAinferSibling1(this); + } + } + + void thisIsAinferSibling2() { + becomeAinferSibling2(this); + } + + void thisIsAinferSibling2_2(boolean b) { + if (b) { + becomeAinferSibling2(this); + } else { + becomeBottom(this); + } + } + + void thisIsAinferSibling2_2_2(boolean b) { + if (b) { + becomeBottom(this); + } else { + becomeAinferSibling2(this); + } + } + + void noEnsures() {} + + void client1(Object arg) { + argIsParent(arg); + // :: warning: (assignment.type.incompatible) + @AinferParent Object p = arg; + } + + void client2(Object arg) { + argIsParent_2(arg, true); + // :: warning: (assignment.type.incompatible) + @AinferParent Object p = arg; + } + + void client3(Object arg) { + argIsAinferSibling2(arg); + // :: warning: (assignment.type.incompatible) + @AinferSibling2 Object x = arg; + } + + void client4(Object arg) { + argIsAinferSibling2_2(arg, true); + // :: warning: (assignment.type.incompatible) + @AinferSibling2 Object x = arg; + } + + void clientThis1() { + thisIsParent(); + // :: warning: (assignment.type.incompatible) + @AinferParent Object o = this; + } + + void clientThis2() { + thisIsParent_2(true); + // :: warning: (assignment.type.incompatible) + @AinferParent Object o = this; + } + + void clientThis2_2() { + thisIsParent_2(false); + // :: warning: (assignment.type.incompatible) + @AinferParent Object o = this; + } + + void clientThis2_3() { + thisIsParent_3(false); + // :: warning: (assignment.type.incompatible) + @AinferParent Object o = this; + } + + void clientThis3() { + thisIsAinferSibling2(); + // :: warning: (assignment.type.incompatible) + @AinferSibling2 Object o = this; + } + + void clientThis4() { + thisIsAinferSibling2_2(true); + // :: warning: (assignment.type.incompatible) + @AinferSibling2 Object o = this; + } + + void clientThis5() { + thisIsAinferSibling2_2_2(true); + // :: warning: (assignment.type.incompatible) + @AinferSibling2 Object o = this; + } + + void clientThis6() { + thisIsParent_2_2(true); + // :: warning: (assignment.type.incompatible) + @AinferParent Object o = this; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierTest.java b/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierTest.java new file mode 100644 index 000000000000..03718f906391 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/EnsuresQualifierTest.java @@ -0,0 +1,82 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferBottom; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferParent; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferTop; + +class EnsuresQualifierTest { + + @AinferTop int field1; + @AinferTop int field2; + + @AinferTop int top; + @AinferParent int parent; + @AinferSibling1 int sibling1; + @AinferSibling2 int sibling2; + @AinferBottom int bottom; + + void field1IsParent() { + field1 = parent; + } + + void field1IsParent_2(boolean b) { + if (b) { + field1 = sibling1; + } else { + field1 = sibling2; + } + } + + void field1IsAinferSibling2() { + field1 = sibling2; + } + + void field1IsAinferSibling2_2(boolean b) { + if (b) { + field1 = sibling2; + } else { + field1 = bottom; + } + } + + void parentIsAinferSibling1() { + parent = sibling1; + } + + // Prevent refinement of the `parent` field variable. + void parentIsParent(@AinferParent int x) { + parent = x; + } + + void noEnsures() {} + + void client1() { + field1IsParent(); + // :: warning: (assignment.type.incompatible) + @AinferParent int p = field1; + } + + void client2() { + field1IsParent_2(true); + // :: warning: (assignment.type.incompatible) + @AinferParent int p = field1; + } + + void client3() { + field1IsAinferSibling2(); + // :: warning: (assignment.type.incompatible) + @AinferSibling2 int x = field1; + } + + void client4() { + field1IsAinferSibling2_2(true); + // :: warning: (assignment.type.incompatible) + @AinferSibling2 int x = field1; + } + + void client5() { + parentIsAinferSibling1(); + // :: warning: (assignment.type.incompatible) + @AinferSibling1 int x = parent; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/EnumConstants.java b/checker/tests/ainfer-testchecker/non-annotated/EnumConstants.java new file mode 100644 index 000000000000..d971edba23d9 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/EnumConstants.java @@ -0,0 +1,21 @@ +// @skip-test + +// Check that types on enum constants can be inferred. This test doesn't succeed for either kind of +// WPI, because WPI doesn't learn anything about enum constants from how they're used. They also +// cannot be assigned to, so there's no way for WPI to learn their types. + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +public class EnumConstants { + enum MyEnum { + ONE, + TWO; + } + + void requiresS1(@AinferSibling1 MyEnum e) {} + + void test() { + // :: warning: argument.type.incompatible + requiresS1(MyEnum.ONE); + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/EnumMapCrash.java b/checker/tests/ainfer-testchecker/non-annotated/EnumMapCrash.java new file mode 100644 index 000000000000..622d6e6ee080 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/EnumMapCrash.java @@ -0,0 +1,26 @@ +// Based on a crash encountered when running WPI with the RLC on Apache Hadoop. + +import java.util.EnumMap; + +@SuppressWarnings("all") // only check for crashes +public class EnumMapCrash { + private class Holder { + public T held; + + public Holder(T held) { + this.held = held; + } + + @Override + public String toString() { + return String.valueOf(held); + } + } + + private enum FSEditLogOpCodes {} + + void callHolder(FSEditLogOpCodes f, EnumMap> opCounts) { + Holder holder = opCounts.get(f); + holder.held++; + } +} diff --git a/framework/tests/whole-program-inference/non-annotated/EnumTest.java b/checker/tests/ainfer-testchecker/non-annotated/EnumTest.java similarity index 100% rename from framework/tests/whole-program-inference/non-annotated/EnumTest.java rename to checker/tests/ainfer-testchecker/non-annotated/EnumTest.java diff --git a/checker/tests/ainfer-testchecker/non-annotated/EnumWithInnerClass.java b/checker/tests/ainfer-testchecker/non-annotated/EnumWithInnerClass.java new file mode 100644 index 000000000000..32a1b3ae4199 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/EnumWithInnerClass.java @@ -0,0 +1,21 @@ +// This test ensures that enums with inner classes are printed properly to avoid crashing the stub +// parser, which was a problem with an earlier version of stub-based WPI. + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +enum EnumWithInnerClass { + CONSTANT; + + private static class MyInnerClass { + int getAinferSibling1() { + return (@AinferSibling1 int) 0; + } + + void requireAinferSibling1(@AinferSibling1 int x) {} + + void test() { + // :: warning: argument.type.incompatible + requireAinferSibling1(getAinferSibling1()); + } + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/ExistingPurityAnnotations.java b/checker/tests/ainfer-testchecker/non-annotated/ExistingPurityAnnotations.java new file mode 100644 index 000000000000..5a8e9d7453db --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/ExistingPurityAnnotations.java @@ -0,0 +1,33 @@ +// This test checks that purity annotations are emitted, as expected, +// when an astub/ajava/jaif file with existing purity annotations is +// supplied. + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.framework.qual.EnsuresQualifierIf; + +public class ExistingPurityAnnotations { + + Object obj; + + public Object pureMethod(Object object) { + return null; + } + + @SuppressWarnings("ainfertest") + @EnsuresQualifierIf(expression = "#1", result = true, qualifier = AinferSibling1.class) + public boolean checkAinferSibling1(Object obj1) { + return true; + } + + public @AinferSibling1 Object usePureMethod() { + + if (checkAinferSibling1(obj)) { + // If pureMethod doesn't have (and can't infer) an @Pure annotation, this call should + // unrefine the type of obj, and an error will be issued when + // we try to return obj on the next line. + pureMethod(obj); + return obj; + } + return null; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/ExpectedErrors.java b/checker/tests/ainfer-testchecker/non-annotated/ExpectedErrors.java new file mode 100644 index 000000000000..9c63af4c83f7 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/ExpectedErrors.java @@ -0,0 +1,244 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferBottom; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferParent; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferToIgnore; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferTop; +import org.checkerframework.framework.qual.IgnoreInWholeProgramInference; + +import java.lang.reflect.Field; + +/** + * This file contains expected errors that should exist even after the jaif type inference occurs. + */ +public class ExpectedErrors { + + // Case where the declared type is a supertype of the refined type. + private @AinferTop int privateDeclaredField; + public @AinferTop int publicDeclaredField; + + // The type of both privateDeclaredField and publicDeclaredField are + // not refined to @AinferBottom. + void assignFieldsToAinferSibling1() { + privateDeclaredField = getAinferSibling1(); + publicDeclaredField = getAinferSibling1(); + } + + void testFields() { + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(privateDeclaredField); + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(publicDeclaredField); + } + + // Case where the declared type is a subtype of the refined type. + private @AinferBottom int privateDeclaredField2; + public @AinferBottom int publicDeclaredField2; + + // The refinement cannot happen and an assignemnt type incompatible error occurs. + void assignFieldsToAinferTop() { + // :: warning: (assignment.type.incompatible) + privateDeclaredField2 = getAinferTop(); + // :: warning: (assignment.type.incompatible) + publicDeclaredField2 = getAinferTop(); + } + + // No errors should be issued below: + void assignFieldsToBot() { + privateDeclaredField2 = getBottom(); + publicDeclaredField2 = getBottom(); + } + + // Testing that the types above were not widened. + void testFields2() { + expectsBottom(privateDeclaredField2); + expectsBottom(publicDeclaredField2); + } + + // LUB TEST + // The default type for fields is @AinferTop. + private static int lubPrivateField; + public static int lubPublicField; + + void assignLubFieldsToAinferSibling1() { + lubPrivateField = getAinferSibling1(); + lubPublicField = getAinferSibling1(); + } + + static { + lubPrivateField = getAinferSibling2(); + lubPublicField = getAinferSibling2(); + } + + void testLUBFields1() { + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(lubPrivateField); + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(lubPublicField); + } + + void testLUBFields2() { + // :: warning: (argument.type.incompatible) + expectsAinferSibling2(lubPrivateField); + // :: warning: (argument.type.incompatible) + expectsAinferSibling2(lubPublicField); + } + + private static boolean bool = false; + + public static int lubTest() { + if (bool) { + return (@AinferSibling1 int) 0; + } else { + return (@AinferSibling2 int) 0; + } + } + + public @AinferSibling1 int getAinferSibling1Wrong() { + int x = lubTest(); + // :: warning: (return.type.incompatible) + return x; + } + + public @AinferSibling2 int getAinferSibling2Wrong() { + int x = lubTest(); + // :: warning: (return.type.incompatible) + return x; + } + + void expectsAinferSibling1(@AinferSibling1 int t) {} + + void expectsAinferSibling2(@AinferSibling2 int t) {} + + void expectsBottom(@AinferBottom int t) {} + + void expectsBottom(@AinferBottom String t) {} + + void expectsAinferTop(@AinferTop int t) {} + + void expectsParent(@AinferParent int t) {} + + static @AinferSibling1 int getAinferSibling1() { + return 0; + } + + static @AinferSibling2 int getAinferSibling2() { + return 0; + } + + @AinferBottom int getBottom() { + return 0; + } + + @AinferTop int getAinferTop() { + return 0; + } + + // Method Field.setBoolean != ExpectedErrors.setBoolean. + // No refinement should happen. + void test(Field f) throws Exception { + f.setBoolean(null, false); + } + + void setBoolean(Object o, boolean b) { + // :: warning: (assignment.type.incompatible) + @AinferBottom Object bot = o; + } + + public class SuppressWarningsTest { + // Tests that whole-program inference in a @SuppressWarnings block is ignored. + private int i; + private int i2; + + @SuppressWarnings("all") + public void suppressWarningsTest() { + i = (@AinferSibling1 int) 0; + i2 = getAinferSibling1(); + } + + public void suppressWarningsTest2() { + SuppressWarningsInner.i = (@AinferSibling1 int) 0; + SuppressWarningsInner.i2 = getAinferSibling1(); + } + + public void suppressWarningsValidation() { + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(i); + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(i2); + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(SuppressWarningsInner.i); + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(SuppressWarningsInner.i2); + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(suppressWarningsMethodReturn()); + + suppressWarningsMethodParams(getAinferSibling1()); + } + + @SuppressWarnings("all") + public int suppressWarningsMethodReturn() { + return getAinferSibling1(); + } + + // It is problematic to automatically test whole-program inference for method params when + // suppressing warnings. + // Since we must use @SuppressWarnings() for the method, we won't be able to catch any error + // inside the method body. Verified manually that in the "annotated" folder param's type + // wasn't updated. + @SuppressWarnings("all") + public void suppressWarningsMethodParams(int param) {} + } + + @SuppressWarnings("all") + static class SuppressWarningsInner { + public static int i; + public static int i2; + } + + class NullTest { + // The default type for fields is @AinferDefaultType. + private String privateField; + public String publicField; + + // The types of both fields are not refined to @AinferBottom, as whole-program + // inference never performs refinement in the presence of the null literal. + @SuppressWarnings("value") + void assignFieldsToBottom() { + privateField = null; + publicField = null; + } + + // Testing the refinement above. + void testFields() { + // :: warning: (argument.type.incompatible) + expectsBottom(privateField); + // :: warning: (argument.type.incompatible) + expectsBottom(publicField); + } + } + + class IgnoreMetaAnnotationTest2 { + @AinferToIgnore int field; + @IgnoreInWholeProgramInference int field2; + + void foo() { + field = getAinferSibling1(); + field2 = getAinferSibling1(); + } + + void test() { + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(field); + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(field2); + } + } + + class AssignParam { + public void f(@AinferBottom Object param) { + // :: warning: assignment.type.incompatible + param = ((@AinferTop Object) null); + } + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/FieldInOtherCompilationUnit.java b/checker/tests/ainfer-testchecker/non-annotated/FieldInOtherCompilationUnit.java new file mode 100644 index 000000000000..b19093a4baf7 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/FieldInOtherCompilationUnit.java @@ -0,0 +1,16 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +import java.util.GregorianCalendar; + +public class FieldInOtherCompilationUnit { + + static @AinferSibling1 int myTime; + + static void test() { + new GregorianCalendar() { + public void newMethod() { + this.time = myTime; + } + }; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/FromReceiver.java b/checker/tests/ainfer-testchecker/non-annotated/FromReceiver.java new file mode 100644 index 000000000000..894bcfa0259b --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/FromReceiver.java @@ -0,0 +1,50 @@ +// A test that an annotation on a receiver can be used when inferring +// the types of the method being invoked. + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +public class FromReceiver { + + public void source(@AinferSibling1 FromReceiver this) { + this.sinkNoThis(); + this.sinkExplicitThis(); + + sinkNoThis2(); + sinkExplicitThis2(); + } + + public void sinkNoThis() { + // :: warning: assignment + @AinferSibling1 FromReceiver f = this; + } + + public void sinkExplicitThis(FromReceiver this) { + // :: warning: assignment + @AinferSibling1 FromReceiver f = this; + } + + public void sinkNoThis2() { + // :: warning: assignment + @AinferSibling1 FromReceiver f = this; + } + + public void sinkExplicitThis2(FromReceiver this) { + // :: warning: assignment + @AinferSibling1 FromReceiver f = this; + } + + public static void source2(@AinferSibling1 FromReceiver f1) { + f1.sinkNoThis3(); + f1.sinkExplicitThis3(); + } + + public void sinkNoThis3() { + // :: warning: assignment + @AinferSibling1 FromReceiver f = this; + } + + public void sinkExplicitThis3(FromReceiver this) { + // :: warning: assignment + @AinferSibling1 FromReceiver f = this; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/IShouldBeSibling1.java b/checker/tests/ainfer-testchecker/non-annotated/IShouldBeSibling1.java new file mode 100644 index 000000000000..7bd459402325 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/IShouldBeSibling1.java @@ -0,0 +1,14 @@ +// Tests the feature that allows checkers to add annotations onto a class +// declaration, which is used for custom inference logic. There is custom +// logic in the AinferTestChecker specifically for classes with the name +// "IShouldBeSibling1" that infers an @Sibling1 annotation for them. + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +@SuppressWarnings("super.invocation") // Intentional. +public class IShouldBeSibling1 { + public static void test(IShouldBeSibling1 s1) { + // :: warning: (assignment.type.incompatible) + @AinferSibling1 IShouldBeSibling1 s = s1; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/IgnoreMetaAnnotationTest1.java b/checker/tests/ainfer-testchecker/non-annotated/IgnoreMetaAnnotationTest1.java new file mode 100644 index 000000000000..ae75e631512e --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/IgnoreMetaAnnotationTest1.java @@ -0,0 +1,22 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +// See ExpectedErrors#IgnoreMetaAnnotationTest2 +public class IgnoreMetaAnnotationTest1 { + + int field2; + + void foo() { + field2 = getAinferSibling1(); + } + + void test() { + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(field2); + } + + void expectsAinferSibling1(@AinferSibling1 int t) {} + + static @AinferSibling1 int getAinferSibling1() { + return 0; + } +} diff --git a/framework/tests/whole-program-inference/non-annotated/ImplicitAnnosTest.java b/checker/tests/ainfer-testchecker/non-annotated/ImplicitAnnosTest.java similarity index 100% rename from framework/tests/whole-program-inference/non-annotated/ImplicitAnnosTest.java rename to checker/tests/ainfer-testchecker/non-annotated/ImplicitAnnosTest.java diff --git a/checker/tests/ainfer-testchecker/non-annotated/InheritanceTest.java b/checker/tests/ainfer-testchecker/non-annotated/InheritanceTest.java new file mode 100644 index 000000000000..05543bdf34de --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/InheritanceTest.java @@ -0,0 +1,24 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferBottom; + +public class InheritanceTest { + class IParent { + int field; + + public void expectsBotNoSignature(int t) { + // :: warning: (argument.type.incompatible) + expectsBot(t); + // :: warning: (argument.type.incompatible) + expectsBot(field); + } + + void expectsBot(@AinferBottom int t) {} + } + + class IChild extends IParent { + void test1() { + @AinferBottom int bot = (@AinferBottom int) 0; + expectsBotNoSignature(bot); + field = bot; + } + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/InnerClassFieldDeclAnno.java b/checker/tests/ainfer-testchecker/non-annotated/InnerClassFieldDeclAnno.java new file mode 100644 index 000000000000..c8b5cb366a74 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/InnerClassFieldDeclAnno.java @@ -0,0 +1,24 @@ +// Tests that ajava-based inference places declaration annotations on +// inner class fields in the correct place. Declaration annotations (unlike +// type annotations) need to be placed before the name of the outer class. +// E.g., "@DeclAnno Outer.Inner field;" rather than "Outer.@DeclAnno Inner field;". + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferTreatAsSibling1; + +public class InnerClassFieldDeclAnno { + static class Outer { + static class Inner {} + } + + public Outer.Inner iShouldBeTreatedAsSibling1 = new Outer.Inner(); + + @AinferTreatAsSibling1 public Outer.Inner preAnnotated = null; + + public static void test(InnerClassFieldDeclAnno a) { + // :: warning: (assignment.type.incompatible) + @AinferSibling1 Object obj = a.iShouldBeTreatedAsSibling1; + // Test that the annotation works as expected. + @AinferSibling1 Object obj2 = a.preAnnotated; + } +} diff --git a/framework/tests/whole-program-inference/non-annotated/InnerTypeTest.java b/checker/tests/ainfer-testchecker/non-annotated/InnerTypeTest.java similarity index 93% rename from framework/tests/whole-program-inference/non-annotated/InnerTypeTest.java rename to checker/tests/ainfer-testchecker/non-annotated/InnerTypeTest.java index 01e3631407ad..b713d30dfc60 100644 --- a/framework/tests/whole-program-inference/non-annotated/InnerTypeTest.java +++ b/checker/tests/ainfer-testchecker/non-annotated/InnerTypeTest.java @@ -1,4 +1,4 @@ -class InnerTypeTest { +public class InnerTypeTest { public static String toStringQuoted(Object[] a) { return toString(a, true); } diff --git a/framework/tests/whole-program-inference/non-annotated/InnerTypeTest2.java b/checker/tests/ainfer-testchecker/non-annotated/InnerTypeTest2.java similarity index 88% rename from framework/tests/whole-program-inference/non-annotated/InnerTypeTest2.java rename to checker/tests/ainfer-testchecker/non-annotated/InnerTypeTest2.java index c0ada6f6d2ed..52e8111d4ec2 100644 --- a/framework/tests/whole-program-inference/non-annotated/InnerTypeTest2.java +++ b/checker/tests/ainfer-testchecker/non-annotated/InnerTypeTest2.java @@ -1,4 +1,4 @@ -class InnerTypeTest2 { +public class InnerTypeTest2 { public static int[] min_max(int[] a) { if (a.length == 0) { return null; diff --git a/framework/tests/whole-program-inference/non-annotated/InnerTypeTest3.java b/checker/tests/ainfer-testchecker/non-annotated/InnerTypeTest3.java similarity index 87% rename from framework/tests/whole-program-inference/non-annotated/InnerTypeTest3.java rename to checker/tests/ainfer-testchecker/non-annotated/InnerTypeTest3.java index 25afc0dbdc2b..eb3386cc5e62 100644 --- a/framework/tests/whole-program-inference/non-annotated/InnerTypeTest3.java +++ b/checker/tests/ainfer-testchecker/non-annotated/InnerTypeTest3.java @@ -1,4 +1,4 @@ -class InnerTypeTest3 { +public class InnerTypeTest3 { private int[] nums; private static byte[] buffer = new byte[4096]; diff --git a/checker/tests/ainfer-testchecker/non-annotated/InterfaceTest.java b/checker/tests/ainfer-testchecker/non-annotated/InterfaceTest.java new file mode 100644 index 000000000000..6ceeb0109a5b --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/InterfaceTest.java @@ -0,0 +1,19 @@ +// checks that types can be inferred for constants defined in interfaces + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +@SuppressWarnings("cast.unsafe") +public interface InterfaceTest { + public String toaster = getAinferSibling1(); + + public static @AinferSibling1 String getAinferSibling1() { + return (@AinferSibling1 String) "foo"; + } + + default void requireAinferSibling1(@AinferSibling1 String x) {} + + default void testX() { + // :: warning: (argument.type.incompatible) + requireAinferSibling1(toaster); + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/InterfaceTestExtension.java b/checker/tests/ainfer-testchecker/non-annotated/InterfaceTestExtension.java new file mode 100644 index 000000000000..60894e14cf12 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/InterfaceTestExtension.java @@ -0,0 +1,4 @@ +// Doesn't actually test anything directly, but used to create a depth-two interface +// chain. + +public interface InterfaceTestExtension extends InterfaceTest {} diff --git a/checker/tests/ainfer-testchecker/non-annotated/LUBAssignmentTest.java b/checker/tests/ainfer-testchecker/non-annotated/LUBAssignmentTest.java new file mode 100644 index 000000000000..6d2e3c262950 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/LUBAssignmentTest.java @@ -0,0 +1,50 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferParent; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; + +public class LUBAssignmentTest { + // The default type for fields is @AinferDefaultType. + private static int privateField; + public static int publicField; + + void assignFieldsToAinferSibling1() { + privateField = getAinferSibling1(); + publicField = getAinferSibling1(); + } + + static { + privateField = getAinferSibling2(); + publicField = getAinferSibling2(); + } + + // LUB between @AinferSibling1 and @AinferSibling2 is @AinferParent, therefore the assignments + // above refine the type of privateField to @AinferParent. + void testFields() { + // :: warning: (argument.type.incompatible) + expectsParent(privateField); + // :: warning: (argument.type.incompatible) + expectsParent(publicField); + } + + void expectsParent(@AinferParent int t) {} + + static @AinferSibling1 int getAinferSibling1() { + return 0; + } + + static @AinferSibling2 int getAinferSibling2() { + return 0; + } + + String lubTest2() { + if (Math.random() > 0.5) { + @SuppressWarnings("cast.unsafe") + @AinferSibling1 String s = (@AinferSibling1 String) ""; + return s; + } else { + @SuppressWarnings("cast.unsafe") + @AinferSibling2 String s = (@AinferSibling2 String) ""; + return s; + } + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/LambdaParamCrash.java b/checker/tests/ainfer-testchecker/non-annotated/LambdaParamCrash.java new file mode 100644 index 000000000000..91497b956b2a --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/LambdaParamCrash.java @@ -0,0 +1,25 @@ +// A test case that caused an assertion to be violated when inferring something +// about e1, the parameter of the lambda. This test case is based on a crash +// encountered in the wild. + +import java.io.IOException; +import java.util.function.BiConsumer; + +@SuppressWarnings("all") // only checking for crashes +public class LambdaParamCrash { + + void groupAndSend() { + addListener( + (r, e1) -> { + if (e1 == null) { + e1 = badResponse(); + } + }); + } + + private IOException badResponse() { + return new IOException(); + } + + public static void addListener(BiConsumer action) {} +} diff --git a/framework/tests/whole-program-inference/non-annotated/LambdaReturn.java b/checker/tests/ainfer-testchecker/non-annotated/LambdaReturn.java similarity index 95% rename from framework/tests/whole-program-inference/non-annotated/LambdaReturn.java rename to checker/tests/ainfer-testchecker/non-annotated/LambdaReturn.java index aef1e63cee43..7b24565b178a 100644 --- a/framework/tests/whole-program-inference/non-annotated/LambdaReturn.java +++ b/checker/tests/ainfer-testchecker/non-annotated/LambdaReturn.java @@ -3,7 +3,7 @@ import java.io.FileFilter; -class LambdaReturn { +public class LambdaReturn { void test() { FileFilter docxFilter = pathname -> { diff --git a/checker/tests/ainfer-testchecker/non-annotated/LocalClassTest.java b/checker/tests/ainfer-testchecker/non-annotated/LocalClassTest.java new file mode 100644 index 000000000000..cc1fb6d4b123 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/LocalClassTest.java @@ -0,0 +1,11 @@ +// test case for https://github.com/typetools/checker-framework/issues/3461 + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +public class LocalClassTest { + public void method() { + class Local { + Object o = (@AinferSibling1 Object) null; + } + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/MethodDefinedInSupertype.java b/checker/tests/ainfer-testchecker/non-annotated/MethodDefinedInSupertype.java new file mode 100644 index 000000000000..3683d4049250 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/MethodDefinedInSupertype.java @@ -0,0 +1,23 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferParent; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +abstract class MethodDefinedInSupertype { + + void test() { + // :: warning: argument.type.incompatible + expectsAinferSibling1(shouldReturnAinferSibling1()); + } + + public void expectsAinferSibling1(@AinferSibling1 int t) {} + + public abstract int shouldReturnAinferSibling1(); + + void testMultipleOverrides() { + // :: warning: argument.type.incompatible + expectsParent(shouldReturnParent()); + } + + public void expectsParent(@AinferParent int t1) {} + + public abstract int shouldReturnParent(); +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/MethodOverrideInSubtype.java b/checker/tests/ainfer-testchecker/non-annotated/MethodOverrideInSubtype.java new file mode 100644 index 000000000000..00d52190aa3e --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/MethodOverrideInSubtype.java @@ -0,0 +1,17 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +public class MethodOverrideInSubtype extends MethodDefinedInSupertype { + @java.lang.Override + public int shouldReturnAinferSibling1() { + return getAinferSibling1(); + } + + private @AinferSibling1 int getAinferSibling1() { + return 0; + } + + @java.lang.Override + public int shouldReturnParent() { + return getAinferSibling1(); + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/MethodOverrideInSubtype2.java b/checker/tests/ainfer-testchecker/non-annotated/MethodOverrideInSubtype2.java new file mode 100644 index 000000000000..0ef69af68a28 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/MethodOverrideInSubtype2.java @@ -0,0 +1,13 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; + +abstract class MethodOverrideInSubtype2 extends MethodDefinedInSupertype { + + private @AinferSibling2 int getAinferSibling2() { + return 0; + } + + @java.lang.Override + public int shouldReturnParent() { + return getAinferSibling2(); + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/MethodParameterInferenceTest.java b/checker/tests/ainfer-testchecker/non-annotated/MethodParameterInferenceTest.java new file mode 100644 index 000000000000..ff01c061fbb3 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/MethodParameterInferenceTest.java @@ -0,0 +1,13 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +// TODO: Like this one, some tests must verify that it contains the expected +// output after performing the whole-program inference. +public class MethodParameterInferenceTest { + void foo(int i) { + i = getAinferSibling1(); // The type of i must be inferred to @AinferSibling1. + } + + @AinferSibling1 int getAinferSibling1() { + return (@AinferSibling1 int) 0; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/MethodReturnTest.java b/checker/tests/ainfer-testchecker/non-annotated/MethodReturnTest.java new file mode 100644 index 000000000000..811d3ead6f09 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/MethodReturnTest.java @@ -0,0 +1,52 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferParent; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; + +public class MethodReturnTest { + + static int getAinferSibling1NotAnnotated() { + return (@AinferSibling1 int) 0; + } + + static @AinferSibling1 int getAinferSibling1() { + // :: warning: (return.type.incompatible) + return getAinferSibling1NotAnnotated(); + } + + public static boolean bool = false; + + public static int lubTest() { + if (bool) { + return (@AinferSibling1 int) 0; + } else { + return (@AinferSibling2 int) 0; + } + } + + public static @AinferParent int getParent() { + int x = lubTest(); + // :: warning: (return.type.incompatible) + return x; + } + + class InnerClass { + int field = 0; + + int getParent2() { + field = getParent(); + return getParent(); + } + + void receivesAinferSibling1(int i) { + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(i); + } + + void expectsAinferSibling1(@AinferSibling1 int i) {} + + void test() { + @AinferSibling1 int sib = (@AinferSibling1 int) 0; + receivesAinferSibling1(sib); + } + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/MultiDimensionalArrays.java b/checker/tests/ainfer-testchecker/non-annotated/MultiDimensionalArrays.java new file mode 100644 index 000000000000..de610342ef92 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/MultiDimensionalArrays.java @@ -0,0 +1,266 @@ +// This test ensures that annotations on different component types of multidimensional arrays +// are printed correctly. + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSiblingWithFields; +import org.checkerframework.common.aliasing.qual.MaybeAliased; +import org.checkerframework.common.aliasing.qual.NonLeaked; +import org.checkerframework.common.aliasing.qual.Unique; + +import java.util.List; + +public class MultiDimensionalArrays { + + // two dimensional arrays + + void requiresS1S2(@AinferSibling1 int @AinferSibling2 [] x) {} + + int[] twoDimArray; + + void testField() { + // :: warning: argument.type.incompatible + requiresS1S2(twoDimArray); + } + + void useField(@AinferSibling1 int @AinferSibling2 [] x) { + twoDimArray = x; + } + + void testParam(int[] x) { + // :: warning: argument.type.incompatible + requiresS1S2(x); + } + + void useParam(@AinferSibling1 int @AinferSibling2 [] x) { + testParam(x); + } + + int[] useReturn(@AinferSibling1 int @AinferSibling2 [] x) { + return x; + } + + void testReturn() { + requiresS1S2( + // :: warning: argument.type.incompatible + useReturn( + // :: warning: argument.type.incompatible + twoDimArray)); + } + + // three dimensional arrays + + void requiresS1S2S1(@AinferSibling1 int @AinferSibling2 [] @AinferSibling1 [] x) {} + + int[][] threeDimArray; + + void testField2() { + // :: warning: argument.type.incompatible + requiresS1S2S1(threeDimArray); + } + + void useField2(@AinferSibling1 int @AinferSibling2 [] @AinferSibling1 [] x) { + threeDimArray = x; + } + + void testParam2(int[][] x) { + // :: warning: argument.type.incompatible + requiresS1S2S1(x); + } + + void useParam2(@AinferSibling1 int @AinferSibling2 [] @AinferSibling1 [] x) { + testParam2(x); + } + + int[][] useReturn2(@AinferSibling1 int @AinferSibling2 [] @AinferSibling1 [] x) { + return x; + } + + void testReturn2() { + // :: warning: argument.type.incompatible + requiresS1S2S1(useReturn2(threeDimArray)); + } + + // three dimensional array with annotations only on two inner types + + void requiresS1S2N(@AinferSibling1 int @AinferSibling2 [][] x) {} + + int[][] threeDimArray2; + + void testField3() { + // :: warning: argument.type.incompatible + requiresS1S2N(threeDimArray2); + } + + void useField3(@AinferSibling1 int @AinferSibling2 [][] x) { + threeDimArray2 = x; + } + + void testParam3(int[][] x) { + // :: warning: argument.type.incompatible + requiresS1S2N(x); + } + + void useParam3(@AinferSibling1 int @AinferSibling2 [][] x) { + testParam3(x); + } + + int[][] useReturn3(@AinferSibling1 int @AinferSibling2 [][] x) { + return x; + } + + void testReturn3() { + // :: warning: argument.type.incompatible + requiresS1S2N(useReturn3(threeDimArray2)); + } + + // three dimensional array with annotations only on two array types, not innermost type + + void requiresS2S1(int @AinferSibling2 [] @AinferSibling1 [] x) {} + + int[][] threeDimArray3; + + void testField4() { + // :: warning: argument.type.incompatible + requiresS2S1(threeDimArray3); + } + + void useField4(int @AinferSibling2 [] @AinferSibling1 [] x) { + threeDimArray3 = x; + } + + void testParam4(int[][] x) { + // :: warning: argument.type.incompatible + requiresS2S1(x); + } + + void useParam4(int @AinferSibling2 [] @AinferSibling1 [] x) { + testParam4(x); + } + + int[][] useReturn4(int @AinferSibling2 [] @AinferSibling1 [] x) { + return x; + } + + void testReturn4() { + // :: warning: argument.type.incompatible + requiresS2S1(useReturn4(threeDimArray3)); + } + + // three-dimensional arrays with arguments in annotations + + void requiresSf1Sf2Sf3( + @AinferSiblingWithFields(value = {"test1", "test1"}) int @AinferSiblingWithFields(value = {"test2", "test2"}) [] + @AinferSiblingWithFields(value = {"test3"}) [] + x) {} + + int[][] threeDimArray4; + + void testField5() { + // :: warning: argument.type.incompatible + requiresSf1Sf2Sf3(threeDimArray4); + } + + void useField5( + @AinferSiblingWithFields(value = {"test1", "test1"}) int @AinferSiblingWithFields(value = {"test2", "test2"}) [] + @AinferSiblingWithFields(value = {"test3"}) [] + x) { + threeDimArray4 = x; + } + + void testParam5(int[][] x) { + // :: warning: argument.type.incompatible + requiresSf1Sf2Sf3(x); + } + + void useParam5( + @AinferSiblingWithFields(value = {"test1", "test1"}) int @AinferSiblingWithFields(value = {"test2", "test2"}) [] + @AinferSiblingWithFields(value = {"test3"}) [] + x) { + testParam5(x); + } + + int[][] useReturn5( + @AinferSiblingWithFields(value = {"test1", "test1"}) int @AinferSiblingWithFields(value = {"test2", "test2"}) [] + @AinferSiblingWithFields(value = {"test3"}) [] + x) { + return x; + } + + void testReturn5() { + // :: warning: argument.type.incompatible + requiresSf1Sf2Sf3(useReturn5(threeDimArray4)); + } + + // three dimensional array with annotations from other hierarchies that ought to be preserved + + int[][] threeDimArray5; + + void testField6() { + // :: warning: argument.type.incompatible + requiresS1S2S1(threeDimArray5); + } + + void useField6( + @AinferSibling1 @Unique int @AinferSibling2 @NonLeaked [] @AinferSibling1 @MaybeAliased [] x) { + threeDimArray5 = x; + } + + void testParam6(int[][] x) { + // :: warning: argument.type.incompatible + requiresS1S2S1(x); + } + + void useParam6( + @AinferSibling1 @Unique int @AinferSibling2 @NonLeaked [] @AinferSibling1 @MaybeAliased [] x) { + testParam6(x); + } + + int[][] useReturn6( + @AinferSibling1 @Unique int @AinferSibling2 @NonLeaked [] @AinferSibling1 @MaybeAliased [] x) { + return x; + } + + void testReturn6() { + // :: warning: argument.type.incompatible + requiresS1S2S1(useReturn6(threeDimArray)); + } + + // Shenanigans with lists + arrays; commented out annotations can't be inferred by either + // jaif or stub based WPI for now due to limitations in generics inference. + + List[] arrayofListsOfStringArrays; + + void testField7() { + // :: warning: argument.type.incompatible + requiresS1S2L(arrayofListsOfStringArrays); + } + + void requiresS1S2L( + @AinferSibling1 List @AinferSibling2 [] la) {} + + void useField7( + @AinferSibling1 List @AinferSibling2 [] x) { + arrayofListsOfStringArrays = x; + } + + void testParam7(List[] x) { + // :: warning: argument.type.incompatible + requiresS1S2L(x); + } + + void useParam7( + @AinferSibling1 List @AinferSibling2 [] x) { + testParam7(x); + } + + List[] useReturn7( + @AinferSibling1 List @AinferSibling2 [] x) { + return x; + } + + void testReturn7() { + // :: warning: argument.type.incompatible + requiresS1S2L(useReturn7(arrayofListsOfStringArrays)); + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/MultidimensionalAnnotatedArray.java b/checker/tests/ainfer-testchecker/non-annotated/MultidimensionalAnnotatedArray.java new file mode 100644 index 000000000000..59fcc6cfd818 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/MultidimensionalAnnotatedArray.java @@ -0,0 +1,11 @@ +// test case for https://github.com/typetools/checker-framework/issues/3422 + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +public class MultidimensionalAnnotatedArray { + boolean[][] field = getArray(); + + public boolean[] @AinferSibling1 [] getArray() { + return null; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/NamedInnerClassInAnonymous.java b/checker/tests/ainfer-testchecker/non-annotated/NamedInnerClassInAnonymous.java new file mode 100644 index 000000000000..8b2935885cb7 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/NamedInnerClassInAnonymous.java @@ -0,0 +1,19 @@ +// Tests whether the stub writer correctly handles named inner classes +// in anonymous classes. + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +public class NamedInnerClassInAnonymous { + void test() { + Object o = + new NamedInnerClassInAnonymous() { + class NamedInner { + // The stub parser cannot parse inner classes, so stub-based WPI should + // not attempt to print a stub file for this. + public int myAinferSibling1() { + return ((@AinferSibling1 int) 0); + } + } + }; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/OptionGroup.java b/checker/tests/ainfer-testchecker/non-annotated/OptionGroup.java new file mode 100644 index 000000000000..14d4870e2e5c --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/OptionGroup.java @@ -0,0 +1,18 @@ +// This annotation from plume-lib options caused an error in +// a version of ajava-based WPI. The problem was that annotation +// declarations weren't considered possible declarations when checking +// whether a location can store a declaration annotation. + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface OptionGroup { + + String value(); + + boolean unpublicized() default false; +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/OtherAnnotations.java b/checker/tests/ainfer-testchecker/non-annotated/OtherAnnotations.java new file mode 100644 index 000000000000..916acb526af7 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/OtherAnnotations.java @@ -0,0 +1,42 @@ +// Test that having other, unrelated annotations on fields/methods/etc doesn't foul up inference. + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.common.aliasing.qual.Unique; + +public class OtherAnnotations { + + void requireAinferSibling1(@AinferSibling1 int a) {} + + @Unique int x; + + void assignX(@AinferSibling1 int y) { + x = y; + } + + void useX() { + // :: warning: argument.type.incompatible + requireAinferSibling1(x); + } + + void methodWithAnnotatedParam(@Unique int z) { + // :: warning: argument.type.incompatible + requireAinferSibling1(z); + } + + void useMethodWithAnnotatedParam(@AinferSibling1 int w) { + methodWithAnnotatedParam(w); + } + + @AinferSibling1 int getAinferSibling1() { + return 5; + } + + @Unique int getIntVal5() { + return getAinferSibling1(); + } + + void useGetIntVal5() { + // :: warning: argument.type.incompatible + requireAinferSibling1(getIntVal5()); + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/OuterClassWithTypeParam.java b/checker/tests/ainfer-testchecker/non-annotated/OuterClassWithTypeParam.java new file mode 100644 index 000000000000..d1142070bb9f --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/OuterClassWithTypeParam.java @@ -0,0 +1,9 @@ +// test file for https://github.com/typetools/checker-framework/issues/3438 + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +public class OuterClassWithTypeParam { + public class InnerClass { + Object o = (@AinferSibling1 Object) null; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/OverloadedMethodsTest.java b/checker/tests/ainfer-testchecker/non-annotated/OverloadedMethodsTest.java new file mode 100644 index 000000000000..f77316e4b521 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/OverloadedMethodsTest.java @@ -0,0 +1,20 @@ +// This test ensures that overloaded methods with different return types aren't confused. + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +public class OverloadedMethodsTest { + + String f; + + String m1() { + return this.f; + } + + String m1(String x) { + return getAinferSibling1(); + } + + @AinferSibling1 String getAinferSibling1() { + return null; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/OverriddenMethodsTest.java b/checker/tests/ainfer-testchecker/non-annotated/OverriddenMethodsTest.java new file mode 100644 index 000000000000..cd99468cf989 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/OverriddenMethodsTest.java @@ -0,0 +1,58 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; + +public class OverriddenMethodsTest { + static class OverriddenMethodsTestParent { + public void foo(@AinferSibling1 Object obj, @AinferSibling2 Object obj2) {} + + public void bar( + @AinferSibling1 OverriddenMethodsTestParent this, @AinferSibling2 Object obj) {} + + public void barz( + @AinferSibling1 OverriddenMethodsTestParent this, @AinferSibling2 Object obj) {} + + public void qux(Object obj1, Object obj2) { + // :: warning: (argument.type.incompatible) + foo(obj1, obj2); + } + + public void thud(Object obj1, Object obj2) { + // :: warning: (argument.type.incompatible) + foo(obj1, obj2); + } + } + + class OverriddenMethodsTestChild extends OverriddenMethodsTestParent { + @Override + public void foo(Object obj, Object obj2) { + // :: warning: (assignment.type.incompatible) + @AinferSibling1 Object o = obj; + // :: warning: (assignment.type.incompatible) + @AinferSibling2 Object o2 = obj2; + } + + @Override + public void bar(Object obj) { + // :: warning: (assignment.type.incompatible) + @AinferSibling1 OverriddenMethodsTestChild child = this; + // :: warning: (assignment.type.incompatible) + @AinferSibling2 Object o = obj; + } + + @SuppressWarnings("all") + @Override + public void barz(Object obj) {} + + public void callbarz(Object obj) { + // If the @SuppressWarnings("all") on the overridden version of barz above is not + // respected, and the annotations on the receiver and parameter of barz are + // inferred, then the following call to barz will result in a method.invocation + // and an argument type checking errors. + barz(obj); + } + + public void callqux(@AinferSibling1 Object obj1, @AinferSibling2 Object obj2) { + qux(obj1, obj2); + } + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/OverriddenMethodsTestChildInAnotherCompilationUnit.java b/checker/tests/ainfer-testchecker/non-annotated/OverriddenMethodsTestChildInAnotherCompilationUnit.java new file mode 100644 index 000000000000..f8265f055293 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/OverriddenMethodsTestChildInAnotherCompilationUnit.java @@ -0,0 +1,9 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; + +public class OverriddenMethodsTestChildInAnotherCompilationUnit + extends OverriddenMethodsTest.OverriddenMethodsTestParent { + public void callthud(@AinferSibling1 Object obj1, @AinferSibling2 Object obj2) { + thud(obj1, obj2); + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/OverrideIncompatiblePurity.java b/checker/tests/ainfer-testchecker/non-annotated/OverrideIncompatiblePurity.java new file mode 100644 index 000000000000..1f4fbd51f419 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/OverrideIncompatiblePurity.java @@ -0,0 +1,48 @@ +// This test case checks for the case where in a superclass a method is pure, but in +// a subclass it is not. In this case, WPI shouldn't infer purity annotations for either +// the superclass or the subclass, because they are unverifiable. + +import java.util.Random; + +public class OverrideIncompatiblePurity { + + interface MyInterface { + // WPI should not infer @Pure for this unless all implementations are pure. + void method(); + } + + class MyImplementation implements MyInterface { + + int field; + + @java.lang.Override + public void method() { + // Side effect! + field = 5; + } + } + + class Foo { + + // This implementation is pure, but an overriding implementation in Bar is not. + String getA(int x) { + return "A"; + } + } + + class Bar extends Foo { + + String y; + + // This implementation is neither deterministic nor side-effect free. + @java.lang.Override + String getA(int x) { + if (new Random().nextInt(5) > x) { + return "B"; + } else { + y = "C"; + return y; + } + } + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/ParameterInferenceTest.java b/checker/tests/ainfer-testchecker/non-annotated/ParameterInferenceTest.java new file mode 100644 index 000000000000..8aeed48d6686 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/ParameterInferenceTest.java @@ -0,0 +1,23 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferParent; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferTop; + +public class ParameterInferenceTest { + + void test1() { + @AinferParent int parent = (@AinferParent int) 0; + expectsParentNoSignature(parent); + } + + void expectsParentNoSignature(int t) { + // :: warning: (assignment.type.incompatible) + @AinferParent int parent = t; + } + + void test2() { + @AinferTop int top = (@AinferTop int) 0; + // :: warning: (argument.type.incompatible) + expectsAinferTopNoSignature(top); + } + + void expectsAinferTopNoSignature(int t) {} +} diff --git a/framework/tests/whole-program-inference/non-annotated/Planet.java b/checker/tests/ainfer-testchecker/non-annotated/Planet.java similarity index 87% rename from framework/tests/whole-program-inference/non-annotated/Planet.java rename to checker/tests/ainfer-testchecker/non-annotated/Planet.java index a175f677ac36..cacf7f10f502 100644 --- a/framework/tests/whole-program-inference/non-annotated/Planet.java +++ b/checker/tests/ainfer-testchecker/non-annotated/Planet.java @@ -1,6 +1,6 @@ // This test checks that enums with fields and methods are handled correctly -import testlib.wholeprograminference.qual.Sibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; @SuppressWarnings( "value" // Do not generate Value Checker annotations, because IndexFileParser cannot handle @@ -45,12 +45,12 @@ private double radius() { return otherMass * surfaceGravity(); } - void test(@Sibling1 int x) { + void test(@AinferSibling1 int x) { foo = x; } void test2() { - // :: error: argument.type.incompatible + // :: warning: argument.type.incompatible test(foo); } } diff --git a/checker/tests/ainfer-testchecker/non-annotated/PublicFieldTest.java b/checker/tests/ainfer-testchecker/non-annotated/PublicFieldTest.java new file mode 100644 index 000000000000..bb221671bc5e --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/PublicFieldTest.java @@ -0,0 +1,67 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferBottom; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferParent; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferTop; + +public class PublicFieldTest { + public static int field1; // parent + public static int field2; // sib2 + + public PublicFieldTest() { + field1 = getAinferSibling1(); + } + + void testPublicInference() { + // :: warning: (argument.type.incompatible) + expectsAinferSibling2(field2); + // :: warning: (argument.type.incompatible) + expectsParent(field1); + // :: warning: (argument.type.incompatible) + expectsParent(field2); + } + + void expectsBottom(@AinferBottom int t) {} + + void expectsAinferSibling1(@AinferSibling1 int t) {} + + void expectsAinferSibling2(@AinferSibling2 int t) {} + + void expectsAinferTop(@AinferTop int t) {} + + void expectsParent(@AinferParent int t) {} + + @AinferSibling1 int getAinferSibling1() { + return (@AinferSibling1 int) 0; + } + + class AnotherClass { + + int innerField; + + public AnotherClass() { + PublicFieldTest.field1 = getAinferSibling2(); + PublicFieldTest.field2 = getAinferSibling2(); + innerField = getAinferSibling2(); + } + + void innerFieldTest() { + // :: warning: (argument.type.incompatible) + expectsAinferSibling2(innerField); + } + + @AinferBottom int getBottom() { + return (@AinferBottom int) 0; + } + + @AinferTop int getAinferTop() { + return (@AinferTop int) 0; + } + + @AinferSibling2 int getAinferSibling2() { + return (@AinferSibling2 int) 0; + } + + void expectsAinferSibling2(@AinferSibling2 int t) {} + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/Purity.java b/checker/tests/ainfer-testchecker/non-annotated/Purity.java new file mode 100644 index 000000000000..4d6b6851f8aa --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/Purity.java @@ -0,0 +1,29 @@ +// Copied from the all-systems tests, because of the expected error +// on line 26. During the first round of WPI, errors are warnings: so the test fails. +// The class has been renamed so that an -AskipDefs=TestPure command-line argument +// can suppress the original errors. + +import org.checkerframework.dataflow.qual.*; + +interface PureFunc { + @Pure + String doNothing(); +} + +class TestPure1 { + + static String myMethod() { + return ""; + } + + @Pure + static String myPureMethod() { + return ""; + } + + void context() { + PureFunc f1 = TestPure1::myPureMethod; + // :: warning: (purity.methodref) + PureFunc f2 = TestPure1::myMethod; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/README.md b/checker/tests/ainfer-testchecker/non-annotated/README.md new file mode 100644 index 000000000000..5f761f444154 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/README.md @@ -0,0 +1,21 @@ +The files in this directory are tested while using the -Ainfer option. +For this reason, each of these files is typechecked twice: once to infer +annotations (the "Generate" phase), and a second time +to ensure no more errors are issued once whole-program inference has been +run (the "Validate" phase). Between these two phases, the files +are copied to the ../annotated/ directory (i.e., the files in that +directory are the inputs to the "Validate" phase) and all expected errors and +warnings are removed. The ../inference-output/ contains the produced +annotation files (i.e., the .jaif, .astub, or .ajava files). + +Because all the annotation files are stored in the same directory, there +are two requirements for test inputs that are typechecked as part of +these tests (that is, the test files in this directory and all all-systems +tests): +* the fully-qualified name of each test class must be unique, because annotation + files use a naming scheme based on the fully-qualified name of the class. This + means, for example, that this directory cannot contain any test files with the + same name as any all-systems test. +* every class must either be or be contained by a class whose name matches the name + of the file containing the class. This requirement is caused by the need to locate + programmatically the annotation files generated from a particular test. diff --git a/checker/tests/ainfer-testchecker/non-annotated/RequiresQualifierTest.java b/checker/tests/ainfer-testchecker/non-annotated/RequiresQualifierTest.java new file mode 100644 index 000000000000..a4700f65e893 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/RequiresQualifierTest.java @@ -0,0 +1,54 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferBottom; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferParent; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferTop; + +class RequiresQualifierTest { + + @AinferTop int field1; + @AinferTop int field2; + + @AinferTop int top; + @AinferParent int parent; + @AinferSibling1 int sibling1; + @AinferSibling2 int sibling2; + @AinferBottom int bottom; + + void field1IsParent() { + // :: warning: (assignment.type.incompatible) + @AinferParent int x = field1; + } + + void field1IsAinferSibling2() { + // :: warning: (assignment.type.incompatible) + @AinferSibling2 int x = field1; + } + + void parentIsAinferSibling1() { + // :: warning: (assignment.type.incompatible) + @AinferSibling1 int x = parent; + } + + void noRequirements() {} + + void client2(@AinferParent int p) { + field1 = p; + field1IsParent(); + } + + void client1() { + noRequirements(); + + field1 = parent; + field1IsParent(); + + field1 = sibling2; + field1IsAinferSibling2(); + field1 = bottom; + field1IsAinferSibling2(); + + parent = sibling1; + parentIsAinferSibling1(); + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/StringConcatenationTest.java b/checker/tests/ainfer-testchecker/non-annotated/StringConcatenationTest.java new file mode 100644 index 000000000000..c5e55eaa80bb --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/StringConcatenationTest.java @@ -0,0 +1,32 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +public class StringConcatenationTest { + + private String options_str; + private String options_str2; + private String options_str3; + + void foo() { + options_str = getAinferSibling1(); + + // Addition lubs the results, so these have no effect on the (default) type of the fields. + // Also, the following two lines should behave identically. + options_str2 += getAinferSibling1(); + options_str3 = options_str3 + getAinferSibling1(); + } + + void test() { + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(options_str); + // :: warning: (argument.type.incompatible) + expectsAinferSibling1(options_str2); + expectsAinferSibling1(options_str3); + } + + void expectsAinferSibling1(@AinferSibling1 String t) {} + + @SuppressWarnings("cast.unsafe") + @AinferSibling1 String getAinferSibling1() { + return (@AinferSibling1 String) " "; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/Tempvars.java b/checker/tests/ainfer-testchecker/non-annotated/Tempvars.java new file mode 100644 index 000000000000..a46b67df0c07 --- /dev/null +++ b/checker/tests/ainfer-testchecker/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/checker/tests/ainfer-testchecker/non-annotated/TreatAsSibling1InferenceTest.java b/checker/tests/ainfer-testchecker/non-annotated/TreatAsSibling1InferenceTest.java new file mode 100644 index 000000000000..b2991170f489 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/TreatAsSibling1InferenceTest.java @@ -0,0 +1,13 @@ +// A simple test that the @AinferTreatAsSibling1 annotation can be inferred. +// This test does actually test inference: the AinferTestChecker's TreeAnnotator +// has logic to add the @AinferTreatAsSibling1 annotation to parameters with +// the name "iShouldBeTreatedAsSibling1". + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +public class TreatAsSibling1InferenceTest { + public void test(Object iShouldBeTreatedAsSibling1) { + // :: warning: (assignment.type.incompatible) + @AinferSibling1 Object x = iShouldBeTreatedAsSibling1; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/TreatAsSibling1Test.java b/checker/tests/ainfer-testchecker/non-annotated/TreatAsSibling1Test.java new file mode 100644 index 000000000000..eef11cac87ea --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/TreatAsSibling1Test.java @@ -0,0 +1,11 @@ +// A simple test that the @AinferTreatAsSibling1 annotation works as intended. +// This test doesn't actually test inference: it's a test for the AinferTestChecker. + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferTreatAsSibling1; + +public class TreatAsSibling1Test { + public void test(@AinferTreatAsSibling1 Object y) { + @AinferSibling1 Object x = y; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/TwoMethodsSameName.java b/checker/tests/ainfer-testchecker/non-annotated/TwoMethodsSameName.java new file mode 100644 index 000000000000..84604f2b7da5 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/TwoMethodsSameName.java @@ -0,0 +1,25 @@ +// This test makes sure that if a class has two methods with the same name, +// the parameters are inferred correctly and are not confused. + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; + +public class TwoMethodsSameName { + + void test(int x, int y) { + // :: warning: assignment.type.incompatible + @AinferSibling1 int x1 = x; + // :: warning: assignment.type.incompatible + @AinferSibling2 int y1 = y; + } + + void test(int z) { + // :: warning: assignment.type.incompatible + @AinferSibling2 int z1 = z; + } + + void uses(@AinferSibling1 int a, @AinferSibling2 int b) { + test(a, b); + test(b); + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/TypeVariablesTest.java b/checker/tests/ainfer-testchecker/non-annotated/TypeVariablesTest.java new file mode 100644 index 000000000000..041d16d9fc88 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/TypeVariablesTest.java @@ -0,0 +1,33 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferParent; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; + +public class TypeVariablesTest { + + // This method's parameter type should not be updated by the whole-program inference. + // Even though there is only one call to foo with argument of type @AinferBottom, + // the method has in its signature that the parameter is a subtype of @AinferParent, + // therefore no annotation should be added. + public static + TypeVariablesTest foo(A a, B b) { + return null; + } + + public static void typeVarWithTypeVarUB( + A a, B b) {} + + void test1() { + @SuppressWarnings("cast.unsafe") + @AinferParent String s = (@AinferParent String) ""; + foo(getAinferSibling1(), getAinferSibling2()); + typeVarWithTypeVarUB(getAinferSibling1(), getAinferSibling2()); + } + + static @AinferSibling1 int getAinferSibling1() { + return 0; + } + + static @AinferSibling2 int getAinferSibling2() { + return 0; + } +} diff --git a/framework/tests/whole-program-inference/non-annotated/TypeVariablesTest2.java b/checker/tests/ainfer-testchecker/non-annotated/TypeVariablesTest2.java similarity index 100% rename from framework/tests/whole-program-inference/non-annotated/TypeVariablesTest2.java rename to checker/tests/ainfer-testchecker/non-annotated/TypeVariablesTest2.java diff --git a/checker/tests/ainfer-testchecker/non-annotated/TypeVariablesTest3.java b/checker/tests/ainfer-testchecker/non-annotated/TypeVariablesTest3.java new file mode 100644 index 000000000000..3705e4fac8d1 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/TypeVariablesTest3.java @@ -0,0 +1,23 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; + +public class TypeVariablesTest3<@AinferSibling1 T extends @AinferSibling1 Object> { + public @AinferSibling2 T sibling2; + public @AinferSibling1 T sibling1; + + public T tField; + + void foo(T param) { + // :: warning: (assignment.type.incompatible) + param = sibling2; + } + + void baz(T param) { + param = sibling1; + } + + void bar(@AinferSibling2 T param) { + // :: warning: (assignment.type.incompatible) + tField = param; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/UsesAnnotationAsClass.java b/checker/tests/ainfer-testchecker/non-annotated/UsesAnnotationAsClass.java new file mode 100644 index 000000000000..c387ad336434 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/UsesAnnotationAsClass.java @@ -0,0 +1,14 @@ +// Tests that WPI doesn't break when an we attempt to infer something +// about a member of an annotation declaration (in this case, the receivers +// of value() and anotherValue() in AinferSibling1's definition). + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; + +public class UsesAnnotationAsClass { + public static String test(@AinferSibling2 AinferSibling1 annotation) { + String value = annotation.value(); + // goal of this is to trigger inference for AinferSibling1's definition. + String anotherValue = annotation.anotherValue(); + return value + anotherValue; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/UsesAnonymous.java b/checker/tests/ainfer-testchecker/non-annotated/UsesAnonymous.java new file mode 100644 index 000000000000..b32f587f9311 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/UsesAnonymous.java @@ -0,0 +1,37 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferBottom; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling2; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferTop; + +public class UsesAnonymous { + void method() { + Anonymous a = + new Anonymous() { + int innerField; + + public void method2() { + Anonymous.field1 = getAinferSibling2(); + Anonymous.field2 = getAinferSibling2(); + innerField = getAinferSibling2(); + } + + void innerFieldTest() { + // :: warning: (argument.type.incompatible) + expectsAinferSibling2(innerField); + } + + @AinferBottom int getBottom() { + return (@AinferBottom int) 0; + } + + @AinferTop int getAinferTop() { + return (@AinferTop int) 0; + } + + @AinferSibling2 int getAinferSibling2() { + return (@AinferSibling2 int) 0; + } + + void expectsAinferSibling2(@AinferSibling2 int t) {} + }; + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/ValueCheck.java b/checker/tests/ainfer-testchecker/non-annotated/ValueCheck.java new file mode 100644 index 000000000000..0b8d48162bac --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/ValueCheck.java @@ -0,0 +1,26 @@ +// Checks that annotations from the Value Checker (which is a subchecker of the WPI test checker) +// are actually present in the generated files, even when there is also an annotation from the main +// checker. + +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; +import org.checkerframework.common.value.qual.IntVal; + +public class ValueCheck { + + // return value should be @AinferSibling1 @IntVal(5) int + int getAinferSibling1withValue5() { + return ((@AinferSibling1 int) 5); + } + + void requireAinferSibling1(@AinferSibling1 int x) {} + + void requireIntVal5(@IntVal(5) int x) {} + + void test() { + int x = getAinferSibling1withValue5(); + // :: warning: argument.type.incompatible + requireAinferSibling1(x); + // :: warning: argument.type.incompatible + requireIntVal5(x); + } +} diff --git a/checker/tests/ainfer-testchecker/non-annotated/VarargsTest.java b/checker/tests/ainfer-testchecker/non-annotated/VarargsTest.java new file mode 100644 index 000000000000..a16423d50f02 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/VarargsTest.java @@ -0,0 +1,33 @@ +import org.checkerframework.checker.testchecker.ainfer.qual.AinferParent; +import org.checkerframework.checker.testchecker.ainfer.qual.AinferSibling1; + +public class VarargsTest { + + static void m1Varargs(Object... args) {} + + static void m1ArrArgs(Object[] args) {} + + static void m2Varargs(Object... args) {} + + static void m2ArrArgs(Object[] args) {} + + static void m3Varargs(Object... args) {} + + static void m3ArrArgs(Object[] args) {} + + static @AinferSibling1 Object @AinferParent [] p_s1_array; + static @AinferSibling1 Object @AinferSibling1 [] s1_s1_array; + + static void client() { + m1Varargs(s1_s1_array); + m1ArrArgs(s1_s1_array); + + m2Varargs(p_s1_array); + m2ArrArgs(p_s1_array); + + m3Varargs(s1_s1_array); + m3ArrArgs(s1_s1_array); + m3Varargs(p_s1_array); + m3ArrArgs(p_s1_array); + } +} diff --git a/framework/tests/whole-program-inference/non-annotated/WildcardReturn.java b/checker/tests/ainfer-testchecker/non-annotated/WildcardReturn.java similarity index 94% rename from framework/tests/whole-program-inference/non-annotated/WildcardReturn.java rename to checker/tests/ainfer-testchecker/non-annotated/WildcardReturn.java index baf731d9aa0c..122dc6e3af0c 100644 --- a/framework/tests/whole-program-inference/non-annotated/WildcardReturn.java +++ b/checker/tests/ainfer-testchecker/non-annotated/WildcardReturn.java @@ -5,7 +5,7 @@ import java.util.Set; import java.util.stream.Collectors; -class WildcardReturn { +public class WildcardReturn { public Set getCredentialIdsForUsername(String username) { return getRegistrationsByUsername(username).stream() .map(registration -> registration.toString()) diff --git a/checker/tests/ainfer-testchecker/non-annotated/all-systems b/checker/tests/ainfer-testchecker/non-annotated/all-systems new file mode 120000 index 000000000000..dda71afed0f2 --- /dev/null +++ b/checker/tests/ainfer-testchecker/non-annotated/all-systems @@ -0,0 +1 @@ +../../../../framework/tests/all-systems \ No newline at end of file diff --git a/checker/tests/calledmethods-autovalue/Animal.java b/checker/tests/calledmethods-autovalue/Animal.java new file mode 100644 index 000000000000..424bcc821476 --- /dev/null +++ b/checker/tests/calledmethods-autovalue/Animal.java @@ -0,0 +1,88 @@ +import com.google.auto.value.AutoValue; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +import java.util.Optional; + +/** + * Adapted from the standard AutoValue example code: + * https://github.com/google/auto/blob/master/value/userguide/builders.md + */ +@AutoValue +abstract class Animal { + abstract String name(); + + abstract @Nullable String habitat(); + + abstract int numberOfLegs(); + + // does not need to be explicitly set + abstract Optional extra(); + + public String getStr() { + return "str"; + } + + public abstract Builder toBuilder(); + + static Builder builder() { + return new AutoValue_Animal.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder setName(String value); + + abstract Builder setNumberOfLegs(int value); + + abstract Builder setHabitat(String value); + + abstract Builder setExtra(String value); + + abstract Animal build(); + } + + public static void buildSomethingWrong() { + Builder b = builder(); + b.setName("Frank"); + // :: error: finalizer.invocation.invalid + b.build(); + } + + public static void buildSomethingRight() { + Builder b = builder(); + b.setName("Frank"); + b.setNumberOfLegs(4); + b.build(); + } + + public static void buildSomethingRightIncludeOptional() { + Builder b = builder(); + b.setName("Frank"); + b.setNumberOfLegs(4); + b.setHabitat("jungle"); + b.build(); + } + + public static void buildSomethingWrongFluent() { + // :: error: finalizer.invocation.invalid + builder().setName("Frank").build(); + } + + public static void buildSomethingRightFluent() { + builder().setName("Jim").setNumberOfLegs(7).build(); + } + + public static void buildWithToBuilder() { + Animal a1 = builder().setName("Jim").setNumberOfLegs(7).build(); + a1.toBuilder().build(); + } + + public static void buildSomethingRightFluentWithLocal() { + Builder b = builder(); + b.setName("Jim").setNumberOfLegs(7); + b.build(); + } +} diff --git a/checker/tests/calledmethods-autovalue/AnimalNoSet.java b/checker/tests/calledmethods-autovalue/AnimalNoSet.java new file mode 100644 index 000000000000..2e011578f77d --- /dev/null +++ b/checker/tests/calledmethods-autovalue/AnimalNoSet.java @@ -0,0 +1,68 @@ +import com.google.auto.value.AutoValue; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +/** + * Adapted from the standard AutoValue example code: + * https://github.com/google/auto/blob/master/value/userguide/builders.md + */ +@AutoValue +abstract class AnimalNoSet { + abstract String name(); + + abstract @Nullable String habitat(); + + abstract int numberOfLegs(); + + public String getStr() { + return "str"; + } + + static Builder builder() { + return new AutoValue_AnimalNoSet.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder name(String value); + + abstract Builder numberOfLegs(int value); + + abstract Builder habitat(String value); + + abstract AnimalNoSet build(); + } + + public static void buildSomethingWrong() { + Builder b = builder(); + b.name("Frank"); + // :: error: finalizer.invocation.invalid + b.build(); + } + + public static void buildSomethingRight() { + Builder b = builder(); + b.name("Frank"); + b.numberOfLegs(4); + b.build(); + } + + public static void buildSomethingRightIncludeOptional() { + Builder b = builder(); + b.name("Frank"); + b.numberOfLegs(4); + b.habitat("jungle"); + b.build(); + } + + public static void buildSomethingWrongFluent() { + // :: error: finalizer.invocation.invalid + builder().name("Frank").build(); + } + + public static void buildSomethingRightFluent() { + builder().name("Jim").numberOfLegs(7).build(); + } +} diff --git a/checker/tests/calledmethods-autovalue/BuilderGetter.java b/checker/tests/calledmethods-autovalue/BuilderGetter.java new file mode 100644 index 000000000000..15b58d870fb1 --- /dev/null +++ b/checker/tests/calledmethods-autovalue/BuilderGetter.java @@ -0,0 +1,37 @@ +import com.google.auto.value.AutoValue; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +@AutoValue +abstract class BuilderGetter { + + public abstract String name(); + + static Builder builder() { + return new AutoValue_BuilderGetter.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder setName(String name); + + abstract String name(); + + abstract BuilderGetter build(); + } + + static void correct() { + Builder b = builder(); + b.setName("Phil"); + b.build(); + } + + static void wrong() { + Builder b = builder(); + b.name(); + // :: error: finalizer.invocation.invalid + b.build(); + } +} diff --git a/checker/tests/calledmethods-autovalue/CallWithinBuilder.java b/checker/tests/calledmethods-autovalue/CallWithinBuilder.java new file mode 100644 index 000000000000..0760d00789db --- /dev/null +++ b/checker/tests/calledmethods-autovalue/CallWithinBuilder.java @@ -0,0 +1,37 @@ +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +import java.util.Collection; + +@AutoValue +abstract class CallWithinBuilder { + + public abstract ImmutableList names(); + + static Builder builder() { + return new AutoValue_CallWithinBuilder.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract ImmutableList.Builder namesBuilder(); + + public Builder addName(String name) { + namesBuilder().add(name); + return this; + } + + public Builder addNames(Collection names) { + for (String n : names) { + addName(n); + } + return this; + } + + abstract CallWithinBuilder build(); + } +} diff --git a/checker/tests/calledmethods-autovalue/FooParcelable.java b/checker/tests/calledmethods-autovalue/FooParcelable.java new file mode 100644 index 000000000000..18b7453af742 --- /dev/null +++ b/checker/tests/calledmethods-autovalue/FooParcelable.java @@ -0,0 +1,37 @@ +import com.google.auto.value.AutoValue; + +import android.os.Parcelable; + +/** + * Test for support of AutoValue Parcel extension. This test currently passes, but only because we + * ignore cases where we cannot find a matching setter for a method we think corresponds to an + * AutoValue property. See https://github.com/kelloggm/object-construction-checker/issues/110 + */ +@AutoValue +abstract class FooParcelable implements Parcelable { + abstract String name(); + + static Builder builder() { + return new AutoValue_FooParcelable.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder setName(String value); + + abstract FooParcelable build(); + } + + public static void buildSomethingWrong() { + Builder b = builder(); + // :: error: finalizer.invocation.invalid + b.build(); + } + + public static void buildSomethingRight() { + Builder b = builder(); + b.setName("Frank"); + b.build(); + } +} diff --git a/checker/tests/calledmethods-autovalue/GetAndIs.java b/checker/tests/calledmethods-autovalue/GetAndIs.java new file mode 100644 index 000000000000..f52a4ce9b8b1 --- /dev/null +++ b/checker/tests/calledmethods-autovalue/GetAndIs.java @@ -0,0 +1,48 @@ +import com.google.auto.value.AutoValue; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +@AutoValue +abstract class GetAndIs { + abstract String get(); + + abstract boolean is(); + + static Builder builder() { + return new AutoValue_GetAndIs.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder setGet(String value); + + abstract Builder setIs(boolean value); + + abstract GetAndIs build(); + } + + public static void buildSomethingWrong() { + Builder b = builder(); + b.setGet("Frank"); + // :: error: finalizer.invocation.invalid + b.build(); + } + + public static void buildSomethingRight() { + Builder b = builder(); + b.setGet("Frank"); + b.setIs(false); + b.build(); + } + + public static void buildSomethingWrongFluent() { + // :: error: finalizer.invocation.invalid + builder().setGet("Frank").build(); + } + + public static void buildSomethingRightFluent() { + builder().setGet("Jim").setIs(true).build(); + } +} diff --git a/checker/tests/calledmethods-autovalue/GetAnimal.java b/checker/tests/calledmethods-autovalue/GetAnimal.java new file mode 100644 index 000000000000..4b83cc4059e7 --- /dev/null +++ b/checker/tests/calledmethods-autovalue/GetAnimal.java @@ -0,0 +1,70 @@ +import com.google.auto.value.AutoValue; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +/** + * Adapted from the standard AutoValue example code: + * https://github.com/google/auto/blob/master/value/userguide/builders.md + */ +@AutoValue +abstract class GetAnimal { + abstract String getName(); + + abstract @Nullable String getHabitat(); + + abstract int getNumberOfLegs(); + + abstract boolean isHasArms(); + + static Builder builder() { + return new AutoValue_GetAnimal.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder setName(String value); + + abstract Builder setNumberOfLegs(int value); + + abstract Builder setHabitat(String value); + + abstract Builder setHasArms(boolean b); + + abstract GetAnimal build(); + } + + public static void buildSomethingWrong() { + Builder b = builder(); + b.setName("Frank"); + // :: error: finalizer.invocation.invalid + b.build(); + } + + public static void buildSomethingRight() { + Builder b = builder(); + b.setName("Frank"); + b.setNumberOfLegs(4); + b.setHasArms(true); + b.build(); + } + + public static void buildSomethingRightIncludeOptional() { + Builder b = builder(); + b.setName("Frank"); + b.setNumberOfLegs(4); + b.setHabitat("jungle"); + b.setHasArms(true); + b.build(); + } + + public static void buildSomethingWrongFluent() { + // :: error: finalizer.invocation.invalid + builder().setName("Frank").build(); + } + + public static void buildSomethingRightFluent() { + builder().setName("Jim").setNumberOfLegs(7).setHasArms(false).build(); + } +} diff --git a/checker/tests/calledmethods-autovalue/GuavaImmutable.java b/checker/tests/calledmethods-autovalue/GuavaImmutable.java new file mode 100644 index 000000000000..1db3f3b23baf --- /dev/null +++ b/checker/tests/calledmethods-autovalue/GuavaImmutable.java @@ -0,0 +1,32 @@ +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +@AutoValue +abstract class GuavaImmutable { + + public abstract ImmutableList names(); + + static Builder builder() { + return new AutoValue_GuavaImmutable.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder names(ImmutableList value); + + abstract GuavaImmutable build(); + } + + public static void buildSomethingWrong() { + // :: error: finalizer.invocation.invalid + builder().build(); + } + + public static void buildSomethingRight() { + builder().names(ImmutableList.of()).build(); + } +} diff --git a/checker/tests/calledmethods-autovalue/GuavaImmutablePropBuilder.java b/checker/tests/calledmethods-autovalue/GuavaImmutablePropBuilder.java new file mode 100644 index 000000000000..88aa9f654fc8 --- /dev/null +++ b/checker/tests/calledmethods-autovalue/GuavaImmutablePropBuilder.java @@ -0,0 +1,28 @@ +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +@AutoValue +abstract class GuavaImmutablePropBuilder { + + public abstract ImmutableList names(); + + static Builder builder() { + return new AutoValue_GuavaImmutablePropBuilder.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract ImmutableList.Builder namesBuilder(); + + abstract GuavaImmutablePropBuilder build(); + } + + public static void buildSomething() { + // don't need to explicitly set the names + builder().build(); + } +} diff --git a/checker/tests/calledmethods-autovalue/Inheritance.java b/checker/tests/calledmethods-autovalue/Inheritance.java new file mode 100644 index 000000000000..fb5643d95b70 --- /dev/null +++ b/checker/tests/calledmethods-autovalue/Inheritance.java @@ -0,0 +1,48 @@ +import com.google.auto.value.AutoValue; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.common.returnsreceiver.qual.This; + +public class Inheritance { + static interface Props { + String name(); + + String somethingElse(); + + abstract class Builder> { + public abstract @This B name(String value); + } + } + + @AutoValue + abstract static class PropHolder implements Props { + abstract int size(); + + @Override + public String somethingElse() { + return "hi"; + } + + static Builder builder() { + return new AutoValue_Inheritance_PropHolder.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder extends Props.Builder { + public abstract Builder size(int value); + + public abstract PropHolder build(); + } + } + + static void correct() { + PropHolder.Builder b = PropHolder.builder(); + b.name("Manu").size(1).build(); + } + + static void wrong() { + PropHolder.Builder b = PropHolder.builder(); + // :: error: finalizer.invocation.invalid + b.size(1).build(); + } +} diff --git a/checker/tests/calledmethods-autovalue/IsPreserved.java b/checker/tests/calledmethods-autovalue/IsPreserved.java new file mode 100644 index 000000000000..697e1ad72cde --- /dev/null +++ b/checker/tests/calledmethods-autovalue/IsPreserved.java @@ -0,0 +1,42 @@ +import com.google.auto.value.AutoValue; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +@AutoValue +abstract class IsPreserved { + + abstract String name(); + + abstract String getAddress(); + + abstract boolean isPresent(); + + static Builder builder() { + return new AutoValue_IsPreserved.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder name(String val); + + abstract Builder getAddress(String val); + + abstract Builder isPresent(boolean value); + + abstract IsPreserved build(); + } + + public static void buildSomethingRight() { + Builder b = builder(); + b.name("Frank"); + b.getAddress("something"); + b.isPresent(true); + b.build(); + } + + public static void buildSomethingRightFluent() { + builder().name("Bill").getAddress("something").isPresent(false).build(); + } +} diff --git a/checker/tests/calledmethods-autovalue/NonAVBuilder.java b/checker/tests/calledmethods-autovalue/NonAVBuilder.java new file mode 100644 index 000000000000..f869e11ddb10 --- /dev/null +++ b/checker/tests/calledmethods-autovalue/NonAVBuilder.java @@ -0,0 +1,19 @@ +import com.google.auto.value.AutoValue; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +@AutoValue +abstract class NonAVBuilder { + abstract String name(); + + public Builder toBuilder() { + return new Builder(this); + } + + // NOT an AutoValue builder + static final class Builder { + + Builder(NonAVBuilder b) {} + } +} diff --git a/checker/tests/calledmethods-autovalue/NonBuildName.java b/checker/tests/calledmethods-autovalue/NonBuildName.java new file mode 100644 index 000000000000..6d1dbc2cdcaa --- /dev/null +++ b/checker/tests/calledmethods-autovalue/NonBuildName.java @@ -0,0 +1,34 @@ +import com.google.auto.value.AutoValue; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +@AutoValue +abstract class NonBuildName { + + public abstract String name(); + + static Builder builder() { + return new AutoValue_NonBuildName.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder setName(String name); + + abstract NonBuildName makeIt(); + } + + static void correct() { + Builder b = builder(); + b.setName("Phil"); + b.makeIt(); + } + + static void wrong() { + Builder b = builder(); + // :: error: finalizer.invocation.invalid + b.makeIt(); + } +} diff --git a/checker/tests/calledmethods-autovalue/Parcel.java b/checker/tests/calledmethods-autovalue/Parcel.java new file mode 100644 index 000000000000..3015789cfc23 --- /dev/null +++ b/checker/tests/calledmethods-autovalue/Parcel.java @@ -0,0 +1,11 @@ +package android.os; + +/** stub to avoid bringing in Android dependence */ +public final class Parcel { + + public String readString() { + return ""; + } + + public void writeString(String val) {} +} diff --git a/checker/tests/calledmethods-autovalue/Parcelable.java b/checker/tests/calledmethods-autovalue/Parcelable.java new file mode 100644 index 000000000000..f01a75dd8df3 --- /dev/null +++ b/checker/tests/calledmethods-autovalue/Parcelable.java @@ -0,0 +1,15 @@ +package android.os; + +/** stub to avoid bringing in Android dependence */ +public interface Parcelable { + public interface Creator { + + public T createFromParcel(Parcel source); + + public T[] newArray(int size); + } + + public int describeContents(); + + public void writeToParcel(Parcel dest, int flags); +} diff --git a/checker/tests/calledmethods-autovalue/SetInsideBuild.java b/checker/tests/calledmethods-autovalue/SetInsideBuild.java new file mode 100644 index 000000000000..691df85f9411 --- /dev/null +++ b/checker/tests/calledmethods-autovalue/SetInsideBuild.java @@ -0,0 +1,41 @@ +import com.google.auto.value.AutoValue; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +@AutoValue +abstract class SetInsideBuild { + public abstract String name(); + + public abstract int size(); + + static Builder builder() { + return new AutoValue_SetInsideBuild.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setName(String name); + + abstract Builder setSize(int value); + + abstract SetInsideBuild autoBuild(); + + public SetInsideBuild build(@CalledMethods({"setName"}) Builder this) { + + return this.setSize(4).autoBuild(); + } + } + + public static void buildSomethingWrong() { + Builder b = builder(); + // :: error: finalizer.invocation.invalid + b.build(); + } + + public static void buildSomethingCorrect() { + Builder b = builder(); + b.setName("Frank"); + b.build(); + } +} diff --git a/checker/tests/calledmethods-autovalue/SetInsideBuildWithCM.java b/checker/tests/calledmethods-autovalue/SetInsideBuildWithCM.java new file mode 100644 index 000000000000..6ed7b9426742 --- /dev/null +++ b/checker/tests/calledmethods-autovalue/SetInsideBuildWithCM.java @@ -0,0 +1,41 @@ +import com.google.auto.value.AutoValue; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +@AutoValue +abstract class SetInsideBuildWithCM { + public abstract String name(); + + public abstract int size(); + + static Builder builder() { + return new AutoValue_SetInsideBuildWithCM.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setName(String name); + + abstract Builder setSize(int value); + + abstract SetInsideBuildWithCM autoBuild(); + + public SetInsideBuildWithCM build() { + return this.autoBuild(); + } + } + + public static void buildSomethingCorrect() { + Builder b = builder(); + b.setName("Frank"); + b.setSize(2); + b.build(); + } + + public static void buildSomethingWrong() { + Builder b = builder(); + // :: error: finalizer.invocation.invalid + b.build(); + } +} diff --git a/checker/tests/calledmethods-autovalue/Validation.java b/checker/tests/calledmethods-autovalue/Validation.java new file mode 100644 index 000000000000..f77bcd710faa --- /dev/null +++ b/checker/tests/calledmethods-autovalue/Validation.java @@ -0,0 +1,42 @@ +import com.google.auto.value.AutoValue; + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +@AutoValue +abstract class Validation { + + public abstract String name(); + + static Builder builder() { + return new AutoValue_Validation.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder setName(String name); + + abstract Validation autoBuild(); + + public Validation build(@CalledMethods("setName") Builder this) { + Validation v = autoBuild(); + if (v.name().length() < 5) { + throw new RuntimeException("name too short!"); + } + return v; + } + } + + static void correct() { + Builder b = builder(); + b.setName("Phil"); + b.build(); + } + + static void wrong() { + Builder b = builder(); + // :: error: finalizer.invocation.invalid + b.build(); + } +} diff --git a/checker/tests/calledmethods-disableframeworks/AnimalSimple.java b/checker/tests/calledmethods-disableframeworks/AnimalSimple.java new file mode 100644 index 000000000000..d0f36c399f90 --- /dev/null +++ b/checker/tests/calledmethods-disableframeworks/AnimalSimple.java @@ -0,0 +1,53 @@ +import com.google.auto.value.AutoValue; + +/** + * Adapted from the standard AutoValue example code: + * https://github.com/google/auto/blob/master/value/userguide/builders.md + */ +@AutoValue +abstract class AnimalSimple { + abstract String name(); + + abstract int numberOfLegs(); + + static Builder builder() { + return new AutoValue_AnimalSimple.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder setName(String value); + + abstract Builder setNumberOfLegs(int value); + + abstract AnimalSimple build(); + } + + public static void buildSomethingWrong() { + Builder b = builder(); + b.setName("Frank"); + b.build(); + } + + public static void buildSomethingRight() { + Builder b = builder(); + b.setName("Frank"); + b.setNumberOfLegs(4); + b.build(); + } + + public static void buildSomethingWrongFluent() { + builder().setName("Frank").build(); + } + + public static void buildSomethingRightFluent() { + builder().setName("Jim").setNumberOfLegs(7).build(); + } + + public static void buildSomethingRightFluentWithLocal() { + Builder b = builder(); + b.setName("Jim").setNumberOfLegs(7); + b.build(); + } +} diff --git a/checker/tests/calledmethods-disableframeworks/LombokBuilderExample.java b/checker/tests/calledmethods-disableframeworks/LombokBuilderExample.java new file mode 100644 index 000000000000..926d12ecdc88 --- /dev/null +++ b/checker/tests/calledmethods-disableframeworks/LombokBuilderExample.java @@ -0,0 +1,82 @@ +// Generated by delombok at Wed Aug 14 15:57:15 PDT 2019 +// A test for support for the builder() method in Lombok builders. +public class LombokBuilderExample { + @lombok.NonNull Object foo; + @lombok.NonNull Object bar; + + static void test() { + builder().build(); + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + LombokBuilderExample(@lombok.NonNull final Object foo, @lombok.NonNull final Object bar) { + if (foo == null) { + throw new java.lang.NullPointerException("foo is marked non-null but is null"); + } + if (bar == null) { + throw new java.lang.NullPointerException("bar is marked non-null but is null"); + } + this.foo = foo; + this.bar = bar; + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public static class LombokBuilderExampleBuilder { + @java.lang.SuppressWarnings("all") + @lombok.Generated + private Object foo; + + @java.lang.SuppressWarnings("all") + @lombok.Generated + private Object bar; + + @java.lang.SuppressWarnings("all") + @lombok.Generated + LombokBuilderExampleBuilder() {} + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public LombokBuilderExampleBuilder foo(@lombok.NonNull final Object foo) { + if (foo == null) { + throw new java.lang.NullPointerException("foo is marked non-null but is null"); + } + this.foo = foo; + return this; + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public LombokBuilderExampleBuilder bar(@lombok.NonNull final Object bar) { + if (bar == null) { + throw new java.lang.NullPointerException("bar is marked non-null but is null"); + } + this.bar = bar; + return this; + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public LombokBuilderExample build() { + return new LombokBuilderExample(foo, bar); + } + + @java.lang.Override + @java.lang.SuppressWarnings("all") + @lombok.Generated + public java.lang.String toString() { + return "LombokBuilderExample.LombokBuilderExampleBuilder(foo=" + + this.foo + + ", bar=" + + this.bar + + ")"; + } + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public static LombokBuilderExampleBuilder builder() { + return new LombokBuilderExampleBuilder(); + } +} diff --git a/checker/tests/calledmethods-disableframeworks/Parcel.java b/checker/tests/calledmethods-disableframeworks/Parcel.java new file mode 100644 index 000000000000..3015789cfc23 --- /dev/null +++ b/checker/tests/calledmethods-disableframeworks/Parcel.java @@ -0,0 +1,11 @@ +package android.os; + +/** stub to avoid bringing in Android dependence */ +public final class Parcel { + + public String readString() { + return ""; + } + + public void writeString(String val) {} +} diff --git a/checker/tests/calledmethods-disableframeworks/Parcelable.java b/checker/tests/calledmethods-disableframeworks/Parcelable.java new file mode 100644 index 000000000000..f01a75dd8df3 --- /dev/null +++ b/checker/tests/calledmethods-disableframeworks/Parcelable.java @@ -0,0 +1,15 @@ +package android.os; + +/** stub to avoid bringing in Android dependence */ +public interface Parcelable { + public interface Creator { + + public T createFromParcel(Parcel source); + + public T[] newArray(int size); + } + + public int describeContents(); + + public void writeToParcel(Parcel dest, int flags); +} diff --git a/checker/tests/calledmethods-disableframeworks/README.md b/checker/tests/calledmethods-disableframeworks/README.md new file mode 100644 index 000000000000..2e53a0debd37 --- /dev/null +++ b/checker/tests/calledmethods-disableframeworks/README.md @@ -0,0 +1,6 @@ +These are test cases for when we disable the framework supports for AutoValue and Lombok, therefore +the checker annotation won't be automatically inserted into the code. +These two test cases are adapted from _autovalue/Animal.java_ and _lombok/LombokBuilderExample.java_ +but removed all `// :: error:` comments. + +Parcel and Parcelable have to be included here to avoid a crash in the auto-value-parcel plugin. diff --git a/checker/tests/calledmethods-disablereturnsreceiver/SimpleFluentInference.java b/checker/tests/calledmethods-disablereturnsreceiver/SimpleFluentInference.java new file mode 100644 index 000000000000..20fbf9605a53 --- /dev/null +++ b/checker/tests/calledmethods-disablereturnsreceiver/SimpleFluentInference.java @@ -0,0 +1,76 @@ +// This is a copy of SimpleFluentInference.java in the main calledmethods +// test directory. The only difference is that the expected errors have been +// modified to account for the Returns Receiver checker having been disabled. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +/* Simple inference of a fluent builder */ +public class SimpleFluentInference { + SimpleFluentInference build(@CalledMethods({"a", "b"}) SimpleFluentInference this) { + return this; + } + + SimpleFluentInference weakbuild(@CalledMethods({"a"}) SimpleFluentInference this) { + return this; + } + + @This SimpleFluentInference a() { + return this; + } + + @This SimpleFluentInference b() { + return this; + } + + // intentionally does not have an @This annotation + SimpleFluentInference c() { + return new SimpleFluentInference(); + } + + static void doStuffCorrect() { + SimpleFluentInference s = + new SimpleFluentInference() + .a() + .b() + // :: error: finalizer.invocation.invalid + .build(); + } + + static void doStuffWrong() { + SimpleFluentInference s = + new SimpleFluentInference() + .a() + // :: error: finalizer.invocation.invalid + .build(); + } + + static void doStuffRightWeak() { + SimpleFluentInference s = + new SimpleFluentInference() + .a() + // :: error: finalizer.invocation.invalid + .weakbuild(); + } + + static void noReturnsReceiverAnno() { + SimpleFluentInference s = + new SimpleFluentInference() + .a() + .b() + .c() + // :: error: finalizer.invocation.invalid + .build(); + } + + static void fluentLoop() { + SimpleFluentInference s = new SimpleFluentInference().a(); + int i = 10; + while (i > 0) { + // :: error: finalizer.invocation.invalid + s.b().build(); + i--; + s = new SimpleFluentInference(); + } + } +} diff --git a/checker/tests/calledmethods-lombok/BuilderTest.java b/checker/tests/calledmethods-lombok/BuilderTest.java new file mode 100644 index 000000000000..40106a0c678c --- /dev/null +++ b/checker/tests/calledmethods-lombok/BuilderTest.java @@ -0,0 +1,27 @@ +import lombok.Builder; +import lombok.NonNull; + +@Builder +public class BuilderTest { + private Integer x; + @NonNull private Integer y; + @Builder.Default @NonNull private Integer z = 5; + + public static void test_simplePattern() { + BuilderTest.builder().x(0).y(0).build(); // good builder + BuilderTest.builder().y(0).build(); // good builder + BuilderTest.builder().y(0).z(5).build(); // good builder + // :: error: (finalizer.invocation.invalid) + BuilderTest.builder().x(0).build(); // bad builder + } + + public static void test_builderVar() { + final BuilderTest.BuilderTestBuilder goodBuilder = new BuilderTestBuilder(); + goodBuilder.x(0); + goodBuilder.y(0); + goodBuilder.build(); + final BuilderTest.BuilderTestBuilder badBuilder = new BuilderTestBuilder(); + // :: error: (finalizer.invocation.invalid) + badBuilder.build(); + } +} diff --git a/checker/tests/calledmethods-lombok/CheckerFrameworkBuilder.java b/checker/tests/calledmethods-lombok/CheckerFrameworkBuilder.java new file mode 100644 index 000000000000..f8fa4a1e0236 --- /dev/null +++ b/checker/tests/calledmethods-lombok/CheckerFrameworkBuilder.java @@ -0,0 +1,203 @@ +// skip-idempotent +import java.util.List; + +public class CheckerFrameworkBuilder { + + /** + * Most of this test was copied from + * https://raw.githubusercontent.com/projectlombok/lombok/master/test/transform/resource/after-delombok/CheckerFrameworkBuilder.java + * with the exception of the following lines until the next long comment. I have made one change + * outside the scope of these comments: - I fixed the placement of the type annotations, which + * were originally on scoping constructs. I think this is a bug in the delombok pretty-printer + * used to generate this code, but I wasn't able to find the configuration options used to + * reproduce it in the public release. + * + *

    This test represents exactly the code that Lombok generates with the checkerframework = + * True option in a lombok.config file, including the weird package names they use for the CF + * and the {@code @NotCalledMethods} annotation that they use even though we don't (and never + * have) supported such a thing. + * + *

    The new code added in this block ensures that the Called Methods checker handles clients + * of the copied code correctly. + */ + public static void testOldCalledMethodsGood( + @org.checkerframework.checker.calledmethods.qual.CalledMethods({"y", "z"}) CheckerFrameworkBuilderBuilder pb) { + pb.build(); + } + + public static void testOldCalledMethodsBad( + @org.checkerframework.checker.calledmethods.qual.CalledMethods({"y"}) CheckerFrameworkBuilderBuilder pb) { + // :: error: finalizer.invocation.invalid + pb.build(); // pb requires y, z + } + + public static void testOldRRGood() { + CheckerFrameworkBuilder b = CheckerFrameworkBuilder.builder().y(5).z(6).build(); + } + + public static void testOldRRBad() { + CheckerFrameworkBuilder b = + // :: error: finalizer.invocation.invalid + CheckerFrameworkBuilder.builder().z(6).build(); // also needs to call y + } + + /** End new, non-copied code. */ + int x; + + int y; + int z; + List names; + + @java.lang.SuppressWarnings("all") + private static int $default$x() { + return 5; + } + + @org.checkerframework.common.aliasing.qual.Unique @java.lang.SuppressWarnings("all") + CheckerFrameworkBuilder(final int x, final int y, final int z, final List names) { + this.x = x; + this.y = y; + this.z = z; + this.names = names; + } + + @java.lang.SuppressWarnings("all") + public static class CheckerFrameworkBuilderBuilder { + @java.lang.SuppressWarnings("all") + private boolean x$set; + + @java.lang.SuppressWarnings("all") + private int x$value; + + @java.lang.SuppressWarnings("all") + private int y; + + @java.lang.SuppressWarnings("all") + private int z; + + @java.lang.SuppressWarnings("all") + private java.util.ArrayList names; + + @org.checkerframework.common.aliasing.qual.Unique @java.lang.SuppressWarnings("all") + CheckerFrameworkBuilderBuilder() {} + + @org.checkerframework.checker.builder.qual.ReturnsReceiver + @java.lang.SuppressWarnings("all") + public CheckerFrameworkBuilder.CheckerFrameworkBuilderBuilder x( + CheckerFrameworkBuilder.@org.checkerframework.checker.builder.qual.NotCalledMethods( + "x") + CheckerFrameworkBuilderBuilder + this, + final int x) { + this.x$value = x; + x$set = true; + return this; + } + + @org.checkerframework.checker.builder.qual.ReturnsReceiver + @java.lang.SuppressWarnings("all") + public CheckerFrameworkBuilder.CheckerFrameworkBuilderBuilder y( + CheckerFrameworkBuilder.@org.checkerframework.checker.builder.qual.NotCalledMethods( + "y") + CheckerFrameworkBuilderBuilder + this, + final int y) { + this.y = y; + return this; + } + + @org.checkerframework.checker.builder.qual.ReturnsReceiver + @java.lang.SuppressWarnings("all") + public CheckerFrameworkBuilder.CheckerFrameworkBuilderBuilder z( + CheckerFrameworkBuilder.@org.checkerframework.checker.builder.qual.NotCalledMethods( + "z") + CheckerFrameworkBuilderBuilder + this, + final int z) { + this.z = z; + return this; + } + + @org.checkerframework.checker.builder.qual.ReturnsReceiver + @java.lang.SuppressWarnings("all") + public CheckerFrameworkBuilder.CheckerFrameworkBuilderBuilder name(final String name) { + if (this.names == null) { + this.names = new java.util.ArrayList(); + } + this.names.add(name); + return this; + } + + @org.checkerframework.checker.builder.qual.ReturnsReceiver + @java.lang.SuppressWarnings("all") + public CheckerFrameworkBuilder.CheckerFrameworkBuilderBuilder names( + final java.util.Collection names) { + if (names == null) { + throw new java.lang.NullPointerException("names cannot be null"); + } + if (this.names == null) { + this.names = new java.util.ArrayList(); + } + this.names.addAll(names); + return this; + } + + @org.checkerframework.checker.builder.qual.ReturnsReceiver + @java.lang.SuppressWarnings("all") + public CheckerFrameworkBuilder.CheckerFrameworkBuilderBuilder clearNames() { + if (this.names != null) { + this.names.clear(); + } + return this; + } + + @org.checkerframework.dataflow.qual.SideEffectFree + @java.lang.SuppressWarnings("all") + public CheckerFrameworkBuilder build( + CheckerFrameworkBuilder.@org.checkerframework.checker.builder.qual.CalledMethods({ + "y", "z" + }) + CheckerFrameworkBuilderBuilder + this) { + java.util.List names; + switch (this.names == null ? 0 : this.names.size()) { + case 0: + names = java.util.Collections.emptyList(); + break; + case 1: + names = java.util.Collections.singletonList(this.names.get(0)); + break; + default: + names = + java.util.Collections.unmodifiableList( + new java.util.ArrayList(this.names)); + } + int x$value = this.x$value; + if (!this.x$set) { + x$value = CheckerFrameworkBuilder.$default$x(); + } + return new CheckerFrameworkBuilder(x$value, this.y, this.z, names); + } + + @org.checkerframework.dataflow.qual.SideEffectFree + @java.lang.Override + @java.lang.SuppressWarnings("all") + public java.lang.String toString() { + return "CheckerFrameworkBuilder.CheckerFrameworkBuilderBuilder(x$value=" + + this.x$value + + ", y=" + + this.y + + ", z=" + + this.z + + ", names=" + + this.names + + ")"; + } + } + + @org.checkerframework.dataflow.qual.SideEffectFree + @java.lang.SuppressWarnings("all") + public static CheckerFrameworkBuilder.@org.checkerframework.common.aliasing.qual.Unique CheckerFrameworkBuilderBuilder builder() { + return new CheckerFrameworkBuilder.CheckerFrameworkBuilderBuilder(); + } +} diff --git a/checker/tests/calledmethods-lombok/DefaultedName.java b/checker/tests/calledmethods-lombok/DefaultedName.java new file mode 100644 index 000000000000..b9974aef1e2a --- /dev/null +++ b/checker/tests/calledmethods-lombok/DefaultedName.java @@ -0,0 +1,18 @@ +import lombok.Builder; + +@Builder +public class DefaultedName { + @Builder.Default @lombok.NonNull String name = "Martin"; + + static void test1() { + builder().build(); + } + + static void test2(Object foo) { + DefaultedNameBuilder b = builder(); + if (foo != null) { + b.name(foo.toString()); + } + b.build(); + } +} diff --git a/checker/tests/calledmethods-lombok/LombokBuilderExample.java b/checker/tests/calledmethods-lombok/LombokBuilderExample.java new file mode 100644 index 000000000000..9a8ca6843e7f --- /dev/null +++ b/checker/tests/calledmethods-lombok/LombokBuilderExample.java @@ -0,0 +1,14 @@ +// A test for support for the builder() method in Lombok builders. + +import lombok.Builder; + +@Builder +public class LombokBuilderExample { + @lombok.NonNull Object foo; + @lombok.NonNull Object bar; + + static void test() { + // :: error: (finalizer.invocation.invalid) + builder().build(); + } +} diff --git a/checker/tests/calledmethods-lombok/LombokBuilderSubclassExample.java b/checker/tests/calledmethods-lombok/LombokBuilderSubclassExample.java new file mode 100644 index 000000000000..2adc444fd714 --- /dev/null +++ b/checker/tests/calledmethods-lombok/LombokBuilderSubclassExample.java @@ -0,0 +1,40 @@ +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; + +import org.checkerframework.checker.calledmethods.qual.CalledMethods; +import org.checkerframework.common.returnsreceiver.qual.This; + +@Builder(builderClassName = "BaseBuilder") +@Value +public class LombokBuilderSubclassExample { + + @NonNull Integer attribute; + + public static LombokBuilderSubclassExampleBuilder builder() { + return new LombokBuilderSubclassExampleBuilder(); + } + + public static class LombokBuilderSubclassExampleBuilder extends BaseBuilder { + + @Override + @This public LombokBuilderSubclassExampleBuilder attribute(@NonNull Integer attribute) { + return (LombokBuilderSubclassExampleBuilder) super.attribute(attribute); + } + + @Override + public LombokBuilderSubclassExample build( + @CalledMethods("attribute") LombokBuilderSubclassExampleBuilder this) { + final LombokBuilderSubclassExample result = super.build(); + // here result.getAttribute() is guaranteed to be non null, so we do not have to check + // this + // ourselves + + if (result.getAttribute() < 0) { + throw new IllegalArgumentException("attribute must be >= 0"); + } + + return result; + } + } +} diff --git a/checker/tests/calledmethods-lombok/LombokDefaultAssignments.java b/checker/tests/calledmethods-lombok/LombokDefaultAssignments.java new file mode 100644 index 000000000000..dcba963492c6 --- /dev/null +++ b/checker/tests/calledmethods-lombok/LombokDefaultAssignments.java @@ -0,0 +1,16 @@ +import lombok.Builder; + +import java.util.Optional; + +@Builder +public class LombokDefaultAssignments { + @lombok.NonNull Optional bar; + + public static class LombokDefaultAssignmentsBuilder { + private Optional bar = Optional.empty(); + } + + static void test() { + LombokDefaultAssignments.builder().build(); + } +} diff --git a/checker/tests/calledmethods-lombok/LombokNoSingularButClearMethodExample.java b/checker/tests/calledmethods-lombok/LombokNoSingularButClearMethodExample.java new file mode 100644 index 000000000000..5ddf7275c78b --- /dev/null +++ b/checker/tests/calledmethods-lombok/LombokNoSingularButClearMethodExample.java @@ -0,0 +1,23 @@ +import lombok.Builder; + +import java.util.List; + +@Builder +public class LombokNoSingularButClearMethodExample { + @lombok.NonNull List items; + + // This one should throw an error, because the field isn't + // automatically initialized. + public static void testNoItems() { + // :: error: finalizer.invocation.invalid + LombokNoSingularButClearMethodExample.builder().build(); + } + + public static void testWithList(List l) { + LombokNoSingularButClearMethodExample.builder().items(l).build(); + } + + public static class LombokNoSingularButClearMethodExampleBuilder { + public void clearItems() {} + } +} diff --git a/checker/tests/calledmethods-lombok/LombokSingularExample.java b/checker/tests/calledmethods-lombok/LombokSingularExample.java new file mode 100644 index 000000000000..647f629f3b1c --- /dev/null +++ b/checker/tests/calledmethods-lombok/LombokSingularExample.java @@ -0,0 +1,25 @@ +import lombok.Builder; +import lombok.Singular; + +import java.util.List; + +@Builder +public class LombokSingularExample { + @Singular @lombok.NonNull List items; + + // This one should be permitted, because @Singular will + // produce an empty list even if one is not specified directly. + public static void testNoItems() { + LombokSingularExample.builder().build(); + } + + // This call should also be permitted, even though items() is + // not called, because the list will be automatically initialized. + public static void testOneItem() { + LombokSingularExample.builder().item(new Object()).build(); + } + + public static void testWithList(List l) { + LombokSingularExample.builder().items(l).build(); + } +} diff --git a/checker/tests/calledmethods-lombok/LombokToBuilderExample.java b/checker/tests/calledmethods-lombok/LombokToBuilderExample.java new file mode 100644 index 000000000000..c1cbdb8e567b --- /dev/null +++ b/checker/tests/calledmethods-lombok/LombokToBuilderExample.java @@ -0,0 +1,17 @@ +// A test for support for the toBuilder() method in Lombok builders. + +import lombok.Builder; + +@Builder(toBuilder = true) +public class LombokToBuilderExample { + @lombok.NonNull String req; + + static void test(LombokToBuilderExample foo) { + foo.toBuilder().build(); + } + + static void ensureThatErrorIssued() { + // :: error: finalizer.invocation.invalid + LombokToBuilderExample.builder().build(); + } +} diff --git a/checker/tests/calledmethods-lombok/OldInherited.java b/checker/tests/calledmethods-lombok/OldInherited.java new file mode 100644 index 000000000000..d163c1b13c65 --- /dev/null +++ b/checker/tests/calledmethods-lombok/OldInherited.java @@ -0,0 +1,45 @@ +// This is a test for the old @ReturnsReceiver annotation, which is inherited. +// No one should ever write that annotation, but Lombok generates it as of version 1.18.10. + +import org.checkerframework.checker.builder.qual.ReturnsReceiver; +import org.checkerframework.checker.calledmethods.qual.*; + +public class OldInherited { + @ReturnsReceiver + OldInherited getThis() { + return this; + } + + static class OldInheritedChild extends OldInherited { + @java.lang.Override + OldInherited getThis() { + return this; + } + } + + void requiresGetThis(@CalledMethods("getThis") OldInherited this) {} + + public static void testGoodParent() { + OldInherited o = new OldInherited(); + o.getThis(); + o.requiresGetThis(); + } + + public static void testGoodChild() { + OldInheritedChild o = new OldInheritedChild(); + o.getThis(); + o.requiresGetThis(); + } + + public static void testBadParent() { + OldInherited o = new OldInherited(); + // :: error: finalizer.invocation.invalid + o.requiresGetThis(); + } + + public static void testBadChild() { + OldInheritedChild o = new OldInheritedChild(); + // :: error: finalizer.invocation.invalid + o.requiresGetThis(); + } +} diff --git a/checker/tests/calledmethods-lombok/lombok.config b/checker/tests/calledmethods-lombok/lombok.config new file mode 100644 index 000000000000..7a21e88040d4 --- /dev/null +++ b/checker/tests/calledmethods-lombok/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/checker/tests/calledmethods-nodelombok/UnsoundnessTest.java b/checker/tests/calledmethods-nodelombok/UnsoundnessTest.java new file mode 100644 index 000000000000..e4ee4ee4006d --- /dev/null +++ b/checker/tests/calledmethods-nodelombok/UnsoundnessTest.java @@ -0,0 +1,21 @@ +// An example of an unsoundness that occurs when running the Called Methods Checker +// on Lombok'd code without running delombok first. + +@lombok.Builder +class UnsoundnessTest { + @lombok.NonNull Object foo; + @lombok.NonNull Object bar; + + static void test() { + // An error should be issued here, but the code has not been delombok'd. + // If the CF and Lombok are ever able to work in the same invocation of javac + // (i.e. without delomboking first), then this error should be changed back to an + // expected error by re-adding the leading "::". + // error: (finalizer.invocation) + builder().build(); + } + + static void test2() { + builder().foo(null).bar(null).build(); + } +} diff --git a/checker/tests/calledmethods-usevaluechecker/Cve.java b/checker/tests/calledmethods-usevaluechecker/Cve.java new file mode 100644 index 000000000000..056d7e2f2674 --- /dev/null +++ b/checker/tests/calledmethods-usevaluechecker/Cve.java @@ -0,0 +1,107 @@ +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.AmazonEC2AsyncClient; +import com.amazonaws.services.ec2.AmazonEC2Client; +import com.amazonaws.services.ec2.model.DescribeImagesRequest; +import com.amazonaws.services.ec2.model.DescribeImagesResult; +import com.amazonaws.services.ec2.model.Filter; + +// https://nvd.nist.gov/vuln/detail/CVE-2018-15869 +public class Cve { + private static final String IMG_NAME = "some_linux_img"; + + public static void onlyNames(AmazonEC2 client) { + // Should not be allowed unless .withOwner is also used + DescribeImagesResult result = + client.describeImages( + new DescribeImagesRequest() + // :: error: argument.type.incompatible + .withFilters(new Filter("name").withValues(IMG_NAME))); + } + + public static void correct1(AmazonEC2 client) { + DescribeImagesResult result = + client.describeImages( + new DescribeImagesRequest() + .withFilters(new Filter("name").withValues(IMG_NAME)) + .withOwners("martin")); + } + + public static void correct2(AmazonEC2 client) { + DescribeImagesResult result = + client.describeImages(new DescribeImagesRequest().withImageIds("myImageId")); + } + + public static void correct3(AmazonEC2 client) { + DescribeImagesResult result = + client.describeImages(new DescribeImagesRequest().withExecutableUsers("myUsers")); + } + + // Using impl class instead of interface + public static void onlyNamesImpl(AmazonEC2Client client) { + // Should not be allowed unless .withOwner is also used + DescribeImagesResult result = + client.describeImages( + new DescribeImagesRequest() + // :: error: argument.type.incompatible + .withFilters(new Filter("name").withValues(IMG_NAME))); + } + + public static void correct1Impl(AmazonEC2Client client) { + DescribeImagesResult result = + client.describeImages( + new DescribeImagesRequest() + .withFilters(new Filter("name").withValues(IMG_NAME)) + .withOwners("martin")); + } + + public static void correct2Impl(AmazonEC2Client client) { + DescribeImagesResult result = + client.describeImages(new DescribeImagesRequest().withImageIds("myImageId")); + } + + // Using async impl class + public static void onlyNamesAsync(AmazonEC2AsyncClient client) { + // Should not be allowed unless .withOwner is also used + DescribeImagesResult result = + client.describeImages( + new DescribeImagesRequest() + // :: error: argument.type.incompatible + .withFilters(new Filter("name").withValues(IMG_NAME))); + } + + public static void correct1Async(AmazonEC2AsyncClient client) { + DescribeImagesResult result = + client.describeImages( + new DescribeImagesRequest() + .withFilters(new Filter("name").withValues(IMG_NAME)) + .withOwners("martin")); + } + + public static void correct2Async(AmazonEC2AsyncClient client) { + DescribeImagesResult result = + client.describeImages(new DescribeImagesRequest().withImageIds("myImageId")); + } + + // Using async methods + public static void onlyNamesAsync2(AmazonEC2AsyncClient client) { + // Should not be allowed unless .withOwner is also used + Object result = + client.describeImagesAsync( + new DescribeImagesRequest() + // :: error: argument.type.incompatible + .withFilters(new Filter("name").withValues(IMG_NAME))); + } + + public static void correct1Async2(AmazonEC2AsyncClient client) { + Object result = + client.describeImagesAsync( + new DescribeImagesRequest() + .withFilters(new Filter("name").withValues(IMG_NAME)) + .withOwners("martin")); + } + + public static void correct2Async2(AmazonEC2AsyncClient client) { + Object result = + client.describeImagesAsync(new DescribeImagesRequest().withImageIds("myImageId")); + } +} diff --git a/checker/tests/calledmethods-usevaluechecker/Cve2.java b/checker/tests/calledmethods-usevaluechecker/Cve2.java new file mode 100644 index 000000000000..844d7a82b447 --- /dev/null +++ b/checker/tests/calledmethods-usevaluechecker/Cve2.java @@ -0,0 +1,42 @@ +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.model.DescribeImagesRequest; +import com.amazonaws.services.ec2.model.DescribeImagesResult; +import com.amazonaws.services.ec2.model.Filter; + +import java.util.Collections; + +// https://nvd.nist.gov/vuln/detail/CVE-2018-15869 +public class Cve2 { + private static final String IMG_NAME = "some_linux_img"; + + public static void onlyNames(AmazonEC2 client) { + // Should not be allowed unless .withOwner is also used + DescribeImagesRequest request = new DescribeImagesRequest(); + request.withFilters(new Filter("name").withValues(IMG_NAME)); + + // :: error: argument.type.incompatible + DescribeImagesResult result = client.describeImages(request); + } + + public static void correct1(AmazonEC2 client) { + DescribeImagesRequest request = new DescribeImagesRequest(); + request.withFilters(new Filter("name").withValues(IMG_NAME)); + request.withOwners("martin"); + + DescribeImagesResult result = client.describeImages(request); + } + + public static void correct2(AmazonEC2 client) { + DescribeImagesRequest request = new DescribeImagesRequest(); + request.withImageIds("myImageId"); + + DescribeImagesResult result = client.describeImages(request); + } + + public static void correct3(AmazonEC2 client) { + DescribeImagesRequest request = new DescribeImagesRequest(); + request.setExecutableUsers(Collections.singletonList("myUser1")); + + DescribeImagesResult result = client.describeImages(request); + } +} diff --git a/checker/tests/calledmethods-usevaluechecker/GenerateDataKeyRequestExamples.java b/checker/tests/calledmethods-usevaluechecker/GenerateDataKeyRequestExamples.java new file mode 100644 index 000000000000..eec56d9792fe --- /dev/null +++ b/checker/tests/calledmethods-usevaluechecker/GenerateDataKeyRequestExamples.java @@ -0,0 +1,113 @@ +// Examples of trying to prove the key size was set correctly on a AWS GenerateDataKeyRequest object + +import com.amazonaws.services.kms.AWSKMS; +import com.amazonaws.services.kms.model.DataKeySpec; +import com.amazonaws.services.kms.model.GenerateDataKeyRequest; + +import org.checkerframework.checker.calledmethods.qual.*; + +public class GenerateDataKeyRequestExamples { + + void correctWithKeySpec(AWSKMS client) { + GenerateDataKeyRequest request = new GenerateDataKeyRequest(); + request.withKeySpec(DataKeySpec.AES_256); + client.generateDataKey(request); + } + + void correctWithNumberOfBytes(AWSKMS client) { + GenerateDataKeyRequest request = new GenerateDataKeyRequest(); + request.withNumberOfBytes(32); + client.generateDataKey(request); + } + + void correctSetKeySpec(AWSKMS client) { + GenerateDataKeyRequest request = new GenerateDataKeyRequest(); + request.setKeySpec(DataKeySpec.AES_256); + client.generateDataKey(request); + } + + void correctSetNumberOfBytes(AWSKMS client) { + GenerateDataKeyRequest request = new GenerateDataKeyRequest(); + request.setNumberOfBytes(32); + client.generateDataKey(request); + } + + // The next four examples are "both" + void incorrect1(AWSKMS client) { + GenerateDataKeyRequest request = new GenerateDataKeyRequest(); + request.setKeySpec(DataKeySpec.AES_256); + request.setNumberOfBytes(32); + // :: error: argument.type.incompatible + client.generateDataKey(request); + } + + void incorrect2(AWSKMS client) { + GenerateDataKeyRequest request = new GenerateDataKeyRequest(); + request.withKeySpec(DataKeySpec.AES_256); + request.setNumberOfBytes(32); + // :: error: argument.type.incompatible + client.generateDataKey(request); + } + + void incorrect3(AWSKMS client) { + GenerateDataKeyRequest request = new GenerateDataKeyRequest(); + request.setKeySpec(DataKeySpec.AES_256); + request.withNumberOfBytes(32); + // :: error: argument.type.incompatible + client.generateDataKey(request); + } + + void incorrect4(AWSKMS client) { + GenerateDataKeyRequest request = new GenerateDataKeyRequest(); + request.withKeySpec(DataKeySpec.AES_256); + request.withNumberOfBytes(32); + // :: error: argument.type.incompatible + client.generateDataKey(request); + } + + // This example is "neither" + void incorrect5(AWSKMS client) { + GenerateDataKeyRequest request = new GenerateDataKeyRequest(); + // :: error: argument.type.incompatible + client.generateDataKey(request); + } + + // Calling these methods are idempotent, including between with/set versions of the same. + // TODO: Verify that these calls should be permitted. + void setTwice1(AWSKMS client) { + GenerateDataKeyRequest request = new GenerateDataKeyRequest(); + request.withKeySpec(DataKeySpec.AES_256); + request.withKeySpec(DataKeySpec.AES_256); + client.generateDataKey(request); + } + + void setTwice2(AWSKMS client) { + GenerateDataKeyRequest request = new GenerateDataKeyRequest(); + request.withKeySpec(DataKeySpec.AES_256); + request.setKeySpec(DataKeySpec.AES_256); + client.generateDataKey(request); + } + + void setTwice3(AWSKMS client) { + GenerateDataKeyRequest request = new GenerateDataKeyRequest(); + request.withNumberOfBytes(32); + request.setNumberOfBytes(32); + client.generateDataKey(request); + } + + void setTwice4(AWSKMS client) { + GenerateDataKeyRequest request = new GenerateDataKeyRequest(); + request.setNumberOfBytes(32); + request.setNumberOfBytes(32); + client.generateDataKey(request); + } + + /// Interprocedural + + void callee2( + AWSKMS client, + @CalledMethodsPredicate("(!withNumberOfBytes) && (!setNumberOfBytes)") GenerateDataKeyRequest request) { + request.withKeySpec(DataKeySpec.AES_256); + client.generateDataKey(request); + } +} diff --git a/checker/tests/calledmethods-usevaluechecker/MorePreciseFilters.java b/checker/tests/calledmethods-usevaluechecker/MorePreciseFilters.java new file mode 100644 index 000000000000..1e04c13a35db --- /dev/null +++ b/checker/tests/calledmethods-usevaluechecker/MorePreciseFilters.java @@ -0,0 +1,70 @@ +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.model.DescribeImagesRequest; +import com.amazonaws.services.ec2.model.DescribeImagesResult; +import com.amazonaws.services.ec2.model.Filter; + +import java.util.Arrays; +import java.util.Collections; + +public class MorePreciseFilters { + + /* TODO: handle lists + void ownerAliasList(AmazonEC2 ec2Client) { + DescribeImagesRequest imagesRequest = new DescribeImagesRequest(); + List imageFilters = new ArrayList(); + imageFilters.add(new Filter().withName("owner-alias").withValues("microsoft")); + ec2Client.describeImages(imagesRequest.withFilters(imageFilters)).getImages(); + } + */ + + void withFilterNameInList(AmazonEC2 ec2Client) { + DescribeImagesRequest request = new DescribeImagesRequest(); + request.setFilters( + Collections.singletonList(new Filter().withName("image-id").withValues("12345"))); + + DescribeImagesResult result = ec2Client.describeImages(request); + } + + void withOwnerId(AmazonEC2 ec2) { + DescribeImagesRequest request = + new DescribeImagesRequest() + .withFilters( + new Filter("name", Arrays.asList("my_image_name")), + new Filter("owner-id", Arrays.asList("12345"))); + DescribeImagesResult result = ec2.describeImages(request); + } + + void withName(AmazonEC2 ec2Client) { + DescribeImagesRequest request = new DescribeImagesRequest(); + request.withFilters(new Filter().withName("image-id").withValues("12345")); + DescribeImagesResult result = ec2Client.describeImages(request); + } + + void withName2(AmazonEC2 ec2Client) { + DescribeImagesRequest request = new DescribeImagesRequest(); + request.withFilters(new Filter().withName("image-id").withName("foo").withValues("12345")); + // :: error: (argument.type.incompatible) + DescribeImagesResult result = ec2Client.describeImages(request); + } + + void withName3(AmazonEC2 ec2Client) { + DescribeImagesRequest request = new DescribeImagesRequest(); + request.withFilters(new Filter().withName("foo").withName("image-id").withValues("12345")); + DescribeImagesResult result = ec2Client.describeImages(request); + } + + void withName4(AmazonEC2 ec2Client) { + DescribeImagesRequest request = new DescribeImagesRequest(); + request.withFilters( + new Filter().withName("owner-id").withName("foo").withValues("12345"), + new Filter("owner-id", Arrays.asList("12345"))); + DescribeImagesResult result = ec2Client.describeImages(request); + } + + void withName5(AmazonEC2 ec2Client) { + DescribeImagesRequest request = new DescribeImagesRequest(); + Filter f = new Filter(); + request.withFilters(f.withName("owner-id").withValues("12345")); + DescribeImagesResult result = ec2Client.describeImages(request); + } +} diff --git a/checker/tests/calledmethods-usevaluechecker/OnlyOwnersFalsePositive.java b/checker/tests/calledmethods-usevaluechecker/OnlyOwnersFalsePositive.java new file mode 100644 index 000000000000..2c303a323fc2 --- /dev/null +++ b/checker/tests/calledmethods-usevaluechecker/OnlyOwnersFalsePositive.java @@ -0,0 +1,16 @@ +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.model.DescribeImagesRequest; +import com.amazonaws.services.ec2.model.DescribeImagesResult; + +import java.util.Collections; + +// Tests that just setting with/setOwners is permitted, since there are legitimate reasons to do +// that. +// Originally, we required with/setFilters && with/setOwners. +public class OnlyOwnersFalsePositive { + void test(AmazonEC2 ec2Client) { + DescribeImagesRequest describeImagesRequest = new DescribeImagesRequest(); + describeImagesRequest.setOwners(Collections.singleton("self")); + DescribeImagesResult describeImagesResult = ec2Client.describeImages(describeImagesRequest); + } +} diff --git a/checker/tests/calledmethods-usevaluechecker/RequestCreatedInCall.java b/checker/tests/calledmethods-usevaluechecker/RequestCreatedInCall.java new file mode 100644 index 000000000000..8396729d9635 --- /dev/null +++ b/checker/tests/calledmethods-usevaluechecker/RequestCreatedInCall.java @@ -0,0 +1,17 @@ +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.model.DescribeImagesRequest; +import com.amazonaws.services.ec2.model.DescribeImagesResult; +import com.amazonaws.services.ec2.model.Filter; + +import java.util.*; + +// A test to ensure that requests that are created in the call to describeImages work correctly. +public class RequestCreatedInCall { + void test(AmazonEC2 ec2) { + List filters = new ArrayList<>(); + filters.add(new Filter().withName("foo").withValues("bar")); + DescribeImagesResult describeImagesResult = + ec2.describeImages( + new DescribeImagesRequest().withOwners("martin").withFilters(filters)); + } +} diff --git a/checker/tests/calledmethods-usevaluechecker/SimpleFalsePositive.java b/checker/tests/calledmethods-usevaluechecker/SimpleFalsePositive.java new file mode 100644 index 000000000000..4cea89cf842b --- /dev/null +++ b/checker/tests/calledmethods-usevaluechecker/SimpleFalsePositive.java @@ -0,0 +1,22 @@ +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.model.DescribeImagesRequest; +import com.amazonaws.services.ec2.model.DescribeImagesResult; +import com.amazonaws.services.ec2.model.Filter; + +import java.util.*; + +// A simple (potential) false positive case with mutliple filters. +public class SimpleFalsePositive { + void test(AmazonEC2 ec2Client, String namePrefix) { + DescribeImagesRequest request = + new DescribeImagesRequest() + .withOwners("martin") + .withFilters( + Arrays.asList( + new Filter("platform", Arrays.asList("windows")), + new Filter( + "name", + Arrays.asList(String.format("%s*", namePrefix))))); + DescribeImagesResult result = ec2Client.describeImages(request); + } +} diff --git a/checker/tests/calledmethods-usevaluechecker/SpecialNames.java b/checker/tests/calledmethods-usevaluechecker/SpecialNames.java new file mode 100644 index 000000000000..74fe28ae8624 --- /dev/null +++ b/checker/tests/calledmethods-usevaluechecker/SpecialNames.java @@ -0,0 +1,60 @@ +// This test ensures that special handling for method names in DescribeImagesRequest isn't used for +// other classes with the same names. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +public class SpecialNames { + @This SpecialNames withFilters() { + return this; + } + + void setFilters() {} + + @This SpecialNames withFilters(SpecialNames f) { + return this; + } + + void setFilters(SpecialNames f) {} + + @This SpecialNames withName() { + return this; + } + + @This SpecialNames withName(String f) { + return this; + } + + SpecialNames() {} + + SpecialNames(String x) {} + + static void test(SpecialNames s) { + // :: error: assignment.type.incompatible + @CalledMethods("withOwners") SpecialNames x = s.withFilters(new SpecialNames().withName("owner")); + } + + static void test2(SpecialNames s) { + s.setFilters(new SpecialNames("owner")); + // :: error: assignment.type.incompatible + @CalledMethods("withOwners") SpecialNames x = s; + } + + static void test3(SpecialNames s) { + // :: error: assignment.type.incompatible + @CalledMethods("withOwners") SpecialNames x = s.withFilters(new SpecialNames().withName("owner")); + } + + static void test4(SpecialNames s) { + s.setFilters(new SpecialNames("owner")); + // :: error: assignment.type.incompatible + @CalledMethods("withOwners") SpecialNames x = s; + } + + static void testForCrashes(SpecialNames s) { + s.setFilters(); + s.withFilters(); + + s.setFilters(new SpecialNames().withName()); + } +} diff --git a/checker/tests/calledmethods-usevaluechecker/WithOwnersFilter.java b/checker/tests/calledmethods-usevaluechecker/WithOwnersFilter.java new file mode 100644 index 000000000000..df3167eb673e --- /dev/null +++ b/checker/tests/calledmethods-usevaluechecker/WithOwnersFilter.java @@ -0,0 +1,31 @@ +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.model.DescribeImagesRequest; +import com.amazonaws.services.ec2.model.DescribeImagesResult; +import com.amazonaws.services.ec2.model.Filter; + +public class WithOwnersFilter { + private static final String IMG_NAME = "some_linux_img"; + + public static void correct1(AmazonEC2 client) { + DescribeImagesResult result = + client.describeImages( + new DescribeImagesRequest() + .withFilters(new Filter("name").withValues(IMG_NAME)) + .withFilters(new Filter("owner").withValues("my_aws_acct"))); + } + + public static void correct2(AmazonEC2 client) { + DescribeImagesRequest request = new DescribeImagesRequest(); + request.withFilters(new Filter("name").withValues(IMG_NAME)); + request.withFilters(new Filter("owner").withValues("my_aws_acct")); + client.describeImages(request); + } + + public static void correct3(AmazonEC2 client) { + DescribeImagesRequest request = new DescribeImagesRequest(); + request.withFilters( + new Filter("name").withValues(IMG_NAME), + new Filter("owner").withValues("my_aws_acct")); + client.describeImages(request); + } +} diff --git a/checker/tests/calledmethods/CmPredicate.java b/checker/tests/calledmethods/CmPredicate.java new file mode 100644 index 000000000000..52046bbdb4ca --- /dev/null +++ b/checker/tests/calledmethods/CmPredicate.java @@ -0,0 +1,607 @@ +import org.checkerframework.checker.calledmethods.qual.*; + +public class CmPredicate { + + 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 { + + @CalledMethods("a") MyClass cmA; + + @CalledMethodsPredicate("a") MyClass cmpA; + + @CalledMethods({"a", "b"}) MyClass aB; + + @CalledMethodsPredicate("a || b") MyClass aOrB; + + @CalledMethodsPredicate("a && b") MyClass aAndB; + + @CalledMethodsPredicate("a || b && c") MyClass bAndCOrA; + + @CalledMethodsPredicate("a || (b && c)") MyClass bAndCOrAParens; + + @CalledMethodsPredicate("a && b || c") MyClass aAndBOrC; + + @CalledMethodsPredicate("(a && b) || c") MyClass aAndBOrCParens; + + @CalledMethodsPredicate("(a || b) && c") MyClass aOrBAndC; + + @CalledMethodsPredicate("a && (b || c)") MyClass bOrCAndA; + + @CalledMethodsPredicate("b && c") MyClass bAndC; + + @CalledMethodsPredicate("(b && c)") MyClass bAndCParens; + + void a() {} + + void b() {} + + void c(@CalledMethodsPredicate("a || b") MyClass this) {} + + void d(@CalledMethodsPredicate("a && b") MyClass this) {} + + void e(@CalledMethodsPredicate("a || (b && c)") MyClass this) {} + + void f(@CalledMethodsPredicate("a && b || c") MyClass this) {} + + static void testAssignability1(@CalledMethodsPredicate("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(@CalledMethodsPredicate("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() { + + @CalledMethods("a") MyClass cmALocal; + @CalledMethodsPredicate("a") MyClass cmpALocal; + @CalledMethodsPredicate("a || b") MyClass aOrBLocal; + @CalledMethods({"a", "b"}) MyClass aBLocal; + @CalledMethodsPredicate("a && b") MyClass aAndBLocal; + @CalledMethodsPredicate("a || b && c") MyClass bAndCOrALocal; + @CalledMethodsPredicate("a || (b && c)") MyClass bAndCOrAParensLocal; + @CalledMethodsPredicate("a && b || c") MyClass aAndBOrCLocal; + @CalledMethodsPredicate("(a && b) || c") MyClass aAndBOrCParensLocal; + @CalledMethodsPredicate("(a || b) && c") MyClass aOrBAndCLocal; + @CalledMethodsPredicate("a && (b || c)") MyClass bOrCAndALocal; + @CalledMethodsPredicate("b && c") MyClass bAndCLocal; + @CalledMethodsPredicate("(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/checker/tests/calledmethods/EnsuresCalledMethodsIfRepeatable.java b/checker/tests/calledmethods/EnsuresCalledMethodsIfRepeatable.java new file mode 100644 index 000000000000..a3c2e20e80b9 --- /dev/null +++ b/checker/tests/calledmethods/EnsuresCalledMethodsIfRepeatable.java @@ -0,0 +1,55 @@ +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsIf; + +import java.io.Closeable; +import java.io.IOException; + +public class EnsuresCalledMethodsIfRepeatable { + + @EnsuresCalledMethodsIf(expression = "#1", result = true, methods = "close") + @EnsuresCalledMethodsIf(expression = "#2", result = true, methods = "close") + public boolean close2MissingFirst(Closeable r1, Closeable r2) throws IOException { + r1.close(); + // ::error: (contracts.conditional.postcondition.not.satisfied) + return true; + } + + @EnsuresCalledMethodsIf(expression = "#1", result = true, methods = "close") + @EnsuresCalledMethodsIf(expression = "#2", result = true, methods = "close") + public boolean close2MissingSecond(Closeable r1, Closeable r2) throws IOException { + r2.close(); + // ::error: (contracts.conditional.postcondition.not.satisfied) + return true; + } + + @EnsuresCalledMethodsIf(expression = "#1", result = true, methods = "close") + @EnsuresCalledMethodsIf(expression = "#2", result = true, methods = "close") + public boolean close2Correct(Closeable r1, Closeable r2) throws IOException { + try { + r1.close(); + } finally { + r2.close(); + } + return true; + } + + @EnsuresCalledMethodsIf(expression = "#1", result = true, methods = "close") + @EnsuresCalledMethodsIf(expression = "#2", result = true, methods = "close") + public boolean close2CorrectViaCall(Closeable r1, Closeable r2) throws IOException { + return close2Correct(r1, r2); + } + + public static class SubclassWrong extends EnsuresCalledMethodsIfRepeatable { + @Override + public boolean close2Correct(Closeable r1, Closeable r2) throws IOException { + // ::error: (contracts.conditional.postcondition.not.satisfied) + return true; + } + } + + public static class SubclassRight extends EnsuresCalledMethodsIfRepeatable { + @Override + public boolean close2Correct(Closeable r1, Closeable r2) throws IOException { + return false; + } + } +} diff --git a/checker/tests/calledmethods/EnsuresCalledMethodsIfSubclass.java b/checker/tests/calledmethods/EnsuresCalledMethodsIfSubclass.java new file mode 100644 index 000000000000..02c896ce10fa --- /dev/null +++ b/checker/tests/calledmethods/EnsuresCalledMethodsIfSubclass.java @@ -0,0 +1,30 @@ +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsIf; + +import java.io.Closeable; +import java.io.IOException; + +public class EnsuresCalledMethodsIfSubclass { + + public static class Parent { + @EnsuresCalledMethodsIf(expression = "#1", result = true, methods = "close") + public boolean method(Closeable x) throws IOException { + x.close(); + return true; + } + } + + public static class SubclassWrong extends Parent { + @Override + public boolean method(Closeable x) throws IOException { + // ::error: (contracts.conditional.postcondition.not.satisfied) + return true; + } + } + + public static class SubclassRight extends Parent { + @Override + public boolean method(Closeable x) throws IOException { + return false; + } + } +} diff --git a/checker/tests/calledmethods/EnsuresCalledMethodsIfTest.java b/checker/tests/calledmethods/EnsuresCalledMethodsIfTest.java new file mode 100644 index 000000000000..7773cd5af433 --- /dev/null +++ b/checker/tests/calledmethods/EnsuresCalledMethodsIfTest.java @@ -0,0 +1,63 @@ +// Test case for https://github.com/typetools/checker-framework/issues/4699 + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsIf; + +import java.io.IOException; + +class EnsuresCalledMethodsIfTest { + + @EnsuresCalledMethods(value = "#1", methods = "close") + // If `sock` is null, `sock.close()` will not be called, and the method will exit normally, as + // the + // NullPointerException is caught. But, the Called Methods Checker + // assumes the program is free of NullPointerExceptions, delegating verification of that + // property to the Nullness Checker. So, the postcondition is verified. + public static void closeSock(EnsuresCalledMethodsIfTest sock) throws Exception { + if (!sock.isOpen()) { + return; + } + try { + sock.close(); + } catch (Exception e) { + } + } + + @EnsuresCalledMethods(value = "#1", methods = "close") + public static void closeSockOK(EnsuresCalledMethodsIfTest sock) throws Exception { + if (!sock.isOpen()) { + return; + } + try { + sock.close(); + } catch (IOException e) { + } + } + + @EnsuresCalledMethods(value = "#1", methods = "close") + public static void closeSockOK1(EnsuresCalledMethodsIfTest sock) throws Exception { + if (!sock.isOpen()) { + return; + } + sock.close(); + } + + @EnsuresCalledMethods(value = "#1", methods = "close") + public static void closeSockOK2(EnsuresCalledMethodsIfTest sock) throws Exception { + if (sock.isOpen()) { + sock.close(); + } + } + + void close() throws IOException {} + + @SuppressWarnings( + "calledmethods") // like the JDK's isOpen methods; makes this test case self-contained + @EnsuresCalledMethodsIf( + expression = "this", + result = false, + methods = {"close"}) + boolean isOpen() { + return true; + } +} diff --git a/checker/tests/calledmethods/EnsuresCalledMethodsOnExceptionRepeatable.java b/checker/tests/calledmethods/EnsuresCalledMethodsOnExceptionRepeatable.java new file mode 100644 index 000000000000..180856f4d1bd --- /dev/null +++ b/checker/tests/calledmethods/EnsuresCalledMethodsOnExceptionRepeatable.java @@ -0,0 +1,46 @@ +// Test that @EnsuresCalledMethodsOnException can be repeated. + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsOnException; + +import java.io.*; + +class EnsuresCalledMethodsOnExceptionRepeatable { + + @EnsuresCalledMethodsOnException(value = "#1", methods = "close") + @EnsuresCalledMethodsOnException(value = "#2", methods = "close") + // ::error: (contracts.exceptional.postcondition.not.satisfied) + public void close2MissingFirst(Closeable r1, Closeable r2) throws IOException { + r1.close(); + } + + @EnsuresCalledMethodsOnException(value = "#1", methods = "close") + @EnsuresCalledMethodsOnException(value = "#2", methods = "close") + // ::error: (contracts.exceptional.postcondition.not.satisfied) + public void close2MissingSecond(Closeable r1, Closeable r2) throws IOException { + r2.close(); + } + + @EnsuresCalledMethodsOnException(value = "#1", methods = "close") + @EnsuresCalledMethodsOnException(value = "#2", methods = "close") + public void close2Correct(Closeable r1, Closeable r2) throws IOException { + try { + r1.close(); + } finally { + r2.close(); + } + } + + @EnsuresCalledMethodsOnException(value = "#1", methods = "close") + @EnsuresCalledMethodsOnException(value = "#2", methods = "close") + public void close2CorrectViaCall(Closeable r1, Closeable r2) throws IOException { + close2Correct(r1, r2); + } + + public static class Subclass extends EnsuresCalledMethodsOnExceptionRepeatable { + @Override + // ::error: (contracts.exceptional.postcondition.not.satisfied) + public void close2Correct(Closeable r1, Closeable r2) throws IOException { + throw new IOException(); + } + } +} diff --git a/checker/tests/calledmethods/EnsuresCalledMethodsOnExceptionSubclass.java b/checker/tests/calledmethods/EnsuresCalledMethodsOnExceptionSubclass.java new file mode 100644 index 000000000000..e799b6f57a78 --- /dev/null +++ b/checker/tests/calledmethods/EnsuresCalledMethodsOnExceptionSubclass.java @@ -0,0 +1,30 @@ +// Test that @EnsuresCalledMethodsOnException is inherited by overridden methods. + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethodsOnException; + +import java.io.*; + +public class EnsuresCalledMethodsOnExceptionSubclass { + + public static class Parent { + @EnsuresCalledMethodsOnException(value = "#1", methods = "close") + public void method(Closeable x) throws IOException { + x.close(); + } + } + + public static class SubclassWrong extends Parent { + @Override + // ::error: (contracts.exceptional.postcondition.not.satisfied) + public void method(Closeable x) throws IOException { + throw new IOException(); + } + } + + public static class SubclassCorrect extends Parent { + @Override + public void method(Closeable x) throws IOException { + // No exception thrown ==> no contract to satisfy! + } + } +} diff --git a/checker/tests/calledmethods/EnsuresCalledMethodsOnExceptionTest.java b/checker/tests/calledmethods/EnsuresCalledMethodsOnExceptionTest.java new file mode 100644 index 000000000000..0defb3e4dbf5 --- /dev/null +++ b/checker/tests/calledmethods/EnsuresCalledMethodsOnExceptionTest.java @@ -0,0 +1,107 @@ +// Test that @EnsuresCalledMethodsOnException behaves as expected. + +import org.checkerframework.checker.calledmethods.qual.*; + +import java.io.IOException; + +public abstract class EnsuresCalledMethodsOnExceptionTest { + + static class Resource { + void a() {} + + void b() throws IOException {} + } + + abstract boolean arbitraryChoice(); + + abstract void throwArbitraryException() throws Exception; + + @EnsuresCalledMethodsOnException(value = "#1", methods = "b") + void blanketCase(Resource r) throws IOException { + // OK: r.b() counts as called even if it itself throws an exception. + r.b(); + } + + @EnsuresCalledMethodsOnException(value = "#1", methods = "a") + void noCall(Resource r) { + // OK: this method does not throw exceptions. + } + + @EnsuresCalledMethodsOnException(value = "#1", methods = "a") + // :: error: (contracts.exceptional.postcondition.not.satisfied) + void callAfterThrow(Resource r) throws Exception { + if (arbitraryChoice()) { + // Not OK: r.a() has not been called yet + throwArbitraryException(); + } + r.a(); + } + + @EnsuresCalledMethodsOnException(value = "#1", methods = "a") + void callInFinallyBlock(Resource r) throws Exception { + try { + if (arbitraryChoice()) { + // OK: r.a() will be called in the finally block + throwArbitraryException(); + } + } finally { + r.a(); + } + } + + @EnsuresCalledMethodsOnException(value = "#1", methods = "a") + void callInCatchBlock(Resource r) throws Exception { + try { + if (arbitraryChoice()) { + // OK: r.a() will be called in the catch block + throwArbitraryException(); + } + } catch (Exception e) { + r.a(); + throw e; + } + } + + @EnsuresCalledMethodsOnException(value = "#1", methods = "a") + // :: error: (contracts.exceptional.postcondition.not.satisfied) + void callInSpecificCatchBlock(Resource r) throws Exception { + try { + if (arbitraryChoice()) { + // Not OK: the catch block only catches IOException + throwArbitraryException(); + } + } catch (IOException e) { + r.a(); + throw e; + } + } + + @EnsuresCalledMethodsOnException(value = "#1", methods = "a") + abstract void callMethodOnException(Resource r) throws Exception; + + @EnsuresCalledMethodsOnException(value = "#1", methods = "a") + void propagateSubtypeOfException(Resource r) throws Exception { + // OK: the call satisfies our contract + callMethodOnException(r); + } + + @EnsuresCalledMethods(value = "#1", methods = "a") + void exploitCalledMethodsOnException(Resource r) throws Exception { + try { + callMethodOnException(r); + } catch (Exception e) { + // OK: the other call ensured the contract + return; + } + // OK: although r.a() was not called, this method promises nothing on exceptional return + throw new Exception("Phooey"); + } + + @EnsuresCalledMethods(value = "#1", methods = "a") + // :: error: (contracts.postcondition.not.satisfied) + void exceptionalCallsDoNotSatisfyNormalPaths(Resource r) throws Exception { + // Not OK: this call is not enough to satisfy our contract, since it only promises something + // on exceptional return. + callMethodOnException(r); + } +} diff --git a/checker/tests/calledmethods/EnsuresCalledMethodsRepeatable.java b/checker/tests/calledmethods/EnsuresCalledMethodsRepeatable.java new file mode 100644 index 000000000000..1b08f5f85286 --- /dev/null +++ b/checker/tests/calledmethods/EnsuresCalledMethodsRepeatable.java @@ -0,0 +1,54 @@ +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; + +import java.io.Closeable; +import java.io.IOException; + +class EnsuresCalledMethodsRepeatable { + + @EnsuresCalledMethods( + value = "#1", + methods = {"toString"}) + @EnsuresCalledMethods( + value = "#1", + methods = {"hashCode"}) + void test(Object obj) { + obj.toString(); + obj.hashCode(); + } + + @EnsuresCalledMethods(value = "#1", methods = "close") + @EnsuresCalledMethods(value = "#2", methods = "close") + // ::error: (contracts.postcondition.not.satisfied) + public void close2MissingFirst(Closeable r1, Closeable r2) throws IOException { + r1.close(); + } + + @EnsuresCalledMethods(value = "#1", methods = "close") + @EnsuresCalledMethods(value = "#2", methods = "close") + // ::error: (contracts.postcondition.not.satisfied) + public void close2MissingSecond(Closeable r1, Closeable r2) throws IOException { + r2.close(); + } + + @EnsuresCalledMethods(value = "#1", methods = "close") + @EnsuresCalledMethods(value = "#2", methods = "close") + public void close2Correct(Closeable r1, Closeable r2) throws IOException { + try { + r1.close(); + } finally { + r2.close(); + } + } + + @EnsuresCalledMethods(value = "#1", methods = "close") + @EnsuresCalledMethods(value = "#2", methods = "close") + public void close2CorrectViaCall(Closeable r1, Closeable r2) throws IOException { + close2Correct(r1, r2); + } + + public static class Subclass extends EnsuresCalledMethodsRepeatable { + @Override + // ::error: (contracts.postcondition.not.satisfied) + public void close2Correct(Closeable r1, Closeable r2) throws IOException {} + } +} diff --git a/checker/tests/calledmethods/EnsuresCalledMethodsSubclass.java b/checker/tests/calledmethods/EnsuresCalledMethodsSubclass.java new file mode 100644 index 000000000000..756529767065 --- /dev/null +++ b/checker/tests/calledmethods/EnsuresCalledMethodsSubclass.java @@ -0,0 +1,20 @@ +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; + +import java.io.Closeable; +import java.io.IOException; + +public class EnsuresCalledMethodsSubclass { + + public static class Parent { + @EnsuresCalledMethods(value = "#1", methods = "close") + public void method(Closeable x) throws IOException { + x.close(); + } + } + + public static class Subclass extends Parent { + @Override + // ::error: (contracts.postcondition.not.satisfied) + public void method(Closeable x) throws IOException {} + } +} diff --git a/checker/tests/calledmethods/EnsuresCalledMethodsThisLub.java b/checker/tests/calledmethods/EnsuresCalledMethodsThisLub.java new file mode 100644 index 000000000000..6e4f676167fd --- /dev/null +++ b/checker/tests/calledmethods/EnsuresCalledMethodsThisLub.java @@ -0,0 +1,43 @@ +import org.checkerframework.checker.calledmethods.qual.CalledMethods; +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; + +class EnsuresCalledMethodsThisLub { + + @EnsuresCalledMethods( + value = "#1", + methods = {"toString", "equals"}) + void call1(Object obj) { + obj.toString(); + obj.equals(null); + } + + @EnsuresCalledMethods( + value = "#1", + methods = {"toString", "hashCode"}) + void call2(Object obj) { + obj.toString(); + obj.hashCode(); + } + + void test(boolean b) { + if (b) { + call1(this); + } else { + call2(this); + } + @CalledMethods("toString") Object obj1 = this; + // :: error: (assignment.type.incompatible) + @CalledMethods({"toString", "equals"}) Object obj2 = this; + } + + void test_arg(Object arg, boolean b) { + if (b) { + call1(arg); + } else { + call2(arg); + } + @CalledMethods("toString") Object obj1 = arg; + // :: error: (assignment.type.incompatible) + @CalledMethods({"toString", "equals"}) Object obj2 = arg; + } +} diff --git a/checker/tests/calledmethods/EnsuresCalledMethodsVarArgsSimple.java b/checker/tests/calledmethods/EnsuresCalledMethodsVarArgsSimple.java new file mode 100644 index 000000000000..ab82f7b0472a --- /dev/null +++ b/checker/tests/calledmethods/EnsuresCalledMethodsVarArgsSimple.java @@ -0,0 +1,39 @@ +// A simple test for the @EnsuresCalledMethodsVarArgs annotation. + +import org.checkerframework.checker.calledmethods.qual.*; + +import java.io.IOException; +import java.net.Socket; +import java.util.List; + +class EnsuresCalledMethodsVarArgsSimple { + + // :: error: ensuresvarargs.unverified + @EnsuresCalledMethodsVarArgs("close") + void closeAll(Socket... sockets) { + for (Socket s : sockets) { + try { + s.close(); + } catch (IOException e) { + } + } + } + + // :: error: ensuresvarargs.unverified + @EnsuresCalledMethodsVarArgs("close") + // :: error: ensuresvarargs.invalid + void closeAllNotVA(List sockets) { + for (Socket s : sockets) { + try { + s.close(); + } catch (IOException e) { + } + } + } + + void test(Socket s1, Socket s2) { + closeAll(s1, s2); + @CalledMethods("close") Socket s1_1 = s1; + @CalledMethods("close") Socket s2_1 = s2; + } +} diff --git a/checker/tests/calledmethods/ExceptionalPath.java b/checker/tests/calledmethods/ExceptionalPath.java new file mode 100644 index 000000000000..93bb908adfcd --- /dev/null +++ b/checker/tests/calledmethods/ExceptionalPath.java @@ -0,0 +1,18 @@ +// A test that calling a method with exceptional exit paths leads to that method being +// considered "definitely called" (i.e. @CalledMethods of the method) on all paths. + +import org.checkerframework.checker.calledmethods.qual.*; + +import java.io.IOException; +import java.net.Socket; + +class ExceptionalPath { + void test(Socket s) { + try { + s.close(); + @CalledMethods("close") Socket s1 = s; + } catch (IOException e) { + @CalledMethods("close") Socket s2 = s; + } + } +} diff --git a/checker/tests/calledmethods/ExceptionalPath2.java b/checker/tests/calledmethods/ExceptionalPath2.java new file mode 100644 index 000000000000..715b729380c0 --- /dev/null +++ b/checker/tests/calledmethods/ExceptionalPath2.java @@ -0,0 +1,38 @@ +import org.checkerframework.checker.calledmethods.qual.*; + +import java.io.IOException; + +class ExceptionalPath2 { + + interface Resource { + void a(); + + void b() throws IOException; + } + + Resource r; + + // Regression test for an obscure bug: in some cases, the called + // methods transfer function would silently fail to update the + // set of known called methods along exceptional paths. That + // would a spurious precondition error on this method. + @EnsuresCalledMethods( + value = "this.r", + methods = {"b"}) + void test() { + try { + try { + r.a(); + } finally { + r.b(); + } + } catch (IOException ignored) { + // The only way to get here is if `r.b()` started running and + // threw an IOException. We no longer know whether `r.a()` + // has been called, since `r.b()` might have overwritten `r` + // before throwing. + // ::error: (assignment.type.incompatible) + @CalledMethods({"a"}) Resource x = r; + } + } +} diff --git a/checker/tests/calledmethods/FinallyClose.java b/checker/tests/calledmethods/FinallyClose.java new file mode 100644 index 000000000000..d3177bb34780 --- /dev/null +++ b/checker/tests/calledmethods/FinallyClose.java @@ -0,0 +1,66 @@ +// Test case involving some complicated try-finally control flow. + +import org.checkerframework.checker.calledmethods.qual.*; + +import java.io.*; + +abstract class FinallyClose { + + abstract Closeable alloc() throws IOException; + + abstract Closeable derive(Closeable r) throws IOException; + + abstract String compute(Closeable resource) throws IOException; + + abstract void makeNotes() throws IOException; + + String run1() throws IOException { + Closeable resource = null; + try { + resource = alloc(); + return compute(resource); + } finally { + try { + makeNotes(); + } finally { + closeResource(resource); + } + } + } + + String run2() throws IOException { + Closeable resource = null; + Closeable subresource = null; + try { + resource = alloc(); + subresource = derive(resource); + return compute(subresource); + } finally { + try { + makeNotes(); + } finally { + try { + closeResource(subresource); + } finally { + closeResource(resource); + } + } + } + } + + @EnsuresCalledMethods( + value = "#1", + methods = {"close"}) + @EnsuresCalledMethodsOnException( + value = "#1", + methods = {"close"}) + void closeResource(Closeable resource) throws IOException { + if (resource != null) { + try { + resource.close(); + } catch (Exception e) { + System.out.println(e); + } + } + } +} diff --git a/checker/tests/calledmethods/Generics.java b/checker/tests/calledmethods/Generics.java new file mode 100644 index 000000000000..3e642a7c03c4 --- /dev/null +++ b/checker/tests/calledmethods/Generics.java @@ -0,0 +1,55 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +public class Generics { + + static interface Symbol { + + boolean isStatic(); + + void finalize(@CalledMethods("isStatic") Symbol this); + } + + static List<@CalledMethods("isStatic") Symbol> makeList(Symbol s) { + s.isStatic(); + ArrayList<@CalledMethods("isStatic") Symbol> l = new ArrayList<>(); + l.add(s); + return l; + } + + static void useList() { + Symbol s = null; + for (Symbol t : makeList(s)) { + t.finalize(); + } + } + + // reduced from real-world code + private <@CalledMethods() T extends Symbol> T getMember(Class type, boolean b) { + if (b) { + T sym = getMember(type, !b); + if (sym != null && sym.isStatic()) { + return sym; + } + } else { + T sym = getMember(type, b); + if (sym != null) { + return sym; + } + } + return null; + } + + static Stream stringList() { + String s = "hi"; + // dummy method call + s.contains("h"); + // should infer type Stream<@CalledMethods() String> + return Arrays.asList(s).stream(); + } +} diff --git a/checker/tests/calledmethods/Issue20.java b/checker/tests/calledmethods/Issue20.java new file mode 100644 index 000000000000..77a95871ea2b --- /dev/null +++ b/checker/tests/calledmethods/Issue20.java @@ -0,0 +1,36 @@ +// test case for issue 20: https://github.com/kelloggm/object-construction-checker/issues/20 + +import java.util.Map; + +public class Issue20 { + + private boolean enableProtoAnnotations; + + @SuppressWarnings({"unchecked"}) + private T getProtoExtension( + E element, GeneratedExtension extension) { + // Use this method as the chokepoint for all field annotations processing, so we can + // toggle on/off annotations processing in one place. + if (!enableProtoAnnotations) { + return null; + } + return (T) element.getOptionFields().get(extension.getDescriptor()); + } + + // stubs of relevant classes + private class Message {} + + private class ProtoElement { + public Map getOptionFields() { + return null; + } + } + + private class FieldDescriptor {} + + private class GeneratedExtension { + public FieldDescriptor getDescriptor() { + return null; + } + } +} diff --git a/checker/tests/calledmethods/Issue5402.java b/checker/tests/calledmethods/Issue5402.java new file mode 100644 index 000000000000..a891fd6806fb --- /dev/null +++ b/checker/tests/calledmethods/Issue5402.java @@ -0,0 +1,70 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5402 + +import org.checkerframework.checker.calledmethods.qual.CalledMethods; +import org.checkerframework.checker.calledmethods.qual.RequiresCalledMethods; +import org.checkerframework.common.returnsreceiver.qual.This; +import org.checkerframework.dataflow.qual.Deterministic; + +public class Issue5402 {} + +class Issue5402_Ok1 { + + public @This Issue5402_Ok1 bar() { + return this; + } + + public void baz(@CalledMethods("bar") Issue5402_Ok1 this) {} + + public static void test() { + final Issue5402_Ok1 foo = new Issue5402_Ok1(); + foo.bar().baz(); // No error + } +} + +class Issue5402_Ok2 { + + public @This Issue5402_Ok2 bar() { + return this; + } + + @RequiresCalledMethods(value = "this", methods = "bar") + public void baz() {} + + public static void test() { + final Issue5402_Ok2 foo = new Issue5402_Ok2(); + final Issue5402_Ok2 foo1 = foo.bar(); + foo1.baz(); // No error + } +} + +class Issue5402_Ok3 { + + @Deterministic + public @This Issue5402_Ok3 bar() { + return this; + } + + @RequiresCalledMethods(value = "this", methods = "bar") + public void baz() {} + + public static void test() { + final Issue5402_Ok2 foo = new Issue5402_Ok2(); + final Issue5402_Ok2 foo1 = foo.bar(); + foo1.baz(); // No error + } +} + +class Issue5402_Bad { + + public @This Issue5402_Bad bar() { + return this; + } + + @RequiresCalledMethods(value = "this", methods = "bar") + public void baz() {} + + public static void test() { + final Issue5402_Bad foo = new Issue5402_Bad(); + foo.bar().baz(); // Error + } +} diff --git a/checker/tests/calledmethods/Not.java b/checker/tests/calledmethods/Not.java new file mode 100644 index 000000000000..9d91fbdd8fd4 --- /dev/null +++ b/checker/tests/calledmethods/Not.java @@ -0,0 +1,75 @@ +import org.checkerframework.checker.calledmethods.qual.*; + +public class Not { + + class Foo { + void a() {} + + void b() {} + + void c() {} + + void notA(@CalledMethodsPredicate("!a") Foo this) {} + + void notB(@CalledMethodsPredicate("!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(@CalledMethods("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/checker/tests/calledmethods/Parens.java b/checker/tests/calledmethods/Parens.java new file mode 100644 index 000000000000..c86f80abec55 --- /dev/null +++ b/checker/tests/calledmethods/Parens.java @@ -0,0 +1,5 @@ +public class Parens { + public synchronized void incrementPushed(long[] pushed, int operationType) { + // ++(pushed[operationType]); + } +} diff --git a/checker/tests/calledmethods/Postconditions.java b/checker/tests/calledmethods/Postconditions.java new file mode 100644 index 000000000000..983341aa949c --- /dev/null +++ b/checker/tests/calledmethods/Postconditions.java @@ -0,0 +1,132 @@ +import org.checkerframework.checker.calledmethods.qual.*; + +/** Test for postcondition support via @EnsuresCalledMethods. */ +public class Postconditions { + void build(@CalledMethods({"a", "b", "c"}) Postconditions this) {} + + void a() {} + + void b() {} + + void c() {} + + @EnsuresCalledMethods(value = "#1", methods = "b") + static void callB(Postconditions x) { + x.b(); + } + + @EnsuresCalledMethods(value = "#1", methods = "b") + // :: error: contracts.postcondition.not.satisfied + static void doesNotCallB(Postconditions x) {} + + @EnsuresCalledMethods( + value = "#1", + methods = {"b", "c"}) + static void callBAndC(Postconditions x) { + x.b(); + x.c(); + } + + static void allInOneMethod() { + Postconditions y = new Postconditions(); + y.a(); + y.b(); + y.c(); + y.build(); + } + + static void invokeCallB() { + Postconditions y = new Postconditions(); + y.a(); + callB(y); + y.c(); + y.build(); + } + + static void invokeCallBLast() { + Postconditions y = new Postconditions(); + y.a(); + y.c(); + callB(y); + y.build(); + } + + static void invokeCallBAndC() { + Postconditions y = new Postconditions(); + y.a(); + callBAndC(y); + y.build(); + } + + static void invokeCallBAndCWrong() { + Postconditions y = new Postconditions(); + callBAndC(y); + // :: error: finalizer.invocation.invalid + y.build(); + } + + @EnsuresCalledMethodsIf( + expression = "#1", + methods = {"a", "b", "c"}, + result = true) + static boolean ensuresABCIfTrue(Postconditions p, boolean b) { + if (b) { + p.a(); + p.b(); + p.c(); + return true; + } + return false; + } + + static void testEnsuresCalledMethodsIf(Postconditions p, boolean b) { + if (ensuresABCIfTrue(p, b)) { + p.build(); + } else { + // :: error: finalizer.invocation.invalid + p.build(); + } + } + + @EnsuresCalledMethods(value = "#1", methods = "a") + static void callWithException(Postconditions p) { + try { + p.a(); + throw new java.io.IOException(); + } catch (java.io.IOException e) { + } + } + + @EnsuresCalledMethods( + value = {"#1", "#2"}, + methods = "a") + static void callAOnBoth(Postconditions p1, Postconditions p2) { + p1.a(); + p2.a(); + } + + @EnsuresCalledMethods( + value = {"#1", "#2"}, + methods = "a") + static void callAOnBothCatchNPE(Postconditions p1, Postconditions p2) { + // postcondition is verified because the checker assumes NullPointerExceptions cannot occur + try { + p1.a(); + } catch (NullPointerException e) { + } + p2.a(); + } + + @EnsuresCalledMethods( + value = {"#1", "#2"}, + methods = "a") + static int callAOnBothFinallyNPE(Postconditions p1, Postconditions p2) { + // postcondition is verified because the checker assumes NullPointerExceptions cannot occur + try { + p1.a(); + } finally { + p2.a(); + return 0; + } + } +} diff --git a/checker/tests/calledmethods/RequiresCalledMethodsRepeatable.java b/checker/tests/calledmethods/RequiresCalledMethodsRepeatable.java new file mode 100644 index 000000000000..77454b59bbc0 --- /dev/null +++ b/checker/tests/calledmethods/RequiresCalledMethodsRepeatable.java @@ -0,0 +1,34 @@ +import org.checkerframework.checker.calledmethods.qual.*; + +import java.io.Closeable; + +public class RequiresCalledMethodsRepeatable { + + @RequiresCalledMethods(value = "#1", methods = "close") + @RequiresCalledMethods(value = "#2", methods = "close") + public void requires2(Closeable r1, Closeable r2) { + @CalledMethods("close") Closeable r3 = r1; + @CalledMethods("close") Closeable r4 = r2; + } + + public void requires2Wrong(Closeable r1, Closeable r2) { + // ::error: (contracts.precondition.not.satisfied) + requires2(r1, r2); + } + + @RequiresCalledMethods(value = "#1", methods = "close") + @RequiresCalledMethods(value = "#2", methods = "close") + public void requires2Correct(Closeable r1, Closeable r2) { + requires2(r1, r2); + } + + public static class Subclass extends RequiresCalledMethodsRepeatable { + @Override + public void requires2Correct(Closeable r1, Closeable r2) {} + + public void caller(Closeable r1, Closeable r2) { + requires2Correct( + r1, r2); // OK: we override requires2Correct() with a weaker precondition + } + } +} diff --git a/checker/tests/calledmethods/RequiresCalledMethodsSubclass.java b/checker/tests/calledmethods/RequiresCalledMethodsSubclass.java new file mode 100644 index 000000000000..11a51ab0f1d7 --- /dev/null +++ b/checker/tests/calledmethods/RequiresCalledMethodsSubclass.java @@ -0,0 +1,26 @@ +import org.checkerframework.checker.calledmethods.qual.RequiresCalledMethods; + +import java.io.Closeable; +import java.io.IOException; + +public class RequiresCalledMethodsSubclass { + + public static class Parent { + @RequiresCalledMethods(value = "#1", methods = "close") + public void method(Closeable x) throws IOException {} + + public void caller(Closeable x) throws IOException { + // ::error: (contracts.precondition.not.satisfied) + method(x); + } + } + + public static class Subclass extends Parent { + @Override + public void method(Closeable x) throws IOException {} + + public void caller(Closeable x) throws IOException { + method(x); // OK: we override method() with a weaker precondition + } + } +} diff --git a/checker/tests/calledmethods/RequiresCalledMethodsTest.java b/checker/tests/calledmethods/RequiresCalledMethodsTest.java new file mode 100644 index 000000000000..741194ae804c --- /dev/null +++ b/checker/tests/calledmethods/RequiresCalledMethodsTest.java @@ -0,0 +1,22 @@ +// A simple test that @RequiresCalledMethods works as expected. + +import org.checkerframework.checker.calledmethods.qual.RequiresCalledMethods; + +class RequiresCalledMethodsTest { + + Object foo; + + @RequiresCalledMethods(value = "this.foo", methods = "toString") + void afterFooToString() {} + + void test_ok() { + foo.toString(); + afterFooToString(); + } + + void test_bad() { + // foo.toString(); + // :: error: contracts.precondition.not.satisfied + afterFooToString(); + } +} diff --git a/checker/tests/calledmethods/SimpleFluentInference.java b/checker/tests/calledmethods/SimpleFluentInference.java new file mode 100644 index 000000000000..ae17946aecda --- /dev/null +++ b/checker/tests/calledmethods/SimpleFluentInference.java @@ -0,0 +1,63 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +/* Simple inference of a fluent builder */ +public class SimpleFluentInference { + SimpleFluentInference build(@CalledMethods({"a", "b"}) SimpleFluentInference this) { + return this; + } + + SimpleFluentInference weakbuild(@CalledMethods({"a"}) SimpleFluentInference this) { + return this; + } + + @This SimpleFluentInference a() { + return this; + } + + @This SimpleFluentInference b() { + return this; + } + + // intentionally does not have an @This annotation + SimpleFluentInference c() { + return new SimpleFluentInference(); + } + + static void doStuffCorrect() { + SimpleFluentInference s = new SimpleFluentInference().a().b().build(); + } + + static void doStuffWrong() { + SimpleFluentInference s = + new SimpleFluentInference() + .a() + // :: error: finalizer.invocation.invalid + .build(); + } + + static void doStuffRightWeak() { + SimpleFluentInference s = new SimpleFluentInference().a().weakbuild(); + } + + static void noReturnsReceiverAnno() { + SimpleFluentInference s = + new SimpleFluentInference() + .a() + .b() + .c() + // :: error: finalizer.invocation.invalid + .build(); + } + + static void fluentLoop() { + SimpleFluentInference s = new SimpleFluentInference().a(); + int i = 10; + while (i > 0) { + // :: error: finalizer.invocation.invalid + s.b().build(); + i--; + s = new SimpleFluentInference(); + } + } +} diff --git a/checker/tests/calledmethods/SimpleInference.java b/checker/tests/calledmethods/SimpleInference.java new file mode 100644 index 000000000000..0f86b96de5b1 --- /dev/null +++ b/checker/tests/calledmethods/SimpleInference.java @@ -0,0 +1,20 @@ +import org.checkerframework.checker.calledmethods.qual.*; + +/* The simplest inference test case Martin could think of */ +public class SimpleInference { + void build(@CalledMethods({"a"}) SimpleInference this) {} + + void a() {} + + static void doStuffCorrect() { + SimpleInference s = new SimpleInference(); + s.a(); + s.build(); + } + + static void doStuffWrong() { + SimpleInference s = new SimpleInference(); + // :: error: finalizer.invocation.invalid + s.build(); + } +} diff --git a/checker/tests/calledmethods/SimpleInferenceMerge.java b/checker/tests/calledmethods/SimpleInferenceMerge.java new file mode 100644 index 000000000000..601e34572f3b --- /dev/null +++ b/checker/tests/calledmethods/SimpleInferenceMerge.java @@ -0,0 +1,38 @@ +import org.checkerframework.checker.calledmethods.qual.*; + +/* The simplest inference test case Martin could think of */ +public class SimpleInferenceMerge { + void build(@CalledMethods({"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: finalizer.invocation.invalid + s.build(); + } +} diff --git a/checker/tests/calledmethods/Subtyping.java b/checker/tests/calledmethods/Subtyping.java new file mode 100644 index 000000000000..4d53e30f6376 --- /dev/null +++ b/checker/tests/calledmethods/Subtyping.java @@ -0,0 +1,74 @@ +import org.checkerframework.checker.calledmethods.qual.*; + +// basic subtyping checks +public class Subtyping { + @CalledMethods({}) Object top_top(@CalledMethods({}) Object o) { + return o; + } + + @CalledMethods({}) Object top_a(@CalledMethods({"a"}) Object o) { + return o; + } + + @CalledMethods({}) Object top_ab(@CalledMethods({"a", "b"}) Object o) { + return o; + } + + @CalledMethods({}) Object top_bot(@CalledMethodsBottom Object o) { + return o; + } + + @CalledMethods({"a"}) Object a_top(@CalledMethods({}) Object o) { + // :: error: return.type.incompatible + return o; + } + + @CalledMethods({"a"}) Object a_a(@CalledMethods({"a"}) Object o) { + return o; + } + + @CalledMethods({"a"}) Object a_ab(@CalledMethods({"a", "b"}) Object o) { + return o; + } + + @CalledMethods({"a"}) Object a_bot(@CalledMethodsBottom Object o) { + return o; + } + + @CalledMethods({"a", "b"}) Object ab_top(@CalledMethods({}) Object o) { + // :: error: return.type.incompatible + return o; + } + + @CalledMethods({"a", "b"}) Object ab_a(@CalledMethods({"a"}) Object o) { + // :: error: return.type.incompatible + return o; + } + + @CalledMethods({"a", "b"}) Object ab_ab(@CalledMethods({"a", "b"}) Object o) { + return o; + } + + @CalledMethods({"a", "b"}) Object ab_bot(@CalledMethodsBottom Object o) { + return o; + } + + @CalledMethodsBottom Object bot_top(@CalledMethods({}) Object o) { + // :: error: return.type.incompatible + return o; + } + + @CalledMethodsBottom Object bot_a(@CalledMethods({"a"}) Object o) { + // :: error: return.type.incompatible + return o; + } + + @CalledMethodsBottom Object bot_ab(@CalledMethods({"a", "b"}) Object o) { + // :: error: return.type.incompatible + return o; + } + + @CalledMethodsBottom Object bot_bot(@CalledMethodsBottom Object o) { + return o; + } +} diff --git a/checker/tests/calledmethods/UnparsablePredicate.java b/checker/tests/calledmethods/UnparsablePredicate.java new file mode 100644 index 000000000000..78bf61d2bfec --- /dev/null +++ b/checker/tests/calledmethods/UnparsablePredicate.java @@ -0,0 +1,50 @@ +import org.checkerframework.checker.calledmethods.qual.*; + +public class UnparsablePredicate { + + // :: error: predicate.invalid + void unclosedOpen(@CalledMethodsPredicate("(foo && bar") Object x) {} + + // :: error: predicate.invalid + void unopenedClose(@CalledMethodsPredicate("foo || bar)") Object x) {} + + // :: error: predicate.invalid + void badKeywords1(@CalledMethodsPredicate("foo OR bar") Object x) {} + + // :: error: predicate.invalid + void badKeywords2(@CalledMethodsPredicate("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(@CalledMethodsPredicate("String") Object x) {} + + void callJls0Example(@CalledMethods("String") Object y) { + jls0Example(y); + } + + void jls1Example(@CalledMethodsPredicate("i3") Object x) {} + + void callJls1Example(@CalledMethods("i3") Object y) { + jls1Example(y); + } + + // TODO: support Unicode. SPEL, which we use to parse expressions, doesn't. + /* void jls2Example(@CalledMethodsPredicate("αρετη") Object x) { } + void callJls2Example(@CalledMethods("αρετη") Object y) { + jls2Example(y); + }*/ + + void jls3Example(@CalledMethodsPredicate("MAX_VALUE") Object x) {} + + void callJls3Example(@CalledMethods("MAX_VALUE") Object y) { + jls3Example(y); + } + + void jls4Example(@CalledMethodsPredicate("isLetterOrDigit") Object x) {} + + void callJls4Example(@CalledMethods("isLetterOrDigit") Object y) { + jls4Example(y); + } +} diff --git a/checker/tests/calledmethods/Xor.java b/checker/tests/calledmethods/Xor.java new file mode 100644 index 000000000000..af65d3a12f58 --- /dev/null +++ b/checker/tests/calledmethods/Xor.java @@ -0,0 +1,66 @@ +import org.checkerframework.checker.calledmethods.qual.*; + +public class Xor { + + class Foo { + void a() {} + + void b() {} + + void c() {} + + // SPEL doesn't support XOR directly (it represents exponentiation as ^ instead), + // so use a standard gate encoding + void aXorB(@CalledMethodsPredicate("(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(@CalledMethods("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/checker/tests/command-line/issue618/Makefile b/checker/tests/command-line/issue618/Makefile index ff7f6c15bf1f..20613fed57ac 100644 --- a/checker/tests/command-line/issue618/Makefile +++ b/checker/tests/command-line/issue618/Makefile @@ -1,5 +1,5 @@ .PHONY: all all: - $(JAVAC) -processor regex,org.checkerframework.checker.tainting.TaintingChecker TwoCheckers.java > out.txt 2>&1 || true + -$(JAVAC) -processor regex,org.checkerframework.checker.tainting.TaintingChecker TwoCheckers.java > out.txt 2>&1 diff -u expected.txt out.txt diff --git a/checker/tests/command-line/issue618/TwoCheckers.java b/checker/tests/command-line/issue618/TwoCheckers.java index 323ab737553a..33e446d6e77d 100644 --- a/checker/tests/command-line/issue618/TwoCheckers.java +++ b/checker/tests/command-line/issue618/TwoCheckers.java @@ -1,22 +1,22 @@ // Expected error appears: -// $ch/bin-devel/javac -processor org.checkerframework.checker.tainting.TaintingChecker +// $ch/bin/javac -processor org.checkerframework.checker.tainting.TaintingChecker \ // TwoCheckers.java -// $ch/bin-devel/javac -processor org.checkerframework.checker.tainting.TaintingChecker,regex +// $ch/bin/javac -processor org.checkerframework.checker.tainting.TaintingChecker,regex \ // TwoCheckers.java // Expected error is suppressed: -// $ch/bin-devel/javac -processor regex,org.checkerframework.checker.tainting.TaintingChecker +// $ch/bin/javac -processor regex,org.checkerframework.checker.tainting.TaintingChecker \ // TwoCheckers.java // Compare these two executions: -// $ch/bin-devel/javac -processor org.checkerframework.checker.tainting.TaintingChecker,regex +// $ch/bin/javac -processor org.checkerframework.checker.tainting.TaintingChecker,regex \ // TwoCheckers.java -AprintAllQualifiers -Ashowchecks > out-good.txt -// $ch/bin-devel/javac -processor regex,org.checkerframework.checker.tainting.TaintingChecker +// $ch/bin/javac -processor regex,org.checkerframework.checker.tainting.TaintingChecker \ // TwoCheckers.java -AprintAllQualifiers -Ashowchecks > out-bad.txt // Turning off caches has no effect: -// $ch/bin-devel/javac -processor regex,org.checkerframework.checker.tainting.TaintingChecker -// TwoCheckers.java -AprintAllQualifiers -Ashowchecks -AatfDoNotCache AAatfDoNotReadCache > +// $ch/bin/javac -processor regex,org.checkerframework.checker.tainting.TaintingChecker \ +// TwoCheckers.java -AprintAllQualifiers -Ashowchecks -AatfDoNotCache AAatfDoNotReadCache > \ // out-bad-nocache.txt import org.checkerframework.checker.tainting.qual.Untainted; diff --git a/checker/tests/command-line/issue618/expected.txt b/checker/tests/command-line/issue618/expected.txt index 41e5f5b1bf9c..5e2af02daf62 100644 --- a/checker/tests/command-line/issue618/expected.txt +++ b/checker/tests/command-line/issue618/expected.txt @@ -1,4 +1,4 @@ -TwoCheckers.java:28: error: [argument.type.incompatible] incompatible types in argument. +TwoCheckers.java:28: error: [argument.type.incompatible] incompatible argument for parameter b of TwoCheckers.requiresUntainted. requiresUntainted(a); ^ found : @Tainted String diff --git a/checker/tests/compilermsg/Basic.java b/checker/tests/compilermsg/Basic.java index 99286d665c48..fb503bc5a8bc 100644 --- a/checker/tests/compilermsg/Basic.java +++ b/checker/tests/compilermsg/Basic.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; -class Basic { +public class Basic { void required(@CompilerMessageKey String in) {} diff --git a/checker/tests/compilermsg/Diamonds.java b/checker/tests/compilermsg/Diamonds.java index 5d4df02f76f0..836dd667d704 100644 --- a/checker/tests/compilermsg/Diamonds.java +++ b/checker/tests/compilermsg/Diamonds.java @@ -1,8 +1,9 @@ +import org.checkerframework.checker.compilermsgs.qual.UnknownCompilerMessageKey; + import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; -import org.checkerframework.checker.compilermsgs.qual.UnknownCompilerMessageKey; public class Diamonds { diff --git a/checker/tests/custom-alias/CustomAliasedAnnotations.java b/checker/tests/custom-alias/CustomAliasedAnnotations.java new file mode 100644 index 000000000000..6ef76da0faa2 --- /dev/null +++ b/checker/tests/custom-alias/CustomAliasedAnnotations.java @@ -0,0 +1,29 @@ +package custom.alias; + +public class CustomAliasedAnnotations { + + void useNonNullAnnotations() { + // :: error: (assignment.type.incompatible) + @org.checkerframework.checker.nullness.qual.NonNull Object nn1 = null; + // :: error: (assignment.type.incompatible) + @NonNull Object nn2 = null; + } + + void useNullableAnnotations1(@org.checkerframework.checker.nullness.qual.Nullable Object nble) { + // :: error: (dereference.of.nullable) + nble.toString(); + } + + void useNullableAnnotations2(@Nullable Object nble) { + // :: error: (dereference.of.nullable) + nble.toString(); + } + + @org.checkerframework.dataflow.qual.Pure + // :: warning: (purity.deterministic.void.method) + void setMutable1() {} + + @Pure + // :: warning: (purity.deterministic.void.method) + void setMutable2() {} +} diff --git a/checker/tests/custom-alias/NonNull.java b/checker/tests/custom-alias/NonNull.java new file mode 100644 index 000000000000..d2429d4e7bc1 --- /dev/null +++ b/checker/tests/custom-alias/NonNull.java @@ -0,0 +1,7 @@ +package custom.alias; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE_USE) +public @interface NonNull {} diff --git a/checker/tests/custom-alias/Nullable.java b/checker/tests/custom-alias/Nullable.java new file mode 100644 index 000000000000..9449f4ac9c7c --- /dev/null +++ b/checker/tests/custom-alias/Nullable.java @@ -0,0 +1,7 @@ +package custom.alias; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE_USE) +public @interface Nullable {} diff --git a/checker/tests/custom-alias/Pure.java b/checker/tests/custom-alias/Pure.java new file mode 100644 index 000000000000..e838b2497b53 --- /dev/null +++ b/checker/tests/custom-alias/Pure.java @@ -0,0 +1,3 @@ +package custom.alias; + +public @interface Pure {} diff --git a/checker/tests/disbaruse-records/DisbarredClass.java b/checker/tests/disbaruse-records/DisbarredClass.java new file mode 100644 index 000000000000..e24cb6430d31 --- /dev/null +++ b/checker/tests/disbaruse-records/DisbarredClass.java @@ -0,0 +1,41 @@ +import org.checkerframework.checker.testchecker.disbaruse.qual.DisbarUse; + +class DisbarredClass { + + @DisbarUse String barred; + String fine; + + DisbarredClass() {} + + @DisbarUse + DisbarredClass(String param) {} + + DisbarredClass(@DisbarUse Integer param) {} + + DisbarredClass(@DisbarUse Long param) { + // :: error: (disbar.use) + param = 7L; + // :: error: (disbar.use) + param.toString(); + } + + @DisbarUse + void disbarredMethod() {} + + void invalid() { + // :: error: (disbar.use) + disbarredMethod(); + // :: error: (disbar.use) + new DisbarredClass(""); + // :: error: (disbar.use) + barred = ""; + // :: error: (disbar.use) + int x = barred.length(); + } + + void valid() { + new DisbarredClass(); + fine = ""; + int x = fine.length(); + } +} diff --git a/checker/tests/disbaruse-records/DisbarredRecord.java b/checker/tests/disbaruse-records/DisbarredRecord.java new file mode 100644 index 000000000000..77a890ac6a09 --- /dev/null +++ b/checker/tests/disbaruse-records/DisbarredRecord.java @@ -0,0 +1,21 @@ +import org.checkerframework.checker.testchecker.disbaruse.qual.DisbarUse; + +record DisbarredRecord(@DisbarUse String barred, String fine) { + + DisbarredRecord { + // :: error: (disbar.use) + int x = barred.length(); + } + + void invalid() { + // :: error: (disbar.use) + barred(); + // :: error: (disbar.use) + int x = barred.length(); + } + + void valid() { + fine(); + int x = fine.length(); + } +} diff --git a/checker/tests/disbaruse-records/DisbarredRecordByStubs.java b/checker/tests/disbaruse-records/DisbarredRecordByStubs.java new file mode 100644 index 000000000000..e31f975f08d5 --- /dev/null +++ b/checker/tests/disbaruse-records/DisbarredRecordByStubs.java @@ -0,0 +1,19 @@ +record DisbarredRecordByStubs(String barred, String fine) { + + DisbarredRecordByStubs { + // :: error: (disbar.use) + int x = barred.length(); + } + + void invalid() { + // :: error: (disbar.use) + barred(); + // :: error: (disbar.use) + int x = barred.length(); + } + + void valid() { + fine(); + int x = fine.length(); + } +} diff --git a/checker/tests/disbaruse-records/DisbarredRecordByStubs2.java b/checker/tests/disbaruse-records/DisbarredRecordByStubs2.java new file mode 100644 index 000000000000..a3b0d9b698ed --- /dev/null +++ b/checker/tests/disbaruse-records/DisbarredRecordByStubs2.java @@ -0,0 +1,22 @@ +record DisbarredRecordByStubs2(String barred, String fine) { + + // Annotation isn't copied to explicitly declared constructor parameters: + DisbarredRecordByStubs2(String barred, String fine) { + int x = barred.length(); + // :: error: (disbar.use) + this.barred = ""; + this.fine = fine; + } + + void invalid() { + // :: error: (disbar.use) + barred(); + // :: error: (disbar.use) + int x = barred.length(); + } + + void valid() { + fine(); + int x = fine.length(); + } +} diff --git a/checker/tests/disbaruse-records/disbar.astub b/checker/tests/disbaruse-records/disbar.astub new file mode 100644 index 000000000000..ee6f712dbd46 --- /dev/null +++ b/checker/tests/disbaruse-records/disbar.astub @@ -0,0 +1,6 @@ +import org.checkerframework.checker.testchecker.disbaruse.qual.DisbarUse; + +record DisbarredRecordByStubs(@DisbarUse String barred, String fine) { +} +record DisbarredRecordByStubs2(@DisbarUse String barred, String fine) { +} diff --git a/checker/tests/fenum/CastsFenum.java b/checker/tests/fenum/CastsFenum.java index e0664990c324..a3be27f799ea 100644 --- a/checker/tests/fenum/CastsFenum.java +++ b/checker/tests/fenum/CastsFenum.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.fenum.qual.Fenum; -class CastsFenum { +public class CastsFenum { @Fenum("A") Object fa; void m(Object p, @Fenum("A") Object a) { diff --git a/checker/tests/fenum/CatchFenumUnqualified.java b/checker/tests/fenum/CatchFenumUnqualified.java index cd4c6780e673..cb54ce747c5e 100644 --- a/checker/tests/fenum/CatchFenumUnqualified.java +++ b/checker/tests/fenum/CatchFenumUnqualified.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.fenum.qual.Fenum; -class CatchFenumUnqualfied { +public class CatchFenumUnqualified { void method() { try { } catch ( diff --git a/checker/tests/fenum/TestSwitch.java b/checker/tests/fenum/TestSwitch.java index d9851ac29cbb..22fdd4de4959 100644 --- a/checker/tests/fenum/TestSwitch.java +++ b/checker/tests/fenum/TestSwitch.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.fenum.qual.Fenum; -class TestSwitch { +public class TestSwitch { void m() { @SuppressWarnings("fenum:assignment.type.incompatible") @Fenum("TEST") final int annotated = 3; diff --git a/checker/tests/fenum/UpperBoundsInByteCode.java b/checker/tests/fenum/UpperBoundsInByteCode.java index 814e34591eaf..680f61eb86b3 100644 --- a/checker/tests/fenum/UpperBoundsInByteCode.java +++ b/checker/tests/fenum/UpperBoundsInByteCode.java @@ -1,5 +1,7 @@ +// @above-java17-jdk-skip-test TODO: reinstate, false positives may be due to issue #979 + import org.checkerframework.checker.fenum.qual.Fenum; -import testlib.lib.UncheckedByteCode; +import org.checkerframework.framework.testchecker.lib.UncheckedByteCode; public class UpperBoundsInByteCode { UncheckedByteCode<@Fenum("Foo") String> foo; diff --git a/checker/tests/fenumswing/FlowBreak.java b/checker/tests/fenumswing/FlowBreak.java index 73d56d96ecde..20c51a69ca39 100644 --- a/checker/tests/fenumswing/FlowBreak.java +++ b/checker/tests/fenumswing/FlowBreak.java @@ -22,10 +22,9 @@ public class FlowBreak { break; } // We can only come here from the then-branch, the else-branch is dead. - // Therefore, we only take the annotations at the end of - // the then-branch and ignore the results of the else-branch. - // Therefore, o is @SwingHorizontalOrientation and the - // following is valid: + // Therefore, we only take the annotations at the end of the then-branch and ignore the + // results of the else-branch. + // Therefore, o is @SwingHorizontalOrientation and the following is valid: @SwingHorizontalOrientation Object pla = o; } // Here we have to merge three paths: diff --git a/checker/tests/fenumswing/IdentityArrayList.java b/checker/tests/fenumswing/IdentityArrayList.java deleted file mode 100644 index 4212f8c37774..000000000000 --- a/checker/tests/fenumswing/IdentityArrayList.java +++ /dev/null @@ -1,36 +0,0 @@ -import java.util.Arrays; -import org.checkerframework.checker.fenum.qual.FenumTop; - -/* - * This test case violates an assertion in the compiler. - * It does not depend on the Fenum Checker, it breaks for any checker. - */ -public class IdentityArrayList { - // The type of the third argument to Arrays.copyOf should be: - // Class - // But the annotated JDK does not have annotations for the Fenum Checker. - @SuppressWarnings("argument.type.incompatible") - public T[] toArray(T[] a) { - // Warnings only with -Alint=cast:strict. - // TODO:: warning: (cast.unsafe) - // :: warning: [unchecked] unchecked cast - return (T[]) Arrays.copyOf(null, 0, a.getClass()); - } - - public T[] toArray2(T[] a) { - wc(null, 0, new java.util.LinkedList()); - // TODO:: warning: (cast.unsafe) - // :: warning: [unchecked] unchecked cast - return (T[]) myCopyOf(null, 0, a.getClass()); - } - - public static T[] myCopyOf( - U[] original, int newLength, Class newType) { - return null; - } - - public static T[] wc( - U[] original, int newLength, java.util.List arr) { - return null; - } -} diff --git a/checker/tests/fenumswing/IdentityArrayListTest.java b/checker/tests/fenumswing/IdentityArrayListTest.java new file mode 100644 index 000000000000..8b60b7c6c1b4 --- /dev/null +++ b/checker/tests/fenumswing/IdentityArrayListTest.java @@ -0,0 +1,37 @@ +import org.checkerframework.checker.fenum.qual.FenumTop; + +import java.util.Arrays; + +/* + * This test case violates an assertion in the compiler. + * It does not depend on the Fenum Checker, it breaks for any checker. + */ +public class IdentityArrayListTest { + // The type of the third argument to Arrays.copyOf should be: + // Class + // But the annotated JDK does not have annotations for the Fenum Checker. + @SuppressWarnings("argument.type.incompatible") + public T[] toArray(T[] a) { + // Warnings only with -Alint=cast:strict. + // TODO:: warning: (cast.unsafe) + // :: warning: [unchecked] unchecked cast + return (T[]) Arrays.copyOf(null, 0, a.getClass()); + } + + public T[] toArray2(T[] a) { + wc(null, 0, new java.util.LinkedList()); + // TODO:: warning: (cast.unsafe) + // :: warning: [unchecked] unchecked cast + return (T[]) myCopyOf(null, 0, a.getClass()); + } + + public static T[] myCopyOf( + U[] original, int newLength, Class newType) { + return null; + } + + public static T[] wc( + U[] original, int newLength, java.util.List arr) { + return null; + } +} diff --git a/checker/tests/formatter-lubglb/Placeholder.java b/checker/tests/formatter-lubglb/Placeholder.java index 7cb44dbe3493..5ee2ef079a60 100644 --- a/checker/tests/formatter-lubglb/Placeholder.java +++ b/checker/tests/formatter-lubglb/Placeholder.java @@ -1,3 +1,3 @@ // We need a file to start the checker. -class Placeholder {} +public class Placeholder {} diff --git a/checker/tests/formatter-unchecked-defaults/TestUncheckedByteCode.java b/checker/tests/formatter-unchecked-defaults/TestUncheckedByteCode.java index dd408887e1be..705681b6dee8 100644 --- a/checker/tests/formatter-unchecked-defaults/TestUncheckedByteCode.java +++ b/checker/tests/formatter-unchecked-defaults/TestUncheckedByteCode.java @@ -1,4 +1,4 @@ -import testlib.lib.UncheckedByteCode; +import org.checkerframework.framework.testchecker.lib.UncheckedByteCode; public class TestUncheckedByteCode { Object field; @@ -7,10 +7,12 @@ void test(UncheckedByteCode param, Integer i) { field = param.getCT(); field = param.getInt(1); field = param.getInteger(i); + field = param.identity("hello"); + + // String and Object are relevant types and must be annotated in bytecode + // :: error: (argument.type.incompatible) field = param.getObject(new Object()); - // Strings are relevant and must be annotated in bytecode - // ::error: (argument.type.incompatible) + // :: error: (argument.type.incompatible) field = param.getString("hello"); - field = param.identity("hello"); } } diff --git a/checker/tests/formatter/ConversionBasic.java b/checker/tests/formatter/ConversionBasic.java index cf0d9d96398c..d1b4413052f5 100644 --- a/checker/tests/formatter/ConversionBasic.java +++ b/checker/tests/formatter/ConversionBasic.java @@ -1,11 +1,12 @@ +import org.checkerframework.checker.formatter.qual.ConversionCategory; +import org.checkerframework.checker.formatter.qual.Format; + import java.math.BigDecimal; import java.math.BigInteger; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.Formatter; -import org.checkerframework.checker.formatter.qual.ConversionCategory; -import org.checkerframework.checker.formatter.qual.Format; public class ConversionBasic { public static void main(String... p) { diff --git a/checker/tests/formatter/ConversionNull.java b/checker/tests/formatter/ConversionNull.java index bc5ccf4c49df..8ac77b13ddee 100644 --- a/checker/tests/formatter/ConversionNull.java +++ b/checker/tests/formatter/ConversionNull.java @@ -8,28 +8,20 @@ public static void main(String... p) { f.format("%d %s", 0, null); f.format("%s", (Object) null); - // :: error: (argument.type.incompatible) f.format("%d %c", 0, null); f.format("%c", (Character) null); - // :: error: (argument.type.incompatible) f.format("%c", (Object) null); - // :: error: (argument.type.incompatible) f.format("%d %d", 0, null); f.format("%d", (Integer) null); - // :: error: (argument.type.incompatible) f.format("%d", (Object) null); - // :: error: (argument.type.incompatible) f.format("%d %f", 0, null); f.format("%f", (Float) null); - // :: error: (argument.type.incompatible) f.format("%f", (Object) null); - // :: error: (argument.type.incompatible) f.format("%d %tD", 0, null); f.format("%tD", (Date) null); - // :: error: (argument.type.incompatible) f.format("%tD", (Object) null); System.out.println(f.toString()); diff --git a/checker/tests/formatter/ConversionNull2.java b/checker/tests/formatter/ConversionNull2.java index 064725bc8f60..a546e0b4f6e7 100644 --- a/checker/tests/formatter/ConversionNull2.java +++ b/checker/tests/formatter/ConversionNull2.java @@ -1,11 +1,10 @@ -import java.util.Formatter; import org.checkerframework.checker.formatter.qual.FormatMethod; +import java.util.Formatter; + public class ConversionNull2 { void foo(Formatter f1, MyFormatter f2) { - // :: error: (argument.type.incompatible) f1.format("%d %c", 0, null); - // :: error: (argument.type.incompatible) f2.format("%d %c", 0, null); } } diff --git a/checker/tests/formatter/FlowFormatter.java b/checker/tests/formatter/FlowFormatter.java index ba28821b72f0..a0350f625a4c 100644 --- a/checker/tests/formatter/FlowFormatter.java +++ b/checker/tests/formatter/FlowFormatter.java @@ -1,10 +1,11 @@ -import java.util.Date; -import java.util.Formatter; -import org.checkerframework.checker.formatter.FormatUtil; import org.checkerframework.checker.formatter.qual.ConversionCategory; import org.checkerframework.checker.formatter.qual.Format; +import org.checkerframework.checker.formatter.util.FormatUtil; import org.junit.Assert; +import java.util.Date; +import java.util.Formatter; + public class FlowFormatter { public static String callUnqual(String u) { return u; diff --git a/checker/tests/formatter/FormatIndexing.java b/checker/tests/formatter/FormatIndexing.java index 048a9f4d7890..387d74daea15 100644 --- a/checker/tests/formatter/FormatIndexing.java +++ b/checker/tests/formatter/FormatIndexing.java @@ -1,8 +1,9 @@ -import java.util.Date; -import java.util.Formatter; import org.checkerframework.checker.formatter.qual.ConversionCategory; import org.checkerframework.checker.formatter.qual.Format; +import java.util.Date; +import java.util.Formatter; + public class FormatIndexing { public static void main(String... p) { Formatter f = new Formatter(); @@ -43,13 +44,13 @@ public static void main(String... p) { @Format({ConversionCategory.NULL, ConversionCategory.INT}) String tf = "%TT % { + + MyClass returnType; + + void method(MyClass result) { + result.returnType = Interface.method(this.returnType); + } + } + + interface Interface { + static > T2 method(T2 object) { + throw new RuntimeException(); + } + } +} diff --git a/checker/tests/formatter/VarargsFormatter.java b/checker/tests/formatter/VarargsFormatter.java index 77162904eb9e..cd612a25b215 100644 --- a/checker/tests/formatter/VarargsFormatter.java +++ b/checker/tests/formatter/VarargsFormatter.java @@ -7,16 +7,13 @@ public static void main(String... p) { // vararg as parameter // :: warning: non-varargs call of varargs method with inexact argument type for last - // parameter; :: warning: (format.indirect.arguments) + // parameter; f.format("Nothing", null); // equivalent to (Object[])null - // :: warning: (format.indirect.arguments) f.format("Nothing", (Object[]) null); - // :: warning: (format.indirect.arguments) f.format("%s", (Object[]) null); - // :: warning: (format.indirect.arguments) f.format("%s %d %x", (Object[]) null); // :: warning: non-varargs call of varargs method with inexact argument type for last - // parameter; :: warning: (format.indirect.arguments) + // parameter; f.format("%s %d %x", null); // equivalent to (Object[])null // :: warning: (format.indirect.arguments) f.format("%d", new Object[1]); @@ -35,6 +32,8 @@ public static void main(String... p) { f.format("%d %s %s", 132, new Object[2]); // :: error: (argument.type.incompatible) f.format("%d %d", new Object[2], 123); + // "error: (format.specifier.null)" could be a warning rather than an error, but that would + // require reasoning about the values in an array construction expression. // :: error: (format.specifier.null) :: warning: (format.indirect.arguments) f.format("%d % ooo; - // :: error: (type.argument.type.incompatible) :: error: (type.invalid.annotations.on.use) + // :: error: (type.invalid.annotations.on.use) List iii; class Inner {} boolean flag = true; + // Type var test void throwTypeVarUI1(E ex1, @UI E ex2) throws PolyUIException { if (flag) { @@ -57,14 +56,10 @@ class Inner {} // Wildcards void throwWildcard( - // :: error: (type.argument.type.incompatible) - List - ui, // Default type of List's type parameter is below @UI so this is - // type.argument.incompatible + List ui, List alwaysSafe) throws PolyUIException { if (flag) { - // :: error: (throw.type.invalid) throw ui.get(0); } throw alwaysSafe.get(0); @@ -96,8 +91,7 @@ void throwDeclared() { // Test Exception parameters void unionTypes() { try { - } catch ( - @AlwaysSafe NullPointerPolyUIException + } catch (@AlwaysSafe NullPointerPolyUIException | @AlwaysSafe ArrayStorePolyUIException unionParam) { } diff --git a/checker/tests/guieffect/TransitiveInheritance.java b/checker/tests/guieffect/TransitiveInheritance.java new file mode 100644 index 000000000000..ffe217505e08 --- /dev/null +++ b/checker/tests/guieffect/TransitiveInheritance.java @@ -0,0 +1,58 @@ +import org.checkerframework.checker.guieffect.qual.UIEffect; + +public class TransitiveInheritance { + + public static class TopLevel { + // Implicitly safe + public void foo() {} + } + + public static interface ITop { + public void bar(); + } + + public static interface IIndirect { + public void baz(); + } + + // Mid-level class and interface that do not redeclare or override the default safe methods + public abstract static class MidLevel extends TopLevel implements IIndirect {} + + public static interface IMid extends ITop {} + + // Issue #3287 is that if foo or bar is overridden with a @UIEffect implementation here, the + // "skip" in declarations causes the override error to not be issued + // We check both classes and interfaces; the reported issue is related only to methods whose + // nearest explicit definition lives in an interface + public static class Base extends MidLevel implements IMid { + + // Should catch when the override is for a method originating in a class two levels up (here + // TopLevel) + @Override + @UIEffect + // :: error: (override.effect.invalid) + public void foo() {} + + // Should catch when the override is for a method originating in an interface two levels up + // (here ITop) + @Override + @UIEffect + // :: error: (override.effect.invalid) + public void bar() {} + + // Should catch when the override is for a method originating in an interface two levels up, + // but which is implemented via class inheritance (here IIndirect, which is implemented by + // MidLevel). + @Override + @UIEffect + // :: error: (override.effect.invalid) + public void baz() {} + } + + public static interface IBase extends IMid { + @Override + @UIEffect + // :: error: (override.effect.invalid) + public void bar(); + } +} diff --git a/checker/tests/i18n-formatter-lubglb/Placeholder.java b/checker/tests/i18n-formatter-lubglb/Placeholder.java index 7cb44dbe3493..5ee2ef079a60 100644 --- a/checker/tests/i18n-formatter-lubglb/Placeholder.java +++ b/checker/tests/i18n-formatter-lubglb/Placeholder.java @@ -1,3 +1,3 @@ // We need a file to start the checker. -class Placeholder {} +public class Placeholder {} diff --git a/checker/tests/i18n-formatter-unchecked-defaults/TestUncheckedByteCode.java b/checker/tests/i18n-formatter-unchecked-defaults/TestUncheckedByteCode.java index dd408887e1be..705681b6dee8 100644 --- a/checker/tests/i18n-formatter-unchecked-defaults/TestUncheckedByteCode.java +++ b/checker/tests/i18n-formatter-unchecked-defaults/TestUncheckedByteCode.java @@ -1,4 +1,4 @@ -import testlib.lib.UncheckedByteCode; +import org.checkerframework.framework.testchecker.lib.UncheckedByteCode; public class TestUncheckedByteCode { Object field; @@ -7,10 +7,12 @@ void test(UncheckedByteCode param, Integer i) { field = param.getCT(); field = param.getInt(1); field = param.getInteger(i); + field = param.identity("hello"); + + // String and Object are relevant types and must be annotated in bytecode + // :: error: (argument.type.incompatible) field = param.getObject(new Object()); - // Strings are relevant and must be annotated in bytecode - // ::error: (argument.type.incompatible) + // :: error: (argument.type.incompatible) field = param.getString("hello"); - field = param.identity("hello"); } } diff --git a/checker/tests/i18n-formatter/HasFormat.java b/checker/tests/i18n-formatter/HasFormat.java index 0404462ccf36..fce01829a38f 100644 --- a/checker/tests/i18n-formatter/HasFormat.java +++ b/checker/tests/i18n-formatter/HasFormat.java @@ -1,7 +1,8 @@ +import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory; +import org.checkerframework.checker.i18nformatter.util.I18nFormatUtil; + import java.text.MessageFormat; import java.util.Date; -import org.checkerframework.checker.i18nformatter.I18nFormatUtil; -import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory; public class HasFormat { diff --git a/checker/tests/i18n-formatter/I18nFormatForTest.java b/checker/tests/i18n-formatter/I18nFormatForTest.java index 56472a866d7c..011c37fdbc61 100644 --- a/checker/tests/i18n-formatter/I18nFormatForTest.java +++ b/checker/tests/i18n-formatter/I18nFormatForTest.java @@ -1,6 +1,7 @@ +import org.checkerframework.checker.i18nformatter.qual.I18nFormatFor; + import java.text.MessageFormat; import java.util.Date; -import org.checkerframework.checker.i18nformatter.qual.I18nFormatFor; public class I18nFormatForTest { @@ -75,6 +76,7 @@ static void c(@I18nFormatFor("#-1") String f, Object... args) { } // @I18nFormatFor needs to be annotated to a string. + // :: error: (anno.on.irrelevant) static void e(@I18nFormatFor("#2") int f, Object... args) {} // The parameter type is not necessary to an array of objects diff --git a/checker/tests/i18n-formatter/IsFormat.java b/checker/tests/i18n-formatter/IsFormat.java index 50aefe47bb63..641862e75ace 100644 --- a/checker/tests/i18n-formatter/IsFormat.java +++ b/checker/tests/i18n-formatter/IsFormat.java @@ -1,6 +1,7 @@ -import java.text.MessageFormat; -import org.checkerframework.checker.i18nformatter.I18nFormatUtil; import org.checkerframework.checker.i18nformatter.qual.I18nConversionCategory; +import org.checkerframework.checker.i18nformatter.util.I18nFormatUtil; + +import java.text.MessageFormat; public class IsFormat { public static void test1(String cc) { diff --git a/checker/tests/i18n-formatter/Syntax.java b/checker/tests/i18n-formatter/Syntax.java index ff6743b7b7e0..3e851eab0323 100644 --- a/checker/tests/i18n-formatter/Syntax.java +++ b/checker/tests/i18n-formatter/Syntax.java @@ -3,8 +3,7 @@ public class Syntax { - // Test 2.1.1: Missing '}' at end of message format (Unmatched braces in the - // pattern) + // Test 2.1.1: Missing '}' at end of message format (Unmatched braces in the pattern) public static void unmatchedBraces() { // :: error: (i18nformat.string.invalid) MessageFormat.format("{0, number", new Date(12)); diff --git a/checker/tests/i18n-unchecked-defaults/TestUncheckedByteCode.java b/checker/tests/i18n-unchecked-defaults/TestUncheckedByteCode.java index dd408887e1be..705681b6dee8 100644 --- a/checker/tests/i18n-unchecked-defaults/TestUncheckedByteCode.java +++ b/checker/tests/i18n-unchecked-defaults/TestUncheckedByteCode.java @@ -1,4 +1,4 @@ -import testlib.lib.UncheckedByteCode; +import org.checkerframework.framework.testchecker.lib.UncheckedByteCode; public class TestUncheckedByteCode { Object field; @@ -7,10 +7,12 @@ void test(UncheckedByteCode param, Integer i) { field = param.getCT(); field = param.getInt(1); field = param.getInteger(i); + field = param.identity("hello"); + + // String and Object are relevant types and must be annotated in bytecode + // :: error: (argument.type.incompatible) field = param.getObject(new Object()); - // Strings are relevant and must be annotated in bytecode - // ::error: (argument.type.incompatible) + // :: error: (argument.type.incompatible) field = param.getString("hello"); - field = param.identity("hello"); } } diff --git a/checker/tests/i18n/Issue2163FinalI18n.java b/checker/tests/i18n/Issue2163FinalI18n.java deleted file mode 100644 index 238d056dad70..000000000000 --- a/checker/tests/i18n/Issue2163FinalI18n.java +++ /dev/null @@ -1,65 +0,0 @@ -import org.checkerframework.checker.i18n.qual.*; - -// @C <: @B <: @A - -// Testing Rule 1 (constructor declaration type <: class type) -@UnknownLocalizableKey class Issue2163FinalAA { - @UnknownLocalizableKey Issue2163FinalAA() {} -} - -@UnknownLocalizableKey class Issue2163FinalAB { - // :: error: (super.invocation.invalid) :: warning: (inconsistent.constructor.type) - @LocalizableKey Issue2163FinalAB() {} -} - -@UnknownLocalizableKey class Issue2163FinalAC { - // :: error: (super.invocation.invalid) :: warning: (inconsistent.constructor.type) - @LocalizableKeyBottom Issue2163FinalAC() {} -} - -@LocalizableKey class Issue2163FinalBA { - // :: error: (type.invalid.annotations.on.use) - @UnknownLocalizableKey Issue2163FinalBA() {} -} - -@LocalizableKey class Issue2163FinalBB { - // :: error: (super.invocation.invalid) :: warning: (inconsistent.constructor.type) - @LocalizableKey Issue2163FinalBB() {} -} - -@LocalizableKey class Issue2163FinalBC { - // :: error: (super.invocation.invalid) :: warning: (inconsistent.constructor.type) - @LocalizableKeyBottom Issue2163FinalBC() {} -} - -@LocalizableKeyBottom class Issue2163FinalCA { - // :: error: (type.invalid.annotations.on.use) - @UnknownLocalizableKey Issue2163FinalCA() {} -} - -@LocalizableKeyBottom class Issue2163FinalCB { - // :: error: (type.invalid.annotations.on.use) :: warning: (inconsistent.constructor.type) :: - // error: (super.invocation.invalid) - @LocalizableKey Issue2163FinalCB() {} -} - -@LocalizableKeyBottom class Issue2163FinalCC { - // :: error: (super.invocation.invalid) :: warning: (inconsistent.constructor.type) - @LocalizableKeyBottom Issue2163FinalCC() {} -} - -// Testing Rule 2 (Issue type cast warning if constuctor declaration type <: invocation type) -class Issue2163FinalAAClient { - void test() { - new @UnknownLocalizableKey Issue2163FinalAA(); - // :: warning: (cast.unsafe.constructor.invocation) - new @LocalizableKey Issue2163FinalAA(); - // :: warning: (cast.unsafe.constructor.invocation) - new @LocalizableKeyBottom Issue2163FinalAA(); - } -} - -// Testing Default -class Issue2163FinalBCClient { - @LocalizableKeyBottom Issue2163FinalBC obj = new Issue2163FinalBC(); -} diff --git a/checker/tests/i18n/Issue2186.java b/checker/tests/i18n/Issue2186.java deleted file mode 100644 index 24052339da38..000000000000 --- a/checker/tests/i18n/Issue2186.java +++ /dev/null @@ -1,24 +0,0 @@ -// Test case for issue #2186 -// https://github.com/typetools/checker-framework/issues/2186 - -import java.util.ArrayList; -import org.checkerframework.checker.i18n.qual.*; - -@LocalizableKey class Issue2186 { - // :: error: (super.invocation.invalid) :: warning: (inconsistent.constructor.type) - Issue2186() {} - - // :: error: (super.invocation.invalid) :: warning: (inconsistent.constructor.type) - @LocalizableKeyBottom Issue2186(int x) {} - - void test() { - @LocalizableKey Issue2186 obj = new Issue2186(); - @LocalizableKeyBottom Issue2186 obj1 = new Issue2186(9); - } - - void testDiamond() { - @LocalizableKeyBottom ArrayList<@LocalizableKeyBottom String> list = - // :: warning: (cast.unsafe.constructor.invocation) - new @LocalizableKeyBottom ArrayList<@LocalizableKeyBottom String>(); - } -} diff --git a/checker/tests/i18n/Issue2264.java b/checker/tests/i18n/Issue2264.java deleted file mode 100644 index ae8a6ec92eca..000000000000 --- a/checker/tests/i18n/Issue2264.java +++ /dev/null @@ -1,27 +0,0 @@ -// Test case for issue 2264 -// https://github.com/typetools/checker-framework/issues/2264 - -import org.checkerframework.checker.i18n.qual.LocalizableKey; -import org.checkerframework.checker.i18n.qual.UnknownLocalizableKey; - -public class Issue2264 extends SuperClass { - // :: warning: (inconsistent.constructor.type) - @LocalizableKey Issue2264() { - // :: error: (super.invocation.invalid) - super(9); - } -} - -class ImplicitSuperCall { - // :: error: (super.invocation.invalid) :: warning: (inconsistent.constructor.type) - @LocalizableKey ImplicitSuperCall() {} -} - -class SuperClass { - @UnknownLocalizableKey SuperClass(int x) {} -} - -@LocalizableKey class TestClass { - // :: error: (type.invalid.annotations.on.use) - @UnknownLocalizableKey TestClass() {} -} diff --git a/checker/tests/i18n/LocalizedMessage.java b/checker/tests/i18n/LocalizedMessage.java index c43186a24f42..bac2f4e14688 100644 --- a/checker/tests/i18n/LocalizedMessage.java +++ b/checker/tests/i18n/LocalizedMessage.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.i18n.qual.Localized; -class LocalizedMessage { +public class LocalizedMessage { @Localized String localize(String s) { throw new RuntimeException(); } diff --git a/checker/tests/index-initializedfields/RequireJavadoc.java b/checker/tests/index-initializedfields/RequireJavadoc.java new file mode 100644 index 000000000000..8c67a2a7c730 --- /dev/null +++ b/checker/tests/index-initializedfields/RequireJavadoc.java @@ -0,0 +1,980 @@ +import static com.github.javaparser.utils.PositionUtils.sortByBeginPosition; + +import com.github.javaparser.ParseProblemException; +import com.github.javaparser.Position; +import com.github.javaparser.Range; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.AnnotationDeclaration; +import com.github.javaparser.ast.body.AnnotationMemberDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.EnumConstantDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.comments.Comment; +import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.ThisExpr; +import com.github.javaparser.ast.expr.UnaryExpr; +import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.stmt.ExpressionStmt; +import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.stmt.Statement; +import com.github.javaparser.ast.type.PrimitiveType; +import com.github.javaparser.ast.type.Type; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; + +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.plumelib.options.Option; +import org.plumelib.options.Options; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * A program that issues an error for any class, constructor, method, or field that lacks a Javadoc + * comment. Does not issue a warning for methods annotated with {@code @Override}. See documentation + * at https://github.com/plume-lib/require-javadoc#readme. + */ +public class RequireJavadoc { + + /** Matches name of file or directory where no problems should be reported. */ + @Option("Don't check files or directories whose pathname matches the regex") + public @MonotonicNonNull Pattern exclude = null; + + // TODO: It would be nice to support matching fully-qualified class names, but matching + // packages will have to do for now. + /** + * Matches simple name of class/constructor/method/field, or full package name, where no + * problems should be reported. + */ + @Option("Don't report problems in Java elements whose name matches the regex") + public @MonotonicNonNull Pattern dont_require = null; + + /** If true, don't check elements with private access. */ + @Option("Don't report problems in elements with private access") + public boolean dont_require_private; + + /** + * If true, don't check constructors with zero formal parameters. These are sometimes called + * "default constructors", though that term means a no-argument constructor that the compiler + * synthesized when the programmer didn't write one. + */ + @Option("Don't report problems in constructors with zero formal parameters") + public boolean dont_require_noarg_constructor; + + /** + * If true, don't check trivial getters and setters. + * + *

    Trivial getters and setters are of the form: + * + *

    {@code
    +     * SomeType getFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * SomeType foo() {
    +     *   return foo;
    +     * }
    +     *
    +     * void setFoo(SomeType foo) {
    +     *   this.foo = foo;
    +     * }
    +     *
    +     * boolean hasFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * boolean isFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * boolean notFoo() {
    +     *   return !foo;
    +     * }
    +     * }
    + */ + @Option("Don't report problems in trivial getters and setters") + public boolean dont_require_trivial_properties; + + /** If true, don't check type declarations: classes, interfaces, enums, annotations. */ + @Option("Don't report problems in type declarations") + public boolean dont_require_type; + + /** If true, don't check fields. */ + @Option("Don't report problems in fields") + public boolean dont_require_field; + + /** If true, don't check methods, constructors, and annotation members. */ + @Option("Don't report problems in methods and constructors") + public boolean dont_require_method; + + /** If true, warn if any package lacks a package-info.java file. */ + @Option("Require package-info.java file to exist") + public boolean require_package_info; + + /** + * If true, print filenames relative to working directory. Setting this only has an effect if + * the command-line arguments were absolute pathnames, or no command-line arguments were + * supplied. + */ + @Option("Report relative rather than absolute filenames") + public boolean relative = false; + + /** If true, output debug information. */ + @Option("Print diagnostic information") + public boolean verbose = false; + + /** All the errors this program will report. */ + private List errors = new ArrayList<>(); + + /** The Java files to be checked. */ + private List javaFiles = new ArrayList(); + + /** The current working directory, for making relative pathnames. */ + private Path workingDirRelative = Paths.get(""); + + /** The current working directory, for making relative pathnames. */ + private Path workingDirAbsolute = Paths.get("").toAbsolutePath(); + + /** + * The main entry point for the require-javadoc program. See documentation at https://github.com/plume-lib/require-javadoc#readme. + * + * @param args the command-line arguments; see the README file + */ + public static void main(String[] args) { + RequireJavadoc rj = new RequireJavadoc(); + Options options = + new Options( + "java org.plumelib.javadoc.RequireJavadoc [options] [directory-or-file ...]", + rj); + String[] remainingArgs = options.parse(true, args); + + rj.setJavaFiles(remainingArgs); + + for (Path javaFile : rj.javaFiles) { + if (rj.verbose) { + System.out.println("Checking " + javaFile); + } + try { + CompilationUnit cu = StaticJavaParser.parse(javaFile); + RequireJavadocVisitor visitor = rj.new RequireJavadocVisitor(javaFile); + visitor.visit(cu, null); + } catch (IOException e) { + System.out.println("Problem while reading " + javaFile + ": " + e.getMessage()); + System.exit(2); + } catch (ParseProblemException e) { + System.out.println("Problem while parsing " + javaFile + ": " + e.getMessage()); + System.exit(2); + } + } + for (String error : rj.errors) { + System.out.println(error); + } + System.exit(rj.errors.isEmpty() ? 0 : 1); + } + + /** Creates a new RequireJavadoc instance. */ + private RequireJavadoc() {} + + /** + * Set the Java files to be processed from the command-line arguments. + * + * @param args the directories and files listed on the command line + */ + @SuppressWarnings("lock:methodref.receiver") // no locking here + private void setJavaFiles(String[] args) { + if (args.length == 0) { + args = new String[] {workingDirAbsolute.toString()}; + } + + FileVisitor walker = new JavaFilesVisitor(); + + for (String arg : args) { + if (shouldExclude(arg)) { + continue; + } + Path p = Paths.get(arg); + File f = p.toFile(); + if (!f.exists()) { + System.out.println("File not found: " + f); + System.exit(2); + } + if (f.isDirectory()) { + try { + Files.walkFileTree(p, walker); + } catch (IOException e) { + System.out.println("Problem while reading " + f + ": " + e.getMessage()); + System.exit(2); + } + } else { + javaFiles.add(Paths.get(arg)); + } + } + + javaFiles.sort(Comparator.comparing(Object::toString)); + + Set missingPackageInfoFiles = new LinkedHashSet<>(); + if (require_package_info) { + for (Path javaFile : javaFiles) { + @SuppressWarnings( + "nullness:assignment") // the file is not "/", so getParent() is non-null + @NonNull Path javaFileParent = javaFile.getParent(); + // Java 11 has Path.of() instead of creating a new File. + Path packageInfo = javaFileParent.resolve(new File("package-info.java").toPath()); + if (!javaFiles.contains(packageInfo)) { + missingPackageInfoFiles.add(packageInfo); + } + } + for (Path packageInfo : missingPackageInfoFiles) { + errors.add("missing package documentation: no file " + packageInfo); + } + } + } + + /** Collects files into the {@link #javaFiles} variable. */ + private class JavaFilesVisitor extends SimpleFileVisitor { + + /** Create a new JavaFilesVisitor. */ + public JavaFilesVisitor() {} + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attr) { + if (attr.isRegularFile() && file.toString().endsWith(".java")) { + if (!shouldExclude(file)) { + javaFiles.add(file); + } + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attr) { + if (shouldExclude(dir)) { + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) { + if (exc != null) { + System.out.println("Problem visiting " + dir + ": " + exc.getMessage()); + System.exit(2); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) { + if (exc != null) { + System.out.println("Problem visiting " + file + ": " + exc.getMessage()); + System.exit(2); + } + return FileVisitResult.CONTINUE; + } + } + + /** + * Return true if the given Java element should not be checked, based on the {@code + * --dont-require} command-line argument. + * + * @param name the name of a Java element. It is a simple name, except for packages. + * @return true if no warnings should be issued about the element + */ + private boolean shouldNotRequire(String name) { + if (dont_require == null) { + return false; + } + boolean result = dont_require.matcher(name).find(); + if (verbose) { + System.out.printf("shouldNotRequire(%s) => %s%n", name, result); + } + return result; + } + + /** + * Return true if the given file or directory should be skipped, based on the {@code --exclude} + * command-line argument. + * + * @param fileName the name of a Java file or directory + * @return true if the file or directory should be skipped + */ + private boolean shouldExclude(String fileName) { + if (exclude == null) { + return false; + } + boolean result = exclude.matcher(fileName).find(); + if (verbose) { + System.out.printf("shouldExclude(%s) => %s%n", fileName, result); + } + return result; + } + + /** + * Return true if the given file or directory should be skipped, based on the {@code --exclude} + * command-line argument. + * + * @param path a Java file or directory + * @return true if the file or directory should be skipped + */ + private boolean shouldExclude(Path path) { + return shouldExclude(path.toString()); + } + + /** A property method's return type. */ + private enum ReturnType { + /** The return type is void. */ + VOID, + /** The return type is boolean. */ + BOOLEAN, + /** The return type is non-void. */ + NON_VOID; + } + + /** The type of property method: a getter or setter. */ + private enum PropertyKind { + /** A method of the form {@code SomeType getFoo()}. */ + GETTER("get", 0, ReturnType.NON_VOID), + /** A method of the form {@code SomeType foo()}. */ + GETTER_NO_PREFIX("", 0, ReturnType.NON_VOID), + /** A method of the form {@code boolean hasFoo()}. */ + GETTER_HAS("has", 0, ReturnType.BOOLEAN), + /** A method of the form {@code boolean isFoo()}. */ + GETTER_IS("is", 0, ReturnType.BOOLEAN), + /** A method of the form {@code boolean notFoo()}. */ + GETTER_NOT("not", 0, ReturnType.BOOLEAN), + /** A method of the form {@code void setFoo(SomeType arg)}. */ + SETTER("set", 1, ReturnType.VOID), + /** Not a getter or setter. */ + NOT_PROPERTY("", -1, ReturnType.VOID); + + /** The prefix for the method name: "get", "", "has", "is", "not", or "set". */ + final String prefix; + + /** The number of required formal parameters: 0 or 1. */ + final int requiredParams; + + /** The return type. */ + final ReturnType returnType; + + /** + * Create a new PropertyKind. + * + * @param prefix the prefix for the method name: "get", "has", "is", "not", or "set" + * @param requiredParams the number of required formal parameters: 0 or 1 + * @param returnType the return type + */ + PropertyKind(String prefix, int requiredParams, ReturnType returnType) { + this.prefix = prefix; + this.requiredParams = requiredParams; + this.returnType = returnType; + } + + /** + * Returns true if this is a getter. + * + * @return true if this is a getter + */ + boolean isGetter() { + return this != SETTER; + } + + /** + * Return the PropertyKind for the given method, or null if it isn't a property accessor + * method. + * + * @param md the method to check + * @return the PropertyKind for the given method + */ + static PropertyKind fromMethodDeclaration(MethodDeclaration md) { + String methodName = md.getNameAsString(); + if (methodName.startsWith("get")) { + return GETTER; + } else if (methodName.startsWith("has")) { + return GETTER_HAS; + } else if (methodName.startsWith("is")) { + return GETTER_IS; + } else if (methodName.startsWith("not")) { + return GETTER_NOT; + } else if (methodName.startsWith("set")) { + return SETTER; + } else { + return GETTER_NO_PREFIX; + } + } + } + + /** + * Return true if this method declaration is a trivial getter or setter. + * + *
      + *
    • A trivial getter is named {@code getFoo}, {@code foo}, {@code hasFoo}, {@code isFoo}, + * or {@code notFoo}, has no formal parameters, and has a body of the form {@code return + * foo} or {@code return this.foo} (except for {@code notFoo}, in which case the body is + * negated). + *
    • A trivial setter is named {@code setFoo}, has one formal parameter named {@code foo}, + * and has a body of the form {@code this.foo = foo}. + *
    + * + * @param md the method to check + * @return true if this method is a trivial getter or setter + */ + private boolean isTrivialGetterOrSetter(MethodDeclaration md) { + PropertyKind kind = PropertyKind.fromMethodDeclaration(md); + if (kind != PropertyKind.GETTER_NO_PREFIX) { + if (isTrivialGetterOrSetter(md, kind)) { + return true; + } + } + return isTrivialGetterOrSetter(md, PropertyKind.GETTER_NO_PREFIX); + } + + /** + * Return true if this method declaration is a trivial getter or setter of the given kind. + * + * @see #isTrivialGetterOrSetter(MethodDeclaration) + * @param md the method to check + * @param propertyKind the kind of property + * @return true if this method is a trivial getter or setter + */ + private boolean isTrivialGetterOrSetter(MethodDeclaration md, PropertyKind propertyKind) { + String propertyName = propertyName(md, propertyKind); + return propertyName != null + && hasCorrectSignature(md, propertyKind, propertyName) + && hasCorrectBody(md, propertyKind, propertyName); + } + + /** + * Returns the name of the property, if the method is a getter or setter of the given kind. + * Otherwise returns null. + * + *

    Examines the method's name, but not its signature or body. Also does not check that the + * given property name corresponds to an existing field. + * + * @param md the method to test + * @param propertyKind the type of property method + * @return the name of the property, or null + */ + private @Nullable String propertyName(MethodDeclaration md, PropertyKind propertyKind) { + String methodName = md.getNameAsString(); + assert methodName.startsWith(propertyKind.prefix); + @SuppressWarnings("index") // https://github.com/typetools/checker-framework/issues/5201 + String upperCamelCaseProperty = methodName.substring(propertyKind.prefix.length()); + if (upperCamelCaseProperty.length() == 0) { + return null; + } + if (propertyKind == PropertyKind.GETTER_NO_PREFIX) { + return upperCamelCaseProperty; + } else if (!Character.isUpperCase(upperCamelCaseProperty.charAt(0))) { + return null; + } else { + return "" + + Character.toLowerCase(upperCamelCaseProperty.charAt(0)) + + upperCamelCaseProperty.substring(1); + } + } + + /** + * Returns true if the signature of the given method is a property accessor of the given kind. + * + * @param md the method + * @param propertyKind the kind of property + * @param propertyName the name of the property + * @return true if the body of the given method is a property accessor + */ + private boolean hasCorrectSignature( + MethodDeclaration md, PropertyKind propertyKind, String propertyName) { + NodeList parameters = md.getParameters(); + if (parameters.size() != propertyKind.requiredParams) { + return false; + } + if (parameters.size() == 1) { + Parameter parameter = parameters.get(0); + if (!parameter.getNameAsString().equals(propertyName)) { + return false; + } + } + // Check presence/absence of return type. (The Java compiler will verify + // that the type is consistent with the method body.) + Type returnType = md.getType(); + switch (propertyKind.returnType) { + case VOID: + if (!returnType.isVoidType()) { + return false; + } + break; + case BOOLEAN: + if (!returnType.equals(PrimitiveType.booleanType())) { + return false; + } + break; + case NON_VOID: + if (returnType.isVoidType()) { + return false; + } + break; + default: + throw new Error("Unexpected enum value " + propertyKind.returnType); + } + return true; + } + + /** + * Returns true if the body of the given method is a property accessor of the given kind. + * + * @param md the method + * @param propertyKind the kind of property + * @param propertyName the name of the property + * @return true if the body of the given method is a property accessor + */ + private boolean hasCorrectBody( + MethodDeclaration md, PropertyKind propertyKind, String propertyName) { + Statement statement = getOnlyStatement(md); + if (propertyKind.isGetter()) { + if (!(statement instanceof ReturnStmt)) { + return false; + } + Optional oReturnExpr = ((ReturnStmt) statement).getExpression(); + if (!oReturnExpr.isPresent()) { + return false; + } + Expression returnExpr = oReturnExpr.get(); + // Does not handle parentheses. + if (propertyKind == PropertyKind.GETTER_NOT) { + if (!(returnExpr instanceof UnaryExpr)) { + return false; + } + UnaryExpr unary = (UnaryExpr) returnExpr; + if (unary.getOperator() != UnaryExpr.Operator.LOGICAL_COMPLEMENT) { + return false; + } + returnExpr = unary.getExpression(); + } + String returnName; + // Does not handle parentheses. + if (returnExpr instanceof NameExpr) { + returnName = ((NameExpr) returnExpr).getNameAsString(); + } else if (returnExpr instanceof FieldAccessExpr) { + FieldAccessExpr fa = (FieldAccessExpr) returnExpr; + Expression receiver = fa.getScope(); + if (!(receiver instanceof ThisExpr)) { + return false; + } + returnName = fa.getNameAsString(); + } else { + return false; + } + if (!returnName.equals(propertyName)) { + return false; + } + return true; + } else if (propertyKind == PropertyKind.SETTER) { + if (!(statement instanceof ExpressionStmt)) { + return false; + } + Expression expr = ((ExpressionStmt) statement).getExpression(); + if (!(expr instanceof AssignExpr)) { + return false; + } + AssignExpr assignExpr = (AssignExpr) expr; + Expression target = assignExpr.getTarget(); + AssignExpr.Operator op = assignExpr.getOperator(); + Expression value = assignExpr.getValue(); + if (!(target instanceof FieldAccessExpr)) { + return false; + } + FieldAccessExpr fa = (FieldAccessExpr) target; + Expression receiver = fa.getScope(); + if (!(receiver instanceof ThisExpr)) { + return false; + } + if (!fa.getNameAsString().equals(propertyName)) { + return false; + } + if (op != AssignExpr.Operator.ASSIGN) { + return false; + } + if (!(value instanceof NameExpr + && ((NameExpr) value).getNameAsString().equals(propertyName))) { + return false; + } + return true; + } else { + throw new Error("unexpected PropertyKind " + propertyKind); + } + } + + /** + * If the body contains exactly one statement, returns it. Otherwise, returns null. + * + * @param md a method declaration + * @return its sole statement, or null + */ + private @Nullable Statement getOnlyStatement(MethodDeclaration md) { + Optional body = md.getBody(); + if (!body.isPresent()) { + return null; + } + NodeList statements = body.get().getStatements(); + if (statements.size() != 1) { + return null; + } + return statements.get(0); + } + + /** Visits an AST and collects warnings about missing Javadoc. */ + private class RequireJavadocVisitor extends VoidVisitorAdapter { + + /** The file being visited. Used for constructing error messages. */ + private Path filename; + + /** + * Create a new RequireJavadocVisitor. + * + * @param filename the file being visited; used for diagnostic messages + */ + public RequireJavadocVisitor(Path filename) { + this.filename = filename; + } + + /** + * Return a string stating that documentation is missing on the given construct. + * + * @param node a Java language construct (class, constructor, method, field, etc.) + * @param simpleName the construct's simple name, used in diagnostic messages + * @return an error message for the given construct + */ + private String errorString(Node node, String simpleName) { + Optional range = node.getRange(); + if (range.isPresent()) { + Position begin = range.get().begin; + Path path = + (relative + ? (filename.isAbsolute() ? workingDirAbsolute : workingDirRelative) + .relativize(filename) + : filename); + return String.format( + "%s:%d:%d: missing documentation for %s", + path, begin.line, begin.column, simpleName); + } else { + return "missing documentation for " + simpleName; + } + } + + @Override + public void visit(CompilationUnit cu, Void ignore) { + Optional opd = cu.getPackageDeclaration(); + if (opd.isPresent()) { + String packageName = opd.get().getName().asString(); + if (shouldNotRequire(packageName)) { + return; + } + Optional oTypeName = cu.getPrimaryTypeName(); + if (oTypeName.isPresent() + && oTypeName.get().equals("package-info") + && !hasJavadocComment(opd.get()) + && !hasJavadocComment(cu)) { + errors.add(errorString(opd.get(), packageName)); + } + } + if (verbose) { + System.out.printf("Visiting compilation unit%n"); + } + super.visit(cu, ignore); + } + + @Override + public void visit(ClassOrInterfaceDeclaration cd, Void ignore) { + if (dont_require_private && cd.isPrivate()) { + return; + } + String name = cd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting type %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(cd)) { + errors.add(errorString(cd, name)); + } + super.visit(cd, ignore); + } + + @Override + public void visit(ConstructorDeclaration cd, Void ignore) { + if (dont_require_private && cd.isPrivate()) { + return; + } + if (dont_require_noarg_constructor && cd.getParameters().size() == 0) { + return; + } + String name = cd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting constructor %s%n", name); + } + if (!dont_require_method && !hasJavadocComment(cd)) { + errors.add(errorString(cd, name)); + } + super.visit(cd, ignore); + } + + @Override + public void visit(MethodDeclaration md, Void ignore) { + if (dont_require_private && md.isPrivate()) { + return; + } + if (dont_require_trivial_properties && isTrivialGetterOrSetter(md)) { + if (verbose) { + System.out.printf( + "skipping trivial property method %s%n", md.getNameAsString()); + } + return; + } + String name = md.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting method %s%n", md.getName()); + } + if (!dont_require_method && !isOverride(md) && !hasJavadocComment(md)) { + errors.add(errorString(md, name)); + } + super.visit(md, ignore); + } + + @Override + public void visit(FieldDeclaration fd, Void ignore) { + if (dont_require_private && fd.isPrivate()) { + return; + } + // True if shouldNotRequire is false for at least one of the fields + boolean shouldRequire = false; + if (verbose) { + System.out.printf("Visiting field %s%n", fd.getVariables().get(0).getName()); + } + boolean hasJavadocComment = hasJavadocComment(fd); + for (VariableDeclarator vd : fd.getVariables()) { + String name = vd.getNameAsString(); + // TODO: Also check the type of the serialVersionUID variable. + if (name.equals("serialVersionUID")) { + continue; + } + if (shouldNotRequire(name)) { + continue; + } + shouldRequire = true; + if (!dont_require_field && !hasJavadocComment) { + errors.add(errorString(vd, name)); + } + } + if (shouldRequire) { + super.visit(fd, ignore); + } + } + + @Override + public void visit(EnumDeclaration ed, Void ignore) { + if (dont_require_private && ed.isPrivate()) { + return; + } + String name = ed.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting enum %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(ed)) { + errors.add(errorString(ed, name)); + } + super.visit(ed, ignore); + } + + @Override + public void visit(EnumConstantDeclaration ecd, Void ignore) { + String name = ecd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting enum constant %s%n", name); + } + if (!dont_require_field && !hasJavadocComment(ecd)) { + errors.add(errorString(ecd, name)); + } + super.visit(ecd, ignore); + } + + @Override + public void visit(AnnotationDeclaration ad, Void ignore) { + if (dont_require_private && ad.isPrivate()) { + return; + } + String name = ad.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting annotation %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(ad)) { + errors.add(errorString(ad, name)); + } + super.visit(ad, ignore); + } + + @Override + public void visit(AnnotationMemberDeclaration amd, Void ignore) { + String name = amd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting annotation member %s%n", name); + } + if (!dont_require_method && !hasJavadocComment(amd)) { + errors.add(errorString(amd, name)); + } + super.visit(amd, ignore); + } + + /** + * Return true if this method is annotated with {@code @Override}. + * + * @param md the method to check for an {@code @Override} annotation + * @return true if this method is annotated with {@code @Override} + */ + private boolean isOverride(MethodDeclaration md) { + for (AnnotationExpr anno : md.getAnnotations()) { + String annoName = anno.getName().toString(); + if (annoName.equals("Override") || annoName.equals("java.lang.Override")) { + return true; + } + } + return false; + } + } + + /** + * Return true if this node has a Javadoc comment. + * + * @param n the node to check for a Javadoc comment + * @return true if this node has a Javadoc comment + */ + private boolean hasJavadocComment(Node n) { + if (n instanceof NodeWithJavadoc && ((NodeWithJavadoc) n).hasJavaDocComment()) { + return true; + } + List orphans = new ArrayList<>(); + getOrphanCommentsBeforeThisChildNode(n, orphans); + for (Comment orphan : orphans) { + if (orphan.isJavadocComment()) { + return true; + } + } + Optional oc = n.getComment(); + if (oc.isPresent() + && (oc.get().isJavadocComment() || oc.get().getContent().startsWith("/**"))) { + return true; + } + return false; + } + + /** + * Get "orphan comments": comments before the comment before this node. For example, in + * + *

    {@code
    +     * /** ... *}{@code /
    +     * // text 1
    +     * // text 2
    +     * void m() { ... }
    +     * }
    + * + * the Javadoc comment and {@code // text 1} are an orphan comment, and only {@code // text2} is + * associated with the method. + * + * @param node the node whose orphan comments to collect + * @param result the list to add orphan comments to. Is side-effected by this method. The + * implementation uses this to minimize the diffs against upstream. + */ + @SuppressWarnings({ + "JdkObsolete", // for LinkedList + "interning:not.interned", // element of a list + "ReferenceEquality", + }) + // This implementation is from Randoop's `Minimize.java` file, and before that from JavaParser's + // PrettyPrintVisitor.printOrphanCommentsBeforeThisChildNode. The JavaParser maintainers refuse + // to provide such functionality in JavaParser proper. + private static void getOrphanCommentsBeforeThisChildNode( + final Node node, List result) { + if (node instanceof Comment) { + return; + } + + Node parent = node.getParentNode().orElse(null); + if (parent == null) { + return; + } + List everything = new LinkedList<>(parent.getChildNodes()); + sortByBeginPosition(everything); + int positionOfTheChild = -1; + for (int i = 0; i < everything.size(); i++) { + if (everything.get(i) == node) positionOfTheChild = i; + } + if (positionOfTheChild == -1) { + throw new AssertionError("I am not a child of my parent."); + } + int positionOfPreviousChild = -1; + for (int i = positionOfTheChild - 1; i >= 0 && positionOfPreviousChild == -1; i--) { + if (!(everything.get(i) instanceof Comment)) positionOfPreviousChild = i; + } + for (int i = positionOfPreviousChild + 1; i < positionOfTheChild; i++) { + Node nodeToPrint = everything.get(i); + if (!(nodeToPrint instanceof Comment)) + throw new RuntimeException( + "Expected comment, instead " + + nodeToPrint.getClass() + + ". Position of previous child: " + + positionOfPreviousChild + + ", position of child " + + positionOfTheChild); + result.add((Comment) nodeToPrint); + } + } +} diff --git a/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.IndexChecker.ajava b/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.IndexChecker.ajava new file mode 100644 index 000000000000..5930e52e1f73 --- /dev/null +++ b/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.IndexChecker.ajava @@ -0,0 +1,948 @@ +import static com.github.javaparser.utils.PositionUtils.sortByBeginPosition; + +import com.github.javaparser.ParseProblemException; +import com.github.javaparser.Position; +import com.github.javaparser.Range; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.AnnotationDeclaration; +import com.github.javaparser.ast.body.AnnotationMemberDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.EnumConstantDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.comments.Comment; +import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.ThisExpr; +import com.github.javaparser.ast.expr.UnaryExpr; +import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.stmt.ExpressionStmt; +import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.stmt.Statement; +import com.github.javaparser.ast.type.PrimitiveType; +import com.github.javaparser.ast.type.Type; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; + +import org.plumelib.options.Options; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * A program that issues an error for any class, constructor, method, or field that lacks a Javadoc + * comment. Does not issue a warning for methods annotated with {@code @Override}. See documentation + * at https://github.com/plume-lib/require-javadoc#readme. + */ +@org.checkerframework.framework.qual.AnnotatedFor("org.checkerframework.checker.index.IndexChecker") +public class RequireJavadoc { + + /** Matches name of file or directory where no problems should be reported. */ + public Pattern exclude = null; + + // TODO: It would be nice to support matching fully-qualified class names, but matching + // packages will have to do for now. + /** + * Matches simple name of class/constructor/method/field, or full package name, where no + * problems should be reported. + */ + public Pattern dont_require = null; + + /** If true, don't check elements with private access. */ + public boolean dont_require_private; + + /** + * If true, don't check constructors with zero formal parameters. These are sometimes called + * "default constructors", though that term means a no-argument constructor that the compiler + * synthesized when the programmer didn't write one. + */ + public boolean dont_require_noarg_constructor; + + /** + * If true, don't check trivial getters and setters. + * + *

    Trivial getters and setters are of the form: + * + *

    {@code
    +     * SomeType getFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * SomeType foo() {
    +     *   return foo;
    +     * }
    +     *
    +     * void setFoo(SomeType foo) {
    +     *   this.foo = foo;
    +     * }
    +     *
    +     * boolean hasFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * boolean isFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * boolean notFoo() {
    +     *   return !foo;
    +     * }
    +     * }
    + */ + public boolean dont_require_trivial_properties; + + /** If true, don't check type declarations: classes, interfaces, enums, annotations. */ + public boolean dont_require_type; + + /** If true, don't check fields. */ + public boolean dont_require_field; + + /** If true, don't check methods, constructors, and annotation members. */ + public boolean dont_require_method; + + /** If true, warn if any package lacks a package-info.java file. */ + public boolean require_package_info; + + /** + * If true, print filenames relative to working directory. Setting this only has an effect if + * the command-line arguments were absolute pathnames, or no command-line arguments were + * supplied. + */ + public boolean relative = false; + + /** If true, output debug information. */ + public boolean verbose = false; + + /** All the errors this program will report. */ + private List errors = new ArrayList<>(); + + /** The Java files to be checked. */ + private List javaFiles = new ArrayList(); + + /** The current working directory, for making relative pathnames. */ + private Path workingDirRelative = Paths.get(""); + + /** The current working directory, for making relative pathnames. */ + private Path workingDirAbsolute = Paths.get("").toAbsolutePath(); + + /** + * The main entry point for the require-javadoc program. See documentation at https://github.com/plume-lib/require-javadoc#readme. + * + * @param args the command-line arguments; see the README file + */ + public static void main(String[] args) { + RequireJavadoc rj = new RequireJavadoc(); + Options options = + new Options( + "java org.plumelib.javadoc.RequireJavadoc [options] [directory-or-file ...]", + rj); + String[] remainingArgs = options.parse(true, args); + rj.setJavaFiles(remainingArgs); + for (Path javaFile : rj.javaFiles) { + if (rj.verbose) { + System.out.println("Checking " + javaFile); + } + try { + CompilationUnit cu = StaticJavaParser.parse(javaFile); + RequireJavadocVisitor visitor = rj.new RequireJavadocVisitor(javaFile); + visitor.visit(cu, null); + } catch (IOException e) { + System.out.println("Problem while reading " + javaFile + ": " + e.getMessage()); + System.exit(2); + } catch (ParseProblemException e) { + System.out.println("Problem while parsing " + javaFile + ": " + e.getMessage()); + System.exit(2); + } + } + for (String error : rj.errors) { + System.out.println(error); + } + System.exit(rj.errors.isEmpty() ? 0 : 1); + } + + /** Creates a new RequireJavadoc instance. */ + @org.checkerframework.dataflow.qual.SideEffectFree + private RequireJavadoc() {} + + /** + * Set the Java files to be processed from the command-line arguments. + * + * @param args the directories and files listed on the command line + */ + private void setJavaFiles(String[] args) { + if (args.length == 0) { + args = new String[] {workingDirAbsolute.toString()}; + } + FileVisitor walker = new JavaFilesVisitor(); + for (String arg : args) { + if (shouldExclude(arg)) { + continue; + } + Path p = Paths.get(arg); + File f = p.toFile(); + if (!f.exists()) { + System.out.println("File not found: " + f); + System.exit(2); + } + if (f.isDirectory()) { + try { + Files.walkFileTree(p, walker); + } catch (IOException e) { + System.out.println("Problem while reading " + f + ": " + e.getMessage()); + System.exit(2); + } + } else { + javaFiles.add(Paths.get(arg)); + } + } + javaFiles.sort(Comparator.comparing(Object::toString)); + Set missingPackageInfoFiles = new LinkedHashSet<>(); + if (require_package_info) { + for (Path javaFile : javaFiles) { + Path javaFileParent = javaFile.getParent(); + // Java 11 has Path.of() instead of creating a new File. + Path packageInfo = javaFileParent.resolve(new File("package-info.java").toPath()); + if (!javaFiles.contains(packageInfo)) { + missingPackageInfoFiles.add(packageInfo); + } + } + for (Path packageInfo : missingPackageInfoFiles) { + errors.add("missing package documentation: no file " + packageInfo); + } + } + } + + /** Collects files into the {@link #javaFiles} variable. */ + private class JavaFilesVisitor extends SimpleFileVisitor { + + /** Create a new JavaFilesVisitor. */ + public JavaFilesVisitor() {} + + public FileVisitResult visitFile( + JavaFilesVisitor this, Path file, BasicFileAttributes attr) { + if (attr.isRegularFile() && file.toString().endsWith(".java")) { + if (!shouldExclude(file)) { + javaFiles.add(file); + } + } + return FileVisitResult.CONTINUE; + } + + public FileVisitResult preVisitDirectory( + JavaFilesVisitor this, Path dir, BasicFileAttributes attr) { + if (shouldExclude(dir)) { + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + + @org.checkerframework.framework.qual.EnsuresQualifier( + expression = {"#2"}, + qualifier = org.checkerframework.checker.index.qual.UpperBoundBottom.class) + public FileVisitResult postVisitDirectory( + JavaFilesVisitor this, Path dir, IOException exc) { + if (exc != null) { + System.out.println("Problem visiting " + dir + ": " + exc.getMessage()); + System.exit(2); + } + return FileVisitResult.CONTINUE; + } + + @org.checkerframework.framework.qual.EnsuresQualifier( + expression = {"#2"}, + qualifier = org.checkerframework.checker.index.qual.UpperBoundBottom.class) + public FileVisitResult visitFileFailed(JavaFilesVisitor this, Path file, IOException exc) { + if (exc != null) { + System.out.println("Problem visiting " + file + ": " + exc.getMessage()); + System.exit(2); + } + return FileVisitResult.CONTINUE; + } + } + + /** + * Return true if the given Java element should not be checked, based on the {@code + * --dont-require} command-line argument. + * + * @param name the name of a Java element. It is a simple name, except for packages. + * @return true if no warnings should be issued about the element + */ + private boolean shouldNotRequire(String name) { + if (dont_require == null) { + return false; + } + boolean result = dont_require.matcher(name).find(); + if (verbose) { + System.out.printf("shouldNotRequire(%s) => %s%n", name, result); + } + return result; + } + + /** + * Return true if the given file or directory should be skipped, based on the {@code --exclude} + * command-line argument. + * + * @param fileName the name of a Java file or directory + * @return true if the file or directory should be skipped + */ + private boolean shouldExclude(String fileName) { + if (exclude == null) { + return false; + } + boolean result = exclude.matcher(fileName).find(); + if (verbose) { + System.out.printf("shouldExclude(%s) => %s%n", fileName, result); + } + return result; + } + + /** + * Return true if the given file or directory should be skipped, based on the {@code --exclude} + * command-line argument. + * + * @param path a Java file or directory + * @return true if the file or directory should be skipped + */ + private boolean shouldExclude(Path path) { + return shouldExclude(path.toString()); + } + + /** A property method's return type. */ + private enum ReturnType { + + /** The return type is void. */ + VOID, + /** The return type is boolean. */ + BOOLEAN, + /** The return type is non-void. */ + NON_VOID + } + + /** The type of property method: a getter or setter. */ + private enum PropertyKind { + + /** A method of the form {@code SomeType getFoo()}. */ + GETTER("get", 0, ReturnType.NON_VOID), + /** A method of the form {@code SomeType foo()}. */ + GETTER_NO_PREFIX("", 0, ReturnType.NON_VOID), + /** A method of the form {@code boolean hasFoo()}. */ + GETTER_HAS("has", 0, ReturnType.BOOLEAN), + /** A method of the form {@code boolean isFoo()}. */ + GETTER_IS("is", 0, ReturnType.BOOLEAN), + /** A method of the form {@code boolean notFoo()}. */ + GETTER_NOT("not", 0, ReturnType.BOOLEAN), + /** A method of the form {@code void setFoo(SomeType arg)}. */ + SETTER("set", 1, ReturnType.VOID), + /** Not a getter or setter. */ + NOT_PROPERTY("", -1, ReturnType.VOID); + + /** The prefix for the method name: "get", "", "has", "is", "not", or "set". */ + final String prefix; + + /** The number of required formal parameters: 0 or 1. */ + final int requiredParams; + + /** The return type. */ + final ReturnType returnType; + + /** + * Create a new PropertyKind. + * + * @param prefix the prefix for the method name: "get", "has", "is", "not", or "set" + * @param requiredParams the number of required formal parameters: 0 or 1 + * @param returnType the return type + */ + PropertyKind(String prefix, int requiredParams, ReturnType returnType) { + this.prefix = prefix; + this.requiredParams = requiredParams; + this.returnType = returnType; + } + + /** + * Returns true if this is a getter. + * + * @return true if this is a getter + */ + @org.checkerframework.dataflow.qual.Pure + boolean isGetter() { + return this != SETTER; + } + + /** + * Return the PropertyKind for the given method. + * + * @param md the method to check + * @return the PropertyKind for the given method + */ + static PropertyKind fromMethodDeclaration(MethodDeclaration md) { + String methodName = md.getNameAsString(); + if (methodName.startsWith("get")) { + return GETTER; + } else if (methodName.startsWith("has")) { + return GETTER_HAS; + } else if (methodName.startsWith("is")) { + return GETTER_IS; + } else if (methodName.startsWith("not")) { + return GETTER_NOT; + } else if (methodName.startsWith("set")) { + return SETTER; + } else { + return GETTER_NO_PREFIX; + } + } + } + + /** + * Return true if this method declaration is a trivial getter or setter. + * + *
      + *
    • A trivial getter is named {@code getFoo}, {@code foo}, {@code hasFoo}, {@code isFoo}, + * or {@code notFoo}, has no formal parameters, and has a body of the form {@code return + * foo} or {@code return this.foo} (except for {@code notFoo}, in which case the body is + * negated). + *
    • A trivial setter is named {@code setFoo}, has one formal parameter named {@code foo}, + * and has a body of the form {@code this.foo = foo}. + *
    + * + * @param md the method to check + * @return true if this method is a trivial getter or setter + */ + private boolean isTrivialGetterOrSetter(MethodDeclaration md) { + PropertyKind kind = PropertyKind.fromMethodDeclaration(md); + if (kind != PropertyKind.GETTER_NO_PREFIX) { + if (isTrivialGetterOrSetter(md, kind)) { + return true; + } + } + return isTrivialGetterOrSetter(md, PropertyKind.GETTER_NO_PREFIX); + } + + /** + * Return true if this method declaration is a trivial getter or setter of the given kind. + * + * @see #isTrivialGetterOrSetter(MethodDeclaration) + * @param md the method to check + * @param propertyKind the kind of property + * @return true if this method is a trivial getter or setter + */ + private @org.checkerframework.checker.index.qual.UpperBoundBottom boolean + isTrivialGetterOrSetter(MethodDeclaration md, PropertyKind propertyKind) { + String propertyName = propertyName(md, propertyKind); + return propertyName != null + && hasCorrectSignature(md, propertyKind, propertyName) + && hasCorrectBody(md, propertyKind, propertyName); + } + + /** + * Returns the name of the property, if the method is a getter or setter of the given kind. + * Otherwise returns null. + * + *

    Examines the method's name, but not its signature or body. Also does not check that the + * given property name corresponds to an existing field. + * + * @param md the method to test + * @param propertyKind the type of property method + * @return the name of the property, or null + */ + private String propertyName(MethodDeclaration md, PropertyKind propertyKind) { + String methodName = md.getNameAsString(); + assert methodName.startsWith(propertyKind.prefix); + String upperCamelCaseProperty = methodName.substring(propertyKind.prefix.length()); + if (upperCamelCaseProperty.length() == 0) { + return null; + } + if (propertyKind == PropertyKind.GETTER_NO_PREFIX) { + return upperCamelCaseProperty; + } else if (!Character.isUpperCase(upperCamelCaseProperty.charAt(0))) { + return null; + } else { + return "" + + Character.toLowerCase(upperCamelCaseProperty.charAt(0)) + + upperCamelCaseProperty.substring(1); + } + } + + /** + * Returns true if the signature of the given method is a property accessor of the given kind. + * + * @param md the method + * @param propertyKind the kind of property + * @param propertyName the name of the property + * @return true if the body of the given method is a property accessor + */ + private boolean hasCorrectSignature( + MethodDeclaration md, PropertyKind propertyKind, String propertyName) { + NodeList parameters = md.getParameters(); + if (parameters.size() != propertyKind.requiredParams) { + return false; + } + if (parameters.size() == 1) { + Parameter parameter = parameters.get(0); + if (!parameter.getNameAsString().equals(propertyName)) { + return false; + } + } + // Check presence/absence of return type. (The Java compiler will verify + // that the type is consistent with the method body.) + Type returnType = md.getType(); + switch (propertyKind.returnType) { + case VOID: + if (!returnType.isVoidType()) { + return false; + } + break; + case BOOLEAN: + if (!returnType.equals(PrimitiveType.booleanType())) { + return false; + } + break; + case NON_VOID: + if (returnType.isVoidType()) { + return false; + } + break; + default: + throw new Error("Unexpected enum value " + propertyKind.returnType); + } + return true; + } + + /** + * Returns true if the body of the given method is a property accessor of the given kind. + * + * @param md the method + * @param propertyKind the kind of property + * @param propertyName the name of the property + * @return true if the body of the given method is a property accessor + */ + private boolean hasCorrectBody( + MethodDeclaration md, PropertyKind propertyKind, String propertyName) { + Statement statement = getOnlyStatement(md); + if (propertyKind.isGetter()) { + if (!(statement instanceof ReturnStmt)) { + return false; + } + Optional oReturnExpr = ((ReturnStmt) statement).getExpression(); + if (!oReturnExpr.isPresent()) { + return false; + } + Expression returnExpr = oReturnExpr.get(); + // Does not handle parentheses. + if (propertyKind == PropertyKind.GETTER_NOT) { + if (!(returnExpr instanceof UnaryExpr)) { + return false; + } + UnaryExpr unary = (UnaryExpr) returnExpr; + if (unary.getOperator() != UnaryExpr.Operator.LOGICAL_COMPLEMENT) { + return false; + } + returnExpr = unary.getExpression(); + } + String returnName; + // Does not handle parentheses. + if (returnExpr instanceof NameExpr) { + returnName = ((NameExpr) returnExpr).getNameAsString(); + } else if (returnExpr instanceof FieldAccessExpr) { + FieldAccessExpr fa = (FieldAccessExpr) returnExpr; + Expression receiver = fa.getScope(); + if (!(receiver instanceof ThisExpr)) { + return false; + } + returnName = fa.getNameAsString(); + } else { + return false; + } + if (!returnName.equals(propertyName)) { + return false; + } + return true; + } else if (propertyKind == PropertyKind.SETTER) { + if (!(statement instanceof ExpressionStmt)) { + return false; + } + Expression expr = ((ExpressionStmt) statement).getExpression(); + if (!(expr instanceof AssignExpr)) { + return false; + } + AssignExpr assignExpr = (AssignExpr) expr; + Expression target = assignExpr.getTarget(); + AssignExpr.Operator op = assignExpr.getOperator(); + Expression value = assignExpr.getValue(); + if (!(target instanceof FieldAccessExpr)) { + return false; + } + FieldAccessExpr fa = (FieldAccessExpr) target; + Expression receiver = fa.getScope(); + if (!(receiver instanceof ThisExpr)) { + return false; + } + if (!fa.getNameAsString().equals(propertyName)) { + return false; + } + if (op != AssignExpr.Operator.ASSIGN) { + return false; + } + if (!(value instanceof NameExpr + && ((NameExpr) value).getNameAsString().equals(propertyName))) { + return false; + } + return true; + } else { + throw new Error("unexpected PropertyKind " + propertyKind); + } + } + + /** + * If the body contains exactly one statement, returns it. Otherwise, returns null. + * + * @param md a method declaration + * @return its sole statement, or null + */ + private Statement getOnlyStatement(MethodDeclaration md) { + Optional body = md.getBody(); + if (!body.isPresent()) { + return null; + } + NodeList statements = body.get().getStatements(); + if (statements.size() != 1) { + return null; + } + return statements.get(0); + } + + /** Visits an AST and collects warnings about missing Javadoc. */ + private class RequireJavadocVisitor extends VoidVisitorAdapter { + + /** The file being visited. Used for constructing error messages. */ + private Path filename; + + /** + * Create a new RequireJavadocVisitor. + * + * @param filename the file being visited; used for diagnostic messages + */ + public RequireJavadocVisitor(Path filename) { + this.filename = filename; + } + + /** + * Return a string stating that documentation is missing on the given construct. + * + * @param node a Java language construct (class, constructor, method, field, etc.) + * @param simpleName the construct's simple name, used in diagnostic messages + * @return an error message for the given construct + */ + private String errorString(Node node, String simpleName) { + Optional range = node.getRange(); + if (range.isPresent()) { + Position begin = range.get().begin; + Path path = + (relative + ? (filename.isAbsolute() ? workingDirAbsolute : workingDirRelative) + .relativize(filename) + : filename); + return String.format( + "%s:%d:%d: missing documentation for %s", + path, begin.line, begin.column, simpleName); + } else { + return "missing documentation for " + simpleName; + } + } + + public void visit(RequireJavadocVisitor this, CompilationUnit cu, Void ignore) { + Optional opd = cu.getPackageDeclaration(); + if (opd.isPresent()) { + String packageName = opd.get().getName().asString(); + if (shouldNotRequire(packageName)) { + return; + } + Optional oTypeName = cu.getPrimaryTypeName(); + if (oTypeName.isPresent() + && oTypeName.get().equals("package-info") + && !hasJavadocComment(opd.get()) + && !hasJavadocComment(cu)) { + errors.add(errorString(opd.get(), packageName)); + } + } + if (verbose) { + System.out.printf("Visiting compilation unit%n"); + } + super.visit(cu, ignore); + } + + public void visit(RequireJavadocVisitor this, ClassOrInterfaceDeclaration cd, Void ignore) { + if (dont_require_private && cd.isPrivate()) { + return; + } + String name = cd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting type %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(cd)) { + errors.add(errorString(cd, name)); + } + super.visit(cd, ignore); + } + + public void visit(RequireJavadocVisitor this, ConstructorDeclaration cd, Void ignore) { + if (dont_require_private && cd.isPrivate()) { + return; + } + if (dont_require_noarg_constructor && cd.getParameters().size() == 0) { + return; + } + String name = cd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting constructor %s%n", name); + } + if (!dont_require_method && !hasJavadocComment(cd)) { + errors.add(errorString(cd, name)); + } + super.visit(cd, ignore); + } + + public void visit(RequireJavadocVisitor this, MethodDeclaration md, Void ignore) { + if (dont_require_private && md.isPrivate()) { + return; + } + if (dont_require_trivial_properties && isTrivialGetterOrSetter(md)) { + if (verbose) { + System.out.printf( + "skipping trivial property method %s%n", md.getNameAsString()); + } + return; + } + String name = md.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting method %s%n", md.getName()); + } + if (!dont_require_method && !isOverride(md) && !hasJavadocComment(md)) { + errors.add(errorString(md, name)); + } + super.visit(md, ignore); + } + + public void visit(RequireJavadocVisitor this, FieldDeclaration fd, Void ignore) { + if (dont_require_private && fd.isPrivate()) { + return; + } + // True if shouldNotRequire is false for at least one of the fields + boolean shouldRequire = false; + if (verbose) { + System.out.printf("Visiting field %s%n", fd.getVariables().get(0).getName()); + } + boolean hasJavadocComment = hasJavadocComment(fd); + for (VariableDeclarator vd : fd.getVariables()) { + String name = vd.getNameAsString(); + // TODO: Also check the type of the serialVersionUID variable. + if (name.equals("serialVersionUID")) { + continue; + } + if (shouldNotRequire(name)) { + continue; + } + shouldRequire = true; + if (!dont_require_field && !hasJavadocComment) { + errors.add(errorString(vd, name)); + } + } + if (shouldRequire) { + super.visit(fd, ignore); + } + } + + public void visit(RequireJavadocVisitor this, EnumDeclaration ed, Void ignore) { + if (dont_require_private && ed.isPrivate()) { + return; + } + String name = ed.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting enum %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(ed)) { + errors.add(errorString(ed, name)); + } + super.visit(ed, ignore); + } + + public void visit(RequireJavadocVisitor this, EnumConstantDeclaration ecd, Void ignore) { + String name = ecd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting enum constant %s%n", name); + } + if (!dont_require_field && !hasJavadocComment(ecd)) { + errors.add(errorString(ecd, name)); + } + super.visit(ecd, ignore); + } + + public void visit(RequireJavadocVisitor this, AnnotationDeclaration ad, Void ignore) { + if (dont_require_private && ad.isPrivate()) { + return; + } + String name = ad.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting annotation %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(ad)) { + errors.add(errorString(ad, name)); + } + super.visit(ad, ignore); + } + + public void visit( + RequireJavadocVisitor this, AnnotationMemberDeclaration amd, Void ignore) { + String name = amd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting annotation member %s%n", name); + } + if (!dont_require_method && !hasJavadocComment(amd)) { + errors.add(errorString(amd, name)); + } + super.visit(amd, ignore); + } + + /** + * Return true if this method is annotated with {@code @Override}. + * + * @param md the method to check for an {@code @Override} annotation + * @return true if this method is annotated with {@code @Override} + */ + private boolean isOverride(MethodDeclaration md) { + for (AnnotationExpr anno : md.getAnnotations()) { + String annoName = anno.getName().toString(); + if (annoName.equals("Override") || annoName.equals("java.lang.Override")) { + return true; + } + } + return false; + } + } + + /** + * Return true if this node has a Javadoc comment. + * + * @param n the node to check for a Javadoc comment + * @return true if this node has a Javadoc comment + */ + private boolean hasJavadocComment(Node n) { + if (n instanceof NodeWithJavadoc && ((NodeWithJavadoc) n).hasJavaDocComment()) { + return true; + } + List orphans = new ArrayList<>(); + getOrphanCommentsBeforeThisChildNode(n, orphans); + for (Comment orphan : orphans) { + if (orphan.isJavadocComment()) { + return true; + } + } + Optional oc = n.getComment(); + if (oc.isPresent() + && (oc.get().isJavadocComment() || oc.get().getContent().startsWith("/**"))) { + return true; + } + return false; + } + + /** + * Get "orphan comments": comments before the comment before this node. For example, in + * + *

    {@code
    +     * /** ... *}{@code /
    +     * // text 1
    +     * // text 2
    +     * void m() { ... }
    +     * }
    + * + * the Javadoc comment and {@code // text 1} are an orphan comment, and only {@code // text2} is + * associated with the method. + * + * @param node the node whose orphan comments to collect + * @param result the list to add orphan comments to. Is side-effected by this method. The + * implementation uses this to minimize the diffs against upstream. + */ + private static // to provide such functionality in JavaParser proper. + void getOrphanCommentsBeforeThisChildNode(final Node node, List result) { + if (node instanceof Comment) { + return; + } + Node parent = node.getParentNode().orElse(null); + if (parent == null) { + return; + } + List everything = new LinkedList<>(parent.getChildNodes()); + sortByBeginPosition(everything); + int positionOfTheChild = -1; + for (int i = 0; i < everything.size(); i++) { + if (everything.get(i) == node) positionOfTheChild = i; + } + if (positionOfTheChild == -1) { + throw new AssertionError("I am not a child of my parent."); + } + int positionOfPreviousChild = -1; + for (int i = positionOfTheChild - 1; i >= 0 && positionOfPreviousChild == -1; i--) { + if (!(everything.get(i) instanceof Comment)) positionOfPreviousChild = i; + } + for (int i = positionOfPreviousChild + 1; i < positionOfTheChild; i++) { + Node nodeToPrint = everything.get(i); + if (!(nodeToPrint instanceof Comment)) + throw new RuntimeException( + "Expected comment, instead " + + nodeToPrint.getClass() + + ". Position of previous child: " + + positionOfPreviousChild + + ", position of child " + + positionOfTheChild); + result.add((Comment) nodeToPrint); + } + } +} diff --git a/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.inequality.LessThanChecker.ajava b/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.inequality.LessThanChecker.ajava new file mode 100644 index 000000000000..8d2c6ce13e71 --- /dev/null +++ b/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.inequality.LessThanChecker.ajava @@ -0,0 +1,949 @@ +import static com.github.javaparser.utils.PositionUtils.sortByBeginPosition; + +import com.github.javaparser.ParseProblemException; +import com.github.javaparser.Position; +import com.github.javaparser.Range; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.AnnotationDeclaration; +import com.github.javaparser.ast.body.AnnotationMemberDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.EnumConstantDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.comments.Comment; +import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.ThisExpr; +import com.github.javaparser.ast.expr.UnaryExpr; +import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.stmt.ExpressionStmt; +import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.stmt.Statement; +import com.github.javaparser.ast.type.PrimitiveType; +import com.github.javaparser.ast.type.Type; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; + +import org.plumelib.options.Options; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * A program that issues an error for any class, constructor, method, or field that lacks a Javadoc + * comment. Does not issue a warning for methods annotated with {@code @Override}. See documentation + * at https://github.com/plume-lib/require-javadoc#readme. + */ +@org.checkerframework.framework.qual.AnnotatedFor( + "org.checkerframework.checker.index.inequality.LessThanChecker") +public class RequireJavadoc { + + /** Matches name of file or directory where no problems should be reported. */ + public Pattern exclude = null; + + // TODO: It would be nice to support matching fully-qualified class names, but matching + // packages will have to do for now. + /** + * Matches simple name of class/constructor/method/field, or full package name, where no + * problems should be reported. + */ + public Pattern dont_require = null; + + /** If true, don't check elements with private access. */ + public boolean dont_require_private; + + /** + * If true, don't check constructors with zero formal parameters. These are sometimes called + * "default constructors", though that term means a no-argument constructor that the compiler + * synthesized when the programmer didn't write one. + */ + public boolean dont_require_noarg_constructor; + + /** + * If true, don't check trivial getters and setters. + * + *

    Trivial getters and setters are of the form: + * + *

    {@code
    +     * SomeType getFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * SomeType foo() {
    +     *   return foo;
    +     * }
    +     *
    +     * void setFoo(SomeType foo) {
    +     *   this.foo = foo;
    +     * }
    +     *
    +     * boolean hasFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * boolean isFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * boolean notFoo() {
    +     *   return !foo;
    +     * }
    +     * }
    + */ + public boolean dont_require_trivial_properties; + + /** If true, don't check type declarations: classes, interfaces, enums, annotations. */ + public boolean dont_require_type; + + /** If true, don't check fields. */ + public boolean dont_require_field; + + /** If true, don't check methods, constructors, and annotation members. */ + public boolean dont_require_method; + + /** If true, warn if any package lacks a package-info.java file. */ + public boolean require_package_info; + + /** + * If true, print filenames relative to working directory. Setting this only has an effect if + * the command-line arguments were absolute pathnames, or no command-line arguments were + * supplied. + */ + public boolean relative = false; + + /** If true, output debug information. */ + public boolean verbose = false; + + /** All the errors this program will report. */ + private List errors = new ArrayList<>(); + + /** The Java files to be checked. */ + private List javaFiles = new ArrayList(); + + /** The current working directory, for making relative pathnames. */ + private Path workingDirRelative = Paths.get(""); + + /** The current working directory, for making relative pathnames. */ + private Path workingDirAbsolute = Paths.get("").toAbsolutePath(); + + /** + * The main entry point for the require-javadoc program. See documentation at https://github.com/plume-lib/require-javadoc#readme. + * + * @param args the command-line arguments; see the README file + */ + public static void main(String[] args) { + RequireJavadoc rj = new RequireJavadoc(); + Options options = + new Options( + "java org.plumelib.javadoc.RequireJavadoc [options] [directory-or-file ...]", + rj); + String[] remainingArgs = options.parse(true, args); + rj.setJavaFiles(remainingArgs); + for (Path javaFile : rj.javaFiles) { + if (rj.verbose) { + System.out.println("Checking " + javaFile); + } + try { + CompilationUnit cu = StaticJavaParser.parse(javaFile); + RequireJavadocVisitor visitor = rj.new RequireJavadocVisitor(javaFile); + visitor.visit(cu, null); + } catch (IOException e) { + System.out.println("Problem while reading " + javaFile + ": " + e.getMessage()); + System.exit(2); + } catch (ParseProblemException e) { + System.out.println("Problem while parsing " + javaFile + ": " + e.getMessage()); + System.exit(2); + } + } + for (String error : rj.errors) { + System.out.println(error); + } + System.exit(rj.errors.isEmpty() ? 0 : 1); + } + + /** Creates a new RequireJavadoc instance. */ + @org.checkerframework.dataflow.qual.SideEffectFree + private RequireJavadoc() {} + + /** + * Set the Java files to be processed from the command-line arguments. + * + * @param args the directories and files listed on the command line + */ + private void setJavaFiles(String[] args) { + if (args.length == 0) { + args = new String[] {workingDirAbsolute.toString()}; + } + FileVisitor walker = new JavaFilesVisitor(); + for (String arg : args) { + if (shouldExclude(arg)) { + continue; + } + Path p = Paths.get(arg); + File f = p.toFile(); + if (!f.exists()) { + System.out.println("File not found: " + f); + System.exit(2); + } + if (f.isDirectory()) { + try { + Files.walkFileTree(p, walker); + } catch (IOException e) { + System.out.println("Problem while reading " + f + ": " + e.getMessage()); + System.exit(2); + } + } else { + javaFiles.add(Paths.get(arg)); + } + } + javaFiles.sort(Comparator.comparing(Object::toString)); + Set missingPackageInfoFiles = new LinkedHashSet<>(); + if (require_package_info) { + for (Path javaFile : javaFiles) { + Path javaFileParent = javaFile.getParent(); + // Java 11 has Path.of() instead of creating a new File. + Path packageInfo = javaFileParent.resolve(new File("package-info.java").toPath()); + if (!javaFiles.contains(packageInfo)) { + missingPackageInfoFiles.add(packageInfo); + } + } + for (Path packageInfo : missingPackageInfoFiles) { + errors.add("missing package documentation: no file " + packageInfo); + } + } + } + + /** Collects files into the {@link #javaFiles} variable. */ + private class JavaFilesVisitor extends SimpleFileVisitor { + + /** Create a new JavaFilesVisitor. */ + public JavaFilesVisitor() {} + + public FileVisitResult visitFile( + JavaFilesVisitor this, Path file, BasicFileAttributes attr) { + if (attr.isRegularFile() && file.toString().endsWith(".java")) { + if (!shouldExclude(file)) { + javaFiles.add(file); + } + } + return FileVisitResult.CONTINUE; + } + + public FileVisitResult preVisitDirectory( + JavaFilesVisitor this, Path dir, BasicFileAttributes attr) { + if (shouldExclude(dir)) { + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + + @org.checkerframework.framework.qual.EnsuresQualifier( + expression = {"#2"}, + qualifier = org.checkerframework.checker.index.qual.LessThanBottom.class) + public FileVisitResult postVisitDirectory( + JavaFilesVisitor this, Path dir, IOException exc) { + if (exc != null) { + System.out.println("Problem visiting " + dir + ": " + exc.getMessage()); + System.exit(2); + } + return FileVisitResult.CONTINUE; + } + + @org.checkerframework.framework.qual.EnsuresQualifier( + expression = {"#2"}, + qualifier = org.checkerframework.checker.index.qual.LessThanBottom.class) + public FileVisitResult visitFileFailed(JavaFilesVisitor this, Path file, IOException exc) { + if (exc != null) { + System.out.println("Problem visiting " + file + ": " + exc.getMessage()); + System.exit(2); + } + return FileVisitResult.CONTINUE; + } + } + + /** + * Return true if the given Java element should not be checked, based on the {@code + * --dont-require} command-line argument. + * + * @param name the name of a Java element. It is a simple name, except for packages. + * @return true if no warnings should be issued about the element + */ + private boolean shouldNotRequire(String name) { + if (dont_require == null) { + return false; + } + boolean result = dont_require.matcher(name).find(); + if (verbose) { + System.out.printf("shouldNotRequire(%s) => %s%n", name, result); + } + return result; + } + + /** + * Return true if the given file or directory should be skipped, based on the {@code --exclude} + * command-line argument. + * + * @param fileName the name of a Java file or directory + * @return true if the file or directory should be skipped + */ + private boolean shouldExclude(String fileName) { + if (exclude == null) { + return false; + } + boolean result = exclude.matcher(fileName).find(); + if (verbose) { + System.out.printf("shouldExclude(%s) => %s%n", fileName, result); + } + return result; + } + + /** + * Return true if the given file or directory should be skipped, based on the {@code --exclude} + * command-line argument. + * + * @param path a Java file or directory + * @return true if the file or directory should be skipped + */ + private boolean shouldExclude(Path path) { + return shouldExclude(path.toString()); + } + + /** A property method's return type. */ + private enum ReturnType { + + /** The return type is void. */ + VOID, + /** The return type is boolean. */ + BOOLEAN, + /** The return type is non-void. */ + NON_VOID + } + + /** The type of property method: a getter or setter. */ + private enum PropertyKind { + + /** A method of the form {@code SomeType getFoo()}. */ + GETTER("get", 0, ReturnType.NON_VOID), + /** A method of the form {@code SomeType foo()}. */ + GETTER_NO_PREFIX("", 0, ReturnType.NON_VOID), + /** A method of the form {@code boolean hasFoo()}. */ + GETTER_HAS("has", 0, ReturnType.BOOLEAN), + /** A method of the form {@code boolean isFoo()}. */ + GETTER_IS("is", 0, ReturnType.BOOLEAN), + /** A method of the form {@code boolean notFoo()}. */ + GETTER_NOT("not", 0, ReturnType.BOOLEAN), + /** A method of the form {@code void setFoo(SomeType arg)}. */ + SETTER("set", 1, ReturnType.VOID), + /** Not a getter or setter. */ + NOT_PROPERTY("", -1, ReturnType.VOID); + + /** The prefix for the method name: "get", "", "has", "is", "not", or "set". */ + final String prefix; + + /** The number of required formal parameters: 0 or 1. */ + final int requiredParams; + + /** The return type. */ + final ReturnType returnType; + + /** + * Create a new PropertyKind. + * + * @param prefix the prefix for the method name: "get", "has", "is", "not", or "set" + * @param requiredParams the number of required formal parameters: 0 or 1 + * @param returnType the return type + */ + PropertyKind(String prefix, int requiredParams, ReturnType returnType) { + this.prefix = prefix; + this.requiredParams = requiredParams; + this.returnType = returnType; + } + + /** + * Returns true if this is a getter. + * + * @return true if this is a getter + */ + @org.checkerframework.dataflow.qual.Pure + boolean isGetter() { + return this != SETTER; + } + + /** + * Return the PropertyKind for the given method, or null if it isn't a property accessor + * method. + * + * @param md the method to check + * @return the PropertyKind for the given method, or null + */ + static PropertyKind fromMethodDeclaration(MethodDeclaration md) { + String methodName = md.getNameAsString(); + if (methodName.startsWith("get")) { + return GETTER; + } else if (methodName.startsWith("has")) { + return GETTER_HAS; + } else if (methodName.startsWith("is")) { + return GETTER_IS; + } else if (methodName.startsWith("not")) { + return GETTER_NOT; + } else if (methodName.startsWith("set")) { + return SETTER; + } else { + return GETTER_NO_PREFIX; + } + } + } + + /** + * Return true if this method declaration is a trivial getter or setter. + * + *
      + *
    • A trivial getter is named {@code getFoo}, {@code foo}, {@code hasFoo}, {@code isFoo}, + * or {@code notFoo}, has no formal parameters, and has a body of the form {@code return + * foo} or {@code return this.foo} (except for {@code notFoo}, in which case the body is + * negated). + *
    • A trivial setter is named {@code setFoo}, has one formal parameter named {@code foo}, + * and has a body of the form {@code this.foo = foo}. + *
    + * + * @param md the method to check + * @return true if this method is a trivial getter or setter + */ + private boolean isTrivialGetterOrSetter(MethodDeclaration md) { + PropertyKind kind = PropertyKind.fromMethodDeclaration(md); + if (kind != PropertyKind.GETTER_NO_PREFIX) { + if (isTrivialGetterOrSetter(md, kind)) { + return true; + } + } + return isTrivialGetterOrSetter(md, PropertyKind.GETTER_NO_PREFIX); + } + + /** + * Return true if this method declaration is a trivial getter or setter of the given kind. + * + * @see #isTrivialGetterOrSetter(MethodDeclaration) + * @param md the method to check + * @param propertyKind the kind of property + * @return true if this method is a trivial getter or setter + */ + private boolean isTrivialGetterOrSetter(MethodDeclaration md, PropertyKind propertyKind) { + String propertyName = propertyName(md, propertyKind); + return propertyName != null + && hasCorrectSignature(md, propertyKind, propertyName) + && hasCorrectBody(md, propertyKind, propertyName); + } + + /** + * Returns the name of the property, if the method is a getter or setter of the given kind. + * Otherwise returns null. + * + *

    Examines the method's name, but not its signature or body. Also does not check that the + * given property name corresponds to an existing field. + * + * @param md the method to test + * @param propertyKind the type of property method + * @return the name of the property, or null + */ + private String propertyName(MethodDeclaration md, PropertyKind propertyKind) { + String methodName = md.getNameAsString(); + assert methodName.startsWith(propertyKind.prefix); + String upperCamelCaseProperty = methodName.substring(propertyKind.prefix.length()); + if (upperCamelCaseProperty.length() == 0) { + return null; + } + if (propertyKind == PropertyKind.GETTER_NO_PREFIX) { + return upperCamelCaseProperty; + } else if (!Character.isUpperCase(upperCamelCaseProperty.charAt(0))) { + return null; + } else { + return "" + + Character.toLowerCase(upperCamelCaseProperty.charAt(0)) + + upperCamelCaseProperty.substring(1); + } + } + + /** + * Returns true if the signature of the given method is a property accessor of the given kind. + * + * @param md the method + * @param propertyKind the kind of property + * @param propertyName the name of the property + * @return true if the body of the given method is a property accessor + */ + private boolean hasCorrectSignature( + MethodDeclaration md, PropertyKind propertyKind, String propertyName) { + NodeList parameters = md.getParameters(); + if (parameters.size() != propertyKind.requiredParams) { + return false; + } + if (parameters.size() == 1) { + Parameter parameter = parameters.get(0); + if (!parameter.getNameAsString().equals(propertyName)) { + return false; + } + } + // Check presence/absence of return type. (The Java compiler will verify + // that the type is consistent with the method body.) + Type returnType = md.getType(); + switch (propertyKind.returnType) { + case VOID: + if (!returnType.isVoidType()) { + return false; + } + break; + case BOOLEAN: + if (!returnType.equals(PrimitiveType.booleanType())) { + return false; + } + break; + case NON_VOID: + if (returnType.isVoidType()) { + return false; + } + break; + default: + throw new Error("Unexpected enum value " + propertyKind.returnType); + } + return true; + } + + /** + * Returns true if the body of the given method is a property accessor of the given kind. + * + * @param md the method + * @param propertyKind the kind of property + * @param propertyName the name of the property + * @return true if the body of the given method is a property accessor + */ + private boolean hasCorrectBody( + MethodDeclaration md, PropertyKind propertyKind, String propertyName) { + Statement statement = getOnlyStatement(md); + if (propertyKind.isGetter()) { + if (!(statement instanceof ReturnStmt)) { + return false; + } + Optional oReturnExpr = ((ReturnStmt) statement).getExpression(); + if (!oReturnExpr.isPresent()) { + return false; + } + Expression returnExpr = oReturnExpr.get(); + // Does not handle parentheses. + if (propertyKind == PropertyKind.GETTER_NOT) { + if (!(returnExpr instanceof UnaryExpr)) { + return false; + } + UnaryExpr unary = (UnaryExpr) returnExpr; + if (unary.getOperator() != UnaryExpr.Operator.LOGICAL_COMPLEMENT) { + return false; + } + returnExpr = unary.getExpression(); + } + String returnName; + // Does not handle parentheses. + if (returnExpr instanceof NameExpr) { + returnName = ((NameExpr) returnExpr).getNameAsString(); + } else if (returnExpr instanceof FieldAccessExpr) { + FieldAccessExpr fa = (FieldAccessExpr) returnExpr; + Expression receiver = fa.getScope(); + if (!(receiver instanceof ThisExpr)) { + return false; + } + returnName = fa.getNameAsString(); + } else { + return false; + } + if (!returnName.equals(propertyName)) { + return false; + } + return true; + } else if (propertyKind == PropertyKind.SETTER) { + if (!(statement instanceof ExpressionStmt)) { + return false; + } + Expression expr = ((ExpressionStmt) statement).getExpression(); + if (!(expr instanceof AssignExpr)) { + return false; + } + AssignExpr assignExpr = (AssignExpr) expr; + Expression target = assignExpr.getTarget(); + AssignExpr.Operator op = assignExpr.getOperator(); + Expression value = assignExpr.getValue(); + if (!(target instanceof FieldAccessExpr)) { + return false; + } + FieldAccessExpr fa = (FieldAccessExpr) target; + Expression receiver = fa.getScope(); + if (!(receiver instanceof ThisExpr)) { + return false; + } + if (!fa.getNameAsString().equals(propertyName)) { + return false; + } + if (op != AssignExpr.Operator.ASSIGN) { + return false; + } + if (!(value instanceof NameExpr + && ((NameExpr) value).getNameAsString().equals(propertyName))) { + return false; + } + return true; + } else { + throw new Error("unexpected PropertyKind " + propertyKind); + } + } + + /** + * If the body contains exactly one statement, returns it. Otherwise, returns null. + * + * @param md a method declaration + * @return its sole statement, or null + */ + private Statement getOnlyStatement(MethodDeclaration md) { + Optional body = md.getBody(); + if (!body.isPresent()) { + return null; + } + NodeList statements = body.get().getStatements(); + if (statements.size() != 1) { + return null; + } + return statements.get(0); + } + + /** Visits an AST and collects warnings about missing Javadoc. */ + private class RequireJavadocVisitor extends VoidVisitorAdapter { + + /** The file being visited. Used for constructing error messages. */ + private Path filename; + + /** + * Create a new RequireJavadocVisitor. + * + * @param filename the file being visited; used for diagnostic messages + */ + public RequireJavadocVisitor(Path filename) { + this.filename = filename; + } + + /** + * Return a string stating that documentation is missing on the given construct. + * + * @param node a Java language construct (class, constructor, method, field, etc.) + * @param simpleName the construct's simple name, used in diagnostic messages + * @return an error message for the given construct + */ + private String errorString(Node node, String simpleName) { + Optional range = node.getRange(); + if (range.isPresent()) { + Position begin = range.get().begin; + Path path = + (relative + ? (filename.isAbsolute() ? workingDirAbsolute : workingDirRelative) + .relativize(filename) + : filename); + return String.format( + "%s:%d:%d: missing documentation for %s", + path, begin.line, begin.column, simpleName); + } else { + return "missing documentation for " + simpleName; + } + } + + public void visit(RequireJavadocVisitor this, CompilationUnit cu, Void ignore) { + Optional opd = cu.getPackageDeclaration(); + if (opd.isPresent()) { + String packageName = opd.get().getName().asString(); + if (shouldNotRequire(packageName)) { + return; + } + Optional oTypeName = cu.getPrimaryTypeName(); + if (oTypeName.isPresent() + && oTypeName.get().equals("package-info") + && !hasJavadocComment(opd.get()) + && !hasJavadocComment(cu)) { + errors.add(errorString(opd.get(), packageName)); + } + } + if (verbose) { + System.out.printf("Visiting compilation unit%n"); + } + super.visit(cu, ignore); + } + + public void visit(RequireJavadocVisitor this, ClassOrInterfaceDeclaration cd, Void ignore) { + if (dont_require_private && cd.isPrivate()) { + return; + } + String name = cd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting type %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(cd)) { + errors.add(errorString(cd, name)); + } + super.visit(cd, ignore); + } + + public void visit(RequireJavadocVisitor this, ConstructorDeclaration cd, Void ignore) { + if (dont_require_private && cd.isPrivate()) { + return; + } + if (dont_require_noarg_constructor && cd.getParameters().size() == 0) { + return; + } + String name = cd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting constructor %s%n", name); + } + if (!dont_require_method && !hasJavadocComment(cd)) { + errors.add(errorString(cd, name)); + } + super.visit(cd, ignore); + } + + public void visit(RequireJavadocVisitor this, MethodDeclaration md, Void ignore) { + if (dont_require_private && md.isPrivate()) { + return; + } + if (dont_require_trivial_properties && isTrivialGetterOrSetter(md)) { + if (verbose) { + System.out.printf( + "skipping trivial property method %s%n", md.getNameAsString()); + } + return; + } + String name = md.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting method %s%n", md.getName()); + } + if (!dont_require_method && !isOverride(md) && !hasJavadocComment(md)) { + errors.add(errorString(md, name)); + } + super.visit(md, ignore); + } + + public void visit(RequireJavadocVisitor this, FieldDeclaration fd, Void ignore) { + if (dont_require_private && fd.isPrivate()) { + return; + } + // True if shouldNotRequire is false for at least one of the fields + boolean shouldRequire = false; + if (verbose) { + System.out.printf("Visiting field %s%n", fd.getVariables().get(0).getName()); + } + boolean hasJavadocComment = hasJavadocComment(fd); + for (VariableDeclarator vd : fd.getVariables()) { + String name = vd.getNameAsString(); + // TODO: Also check the type of the serialVersionUID variable. + if (name.equals("serialVersionUID")) { + continue; + } + if (shouldNotRequire(name)) { + continue; + } + shouldRequire = true; + if (!dont_require_field && !hasJavadocComment) { + errors.add(errorString(vd, name)); + } + } + if (shouldRequire) { + super.visit(fd, ignore); + } + } + + public void visit(RequireJavadocVisitor this, EnumDeclaration ed, Void ignore) { + if (dont_require_private && ed.isPrivate()) { + return; + } + String name = ed.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting enum %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(ed)) { + errors.add(errorString(ed, name)); + } + super.visit(ed, ignore); + } + + public void visit(RequireJavadocVisitor this, EnumConstantDeclaration ecd, Void ignore) { + String name = ecd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting enum constant %s%n", name); + } + if (!dont_require_field && !hasJavadocComment(ecd)) { + errors.add(errorString(ecd, name)); + } + super.visit(ecd, ignore); + } + + public void visit(RequireJavadocVisitor this, AnnotationDeclaration ad, Void ignore) { + if (dont_require_private && ad.isPrivate()) { + return; + } + String name = ad.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting annotation %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(ad)) { + errors.add(errorString(ad, name)); + } + super.visit(ad, ignore); + } + + public void visit( + RequireJavadocVisitor this, AnnotationMemberDeclaration amd, Void ignore) { + String name = amd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting annotation member %s%n", name); + } + if (!dont_require_method && !hasJavadocComment(amd)) { + errors.add(errorString(amd, name)); + } + super.visit(amd, ignore); + } + + /** + * Return true if this method is annotated with {@code @Override}. + * + * @param md the method to check for an {@code @Override} annotation + * @return true if this method is annotated with {@code @Override} + */ + private boolean isOverride(MethodDeclaration md) { + for (AnnotationExpr anno : md.getAnnotations()) { + String annoName = anno.getName().toString(); + if (annoName.equals("Override") || annoName.equals("java.lang.Override")) { + return true; + } + } + return false; + } + } + + /** + * Return true if this node has a Javadoc comment. + * + * @param n the node to check for a Javadoc comment + * @return true if this node has a Javadoc comment + */ + private boolean hasJavadocComment(Node n) { + if (n instanceof NodeWithJavadoc && ((NodeWithJavadoc) n).hasJavaDocComment()) { + return true; + } + List orphans = new ArrayList<>(); + getOrphanCommentsBeforeThisChildNode(n, orphans); + for (Comment orphan : orphans) { + if (orphan.isJavadocComment()) { + return true; + } + } + Optional oc = n.getComment(); + if (oc.isPresent() + && (oc.get().isJavadocComment() || oc.get().getContent().startsWith("/**"))) { + return true; + } + return false; + } + + /** + * Get "orphan comments": comments before the comment before this node. For example, in + * + *

    {@code
    +     * /** ... *}{@code /
    +     * // text 1
    +     * // text 2
    +     * void m() { ... }
    +     * }
    + * + * the Javadoc comment and {@code // text 1} are an orphan comment, and only {@code // text2} is + * associated with the method. + * + * @param node the node whose orphan comments to collect + * @param result the list to add orphan comments to. Is side-effected by this method. The + * implementation uses this to minimize the diffs against upstream. + */ + private static // to provide such functionality in JavaParser proper. + void getOrphanCommentsBeforeThisChildNode(final Node node, List result) { + if (node instanceof Comment) { + return; + } + Node parent = node.getParentNode().orElse(null); + if (parent == null) { + return; + } + List everything = new LinkedList<>(parent.getChildNodes()); + sortByBeginPosition(everything); + int positionOfTheChild = -1; + for (int i = 0; i < everything.size(); i++) { + if (everything.get(i) == node) positionOfTheChild = i; + } + if (positionOfTheChild == -1) { + throw new AssertionError("I am not a child of my parent."); + } + int positionOfPreviousChild = -1; + for (int i = positionOfTheChild - 1; i >= 0 && positionOfPreviousChild == -1; i--) { + if (!(everything.get(i) instanceof Comment)) positionOfPreviousChild = i; + } + for (int i = positionOfPreviousChild + 1; i < positionOfTheChild; i++) { + Node nodeToPrint = everything.get(i); + if (!(nodeToPrint instanceof Comment)) + throw new RuntimeException( + "Expected comment, instead " + + nodeToPrint.getClass() + + ". Position of previous child: " + + positionOfPreviousChild + + ", position of child " + + positionOfTheChild); + result.add((Comment) nodeToPrint); + } + } +} diff --git a/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.lowerbound.LowerBoundChecker.ajava b/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.lowerbound.LowerBoundChecker.ajava new file mode 100644 index 000000000000..0e224203a8ab --- /dev/null +++ b/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.lowerbound.LowerBoundChecker.ajava @@ -0,0 +1,953 @@ +import static com.github.javaparser.utils.PositionUtils.sortByBeginPosition; + +import com.github.javaparser.ParseProblemException; +import com.github.javaparser.Position; +import com.github.javaparser.Range; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.AnnotationDeclaration; +import com.github.javaparser.ast.body.AnnotationMemberDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.EnumConstantDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.comments.Comment; +import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.ThisExpr; +import com.github.javaparser.ast.expr.UnaryExpr; +import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.stmt.ExpressionStmt; +import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.stmt.Statement; +import com.github.javaparser.ast.type.PrimitiveType; +import com.github.javaparser.ast.type.Type; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; + +import org.plumelib.options.Options; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * A program that issues an error for any class, constructor, method, or field that lacks a Javadoc + * comment. Does not issue a warning for methods annotated with {@code @Override}. See documentation + * at https://github.com/plume-lib/require-javadoc#readme. + */ +@org.checkerframework.framework.qual.AnnotatedFor( + "org.checkerframework.checker.index.lowerbound.LowerBoundChecker") +public class RequireJavadoc { + + /** Matches name of file or directory where no problems should be reported. */ + public Pattern exclude = null; + + // TODO: It would be nice to support matching fully-qualified class names, but matching + // packages will have to do for now. + /** + * Matches simple name of class/constructor/method/field, or full package name, where no + * problems should be reported. + */ + public Pattern dont_require = null; + + /** If true, don't check elements with private access. */ + public boolean dont_require_private; + + /** + * If true, don't check constructors with zero formal parameters. These are sometimes called + * "default constructors", though that term means a no-argument constructor that the compiler + * synthesized when the programmer didn't write one. + */ + public boolean dont_require_noarg_constructor; + + /** + * If true, don't check trivial getters and setters. + * + *

    Trivial getters and setters are of the form: + * + *

    {@code
    +     * SomeType getFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * SomeType foo() {
    +     *   return foo;
    +     * }
    +     *
    +     * void setFoo(SomeType foo) {
    +     *   this.foo = foo;
    +     * }
    +     *
    +     * boolean hasFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * boolean isFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * boolean notFoo() {
    +     *   return !foo;
    +     * }
    +     * }
    + */ + public boolean dont_require_trivial_properties; + + /** If true, don't check type declarations: classes, interfaces, enums, annotations. */ + public boolean dont_require_type; + + /** If true, don't check fields. */ + public boolean dont_require_field; + + /** If true, don't check methods, constructors, and annotation members. */ + public boolean dont_require_method; + + /** If true, warn if any package lacks a package-info.java file. */ + public boolean require_package_info; + + /** + * If true, print filenames relative to working directory. Setting this only has an effect if + * the command-line arguments were absolute pathnames, or no command-line arguments were + * supplied. + */ + public boolean relative = false; + + /** If true, output debug information. */ + public boolean verbose = false; + + /** All the errors this program will report. */ + private List errors = new ArrayList<>(); + + /** The Java files to be checked. */ + private List javaFiles = new ArrayList(); + + /** The current working directory, for making relative pathnames. */ + private Path workingDirRelative = Paths.get(""); + + /** The current working directory, for making relative pathnames. */ + private Path workingDirAbsolute = Paths.get("").toAbsolutePath(); + + /** + * The main entry point for the require-javadoc program. See documentation at https://github.com/plume-lib/require-javadoc#readme. + * + * @param args the command-line arguments; see the README file + */ + public static void main(String[] args) { + RequireJavadoc rj = new RequireJavadoc(); + Options options = + new Options( + "java org.plumelib.javadoc.RequireJavadoc [options] [directory-or-file ...]", + rj); + String[] remainingArgs = options.parse(true, args); + rj.setJavaFiles(remainingArgs); + for (Path javaFile : rj.javaFiles) { + if (rj.verbose) { + System.out.println("Checking " + javaFile); + } + try { + CompilationUnit cu = StaticJavaParser.parse(javaFile); + RequireJavadocVisitor visitor = rj.new RequireJavadocVisitor(javaFile); + visitor.visit(cu, null); + } catch (IOException e) { + System.out.println("Problem while reading " + javaFile + ": " + e.getMessage()); + System.exit(2); + } catch (ParseProblemException e) { + System.out.println("Problem while parsing " + javaFile + ": " + e.getMessage()); + System.exit(2); + } + } + for (String error : rj.errors) { + System.out.println(error); + } + System.exit(rj.errors.isEmpty() ? 0 : 1); + } + + /** Creates a new RequireJavadoc instance. */ + @org.checkerframework.dataflow.qual.SideEffectFree + private RequireJavadoc() {} + + /** + * Set the Java files to be processed from the command-line arguments. + * + * @param args the directories and files listed on the command line + */ + private void setJavaFiles(String[] args) { + if (args.length == 0) { + args = new String[] {workingDirAbsolute.toString()}; + } + FileVisitor walker = new JavaFilesVisitor(); + for (String arg : args) { + if (shouldExclude(arg)) { + continue; + } + Path p = Paths.get(arg); + File f = p.toFile(); + if (!f.exists()) { + System.out.println("File not found: " + f); + System.exit(2); + } + if (f.isDirectory()) { + try { + Files.walkFileTree(p, walker); + } catch (IOException e) { + System.out.println("Problem while reading " + f + ": " + e.getMessage()); + System.exit(2); + } + } else { + javaFiles.add(Paths.get(arg)); + } + } + javaFiles.sort(Comparator.comparing(Object::toString)); + Set missingPackageInfoFiles = new LinkedHashSet<>(); + if (require_package_info) { + for (Path javaFile : javaFiles) { + Path javaFileParent = javaFile.getParent(); + // Java 11 has Path.of() instead of creating a new File. + Path packageInfo = javaFileParent.resolve(new File("package-info.java").toPath()); + if (!javaFiles.contains(packageInfo)) { + missingPackageInfoFiles.add(packageInfo); + } + } + for (Path packageInfo : missingPackageInfoFiles) { + errors.add("missing package documentation: no file " + packageInfo); + } + } + } + + /** Collects files into the {@link #javaFiles} variable. */ + private class JavaFilesVisitor extends SimpleFileVisitor { + + /** Create a new JavaFilesVisitor. */ + public JavaFilesVisitor() {} + + public FileVisitResult visitFile( + JavaFilesVisitor this, Path file, BasicFileAttributes attr) { + if (attr.isRegularFile() && file.toString().endsWith(".java")) { + if (!shouldExclude(file)) { + javaFiles.add(file); + } + } + return FileVisitResult.CONTINUE; + } + + public FileVisitResult preVisitDirectory( + JavaFilesVisitor this, Path dir, BasicFileAttributes attr) { + if (shouldExclude(dir)) { + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + + @org.checkerframework.framework.qual.EnsuresQualifier( + expression = {"#2"}, + qualifier = org.checkerframework.checker.index.qual.LowerBoundBottom.class) + public FileVisitResult postVisitDirectory( + JavaFilesVisitor this, Path dir, IOException exc) { + if (exc != null) { + System.out.println("Problem visiting " + dir + ": " + exc.getMessage()); + System.exit(2); + } + return FileVisitResult.CONTINUE; + } + + @org.checkerframework.framework.qual.EnsuresQualifier( + expression = {"#2"}, + qualifier = org.checkerframework.checker.index.qual.LowerBoundBottom.class) + public FileVisitResult visitFileFailed(JavaFilesVisitor this, Path file, IOException exc) { + if (exc != null) { + System.out.println("Problem visiting " + file + ": " + exc.getMessage()); + System.exit(2); + } + return FileVisitResult.CONTINUE; + } + } + + /** + * Return true if the given Java element should not be checked, based on the {@code + * --dont-require} command-line argument. + * + * @param name the name of a Java element. It is a simple name, except for packages. + * @return true if no warnings should be issued about the element + */ + private boolean shouldNotRequire(String name) { + if (dont_require == null) { + return false; + } + boolean result = dont_require.matcher(name).find(); + if (verbose) { + System.out.printf("shouldNotRequire(%s) => %s%n", name, result); + } + return result; + } + + /** + * Return true if the given file or directory should be skipped, based on the {@code --exclude} + * command-line argument. + * + * @param fileName the name of a Java file or directory + * @return true if the file or directory should be skipped + */ + private boolean shouldExclude(String fileName) { + if (exclude == null) { + return false; + } + boolean result = exclude.matcher(fileName).find(); + if (verbose) { + System.out.printf("shouldExclude(%s) => %s%n", fileName, result); + } + return result; + } + + /** + * Return true if the given file or directory should be skipped, based on the {@code --exclude} + * command-line argument. + * + * @param path a Java file or directory + * @return true if the file or directory should be skipped + */ + private boolean shouldExclude(Path path) { + return shouldExclude(path.toString()); + } + + /** A property method's return type. */ + private enum ReturnType { + + /** The return type is void. */ + VOID, + /** The return type is boolean. */ + BOOLEAN, + /** The return type is non-void. */ + NON_VOID + } + + /** The type of property method: a getter or setter. */ + private enum PropertyKind { + + /** A method of the form {@code SomeType getFoo()}. */ + GETTER("get", 0, ReturnType.NON_VOID), + /** A method of the form {@code SomeType foo()}. */ + GETTER_NO_PREFIX("", 0, ReturnType.NON_VOID), + /** A method of the form {@code boolean hasFoo()}. */ + GETTER_HAS("has", 0, ReturnType.BOOLEAN), + /** A method of the form {@code boolean isFoo()}. */ + GETTER_IS("is", 0, ReturnType.BOOLEAN), + /** A method of the form {@code boolean notFoo()}. */ + GETTER_NOT("not", 0, ReturnType.BOOLEAN), + /** A method of the form {@code void setFoo(SomeType arg)}. */ + SETTER("set", 1, ReturnType.VOID), + /** Not a getter or setter. */ + NOT_PROPERTY("", -1, ReturnType.VOID); + + /** The prefix for the method name: "get", "", "has", "is", "not", or "set". */ + final String prefix; + + /** The number of required formal parameters: 0 or 1. */ + final @org.checkerframework.checker.index.qual.GTENegativeOne int requiredParams; + + /** The return type. */ + final ReturnType returnType; + + /** + * Create a new PropertyKind. + * + * @param prefix the prefix for the method name: "get", "has", "is", "not", or "set" + * @param requiredParams the number of required formal parameters: 0 or 1 + * @param returnType the return type + */ + PropertyKind( + String prefix, + @org.checkerframework.checker.index.qual.GTENegativeOne int requiredParams, + ReturnType returnType) { + this.prefix = prefix; + this.requiredParams = requiredParams; + this.returnType = returnType; + } + + /** + * Returns true if this is a getter. + * + * @return true if this is a getter + */ + @org.checkerframework.dataflow.qual.Pure + boolean isGetter() { + return this != SETTER; + } + + /** + * Return the PropertyKind for the given method, or null if it isn't a property accessor + * method. + * + * @param md the method to check + * @return the PropertyKind for the given method, or null + */ + static PropertyKind fromMethodDeclaration(MethodDeclaration md) { + String methodName = md.getNameAsString(); + if (methodName.startsWith("get")) { + return GETTER; + } else if (methodName.startsWith("has")) { + return GETTER_HAS; + } else if (methodName.startsWith("is")) { + return GETTER_IS; + } else if (methodName.startsWith("not")) { + return GETTER_NOT; + } else if (methodName.startsWith("set")) { + return SETTER; + } else { + return GETTER_NO_PREFIX; + } + } + } + + /** + * Return true if this method declaration is a trivial getter or setter. + * + *
      + *
    • A trivial getter is named {@code getFoo}, {@code foo}, {@code hasFoo}, {@code isFoo}, + * or {@code notFoo}, has no formal parameters, and has a body of the form {@code return + * foo} or {@code return this.foo} (except for {@code notFoo}, in which case the body is + * negated). + *
    • A trivial setter is named {@code setFoo}, has one formal parameter named {@code foo}, + * and has a body of the form {@code this.foo = foo}. + *
    + * + * @param md the method to check + * @return true if this method is a trivial getter or setter + */ + private boolean isTrivialGetterOrSetter(MethodDeclaration md) { + PropertyKind kind = PropertyKind.fromMethodDeclaration(md); + if (kind != PropertyKind.GETTER_NO_PREFIX) { + if (isTrivialGetterOrSetter(md, kind)) { + return true; + } + } + return isTrivialGetterOrSetter(md, PropertyKind.GETTER_NO_PREFIX); + } + + /** + * Return true if this method declaration is a trivial getter or setter of the given kind. + * + * @see #isTrivialGetterOrSetter(MethodDeclaration) + * @param md the method to check + * @param propertyKind the kind of property + * @return true if this method is a trivial getter or setter + */ + private @org.checkerframework.checker.index.qual.LowerBoundBottom boolean + isTrivialGetterOrSetter(MethodDeclaration md, PropertyKind propertyKind) { + String propertyName = propertyName(md, propertyKind); + return propertyName != null + && hasCorrectSignature(md, propertyKind, propertyName) + && hasCorrectBody(md, propertyKind, propertyName); + } + + /** + * Returns the name of the property, if the method is a getter or setter of the given kind. + * Otherwise returns null. + * + *

    Examines the method's name, but not its signature or body. Also does not check that the + * given property name corresponds to an existing field. + * + * @param md the method to test + * @param propertyKind the type of property method + * @return the name of the property, or null + */ + private String propertyName(MethodDeclaration md, PropertyKind propertyKind) { + String methodName = md.getNameAsString(); + assert methodName.startsWith(propertyKind.prefix); + String upperCamelCaseProperty = methodName.substring(propertyKind.prefix.length()); + if (upperCamelCaseProperty.length() == 0) { + return null; + } + if (propertyKind == PropertyKind.GETTER_NO_PREFIX) { + return upperCamelCaseProperty; + } else if (!Character.isUpperCase(upperCamelCaseProperty.charAt(0))) { + return null; + } else { + return "" + + Character.toLowerCase(upperCamelCaseProperty.charAt(0)) + + upperCamelCaseProperty.substring(1); + } + } + + /** + * Returns true if the signature of the given method is a property accessor of the given kind. + * + * @param md the method + * @param propertyKind the kind of property + * @param propertyName the name of the property + * @return true if the body of the given method is a property accessor + */ + private boolean hasCorrectSignature( + MethodDeclaration md, PropertyKind propertyKind, String propertyName) { + NodeList parameters = md.getParameters(); + if (parameters.size() != propertyKind.requiredParams) { + return false; + } + if (parameters.size() == 1) { + Parameter parameter = parameters.get(0); + if (!parameter.getNameAsString().equals(propertyName)) { + return false; + } + } + // Check presence/absence of return type. (The Java compiler will verify + // that the type is consistent with the method body.) + Type returnType = md.getType(); + switch (propertyKind.returnType) { + case VOID: + if (!returnType.isVoidType()) { + return false; + } + break; + case BOOLEAN: + if (!returnType.equals(PrimitiveType.booleanType())) { + return false; + } + break; + case NON_VOID: + if (returnType.isVoidType()) { + return false; + } + break; + default: + throw new Error("Unexpected enum value " + propertyKind.returnType); + } + return true; + } + + /** + * Returns true if the body of the given method is a property accessor of the given kind. + * + * @param md the method + * @param propertyKind the kind of property + * @param propertyName the name of the property + * @return true if the body of the given method is a property accessor + */ + private boolean hasCorrectBody( + MethodDeclaration md, PropertyKind propertyKind, String propertyName) { + Statement statement = getOnlyStatement(md); + if (propertyKind.isGetter()) { + if (!(statement instanceof ReturnStmt)) { + return false; + } + Optional oReturnExpr = ((ReturnStmt) statement).getExpression(); + if (!oReturnExpr.isPresent()) { + return false; + } + Expression returnExpr = oReturnExpr.get(); + // Does not handle parentheses. + if (propertyKind == PropertyKind.GETTER_NOT) { + if (!(returnExpr instanceof UnaryExpr)) { + return false; + } + UnaryExpr unary = (UnaryExpr) returnExpr; + if (unary.getOperator() != UnaryExpr.Operator.LOGICAL_COMPLEMENT) { + return false; + } + returnExpr = unary.getExpression(); + } + String returnName; + // Does not handle parentheses. + if (returnExpr instanceof NameExpr) { + returnName = ((NameExpr) returnExpr).getNameAsString(); + } else if (returnExpr instanceof FieldAccessExpr) { + FieldAccessExpr fa = (FieldAccessExpr) returnExpr; + Expression receiver = fa.getScope(); + if (!(receiver instanceof ThisExpr)) { + return false; + } + returnName = fa.getNameAsString(); + } else { + return false; + } + if (!returnName.equals(propertyName)) { + return false; + } + return true; + } else if (propertyKind == PropertyKind.SETTER) { + if (!(statement instanceof ExpressionStmt)) { + return false; + } + Expression expr = ((ExpressionStmt) statement).getExpression(); + if (!(expr instanceof AssignExpr)) { + return false; + } + AssignExpr assignExpr = (AssignExpr) expr; + Expression target = assignExpr.getTarget(); + AssignExpr.Operator op = assignExpr.getOperator(); + Expression value = assignExpr.getValue(); + if (!(target instanceof FieldAccessExpr)) { + return false; + } + FieldAccessExpr fa = (FieldAccessExpr) target; + Expression receiver = fa.getScope(); + if (!(receiver instanceof ThisExpr)) { + return false; + } + if (!fa.getNameAsString().equals(propertyName)) { + return false; + } + if (op != AssignExpr.Operator.ASSIGN) { + return false; + } + if (!(value instanceof NameExpr + && ((NameExpr) value).getNameAsString().equals(propertyName))) { + return false; + } + return true; + } else { + throw new Error("unexpected PropertyKind " + propertyKind); + } + } + + /** + * If the body contains exactly one statement, returns it. Otherwise, returns null. + * + * @param md a method declaration + * @return its sole statement, or null + */ + private Statement getOnlyStatement(MethodDeclaration md) { + Optional body = md.getBody(); + if (!body.isPresent()) { + return null; + } + NodeList statements = body.get().getStatements(); + if (statements.size() != 1) { + return null; + } + return statements.get(0); + } + + /** Visits an AST and collects warnings about missing Javadoc. */ + private class RequireJavadocVisitor extends VoidVisitorAdapter { + + /** The file being visited. Used for constructing error messages. */ + private Path filename; + + /** + * Create a new RequireJavadocVisitor. + * + * @param filename the file being visited; used for diagnostic messages + */ + public RequireJavadocVisitor(Path filename) { + this.filename = filename; + } + + /** + * Return a string stating that documentation is missing on the given construct. + * + * @param node a Java language construct (class, constructor, method, field, etc.) + * @param simpleName the construct's simple name, used in diagnostic messages + * @return an error message for the given construct + */ + private String errorString(Node node, String simpleName) { + Optional range = node.getRange(); + if (range.isPresent()) { + Position begin = range.get().begin; + Path path = + (relative + ? (filename.isAbsolute() ? workingDirAbsolute : workingDirRelative) + .relativize(filename) + : filename); + return String.format( + "%s:%d:%d: missing documentation for %s", + path, begin.line, begin.column, simpleName); + } else { + return "missing documentation for " + simpleName; + } + } + + public void visit(RequireJavadocVisitor this, CompilationUnit cu, Void ignore) { + Optional opd = cu.getPackageDeclaration(); + if (opd.isPresent()) { + String packageName = opd.get().getName().asString(); + if (shouldNotRequire(packageName)) { + return; + } + Optional oTypeName = cu.getPrimaryTypeName(); + if (oTypeName.isPresent() + && oTypeName.get().equals("package-info") + && !hasJavadocComment(opd.get()) + && !hasJavadocComment(cu)) { + errors.add(errorString(opd.get(), packageName)); + } + } + if (verbose) { + System.out.printf("Visiting compilation unit%n"); + } + super.visit(cu, ignore); + } + + public void visit(RequireJavadocVisitor this, ClassOrInterfaceDeclaration cd, Void ignore) { + if (dont_require_private && cd.isPrivate()) { + return; + } + String name = cd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting type %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(cd)) { + errors.add(errorString(cd, name)); + } + super.visit(cd, ignore); + } + + public void visit(RequireJavadocVisitor this, ConstructorDeclaration cd, Void ignore) { + if (dont_require_private && cd.isPrivate()) { + return; + } + if (dont_require_noarg_constructor && cd.getParameters().size() == 0) { + return; + } + String name = cd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting constructor %s%n", name); + } + if (!dont_require_method && !hasJavadocComment(cd)) { + errors.add(errorString(cd, name)); + } + super.visit(cd, ignore); + } + + public void visit(RequireJavadocVisitor this, MethodDeclaration md, Void ignore) { + if (dont_require_private && md.isPrivate()) { + return; + } + if (dont_require_trivial_properties && isTrivialGetterOrSetter(md)) { + if (verbose) { + System.out.printf( + "skipping trivial property method %s%n", md.getNameAsString()); + } + return; + } + String name = md.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting method %s%n", md.getName()); + } + if (!dont_require_method && !isOverride(md) && !hasJavadocComment(md)) { + errors.add(errorString(md, name)); + } + super.visit(md, ignore); + } + + public void visit(RequireJavadocVisitor this, FieldDeclaration fd, Void ignore) { + if (dont_require_private && fd.isPrivate()) { + return; + } + // True if shouldNotRequire is false for at least one of the fields + boolean shouldRequire = false; + if (verbose) { + System.out.printf("Visiting field %s%n", fd.getVariables().get(0).getName()); + } + boolean hasJavadocComment = hasJavadocComment(fd); + for (VariableDeclarator vd : fd.getVariables()) { + String name = vd.getNameAsString(); + // TODO: Also check the type of the serialVersionUID variable. + if (name.equals("serialVersionUID")) { + continue; + } + if (shouldNotRequire(name)) { + continue; + } + shouldRequire = true; + if (!dont_require_field && !hasJavadocComment) { + errors.add(errorString(vd, name)); + } + } + if (shouldRequire) { + super.visit(fd, ignore); + } + } + + public void visit(RequireJavadocVisitor this, EnumDeclaration ed, Void ignore) { + if (dont_require_private && ed.isPrivate()) { + return; + } + String name = ed.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting enum %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(ed)) { + errors.add(errorString(ed, name)); + } + super.visit(ed, ignore); + } + + public void visit(RequireJavadocVisitor this, EnumConstantDeclaration ecd, Void ignore) { + String name = ecd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting enum constant %s%n", name); + } + if (!dont_require_field && !hasJavadocComment(ecd)) { + errors.add(errorString(ecd, name)); + } + super.visit(ecd, ignore); + } + + public void visit(RequireJavadocVisitor this, AnnotationDeclaration ad, Void ignore) { + if (dont_require_private && ad.isPrivate()) { + return; + } + String name = ad.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting annotation %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(ad)) { + errors.add(errorString(ad, name)); + } + super.visit(ad, ignore); + } + + public void visit( + RequireJavadocVisitor this, AnnotationMemberDeclaration amd, Void ignore) { + String name = amd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting annotation member %s%n", name); + } + if (!dont_require_method && !hasJavadocComment(amd)) { + errors.add(errorString(amd, name)); + } + super.visit(amd, ignore); + } + + /** + * Return true if this method is annotated with {@code @Override}. + * + * @param md the method to check for an {@code @Override} annotation + * @return true if this method is annotated with {@code @Override} + */ + private boolean isOverride(MethodDeclaration md) { + for (AnnotationExpr anno : md.getAnnotations()) { + String annoName = anno.getName().toString(); + if (annoName.equals("Override") || annoName.equals("java.lang.Override")) { + return true; + } + } + return false; + } + } + + /** + * Return true if this node has a Javadoc comment. + * + * @param n the node to check for a Javadoc comment + * @return true if this node has a Javadoc comment + */ + private boolean hasJavadocComment(Node n) { + if (n instanceof NodeWithJavadoc && ((NodeWithJavadoc) n).hasJavaDocComment()) { + return true; + } + List orphans = new ArrayList<>(); + getOrphanCommentsBeforeThisChildNode(n, orphans); + for (Comment orphan : orphans) { + if (orphan.isJavadocComment()) { + return true; + } + } + Optional oc = n.getComment(); + if (oc.isPresent() + && (oc.get().isJavadocComment() || oc.get().getContent().startsWith("/**"))) { + return true; + } + return false; + } + + /** + * Get "orphan comments": comments before the comment before this node. For example, in + * + *

    {@code
    +     * /** ... *}{@code /
    +     * // text 1
    +     * // text 2
    +     * void m() { ... }
    +     * }
    + * + * the Javadoc comment and {@code // text 1} are an orphan comment, and only {@code // text2} is + * associated with the method. + * + * @param node the node whose orphan comments to collect + * @param result the list to add orphan comments to. Is side-effected by this method. The + * implementation uses this to minimize the diffs against upstream. + */ + private static // to provide such functionality in JavaParser proper. + void getOrphanCommentsBeforeThisChildNode(final Node node, List result) { + if (node instanceof Comment) { + return; + } + Node parent = node.getParentNode().orElse(null); + if (parent == null) { + return; + } + List everything = new LinkedList<>(parent.getChildNodes()); + sortByBeginPosition(everything); + int positionOfTheChild = -1; + for (int i = 0; i < everything.size(); i++) { + if (everything.get(i) == node) positionOfTheChild = i; + } + if (positionOfTheChild == -1) { + throw new AssertionError("I am not a child of my parent."); + } + int positionOfPreviousChild = -1; + for (int i = positionOfTheChild - 1; i >= 0 && positionOfPreviousChild == -1; i--) { + if (!(everything.get(i) instanceof Comment)) positionOfPreviousChild = i; + } + for (int i = positionOfPreviousChild + 1; i < positionOfTheChild; i++) { + Node nodeToPrint = everything.get(i); + if (!(nodeToPrint instanceof Comment)) + throw new RuntimeException( + "Expected comment, instead " + + nodeToPrint.getClass() + + ". Position of previous child: " + + positionOfPreviousChild + + ", position of child " + + positionOfTheChild); + result.add((Comment) nodeToPrint); + } + } +} diff --git a/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.searchindex.SearchIndexChecker.ajava b/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.searchindex.SearchIndexChecker.ajava new file mode 100644 index 000000000000..020c5fdb2735 --- /dev/null +++ b/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.searchindex.SearchIndexChecker.ajava @@ -0,0 +1,949 @@ +import static com.github.javaparser.utils.PositionUtils.sortByBeginPosition; + +import com.github.javaparser.ParseProblemException; +import com.github.javaparser.Position; +import com.github.javaparser.Range; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.AnnotationDeclaration; +import com.github.javaparser.ast.body.AnnotationMemberDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.EnumConstantDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.comments.Comment; +import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.ThisExpr; +import com.github.javaparser.ast.expr.UnaryExpr; +import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.stmt.ExpressionStmt; +import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.stmt.Statement; +import com.github.javaparser.ast.type.PrimitiveType; +import com.github.javaparser.ast.type.Type; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; + +import org.plumelib.options.Options; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * A program that issues an error for any class, constructor, method, or field that lacks a Javadoc + * comment. Does not issue a warning for methods annotated with {@code @Override}. See documentation + * at https://github.com/plume-lib/require-javadoc#readme. + */ +@org.checkerframework.framework.qual.AnnotatedFor( + "org.checkerframework.checker.index.searchindex.SearchIndexChecker") +public class RequireJavadoc { + + /** Matches name of file or directory where no problems should be reported. */ + public Pattern exclude = null; + + // TODO: It would be nice to support matching fully-qualified class names, but matching + // packages will have to do for now. + /** + * Matches simple name of class/constructor/method/field, or full package name, where no + * problems should be reported. + */ + public Pattern dont_require = null; + + /** If true, don't check elements with private access. */ + public boolean dont_require_private; + + /** + * If true, don't check constructors with zero formal parameters. These are sometimes called + * "default constructors", though that term means a no-argument constructor that the compiler + * synthesized when the programmer didn't write one. + */ + public boolean dont_require_noarg_constructor; + + /** + * If true, don't check trivial getters and setters. + * + *

    Trivial getters and setters are of the form: + * + *

    {@code
    +     * SomeType getFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * SomeType foo() {
    +     *   return foo;
    +     * }
    +     *
    +     * void setFoo(SomeType foo) {
    +     *   this.foo = foo;
    +     * }
    +     *
    +     * boolean hasFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * boolean isFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * boolean notFoo() {
    +     *   return !foo;
    +     * }
    +     * }
    + */ + public boolean dont_require_trivial_properties; + + /** If true, don't check type declarations: classes, interfaces, enums, annotations. */ + public boolean dont_require_type; + + /** If true, don't check fields. */ + public boolean dont_require_field; + + /** If true, don't check methods, constructors, and annotation members. */ + public boolean dont_require_method; + + /** If true, warn if any package lacks a package-info.java file. */ + public boolean require_package_info; + + /** + * If true, print filenames relative to working directory. Setting this only has an effect if + * the command-line arguments were absolute pathnames, or no command-line arguments were + * supplied. + */ + public boolean relative = false; + + /** If true, output debug information. */ + public boolean verbose = false; + + /** All the errors this program will report. */ + private List errors = new ArrayList<>(); + + /** The Java files to be checked. */ + private List javaFiles = new ArrayList(); + + /** The current working directory, for making relative pathnames. */ + private Path workingDirRelative = Paths.get(""); + + /** The current working directory, for making relative pathnames. */ + private Path workingDirAbsolute = Paths.get("").toAbsolutePath(); + + /** + * The main entry point for the require-javadoc program. See documentation at https://github.com/plume-lib/require-javadoc#readme. + * + * @param args the command-line arguments; see the README file + */ + public static void main(String[] args) { + RequireJavadoc rj = new RequireJavadoc(); + Options options = + new Options( + "java org.plumelib.javadoc.RequireJavadoc [options] [directory-or-file ...]", + rj); + String[] remainingArgs = options.parse(true, args); + rj.setJavaFiles(remainingArgs); + for (Path javaFile : rj.javaFiles) { + if (rj.verbose) { + System.out.println("Checking " + javaFile); + } + try { + CompilationUnit cu = StaticJavaParser.parse(javaFile); + RequireJavadocVisitor visitor = rj.new RequireJavadocVisitor(javaFile); + visitor.visit(cu, null); + } catch (IOException e) { + System.out.println("Problem while reading " + javaFile + ": " + e.getMessage()); + System.exit(2); + } catch (ParseProblemException e) { + System.out.println("Problem while parsing " + javaFile + ": " + e.getMessage()); + System.exit(2); + } + } + for (String error : rj.errors) { + System.out.println(error); + } + System.exit(rj.errors.isEmpty() ? 0 : 1); + } + + /** Creates a new RequireJavadoc instance. */ + @org.checkerframework.dataflow.qual.SideEffectFree + private RequireJavadoc() {} + + /** + * Set the Java files to be processed from the command-line arguments. + * + * @param args the directories and files listed on the command line + */ + private void setJavaFiles(String[] args) { + if (args.length == 0) { + args = new String[] {workingDirAbsolute.toString()}; + } + FileVisitor walker = new JavaFilesVisitor(); + for (String arg : args) { + if (shouldExclude(arg)) { + continue; + } + Path p = Paths.get(arg); + File f = p.toFile(); + if (!f.exists()) { + System.out.println("File not found: " + f); + System.exit(2); + } + if (f.isDirectory()) { + try { + Files.walkFileTree(p, walker); + } catch (IOException e) { + System.out.println("Problem while reading " + f + ": " + e.getMessage()); + System.exit(2); + } + } else { + javaFiles.add(Paths.get(arg)); + } + } + javaFiles.sort(Comparator.comparing(Object::toString)); + Set missingPackageInfoFiles = new LinkedHashSet<>(); + if (require_package_info) { + for (Path javaFile : javaFiles) { + Path javaFileParent = javaFile.getParent(); + // Java 11 has Path.of() instead of creating a new File. + Path packageInfo = javaFileParent.resolve(new File("package-info.java").toPath()); + if (!javaFiles.contains(packageInfo)) { + missingPackageInfoFiles.add(packageInfo); + } + } + for (Path packageInfo : missingPackageInfoFiles) { + errors.add("missing package documentation: no file " + packageInfo); + } + } + } + + /** Collects files into the {@link #javaFiles} variable. */ + private class JavaFilesVisitor extends SimpleFileVisitor { + + /** Create a new JavaFilesVisitor. */ + public JavaFilesVisitor() {} + + public FileVisitResult visitFile( + JavaFilesVisitor this, Path file, BasicFileAttributes attr) { + if (attr.isRegularFile() && file.toString().endsWith(".java")) { + if (!shouldExclude(file)) { + javaFiles.add(file); + } + } + return FileVisitResult.CONTINUE; + } + + public FileVisitResult preVisitDirectory( + JavaFilesVisitor this, Path dir, BasicFileAttributes attr) { + if (shouldExclude(dir)) { + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + + @org.checkerframework.framework.qual.EnsuresQualifier( + expression = {"#2"}, + qualifier = org.checkerframework.checker.index.qual.SearchIndexBottom.class) + public FileVisitResult postVisitDirectory( + JavaFilesVisitor this, Path dir, IOException exc) { + if (exc != null) { + System.out.println("Problem visiting " + dir + ": " + exc.getMessage()); + System.exit(2); + } + return FileVisitResult.CONTINUE; + } + + @org.checkerframework.framework.qual.EnsuresQualifier( + expression = {"#2"}, + qualifier = org.checkerframework.checker.index.qual.SearchIndexBottom.class) + public FileVisitResult visitFileFailed(JavaFilesVisitor this, Path file, IOException exc) { + if (exc != null) { + System.out.println("Problem visiting " + file + ": " + exc.getMessage()); + System.exit(2); + } + return FileVisitResult.CONTINUE; + } + } + + /** + * Return true if the given Java element should not be checked, based on the {@code + * --dont-require} command-line argument. + * + * @param name the name of a Java element. It is a simple name, except for packages. + * @return true if no warnings should be issued about the element + */ + private boolean shouldNotRequire(String name) { + if (dont_require == null) { + return false; + } + boolean result = dont_require.matcher(name).find(); + if (verbose) { + System.out.printf("shouldNotRequire(%s) => %s%n", name, result); + } + return result; + } + + /** + * Return true if the given file or directory should be skipped, based on the {@code --exclude} + * command-line argument. + * + * @param fileName the name of a Java file or directory + * @return true if the file or directory should be skipped + */ + private boolean shouldExclude(String fileName) { + if (exclude == null) { + return false; + } + boolean result = exclude.matcher(fileName).find(); + if (verbose) { + System.out.printf("shouldExclude(%s) => %s%n", fileName, result); + } + return result; + } + + /** + * Return true if the given file or directory should be skipped, based on the {@code --exclude} + * command-line argument. + * + * @param path a Java file or directory + * @return true if the file or directory should be skipped + */ + private boolean shouldExclude(Path path) { + return shouldExclude(path.toString()); + } + + /** A property method's return type. */ + private enum ReturnType { + + /** The return type is void. */ + VOID, + /** The return type is boolean. */ + BOOLEAN, + /** The return type is non-void. */ + NON_VOID + } + + /** The type of property method: a getter or setter. */ + private enum PropertyKind { + + /** A method of the form {@code SomeType getFoo()}. */ + GETTER("get", 0, ReturnType.NON_VOID), + /** A method of the form {@code SomeType foo()}. */ + GETTER_NO_PREFIX("", 0, ReturnType.NON_VOID), + /** A method of the form {@code boolean hasFoo()}. */ + GETTER_HAS("has", 0, ReturnType.BOOLEAN), + /** A method of the form {@code boolean isFoo()}. */ + GETTER_IS("is", 0, ReturnType.BOOLEAN), + /** A method of the form {@code boolean notFoo()}. */ + GETTER_NOT("not", 0, ReturnType.BOOLEAN), + /** A method of the form {@code void setFoo(SomeType arg)}. */ + SETTER("set", 1, ReturnType.VOID), + /** Not a getter or setter. */ + NOT_PROPERTY("", -1, ReturnType.VOID); + + /** The prefix for the method name: "get", "", "has", "is", "not", or "set". */ + final String prefix; + + /** The number of required formal parameters: 0 or 1. */ + final int requiredParams; + + /** The return type. */ + final ReturnType returnType; + + /** + * Create a new PropertyKind. + * + * @param prefix the prefix for the method name: "get", "has", "is", "not", or "set" + * @param requiredParams the number of required formal parameters: 0 or 1 + * @param returnType the return type + */ + PropertyKind(String prefix, int requiredParams, ReturnType returnType) { + this.prefix = prefix; + this.requiredParams = requiredParams; + this.returnType = returnType; + } + + /** + * Returns true if this is a getter. + * + * @return true if this is a getter + */ + @org.checkerframework.dataflow.qual.Pure + boolean isGetter() { + return this != SETTER; + } + + /** + * Return the PropertyKind for the given method, or null if it isn't a property accessor + * method. + * + * @param md the method to check + * @return the PropertyKind for the given method, or null + */ + static PropertyKind fromMethodDeclaration(MethodDeclaration md) { + String methodName = md.getNameAsString(); + if (methodName.startsWith("get")) { + return GETTER; + } else if (methodName.startsWith("has")) { + return GETTER_HAS; + } else if (methodName.startsWith("is")) { + return GETTER_IS; + } else if (methodName.startsWith("not")) { + return GETTER_NOT; + } else if (methodName.startsWith("set")) { + return SETTER; + } else { + return GETTER_NO_PREFIX; + } + } + } + + /** + * Return true if this method declaration is a trivial getter or setter. + * + *
      + *
    • A trivial getter is named {@code getFoo}, {@code foo}, {@code hasFoo}, {@code isFoo}, + * or {@code notFoo}, has no formal parameters, and has a body of the form {@code return + * foo} or {@code return this.foo} (except for {@code notFoo}, in which case the body is + * negated). + *
    • A trivial setter is named {@code setFoo}, has one formal parameter named {@code foo}, + * and has a body of the form {@code this.foo = foo}. + *
    + * + * @param md the method to check + * @return true if this method is a trivial getter or setter + */ + private boolean isTrivialGetterOrSetter(MethodDeclaration md) { + PropertyKind kind = PropertyKind.fromMethodDeclaration(md); + if (kind != PropertyKind.GETTER_NO_PREFIX) { + if (isTrivialGetterOrSetter(md, kind)) { + return true; + } + } + return isTrivialGetterOrSetter(md, PropertyKind.GETTER_NO_PREFIX); + } + + /** + * Return true if this method declaration is a trivial getter or setter of the given kind. + * + * @see #isTrivialGetterOrSetter(MethodDeclaration) + * @param md the method to check + * @param propertyKind the kind of property + * @return true if this method is a trivial getter or setter + */ + private boolean isTrivialGetterOrSetter(MethodDeclaration md, PropertyKind propertyKind) { + String propertyName = propertyName(md, propertyKind); + return propertyName != null + && hasCorrectSignature(md, propertyKind, propertyName) + && hasCorrectBody(md, propertyKind, propertyName); + } + + /** + * Returns the name of the property, if the method is a getter or setter of the given kind. + * Otherwise returns null. + * + *

    Examines the method's name, but not its signature or body. Also does not check that the + * given property name corresponds to an existing field. + * + * @param md the method to test + * @param propertyKind the type of property method + * @return the name of the property, or null + */ + private String propertyName(MethodDeclaration md, PropertyKind propertyKind) { + String methodName = md.getNameAsString(); + assert methodName.startsWith(propertyKind.prefix); + String upperCamelCaseProperty = methodName.substring(propertyKind.prefix.length()); + if (upperCamelCaseProperty.length() == 0) { + return null; + } + if (propertyKind == PropertyKind.GETTER_NO_PREFIX) { + return upperCamelCaseProperty; + } else if (!Character.isUpperCase(upperCamelCaseProperty.charAt(0))) { + return null; + } else { + return "" + + Character.toLowerCase(upperCamelCaseProperty.charAt(0)) + + upperCamelCaseProperty.substring(1); + } + } + + /** + * Returns true if the signature of the given method is a property accessor of the given kind. + * + * @param md the method + * @param propertyKind the kind of property + * @param propertyName the name of the property + * @return true if the body of the given method is a property accessor + */ + private boolean hasCorrectSignature( + MethodDeclaration md, PropertyKind propertyKind, String propertyName) { + NodeList parameters = md.getParameters(); + if (parameters.size() != propertyKind.requiredParams) { + return false; + } + if (parameters.size() == 1) { + Parameter parameter = parameters.get(0); + if (!parameter.getNameAsString().equals(propertyName)) { + return false; + } + } + // Check presence/absence of return type. (The Java compiler will verify + // that the type is consistent with the method body.) + Type returnType = md.getType(); + switch (propertyKind.returnType) { + case VOID: + if (!returnType.isVoidType()) { + return false; + } + break; + case BOOLEAN: + if (!returnType.equals(PrimitiveType.booleanType())) { + return false; + } + break; + case NON_VOID: + if (returnType.isVoidType()) { + return false; + } + break; + default: + throw new Error("Unexpected enum value " + propertyKind.returnType); + } + return true; + } + + /** + * Returns true if the body of the given method is a property accessor of the given kind. + * + * @param md the method + * @param propertyKind the kind of property + * @param propertyName the name of the property + * @return true if the body of the given method is a property accessor + */ + private boolean hasCorrectBody( + MethodDeclaration md, PropertyKind propertyKind, String propertyName) { + Statement statement = getOnlyStatement(md); + if (propertyKind.isGetter()) { + if (!(statement instanceof ReturnStmt)) { + return false; + } + Optional oReturnExpr = ((ReturnStmt) statement).getExpression(); + if (!oReturnExpr.isPresent()) { + return false; + } + Expression returnExpr = oReturnExpr.get(); + // Does not handle parentheses. + if (propertyKind == PropertyKind.GETTER_NOT) { + if (!(returnExpr instanceof UnaryExpr)) { + return false; + } + UnaryExpr unary = (UnaryExpr) returnExpr; + if (unary.getOperator() != UnaryExpr.Operator.LOGICAL_COMPLEMENT) { + return false; + } + returnExpr = unary.getExpression(); + } + String returnName; + // Does not handle parentheses. + if (returnExpr instanceof NameExpr) { + returnName = ((NameExpr) returnExpr).getNameAsString(); + } else if (returnExpr instanceof FieldAccessExpr) { + FieldAccessExpr fa = (FieldAccessExpr) returnExpr; + Expression receiver = fa.getScope(); + if (!(receiver instanceof ThisExpr)) { + return false; + } + returnName = fa.getNameAsString(); + } else { + return false; + } + if (!returnName.equals(propertyName)) { + return false; + } + return true; + } else if (propertyKind == PropertyKind.SETTER) { + if (!(statement instanceof ExpressionStmt)) { + return false; + } + Expression expr = ((ExpressionStmt) statement).getExpression(); + if (!(expr instanceof AssignExpr)) { + return false; + } + AssignExpr assignExpr = (AssignExpr) expr; + Expression target = assignExpr.getTarget(); + AssignExpr.Operator op = assignExpr.getOperator(); + Expression value = assignExpr.getValue(); + if (!(target instanceof FieldAccessExpr)) { + return false; + } + FieldAccessExpr fa = (FieldAccessExpr) target; + Expression receiver = fa.getScope(); + if (!(receiver instanceof ThisExpr)) { + return false; + } + if (!fa.getNameAsString().equals(propertyName)) { + return false; + } + if (op != AssignExpr.Operator.ASSIGN) { + return false; + } + if (!(value instanceof NameExpr + && ((NameExpr) value).getNameAsString().equals(propertyName))) { + return false; + } + return true; + } else { + throw new Error("unexpected PropertyKind " + propertyKind); + } + } + + /** + * If the body contains exactly one statement, returns it. Otherwise, returns null. + * + * @param md a method declaration + * @return its sole statement, or null + */ + private Statement getOnlyStatement(MethodDeclaration md) { + Optional body = md.getBody(); + if (!body.isPresent()) { + return null; + } + NodeList statements = body.get().getStatements(); + if (statements.size() != 1) { + return null; + } + return statements.get(0); + } + + /** Visits an AST and collects warnings about missing Javadoc. */ + private class RequireJavadocVisitor extends VoidVisitorAdapter { + + /** The file being visited. Used for constructing error messages. */ + private Path filename; + + /** + * Create a new RequireJavadocVisitor. + * + * @param filename the file being visited; used for diagnostic messages + */ + public RequireJavadocVisitor(Path filename) { + this.filename = filename; + } + + /** + * Return a string stating that documentation is missing on the given construct. + * + * @param node a Java language construct (class, constructor, method, field, etc.) + * @param simpleName the construct's simple name, used in diagnostic messages + * @return an error message for the given construct + */ + private String errorString(Node node, String simpleName) { + Optional range = node.getRange(); + if (range.isPresent()) { + Position begin = range.get().begin; + Path path = + (relative + ? (filename.isAbsolute() ? workingDirAbsolute : workingDirRelative) + .relativize(filename) + : filename); + return String.format( + "%s:%d:%d: missing documentation for %s", + path, begin.line, begin.column, simpleName); + } else { + return "missing documentation for " + simpleName; + } + } + + public void visit(RequireJavadocVisitor this, CompilationUnit cu, Void ignore) { + Optional opd = cu.getPackageDeclaration(); + if (opd.isPresent()) { + String packageName = opd.get().getName().asString(); + if (shouldNotRequire(packageName)) { + return; + } + Optional oTypeName = cu.getPrimaryTypeName(); + if (oTypeName.isPresent() + && oTypeName.get().equals("package-info") + && !hasJavadocComment(opd.get()) + && !hasJavadocComment(cu)) { + errors.add(errorString(opd.get(), packageName)); + } + } + if (verbose) { + System.out.printf("Visiting compilation unit%n"); + } + super.visit(cu, ignore); + } + + public void visit(RequireJavadocVisitor this, ClassOrInterfaceDeclaration cd, Void ignore) { + if (dont_require_private && cd.isPrivate()) { + return; + } + String name = cd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting type %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(cd)) { + errors.add(errorString(cd, name)); + } + super.visit(cd, ignore); + } + + public void visit(RequireJavadocVisitor this, ConstructorDeclaration cd, Void ignore) { + if (dont_require_private && cd.isPrivate()) { + return; + } + if (dont_require_noarg_constructor && cd.getParameters().size() == 0) { + return; + } + String name = cd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting constructor %s%n", name); + } + if (!dont_require_method && !hasJavadocComment(cd)) { + errors.add(errorString(cd, name)); + } + super.visit(cd, ignore); + } + + public void visit(RequireJavadocVisitor this, MethodDeclaration md, Void ignore) { + if (dont_require_private && md.isPrivate()) { + return; + } + if (dont_require_trivial_properties && isTrivialGetterOrSetter(md)) { + if (verbose) { + System.out.printf( + "skipping trivial property method %s%n", md.getNameAsString()); + } + return; + } + String name = md.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting method %s%n", md.getName()); + } + if (!dont_require_method && !isOverride(md) && !hasJavadocComment(md)) { + errors.add(errorString(md, name)); + } + super.visit(md, ignore); + } + + public void visit(RequireJavadocVisitor this, FieldDeclaration fd, Void ignore) { + if (dont_require_private && fd.isPrivate()) { + return; + } + // True if shouldNotRequire is false for at least one of the fields + boolean shouldRequire = false; + if (verbose) { + System.out.printf("Visiting field %s%n", fd.getVariables().get(0).getName()); + } + boolean hasJavadocComment = hasJavadocComment(fd); + for (VariableDeclarator vd : fd.getVariables()) { + String name = vd.getNameAsString(); + // TODO: Also check the type of the serialVersionUID variable. + if (name.equals("serialVersionUID")) { + continue; + } + if (shouldNotRequire(name)) { + continue; + } + shouldRequire = true; + if (!dont_require_field && !hasJavadocComment) { + errors.add(errorString(vd, name)); + } + } + if (shouldRequire) { + super.visit(fd, ignore); + } + } + + public void visit(RequireJavadocVisitor this, EnumDeclaration ed, Void ignore) { + if (dont_require_private && ed.isPrivate()) { + return; + } + String name = ed.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting enum %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(ed)) { + errors.add(errorString(ed, name)); + } + super.visit(ed, ignore); + } + + public void visit(RequireJavadocVisitor this, EnumConstantDeclaration ecd, Void ignore) { + String name = ecd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting enum constant %s%n", name); + } + if (!dont_require_field && !hasJavadocComment(ecd)) { + errors.add(errorString(ecd, name)); + } + super.visit(ecd, ignore); + } + + public void visit(RequireJavadocVisitor this, AnnotationDeclaration ad, Void ignore) { + if (dont_require_private && ad.isPrivate()) { + return; + } + String name = ad.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting annotation %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(ad)) { + errors.add(errorString(ad, name)); + } + super.visit(ad, ignore); + } + + public void visit( + RequireJavadocVisitor this, AnnotationMemberDeclaration amd, Void ignore) { + String name = amd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting annotation member %s%n", name); + } + if (!dont_require_method && !hasJavadocComment(amd)) { + errors.add(errorString(amd, name)); + } + super.visit(amd, ignore); + } + + /** + * Return true if this method is annotated with {@code @Override}. + * + * @param md the method to check for an {@code @Override} annotation + * @return true if this method is annotated with {@code @Override} + */ + private boolean isOverride(MethodDeclaration md) { + for (AnnotationExpr anno : md.getAnnotations()) { + String annoName = anno.getName().toString(); + if (annoName.equals("Override") || annoName.equals("java.lang.Override")) { + return true; + } + } + return false; + } + } + + /** + * Return true if this node has a Javadoc comment. + * + * @param n the node to check for a Javadoc comment + * @return true if this node has a Javadoc comment + */ + private boolean hasJavadocComment(Node n) { + if (n instanceof NodeWithJavadoc && ((NodeWithJavadoc) n).hasJavaDocComment()) { + return true; + } + List orphans = new ArrayList<>(); + getOrphanCommentsBeforeThisChildNode(n, orphans); + for (Comment orphan : orphans) { + if (orphan.isJavadocComment()) { + return true; + } + } + Optional oc = n.getComment(); + if (oc.isPresent() + && (oc.get().isJavadocComment() || oc.get().getContent().startsWith("/**"))) { + return true; + } + return false; + } + + /** + * Get "orphan comments": comments before the comment before this node. For example, in + * + *

    {@code
    +     * /** ... *}{@code /
    +     * // text 1
    +     * // text 2
    +     * void m() { ... }
    +     * }
    + * + * the Javadoc comment and {@code // text 1} are an orphan comment, and only {@code // text2} is + * associated with the method. + * + * @param node the node whose orphan comments to collect + * @param result the list to add orphan comments to. Is side-effected by this method. The + * implementation uses this to minimize the diffs against upstream. + */ + private static // to provide such functionality in JavaParser proper. + void getOrphanCommentsBeforeThisChildNode(final Node node, List result) { + if (node instanceof Comment) { + return; + } + Node parent = node.getParentNode().orElse(null); + if (parent == null) { + return; + } + List everything = new LinkedList<>(parent.getChildNodes()); + sortByBeginPosition(everything); + int positionOfTheChild = -1; + for (int i = 0; i < everything.size(); i++) { + if (everything.get(i) == node) positionOfTheChild = i; + } + if (positionOfTheChild == -1) { + throw new AssertionError("I am not a child of my parent."); + } + int positionOfPreviousChild = -1; + for (int i = positionOfTheChild - 1; i >= 0 && positionOfPreviousChild == -1; i--) { + if (!(everything.get(i) instanceof Comment)) positionOfPreviousChild = i; + } + for (int i = positionOfPreviousChild + 1; i < positionOfTheChild; i++) { + Node nodeToPrint = everything.get(i); + if (!(nodeToPrint instanceof Comment)) + throw new RuntimeException( + "Expected comment, instead " + + nodeToPrint.getClass() + + ". Position of previous child: " + + positionOfPreviousChild + + ", position of child " + + positionOfTheChild); + result.add((Comment) nodeToPrint); + } + } +} diff --git a/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.substringindex.SubstringIndexChecker.ajava b/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.substringindex.SubstringIndexChecker.ajava new file mode 100644 index 000000000000..5dece81dc7c2 --- /dev/null +++ b/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.checker.index.substringindex.SubstringIndexChecker.ajava @@ -0,0 +1,949 @@ +import static com.github.javaparser.utils.PositionUtils.sortByBeginPosition; + +import com.github.javaparser.ParseProblemException; +import com.github.javaparser.Position; +import com.github.javaparser.Range; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.AnnotationDeclaration; +import com.github.javaparser.ast.body.AnnotationMemberDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.EnumConstantDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.comments.Comment; +import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.ThisExpr; +import com.github.javaparser.ast.expr.UnaryExpr; +import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.stmt.ExpressionStmt; +import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.stmt.Statement; +import com.github.javaparser.ast.type.PrimitiveType; +import com.github.javaparser.ast.type.Type; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; + +import org.plumelib.options.Options; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * A program that issues an error for any class, constructor, method, or field that lacks a Javadoc + * comment. Does not issue a warning for methods annotated with {@code @Override}. See documentation + * at https://github.com/plume-lib/require-javadoc#readme. + */ +@org.checkerframework.framework.qual.AnnotatedFor( + "org.checkerframework.checker.index.substringindex.SubstringIndexChecker") +public class RequireJavadoc { + + /** Matches name of file or directory where no problems should be reported. */ + public Pattern exclude = null; + + // TODO: It would be nice to support matching fully-qualified class names, but matching + // packages will have to do for now. + /** + * Matches simple name of class/constructor/method/field, or full package name, where no + * problems should be reported. + */ + public Pattern dont_require = null; + + /** If true, don't check elements with private access. */ + public boolean dont_require_private; + + /** + * If true, don't check constructors with zero formal parameters. These are sometimes called + * "default constructors", though that term means a no-argument constructor that the compiler + * synthesized when the programmer didn't write one. + */ + public boolean dont_require_noarg_constructor; + + /** + * If true, don't check trivial getters and setters. + * + *

    Trivial getters and setters are of the form: + * + *

    {@code
    +     * SomeType getFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * SomeType foo() {
    +     *   return foo;
    +     * }
    +     *
    +     * void setFoo(SomeType foo) {
    +     *   this.foo = foo;
    +     * }
    +     *
    +     * boolean hasFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * boolean isFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * boolean notFoo() {
    +     *   return !foo;
    +     * }
    +     * }
    + */ + public boolean dont_require_trivial_properties; + + /** If true, don't check type declarations: classes, interfaces, enums, annotations. */ + public boolean dont_require_type; + + /** If true, don't check fields. */ + public boolean dont_require_field; + + /** If true, don't check methods, constructors, and annotation members. */ + public boolean dont_require_method; + + /** If true, warn if any package lacks a package-info.java file. */ + public boolean require_package_info; + + /** + * If true, print filenames relative to working directory. Setting this only has an effect if + * the command-line arguments were absolute pathnames, or no command-line arguments were + * supplied. + */ + public boolean relative = false; + + /** If true, output debug information. */ + public boolean verbose = false; + + /** All the errors this program will report. */ + private List errors = new ArrayList<>(); + + /** The Java files to be checked. */ + private List javaFiles = new ArrayList(); + + /** The current working directory, for making relative pathnames. */ + private Path workingDirRelative = Paths.get(""); + + /** The current working directory, for making relative pathnames. */ + private Path workingDirAbsolute = Paths.get("").toAbsolutePath(); + + /** + * The main entry point for the require-javadoc program. See documentation at https://github.com/plume-lib/require-javadoc#readme. + * + * @param args the command-line arguments; see the README file + */ + public static void main(String[] args) { + RequireJavadoc rj = new RequireJavadoc(); + Options options = + new Options( + "java org.plumelib.javadoc.RequireJavadoc [options] [directory-or-file ...]", + rj); + String[] remainingArgs = options.parse(true, args); + rj.setJavaFiles(remainingArgs); + for (Path javaFile : rj.javaFiles) { + if (rj.verbose) { + System.out.println("Checking " + javaFile); + } + try { + CompilationUnit cu = StaticJavaParser.parse(javaFile); + RequireJavadocVisitor visitor = rj.new RequireJavadocVisitor(javaFile); + visitor.visit(cu, null); + } catch (IOException e) { + System.out.println("Problem while reading " + javaFile + ": " + e.getMessage()); + System.exit(2); + } catch (ParseProblemException e) { + System.out.println("Problem while parsing " + javaFile + ": " + e.getMessage()); + System.exit(2); + } + } + for (String error : rj.errors) { + System.out.println(error); + } + System.exit(rj.errors.isEmpty() ? 0 : 1); + } + + /** Creates a new RequireJavadoc instance. */ + @org.checkerframework.dataflow.qual.SideEffectFree + private RequireJavadoc() {} + + /** + * Set the Java files to be processed from the command-line arguments. + * + * @param args the directories and files listed on the command line + */ + private void setJavaFiles(String[] args) { + if (args.length == 0) { + args = new String[] {workingDirAbsolute.toString()}; + } + FileVisitor walker = new JavaFilesVisitor(); + for (String arg : args) { + if (shouldExclude(arg)) { + continue; + } + Path p = Paths.get(arg); + File f = p.toFile(); + if (!f.exists()) { + System.out.println("File not found: " + f); + System.exit(2); + } + if (f.isDirectory()) { + try { + Files.walkFileTree(p, walker); + } catch (IOException e) { + System.out.println("Problem while reading " + f + ": " + e.getMessage()); + System.exit(2); + } + } else { + javaFiles.add(Paths.get(arg)); + } + } + javaFiles.sort(Comparator.comparing(Object::toString)); + Set missingPackageInfoFiles = new LinkedHashSet<>(); + if (require_package_info) { + for (Path javaFile : javaFiles) { + Path javaFileParent = javaFile.getParent(); + // Java 11 has Path.of() instead of creating a new File. + Path packageInfo = javaFileParent.resolve(new File("package-info.java").toPath()); + if (!javaFiles.contains(packageInfo)) { + missingPackageInfoFiles.add(packageInfo); + } + } + for (Path packageInfo : missingPackageInfoFiles) { + errors.add("missing package documentation: no file " + packageInfo); + } + } + } + + /** Collects files into the {@link #javaFiles} variable. */ + private class JavaFilesVisitor extends SimpleFileVisitor { + + /** Create a new JavaFilesVisitor. */ + public JavaFilesVisitor() {} + + public FileVisitResult visitFile( + JavaFilesVisitor this, Path file, BasicFileAttributes attr) { + if (attr.isRegularFile() && file.toString().endsWith(".java")) { + if (!shouldExclude(file)) { + javaFiles.add(file); + } + } + return FileVisitResult.CONTINUE; + } + + public FileVisitResult preVisitDirectory( + JavaFilesVisitor this, Path dir, BasicFileAttributes attr) { + if (shouldExclude(dir)) { + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + + @org.checkerframework.framework.qual.EnsuresQualifier( + expression = {"#2"}, + qualifier = org.checkerframework.checker.index.qual.SubstringIndexBottom.class) + public FileVisitResult postVisitDirectory( + JavaFilesVisitor this, Path dir, IOException exc) { + if (exc != null) { + System.out.println("Problem visiting " + dir + ": " + exc.getMessage()); + System.exit(2); + } + return FileVisitResult.CONTINUE; + } + + @org.checkerframework.framework.qual.EnsuresQualifier( + expression = {"#2"}, + qualifier = org.checkerframework.checker.index.qual.SubstringIndexBottom.class) + public FileVisitResult visitFileFailed(JavaFilesVisitor this, Path file, IOException exc) { + if (exc != null) { + System.out.println("Problem visiting " + file + ": " + exc.getMessage()); + System.exit(2); + } + return FileVisitResult.CONTINUE; + } + } + + /** + * Return true if the given Java element should not be checked, based on the {@code + * --dont-require} command-line argument. + * + * @param name the name of a Java element. It is a simple name, except for packages. + * @return true if no warnings should be issued about the element + */ + private boolean shouldNotRequire(String name) { + if (dont_require == null) { + return false; + } + boolean result = dont_require.matcher(name).find(); + if (verbose) { + System.out.printf("shouldNotRequire(%s) => %s%n", name, result); + } + return result; + } + + /** + * Return true if the given file or directory should be skipped, based on the {@code --exclude} + * command-line argument. + * + * @param fileName the name of a Java file or directory + * @return true if the file or directory should be skipped + */ + private boolean shouldExclude(String fileName) { + if (exclude == null) { + return false; + } + boolean result = exclude.matcher(fileName).find(); + if (verbose) { + System.out.printf("shouldExclude(%s) => %s%n", fileName, result); + } + return result; + } + + /** + * Return true if the given file or directory should be skipped, based on the {@code --exclude} + * command-line argument. + * + * @param path a Java file or directory + * @return true if the file or directory should be skipped + */ + private boolean shouldExclude(Path path) { + return shouldExclude(path.toString()); + } + + /** A property method's return type. */ + private enum ReturnType { + + /** The return type is void. */ + VOID, + /** The return type is boolean. */ + BOOLEAN, + /** The return type is non-void. */ + NON_VOID + } + + /** The type of property method: a getter or setter. */ + private enum PropertyKind { + + /** A method of the form {@code SomeType getFoo()}. */ + GETTER("get", 0, ReturnType.NON_VOID), + /** A method of the form {@code SomeType foo()}. */ + GETTER_NO_PREFIX("", 0, ReturnType.NON_VOID), + /** A method of the form {@code boolean hasFoo()}. */ + GETTER_HAS("has", 0, ReturnType.BOOLEAN), + /** A method of the form {@code boolean isFoo()}. */ + GETTER_IS("is", 0, ReturnType.BOOLEAN), + /** A method of the form {@code boolean notFoo()}. */ + GETTER_NOT("not", 0, ReturnType.BOOLEAN), + /** A method of the form {@code void setFoo(SomeType arg)}. */ + SETTER("set", 1, ReturnType.VOID), + /** Not a getter or setter. */ + NOT_PROPERTY("", -1, ReturnType.VOID); + + /** The prefix for the method name: "get", "", "has", "is", "not", or "set". */ + final String prefix; + + /** The number of required formal parameters: 0 or 1. */ + final int requiredParams; + + /** The return type. */ + final ReturnType returnType; + + /** + * Create a new PropertyKind. + * + * @param prefix the prefix for the method name: "get", "has", "is", "not", or "set" + * @param requiredParams the number of required formal parameters: 0 or 1 + * @param returnType the return type + */ + PropertyKind(String prefix, int requiredParams, ReturnType returnType) { + this.prefix = prefix; + this.requiredParams = requiredParams; + this.returnType = returnType; + } + + /** + * Returns true if this is a getter. + * + * @return true if this is a getter + */ + @org.checkerframework.dataflow.qual.Pure + boolean isGetter() { + return this != SETTER; + } + + /** + * Return the PropertyKind for the given method, or null if it isn't a property accessor + * method. + * + * @param md the method to check + * @return the PropertyKind for the given method, or null + */ + static PropertyKind fromMethodDeclaration(MethodDeclaration md) { + String methodName = md.getNameAsString(); + if (methodName.startsWith("get")) { + return GETTER; + } else if (methodName.startsWith("has")) { + return GETTER_HAS; + } else if (methodName.startsWith("is")) { + return GETTER_IS; + } else if (methodName.startsWith("not")) { + return GETTER_NOT; + } else if (methodName.startsWith("set")) { + return SETTER; + } else { + return GETTER_NO_PREFIX; + } + } + } + + /** + * Return true if this method declaration is a trivial getter or setter. + * + *
      + *
    • A trivial getter is named {@code getFoo}, {@code foo}, {@code hasFoo}, {@code isFoo}, + * or {@code notFoo}, has no formal parameters, and has a body of the form {@code return + * foo} or {@code return this.foo} (except for {@code notFoo}, in which case the body is + * negated). + *
    • A trivial setter is named {@code setFoo}, has one formal parameter named {@code foo}, + * and has a body of the form {@code this.foo = foo}. + *
    + * + * @param md the method to check + * @return true if this method is a trivial getter or setter + */ + private boolean isTrivialGetterOrSetter(MethodDeclaration md) { + PropertyKind kind = PropertyKind.fromMethodDeclaration(md); + if (kind != PropertyKind.GETTER_NO_PREFIX) { + if (isTrivialGetterOrSetter(md, kind)) { + return true; + } + } + return isTrivialGetterOrSetter(md, PropertyKind.GETTER_NO_PREFIX); + } + + /** + * Return true if this method declaration is a trivial getter or setter of the given kind. + * + * @see #isTrivialGetterOrSetter(MethodDeclaration) + * @param md the method to check + * @param propertyKind the kind of property + * @return true if this method is a trivial getter or setter + */ + private boolean isTrivialGetterOrSetter(MethodDeclaration md, PropertyKind propertyKind) { + String propertyName = propertyName(md, propertyKind); + return propertyName != null + && hasCorrectSignature(md, propertyKind, propertyName) + && hasCorrectBody(md, propertyKind, propertyName); + } + + /** + * Returns the name of the property, if the method is a getter or setter of the given kind. + * Otherwise returns null. + * + *

    Examines the method's name, but not its signature or body. Also does not check that the + * given property name corresponds to an existing field. + * + * @param md the method to test + * @param propertyKind the type of property method + * @return the name of the property, or null + */ + private String propertyName(MethodDeclaration md, PropertyKind propertyKind) { + String methodName = md.getNameAsString(); + assert methodName.startsWith(propertyKind.prefix); + String upperCamelCaseProperty = methodName.substring(propertyKind.prefix.length()); + if (upperCamelCaseProperty.length() == 0) { + return null; + } + if (propertyKind == PropertyKind.GETTER_NO_PREFIX) { + return upperCamelCaseProperty; + } else if (!Character.isUpperCase(upperCamelCaseProperty.charAt(0))) { + return null; + } else { + return "" + + Character.toLowerCase(upperCamelCaseProperty.charAt(0)) + + upperCamelCaseProperty.substring(1); + } + } + + /** + * Returns true if the signature of the given method is a property accessor of the given kind. + * + * @param md the method + * @param propertyKind the kind of property + * @param propertyName the name of the property + * @return true if the body of the given method is a property accessor + */ + private boolean hasCorrectSignature( + MethodDeclaration md, PropertyKind propertyKind, String propertyName) { + NodeList parameters = md.getParameters(); + if (parameters.size() != propertyKind.requiredParams) { + return false; + } + if (parameters.size() == 1) { + Parameter parameter = parameters.get(0); + if (!parameter.getNameAsString().equals(propertyName)) { + return false; + } + } + // Check presence/absence of return type. (The Java compiler will verify + // that the type is consistent with the method body.) + Type returnType = md.getType(); + switch (propertyKind.returnType) { + case VOID: + if (!returnType.isVoidType()) { + return false; + } + break; + case BOOLEAN: + if (!returnType.equals(PrimitiveType.booleanType())) { + return false; + } + break; + case NON_VOID: + if (returnType.isVoidType()) { + return false; + } + break; + default: + throw new Error("Unexpected enum value " + propertyKind.returnType); + } + return true; + } + + /** + * Returns true if the body of the given method is a property accessor of the given kind. + * + * @param md the method + * @param propertyKind the kind of property + * @param propertyName the name of the property + * @return true if the body of the given method is a property accessor + */ + private boolean hasCorrectBody( + MethodDeclaration md, PropertyKind propertyKind, String propertyName) { + Statement statement = getOnlyStatement(md); + if (propertyKind.isGetter()) { + if (!(statement instanceof ReturnStmt)) { + return false; + } + Optional oReturnExpr = ((ReturnStmt) statement).getExpression(); + if (!oReturnExpr.isPresent()) { + return false; + } + Expression returnExpr = oReturnExpr.get(); + // Does not handle parentheses. + if (propertyKind == PropertyKind.GETTER_NOT) { + if (!(returnExpr instanceof UnaryExpr)) { + return false; + } + UnaryExpr unary = (UnaryExpr) returnExpr; + if (unary.getOperator() != UnaryExpr.Operator.LOGICAL_COMPLEMENT) { + return false; + } + returnExpr = unary.getExpression(); + } + String returnName; + // Does not handle parentheses. + if (returnExpr instanceof NameExpr) { + returnName = ((NameExpr) returnExpr).getNameAsString(); + } else if (returnExpr instanceof FieldAccessExpr) { + FieldAccessExpr fa = (FieldAccessExpr) returnExpr; + Expression receiver = fa.getScope(); + if (!(receiver instanceof ThisExpr)) { + return false; + } + returnName = fa.getNameAsString(); + } else { + return false; + } + if (!returnName.equals(propertyName)) { + return false; + } + return true; + } else if (propertyKind == PropertyKind.SETTER) { + if (!(statement instanceof ExpressionStmt)) { + return false; + } + Expression expr = ((ExpressionStmt) statement).getExpression(); + if (!(expr instanceof AssignExpr)) { + return false; + } + AssignExpr assignExpr = (AssignExpr) expr; + Expression target = assignExpr.getTarget(); + AssignExpr.Operator op = assignExpr.getOperator(); + Expression value = assignExpr.getValue(); + if (!(target instanceof FieldAccessExpr)) { + return false; + } + FieldAccessExpr fa = (FieldAccessExpr) target; + Expression receiver = fa.getScope(); + if (!(receiver instanceof ThisExpr)) { + return false; + } + if (!fa.getNameAsString().equals(propertyName)) { + return false; + } + if (op != AssignExpr.Operator.ASSIGN) { + return false; + } + if (!(value instanceof NameExpr + && ((NameExpr) value).getNameAsString().equals(propertyName))) { + return false; + } + return true; + } else { + throw new Error("unexpected PropertyKind " + propertyKind); + } + } + + /** + * If the body contains exactly one statement, returns it. Otherwise, returns null. + * + * @param md a method declaration + * @return its sole statement, or null + */ + private Statement getOnlyStatement(MethodDeclaration md) { + Optional body = md.getBody(); + if (!body.isPresent()) { + return null; + } + NodeList statements = body.get().getStatements(); + if (statements.size() != 1) { + return null; + } + return statements.get(0); + } + + /** Visits an AST and collects warnings about missing Javadoc. */ + private class RequireJavadocVisitor extends VoidVisitorAdapter { + + /** The file being visited. Used for constructing error messages. */ + private Path filename; + + /** + * Create a new RequireJavadocVisitor. + * + * @param filename the file being visited; used for diagnostic messages + */ + public RequireJavadocVisitor(Path filename) { + this.filename = filename; + } + + /** + * Return a string stating that documentation is missing on the given construct. + * + * @param node a Java language construct (class, constructor, method, field, etc.) + * @param simpleName the construct's simple name, used in diagnostic messages + * @return an error message for the given construct + */ + private String errorString(Node node, String simpleName) { + Optional range = node.getRange(); + if (range.isPresent()) { + Position begin = range.get().begin; + Path path = + (relative + ? (filename.isAbsolute() ? workingDirAbsolute : workingDirRelative) + .relativize(filename) + : filename); + return String.format( + "%s:%d:%d: missing documentation for %s", + path, begin.line, begin.column, simpleName); + } else { + return "missing documentation for " + simpleName; + } + } + + public void visit(RequireJavadocVisitor this, CompilationUnit cu, Void ignore) { + Optional opd = cu.getPackageDeclaration(); + if (opd.isPresent()) { + String packageName = opd.get().getName().asString(); + if (shouldNotRequire(packageName)) { + return; + } + Optional oTypeName = cu.getPrimaryTypeName(); + if (oTypeName.isPresent() + && oTypeName.get().equals("package-info") + && !hasJavadocComment(opd.get()) + && !hasJavadocComment(cu)) { + errors.add(errorString(opd.get(), packageName)); + } + } + if (verbose) { + System.out.printf("Visiting compilation unit%n"); + } + super.visit(cu, ignore); + } + + public void visit(RequireJavadocVisitor this, ClassOrInterfaceDeclaration cd, Void ignore) { + if (dont_require_private && cd.isPrivate()) { + return; + } + String name = cd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting type %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(cd)) { + errors.add(errorString(cd, name)); + } + super.visit(cd, ignore); + } + + public void visit(RequireJavadocVisitor this, ConstructorDeclaration cd, Void ignore) { + if (dont_require_private && cd.isPrivate()) { + return; + } + if (dont_require_noarg_constructor && cd.getParameters().size() == 0) { + return; + } + String name = cd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting constructor %s%n", name); + } + if (!dont_require_method && !hasJavadocComment(cd)) { + errors.add(errorString(cd, name)); + } + super.visit(cd, ignore); + } + + public void visit(RequireJavadocVisitor this, MethodDeclaration md, Void ignore) { + if (dont_require_private && md.isPrivate()) { + return; + } + if (dont_require_trivial_properties && isTrivialGetterOrSetter(md)) { + if (verbose) { + System.out.printf( + "skipping trivial property method %s%n", md.getNameAsString()); + } + return; + } + String name = md.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting method %s%n", md.getName()); + } + if (!dont_require_method && !isOverride(md) && !hasJavadocComment(md)) { + errors.add(errorString(md, name)); + } + super.visit(md, ignore); + } + + public void visit(RequireJavadocVisitor this, FieldDeclaration fd, Void ignore) { + if (dont_require_private && fd.isPrivate()) { + return; + } + // True if shouldNotRequire is false for at least one of the fields + boolean shouldRequire = false; + if (verbose) { + System.out.printf("Visiting field %s%n", fd.getVariables().get(0).getName()); + } + boolean hasJavadocComment = hasJavadocComment(fd); + for (VariableDeclarator vd : fd.getVariables()) { + String name = vd.getNameAsString(); + // TODO: Also check the type of the serialVersionUID variable. + if (name.equals("serialVersionUID")) { + continue; + } + if (shouldNotRequire(name)) { + continue; + } + shouldRequire = true; + if (!dont_require_field && !hasJavadocComment) { + errors.add(errorString(vd, name)); + } + } + if (shouldRequire) { + super.visit(fd, ignore); + } + } + + public void visit(RequireJavadocVisitor this, EnumDeclaration ed, Void ignore) { + if (dont_require_private && ed.isPrivate()) { + return; + } + String name = ed.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting enum %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(ed)) { + errors.add(errorString(ed, name)); + } + super.visit(ed, ignore); + } + + public void visit(RequireJavadocVisitor this, EnumConstantDeclaration ecd, Void ignore) { + String name = ecd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting enum constant %s%n", name); + } + if (!dont_require_field && !hasJavadocComment(ecd)) { + errors.add(errorString(ecd, name)); + } + super.visit(ecd, ignore); + } + + public void visit(RequireJavadocVisitor this, AnnotationDeclaration ad, Void ignore) { + if (dont_require_private && ad.isPrivate()) { + return; + } + String name = ad.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting annotation %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(ad)) { + errors.add(errorString(ad, name)); + } + super.visit(ad, ignore); + } + + public void visit( + RequireJavadocVisitor this, AnnotationMemberDeclaration amd, Void ignore) { + String name = amd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting annotation member %s%n", name); + } + if (!dont_require_method && !hasJavadocComment(amd)) { + errors.add(errorString(amd, name)); + } + super.visit(amd, ignore); + } + + /** + * Return true if this method is annotated with {@code @Override}. + * + * @param md the method to check for an {@code @Override} annotation + * @return true if this method is annotated with {@code @Override} + */ + private boolean isOverride(MethodDeclaration md) { + for (AnnotationExpr anno : md.getAnnotations()) { + String annoName = anno.getName().toString(); + if (annoName.equals("Override") || annoName.equals("java.lang.Override")) { + return true; + } + } + return false; + } + } + + /** + * Return true if this node has a Javadoc comment. + * + * @param n the node to check for a Javadoc comment + * @return true if this node has a Javadoc comment + */ + private boolean hasJavadocComment(Node n) { + if (n instanceof NodeWithJavadoc && ((NodeWithJavadoc) n).hasJavaDocComment()) { + return true; + } + List orphans = new ArrayList<>(); + getOrphanCommentsBeforeThisChildNode(n, orphans); + for (Comment orphan : orphans) { + if (orphan.isJavadocComment()) { + return true; + } + } + Optional oc = n.getComment(); + if (oc.isPresent() + && (oc.get().isJavadocComment() || oc.get().getContent().startsWith("/**"))) { + return true; + } + return false; + } + + /** + * Get "orphan comments": comments before the comment before this node. For example, in + * + *

    {@code
    +     * /** ... *}{@code /
    +     * // text 1
    +     * // text 2
    +     * void m() { ... }
    +     * }
    + * + * the Javadoc comment and {@code // text 1} are an orphan comment, and only {@code // text2} is + * associated with the method. + * + * @param node the node whose orphan comments to collect + * @param result the list to add orphan comments to. Is side-effected by this method. The + * implementation uses this to minimize the diffs against upstream. + */ + private static // to provide such functionality in JavaParser proper. + void getOrphanCommentsBeforeThisChildNode(final Node node, List result) { + if (node instanceof Comment) { + return; + } + Node parent = node.getParentNode().orElse(null); + if (parent == null) { + return; + } + List everything = new LinkedList<>(parent.getChildNodes()); + sortByBeginPosition(everything); + int positionOfTheChild = -1; + for (int i = 0; i < everything.size(); i++) { + if (everything.get(i) == node) positionOfTheChild = i; + } + if (positionOfTheChild == -1) { + throw new AssertionError("I am not a child of my parent."); + } + int positionOfPreviousChild = -1; + for (int i = positionOfTheChild - 1; i >= 0 && positionOfPreviousChild == -1; i--) { + if (!(everything.get(i) instanceof Comment)) positionOfPreviousChild = i; + } + for (int i = positionOfPreviousChild + 1; i < positionOfTheChild; i++) { + Node nodeToPrint = everything.get(i); + if (!(nodeToPrint instanceof Comment)) + throw new RuntimeException( + "Expected comment, instead " + + nodeToPrint.getClass() + + ". Position of previous child: " + + positionOfPreviousChild + + ", position of child " + + positionOfTheChild); + result.add((Comment) nodeToPrint); + } + } +} diff --git a/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.common.value.ValueChecker.ajava b/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.common.value.ValueChecker.ajava new file mode 100644 index 000000000000..847d60dbb5e9 --- /dev/null +++ b/checker/tests/index-initializedfields/input-annotation-files/RequireJavadoc-org.checkerframework.common.value.ValueChecker.ajava @@ -0,0 +1,1008 @@ +import static com.github.javaparser.utils.PositionUtils.sortByBeginPosition; + +import com.github.javaparser.ParseProblemException; +import com.github.javaparser.Position; +import com.github.javaparser.Range; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.PackageDeclaration; +import com.github.javaparser.ast.body.AnnotationDeclaration; +import com.github.javaparser.ast.body.AnnotationMemberDeclaration; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.EnumConstantDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.comments.Comment; +import com.github.javaparser.ast.expr.AnnotationExpr; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.FieldAccessExpr; +import com.github.javaparser.ast.expr.NameExpr; +import com.github.javaparser.ast.expr.ThisExpr; +import com.github.javaparser.ast.expr.UnaryExpr; +import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.stmt.ExpressionStmt; +import com.github.javaparser.ast.stmt.ReturnStmt; +import com.github.javaparser.ast.stmt.Statement; +import com.github.javaparser.ast.type.PrimitiveType; +import com.github.javaparser.ast.type.Type; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; + +import org.plumelib.options.Options; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * A program that issues an error for any class, constructor, method, or field that lacks a Javadoc + * comment. Does not issue a warning for methods annotated with {@code @Override}. See documentation + * at https://github.com/plume-lib/require-javadoc#readme. + */ +@org.checkerframework.framework.qual.AnnotatedFor("org.checkerframework.common.value.ValueChecker") +public class RequireJavadoc { + + /** Matches name of file or directory where no problems should be reported. */ + public Pattern exclude = null; + + // TODO: It would be nice to support matching fully-qualified class names, but matching + // packages will have to do for now. + /** + * Matches simple name of class/constructor/method/field, or full package name, where no + * problems should be reported. + */ + public Pattern dont_require = null; + + /** If true, don't check elements with private access. */ + public boolean dont_require_private; + + /** + * If true, don't check constructors with zero formal parameters. These are sometimes called + * "default constructors", though that term means a no-argument constructor that the compiler + * synthesized when the programmer didn't write one. + */ + public boolean dont_require_noarg_constructor; + + /** + * If true, don't check trivial getters and setters. + * + *

    Trivial getters and setters are of the form: + * + *

    {@code
    +     * SomeType getFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * SomeType foo() {
    +     *   return foo;
    +     * }
    +     *
    +     * void setFoo(SomeType foo) {
    +     *   this.foo = foo;
    +     * }
    +     *
    +     * boolean hasFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * boolean isFoo() {
    +     *   return foo;
    +     * }
    +     *
    +     * boolean notFoo() {
    +     *   return !foo;
    +     * }
    +     * }
    + */ + public boolean dont_require_trivial_properties; + + /** If true, don't check type declarations: classes, interfaces, enums, annotations. */ + public boolean dont_require_type; + + /** If true, don't check fields. */ + public boolean dont_require_field; + + /** If true, don't check methods, constructors, and annotation members. */ + public boolean dont_require_method; + + /** If true, warn if any package lacks a package-info.java file. */ + public boolean require_package_info; + + /** + * If true, print filenames relative to working directory. Setting this only has an effect if + * the command-line arguments were absolute pathnames, or no command-line arguments were + * supplied. + */ + public boolean relative = false; + + /** If true, output debug information. */ + public boolean verbose = false; + + /** All the errors this program will report. */ + private List errors = new ArrayList<>(); + + /** The Java files to be checked. */ + private List javaFiles = new ArrayList(); + + /** The current working directory, for making relative pathnames. */ + private Path workingDirRelative = Paths.get(""); + + /** The current working directory, for making relative pathnames. */ + private Path workingDirAbsolute = Paths.get("").toAbsolutePath(); + + /** + * The main entry point for the require-javadoc program. See documentation at https://github.com/plume-lib/require-javadoc#readme. + * + * @param args the command-line arguments; see the README file + */ + public static void main(String[] args) { + RequireJavadoc rj = new RequireJavadoc(); + Options options = + new Options( + "java org.plumelib.javadoc.RequireJavadoc [options] [directory-or-file ...]", + rj); + String[] remainingArgs = options.parse(true, args); + rj.setJavaFiles(remainingArgs); + for (Path javaFile : rj.javaFiles) { + if (rj.verbose) { + System.out.println("Checking " + javaFile); + } + try { + CompilationUnit cu = StaticJavaParser.parse(javaFile); + RequireJavadocVisitor visitor = rj.new RequireJavadocVisitor(javaFile); + visitor.visit(cu, null); + } catch (IOException e) { + System.out.println("Problem while reading " + javaFile + ": " + e.getMessage()); + System.exit(2); + } catch (ParseProblemException e) { + System.out.println("Problem while parsing " + javaFile + ": " + e.getMessage()); + System.exit(2); + } + } + for (String error : rj.errors) { + System.out.println(error); + } + System.exit(rj.errors.isEmpty() ? 0 : 1); + } + + /** Creates a new RequireJavadoc instance. */ + @org.checkerframework.dataflow.qual.SideEffectFree + private RequireJavadoc() {} + + /** + * Set the Java files to be processed from the command-line arguments. + * + * @param args the directories and files listed on the command line + */ + private void setJavaFiles(String[] args) { + if (args.length == 0) { + args = new String[] {workingDirAbsolute.toString()}; + } + FileVisitor walker = new JavaFilesVisitor(); + for (String arg : args) { + if (shouldExclude(arg)) { + continue; + } + Path p = Paths.get(arg); + File f = p.toFile(); + if (!f.exists()) { + System.out.println("File not found: " + f); + System.exit(2); + } + if (f.isDirectory()) { + try { + Files.walkFileTree(p, walker); + } catch (IOException e) { + System.out.println("Problem while reading " + f + ": " + e.getMessage()); + System.exit(2); + } + } else { + javaFiles.add(Paths.get(arg)); + } + } + javaFiles.sort(Comparator.comparing(Object::toString)); + Set missingPackageInfoFiles = new LinkedHashSet<>(); + if (require_package_info) { + for (Path javaFile : javaFiles) { + Path javaFileParent = javaFile.getParent(); + // Java 11 has Path.of() instead of creating a new File. + Path packageInfo = javaFileParent.resolve(new File("package-info.java").toPath()); + if (!javaFiles.contains(packageInfo)) { + missingPackageInfoFiles.add(packageInfo); + } + } + for (Path packageInfo : missingPackageInfoFiles) { + errors.add("missing package documentation: no file " + packageInfo); + } + } + } + + /** Collects files into the {@link #javaFiles} variable. */ + private class JavaFilesVisitor extends SimpleFileVisitor { + + /** Create a new JavaFilesVisitor. */ + public JavaFilesVisitor() {} + + public @org.checkerframework.common.value.qual.StringVal({"CONTINUE"}) FileVisitResult + visitFile(JavaFilesVisitor this, Path file, BasicFileAttributes attr) { + if (attr.isRegularFile() && file.toString().endsWith(".java")) { + if (!shouldExclude(file)) { + javaFiles.add(file); + } + } + return FileVisitResult.CONTINUE; + } + + public @org.checkerframework.common.value.qual.StringVal({"CONTINUE", "SKIP_SUBTREE"}) FileVisitResult preVisitDirectory( + JavaFilesVisitor this, Path dir, BasicFileAttributes attr) { + if (shouldExclude(dir)) { + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + + @org.checkerframework.framework.qual.EnsuresQualifier( + expression = {"#2"}, + qualifier = org.checkerframework.common.value.qual.BottomVal.class) + public @org.checkerframework.common.value.qual.StringVal({"CONTINUE"}) FileVisitResult + postVisitDirectory(JavaFilesVisitor this, Path dir, IOException exc) { + if (exc != null) { + System.out.println("Problem visiting " + dir + ": " + exc.getMessage()); + System.exit(2); + } + return FileVisitResult.CONTINUE; + } + + @org.checkerframework.framework.qual.EnsuresQualifier( + expression = {"#2"}, + qualifier = org.checkerframework.common.value.qual.BottomVal.class) + public @org.checkerframework.common.value.qual.StringVal({"CONTINUE"}) FileVisitResult + visitFileFailed(JavaFilesVisitor this, Path file, IOException exc) { + if (exc != null) { + System.out.println("Problem visiting " + file + ": " + exc.getMessage()); + System.exit(2); + } + return FileVisitResult.CONTINUE; + } + } + + /** + * Return true if the given Java element should not be checked, based on the {@code + * --dont-require} command-line argument. + * + * @param name the name of a Java element. It is a simple name, except for packages. + * @return true if no warnings should be issued about the element + */ + private boolean shouldNotRequire(String name) { + if (dont_require == null) { + return false; + } + boolean result = dont_require.matcher(name).find(); + if (verbose) { + System.out.printf("shouldNotRequire(%s) => %s%n", name, result); + } + return result; + } + + /** + * Return true if the given file or directory should be skipped, based on the {@code --exclude} + * command-line argument. + * + * @param fileName the name of a Java file or directory + * @return true if the file or directory should be skipped + */ + private boolean shouldExclude(String fileName) { + if (exclude == null) { + return false; + } + boolean result = exclude.matcher(fileName).find(); + if (verbose) { + System.out.printf("shouldExclude(%s) => %s%n", fileName, result); + } + return result; + } + + /** + * Return true if the given file or directory should be skipped, based on the {@code --exclude} + * command-line argument. + * + * @param path a Java file or directory + * @return true if the file or directory should be skipped + */ + private boolean shouldExclude(Path path) { + return shouldExclude(path.toString()); + } + + /** A property method's return type. */ + private enum ReturnType { + + /** The return type is void. */ + VOID, + /** The return type is boolean. */ + BOOLEAN, + /** The return type is non-void. */ + NON_VOID + } + + /** The type of property method: a getter or setter. */ + private enum PropertyKind { + + /** A method of the form {@code SomeType getFoo()}. */ + GETTER("get", 0, ReturnType.NON_VOID), + /** A method of the form {@code SomeType foo()}. */ + GETTER_NO_PREFIX("", 0, ReturnType.NON_VOID), + /** A method of the form {@code boolean hasFoo()}. */ + GETTER_HAS("has", 0, ReturnType.BOOLEAN), + /** A method of the form {@code boolean isFoo()}. */ + GETTER_IS("is", 0, ReturnType.BOOLEAN), + /** A method of the form {@code boolean notFoo()}. */ + GETTER_NOT("not", 0, ReturnType.BOOLEAN), + /** A method of the form {@code void setFoo(SomeType arg)}. */ + SETTER("set", 1, ReturnType.VOID), + /** Not a getter or setter. */ + NOT_PROPERTY("", -1, ReturnType.VOID); + + /** The prefix for the method name: "get", "", "has", "is", "not", or "set". */ + final @org.checkerframework.common.value.qual.StringVal({ + "set", "not", "is", "has", "", "get" + }) String prefix; + + /** The number of required formal parameters: 0 or 1. */ + final @org.checkerframework.common.value.qual.IntVal({-1, 1, 0}) int requiredParams; + + /** The return type. */ + final @org.checkerframework.common.value.qual.StringVal({"VOID", "BOOLEAN", "NON_VOID"}) ReturnType returnType; + + /** + * Create a new PropertyKind. + * + * @param prefix the prefix for the method name: "get", "has", "is", "not", or "set" + * @param requiredParams the number of required formal parameters: 0 or 1 + * @param returnType the return type + */ + PropertyKind( + @org.checkerframework.common.value.qual.StringVal({ + "set", "not", "is", "has", "", "get" + }) + String prefix, + @org.checkerframework.common.value.qual.IntVal({-1, 1, 0}) int requiredParams, + @org.checkerframework.common.value.qual.StringVal({"VOID", "BOOLEAN", "NON_VOID"}) ReturnType returnType) { + this.prefix = prefix; + this.requiredParams = requiredParams; + this.returnType = returnType; + } + + /** + * Returns true if this is a getter. + * + * @return true if this is a getter + */ + @org.checkerframework.dataflow.qual.Pure + boolean isGetter() { + return this != SETTER; + } + + /** + * Return the PropertyKind for the given method, or null if it isn't a property accessor + * method. + * + * @param md the method to check + * @return the PropertyKind for the given method, or null + */ + static @org.checkerframework.common.value.qual.StringVal({ + "GETTER_NO_PREFIX", + "SETTER", + "GETTER_NOT", + "GETTER_IS", + "GETTER_HAS", + "GETTER" + }) PropertyKind fromMethodDeclaration(MethodDeclaration md) { + String methodName = md.getNameAsString(); + if (methodName.startsWith("get")) { + return GETTER; + } else if (methodName.startsWith("has")) { + return GETTER_HAS; + } else if (methodName.startsWith("is")) { + return GETTER_IS; + } else if (methodName.startsWith("not")) { + return GETTER_NOT; + } else if (methodName.startsWith("set")) { + return SETTER; + } else { + return GETTER_NO_PREFIX; + } + } + } + + /** + * Return true if this method declaration is a trivial getter or setter. + * + *
      + *
    • A trivial getter is named {@code getFoo}, {@code foo}, {@code hasFoo}, {@code isFoo}, + * or {@code notFoo}, has no formal parameters, and has a body of the form {@code return + * foo} or {@code return this.foo} (except for {@code notFoo}, in which case the body is + * negated). + *
    • A trivial setter is named {@code setFoo}, has one formal parameter named {@code foo}, + * and has a body of the form {@code this.foo = foo}. + *
    + * + * @param md the method to check + * @return true if this method is a trivial getter or setter + */ + private boolean isTrivialGetterOrSetter(MethodDeclaration md) { + PropertyKind kind = PropertyKind.fromMethodDeclaration(md); + if (kind != PropertyKind.GETTER_NO_PREFIX) { + if (isTrivialGetterOrSetter(md, kind)) { + return true; + } + } + return isTrivialGetterOrSetter(md, PropertyKind.GETTER_NO_PREFIX); + } + + /** + * Return true if this method declaration is a trivial getter or setter of the given kind. + * + * @see #isTrivialGetterOrSetter(MethodDeclaration) + * @param md the method to check + * @param propertyKind the kind of property + * @return true if this method is a trivial getter or setter + */ + private boolean isTrivialGetterOrSetter( + MethodDeclaration md, + @org.checkerframework.common.value.qual.StringVal({ + "GETTER_NO_PREFIX", + "SETTER", + "GETTER_NOT", + "GETTER_IS", + "GETTER_HAS", + "GETTER" + }) + PropertyKind propertyKind) { + String propertyName = propertyName(md, propertyKind); + return propertyName != null + && hasCorrectSignature(md, propertyKind, propertyName) + && hasCorrectBody(md, propertyKind, propertyName); + } + + /** + * Returns the name of the property, if the method is a getter or setter of the given kind. + * Otherwise returns null. + * + *

    Examines the method's name, but not its signature or body. Also does not check that the + * given property name corresponds to an existing field. + * + * @param md the method to test + * @param propertyKind the type of property method + * @return the name of the property, or null + */ + private @org.checkerframework.common.value.qual.ArrayLenRange(from = 1, to = 2147483647) String + propertyName( + MethodDeclaration md, + @org.checkerframework.common.value.qual.StringVal({ + "GETTER_NO_PREFIX", + "SETTER", + "GETTER_NOT", + "GETTER_IS", + "GETTER_HAS", + "GETTER" + }) + PropertyKind propertyKind) { + String methodName = md.getNameAsString(); + assert methodName.startsWith(propertyKind.prefix); + String upperCamelCaseProperty = methodName.substring(propertyKind.prefix.length()); + if (upperCamelCaseProperty.length() == 0) { + return null; + } + if (propertyKind == PropertyKind.GETTER_NO_PREFIX) { + return upperCamelCaseProperty; + } else if (!Character.isUpperCase(upperCamelCaseProperty.charAt(0))) { + return null; + } else { + return "" + + Character.toLowerCase(upperCamelCaseProperty.charAt(0)) + + upperCamelCaseProperty.substring(1); + } + } + + /** + * Returns true if the signature of the given method is a property accessor of the given kind. + * + * @param md the method + * @param propertyKind the kind of property + * @param propertyName the name of the property + * @return true if the body of the given method is a property accessor + */ + private @org.checkerframework.common.value.qual.BoolVal({true, false}) boolean + hasCorrectSignature( + MethodDeclaration md, + @org.checkerframework.common.value.qual.StringVal({ + "GETTER_NO_PREFIX", + "SETTER", + "GETTER_NOT", + "GETTER_IS", + "GETTER_HAS", + "GETTER" + }) + PropertyKind propertyKind, + @org.checkerframework.common.value.qual.ArrayLenRange(from = 1, to = 2147483647) String propertyName) { + NodeList parameters = md.getParameters(); + if (parameters.size() != propertyKind.requiredParams) { + return false; + } + if (parameters.size() == 1) { + Parameter parameter = parameters.get(0); + if (!parameter.getNameAsString().equals(propertyName)) { + return false; + } + } + // Check presence/absence of return type. (The Java compiler will verify + // that the type is consistent with the method body.) + Type returnType = md.getType(); + switch (propertyKind.returnType) { + case VOID: + if (!returnType.isVoidType()) { + return false; + } + break; + case BOOLEAN: + if (!returnType.equals(PrimitiveType.booleanType())) { + return false; + } + break; + case NON_VOID: + if (returnType.isVoidType()) { + return false; + } + break; + default: + throw new Error("Unexpected enum value " + propertyKind.returnType); + } + return true; + } + + /** + * Returns true if the body of the given method is a property accessor of the given kind. + * + * @param md the method + * @param propertyKind the kind of property + * @param propertyName the name of the property + * @return true if the body of the given method is a property accessor + */ + private @org.checkerframework.common.value.qual.BoolVal({true, false}) boolean hasCorrectBody( + MethodDeclaration md, + @org.checkerframework.common.value.qual.StringVal({ + "GETTER_NO_PREFIX", + "SETTER", + "GETTER_NOT", + "GETTER_IS", + "GETTER_HAS", + "GETTER" + }) + PropertyKind propertyKind, + @org.checkerframework.common.value.qual.ArrayLenRange(from = 1, to = 2147483647) String propertyName) { + Statement statement = getOnlyStatement(md); + if (propertyKind.isGetter()) { + if (!(statement instanceof ReturnStmt)) { + return false; + } + Optional oReturnExpr = ((ReturnStmt) statement).getExpression(); + if (!oReturnExpr.isPresent()) { + return false; + } + Expression returnExpr = oReturnExpr.get(); + // Does not handle parentheses. + if (propertyKind == PropertyKind.GETTER_NOT) { + if (!(returnExpr instanceof UnaryExpr)) { + return false; + } + UnaryExpr unary = (UnaryExpr) returnExpr; + if (unary.getOperator() != UnaryExpr.Operator.LOGICAL_COMPLEMENT) { + return false; + } + returnExpr = unary.getExpression(); + } + String returnName; + // Does not handle parentheses. + if (returnExpr instanceof NameExpr) { + returnName = ((NameExpr) returnExpr).getNameAsString(); + } else if (returnExpr instanceof FieldAccessExpr) { + FieldAccessExpr fa = (FieldAccessExpr) returnExpr; + Expression receiver = fa.getScope(); + if (!(receiver instanceof ThisExpr)) { + return false; + } + returnName = fa.getNameAsString(); + } else { + return false; + } + if (!returnName.equals(propertyName)) { + return false; + } + return true; + } else if (propertyKind == PropertyKind.SETTER) { + if (!(statement instanceof ExpressionStmt)) { + return false; + } + Expression expr = ((ExpressionStmt) statement).getExpression(); + if (!(expr instanceof AssignExpr)) { + return false; + } + AssignExpr assignExpr = (AssignExpr) expr; + Expression target = assignExpr.getTarget(); + AssignExpr.Operator op = assignExpr.getOperator(); + Expression value = assignExpr.getValue(); + if (!(target instanceof FieldAccessExpr)) { + return false; + } + FieldAccessExpr fa = (FieldAccessExpr) target; + Expression receiver = fa.getScope(); + if (!(receiver instanceof ThisExpr)) { + return false; + } + if (!fa.getNameAsString().equals(propertyName)) { + return false; + } + if (op != AssignExpr.Operator.ASSIGN) { + return false; + } + if (!(value instanceof NameExpr + && ((NameExpr) value).getNameAsString().equals(propertyName))) { + return false; + } + return true; + } else { + throw new Error("unexpected PropertyKind " + propertyKind); + } + } + + /** + * If the body contains exactly one statement, returns it. Otherwise, returns null. + * + * @param md a method declaration + * @return its sole statement, or null + */ + private Statement getOnlyStatement(MethodDeclaration md) { + Optional body = md.getBody(); + if (!body.isPresent()) { + return null; + } + NodeList statements = body.get().getStatements(); + if (statements.size() != 1) { + return null; + } + return statements.get(0); + } + + /** Visits an AST and collects warnings about missing Javadoc. */ + private class RequireJavadocVisitor extends VoidVisitorAdapter { + + /** The file being visited. Used for constructing error messages. */ + private Path filename; + + /** + * Create a new RequireJavadocVisitor. + * + * @param filename the file being visited; used for diagnostic messages + */ + public RequireJavadocVisitor(Path filename) { + this.filename = filename; + } + + /** + * Return a string stating that documentation is missing on the given construct. + * + * @param node a Java language construct (class, constructor, method, field, etc.) + * @param simpleName the construct's simple name, used in diagnostic messages + * @return an error message for the given construct + */ + private String errorString(Node node, String simpleName) { + Optional range = node.getRange(); + if (range.isPresent()) { + Position begin = range.get().begin; + Path path = + (relative + ? (filename.isAbsolute() ? workingDirAbsolute : workingDirRelative) + .relativize(filename) + : filename); + return String.format( + "%s:%d:%d: missing documentation for %s", + path, begin.line, begin.column, simpleName); + } else { + return "missing documentation for " + simpleName; + } + } + + public void visit(RequireJavadocVisitor this, CompilationUnit cu, Void ignore) { + Optional opd = cu.getPackageDeclaration(); + if (opd.isPresent()) { + String packageName = opd.get().getName().asString(); + if (shouldNotRequire(packageName)) { + return; + } + Optional oTypeName = cu.getPrimaryTypeName(); + if (oTypeName.isPresent() + && oTypeName.get().equals("package-info") + && !hasJavadocComment(opd.get()) + && !hasJavadocComment(cu)) { + errors.add(errorString(opd.get(), packageName)); + } + } + if (verbose) { + System.out.printf("Visiting compilation unit%n"); + } + super.visit(cu, ignore); + } + + public void visit(RequireJavadocVisitor this, ClassOrInterfaceDeclaration cd, Void ignore) { + if (dont_require_private && cd.isPrivate()) { + return; + } + String name = cd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting type %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(cd)) { + errors.add(errorString(cd, name)); + } + super.visit(cd, ignore); + } + + public void visit(RequireJavadocVisitor this, ConstructorDeclaration cd, Void ignore) { + if (dont_require_private && cd.isPrivate()) { + return; + } + if (dont_require_noarg_constructor && cd.getParameters().size() == 0) { + return; + } + String name = cd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting constructor %s%n", name); + } + if (!dont_require_method && !hasJavadocComment(cd)) { + errors.add(errorString(cd, name)); + } + super.visit(cd, ignore); + } + + public void visit(RequireJavadocVisitor this, MethodDeclaration md, Void ignore) { + if (dont_require_private && md.isPrivate()) { + return; + } + if (dont_require_trivial_properties && isTrivialGetterOrSetter(md)) { + if (verbose) { + System.out.printf( + "skipping trivial property method %s%n", md.getNameAsString()); + } + return; + } + String name = md.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting method %s%n", md.getName()); + } + if (!dont_require_method && !isOverride(md) && !hasJavadocComment(md)) { + errors.add(errorString(md, name)); + } + super.visit(md, ignore); + } + + public void visit(RequireJavadocVisitor this, FieldDeclaration fd, Void ignore) { + if (dont_require_private && fd.isPrivate()) { + return; + } + // True if shouldNotRequire is false for at least one of the fields + boolean shouldRequire = false; + if (verbose) { + System.out.printf("Visiting field %s%n", fd.getVariables().get(0).getName()); + } + boolean hasJavadocComment = hasJavadocComment(fd); + for (VariableDeclarator vd : fd.getVariables()) { + String name = vd.getNameAsString(); + // TODO: Also check the type of the serialVersionUID variable. + if (name.equals("serialVersionUID")) { + continue; + } + if (shouldNotRequire(name)) { + continue; + } + shouldRequire = true; + if (!dont_require_field && !hasJavadocComment) { + errors.add(errorString(vd, name)); + } + } + if (shouldRequire) { + super.visit(fd, ignore); + } + } + + public void visit(RequireJavadocVisitor this, EnumDeclaration ed, Void ignore) { + if (dont_require_private && ed.isPrivate()) { + return; + } + String name = ed.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting enum %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(ed)) { + errors.add(errorString(ed, name)); + } + super.visit(ed, ignore); + } + + public void visit(RequireJavadocVisitor this, EnumConstantDeclaration ecd, Void ignore) { + String name = ecd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting enum constant %s%n", name); + } + if (!dont_require_field && !hasJavadocComment(ecd)) { + errors.add(errorString(ecd, name)); + } + super.visit(ecd, ignore); + } + + public void visit(RequireJavadocVisitor this, AnnotationDeclaration ad, Void ignore) { + if (dont_require_private && ad.isPrivate()) { + return; + } + String name = ad.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting annotation %s%n", name); + } + if (!dont_require_type && !hasJavadocComment(ad)) { + errors.add(errorString(ad, name)); + } + super.visit(ad, ignore); + } + + public void visit( + RequireJavadocVisitor this, AnnotationMemberDeclaration amd, Void ignore) { + String name = amd.getNameAsString(); + if (shouldNotRequire(name)) { + return; + } + if (verbose) { + System.out.printf("Visiting annotation member %s%n", name); + } + if (!dont_require_method && !hasJavadocComment(amd)) { + errors.add(errorString(amd, name)); + } + super.visit(amd, ignore); + } + + /** + * Return true if this method is annotated with {@code @Override}. + * + * @param md the method to check for an {@code @Override} annotation + * @return true if this method is annotated with {@code @Override} + */ + private @org.checkerframework.common.value.qual.BoolVal({false, true}) boolean isOverride( + MethodDeclaration md) { + for (AnnotationExpr anno : md.getAnnotations()) { + String annoName = anno.getName().toString(); + if (annoName.equals("Override") || annoName.equals("java.lang.Override")) { + return true; + } + } + return false; + } + } + + /** + * Return true if this node has a Javadoc comment. + * + * @param n the node to check for a Javadoc comment + * @return true if this node has a Javadoc comment + */ + private @org.checkerframework.common.value.qual.BoolVal({false, true}) boolean + hasJavadocComment(Node n) { + if (n instanceof NodeWithJavadoc && ((NodeWithJavadoc) n).hasJavaDocComment()) { + return true; + } + List orphans = new ArrayList<>(); + getOrphanCommentsBeforeThisChildNode(n, orphans); + for (Comment orphan : orphans) { + if (orphan.isJavadocComment()) { + return true; + } + } + Optional oc = n.getComment(); + if (oc.isPresent() + && (oc.get().isJavadocComment() || oc.get().getContent().startsWith("/**"))) { + return true; + } + return false; + } + + /** + * Get "orphan comments": comments before the comment before this node. For example, in + * + *

    {@code
    +     * /** ... *}{@code /
    +     * // text 1
    +     * // text 2
    +     * void m() { ... }
    +     * }
    + * + * the Javadoc comment and {@code // text 1} are an orphan comment, and only {@code // text2} is + * associated with the method. + * + * @param node the node whose orphan comments to collect + * @param result the list to add orphan comments to. Is side-effected by this method. The + * implementation uses this to minimize the diffs against upstream. + */ + private static // to provide such functionality in JavaParser proper. + void getOrphanCommentsBeforeThisChildNode(final Node node, List result) { + if (node instanceof Comment) { + return; + } + Node parent = node.getParentNode().orElse(null); + if (parent == null) { + return; + } + List everything = new LinkedList<>(parent.getChildNodes()); + sortByBeginPosition(everything); + int positionOfTheChild = -1; + for (int i = 0; i < everything.size(); i++) { + if (everything.get(i) == node) positionOfTheChild = i; + } + if (positionOfTheChild == -1) { + throw new AssertionError("I am not a child of my parent."); + } + int positionOfPreviousChild = -1; + for (int i = positionOfTheChild - 1; i >= 0 && positionOfPreviousChild == -1; i--) { + if (!(everything.get(i) instanceof Comment)) positionOfPreviousChild = i; + } + for (int i = positionOfPreviousChild + 1; i < positionOfTheChild; i++) { + Node nodeToPrint = everything.get(i); + if (!(nodeToPrint instanceof Comment)) + throw new RuntimeException( + "Expected comment, instead " + + nodeToPrint.getClass() + + ". Position of previous child: " + + positionOfPreviousChild + + ", position of child " + + positionOfTheChild); + result.add((Comment) nodeToPrint); + } + } +} diff --git a/checker/tests/index/ArrayAsList.java b/checker/tests/index/ArrayAsList.java index 6a85da32578b..5b143003bffd 100644 --- a/checker/tests/index/ArrayAsList.java +++ b/checker/tests/index/ArrayAsList.java @@ -1,6 +1,7 @@ +import org.checkerframework.common.value.qual.MinLen; + import java.util.Arrays; import java.util.List; -import org.checkerframework.common.value.qual.MinLen; // @skip-test until we bring list support back diff --git a/checker/tests/index/ArrayCopy.java b/checker/tests/index/ArrayCopy.java index 483a4ff4b40c..10d185802387 100644 --- a/checker/tests/index/ArrayCopy.java +++ b/checker/tests/index/ArrayCopy.java @@ -1,6 +1,6 @@ import org.checkerframework.common.value.qual.MinLen; -class ArrayCopy { +public class ArrayCopy { void copy(int @MinLen(1) [] nums) { int[] nums_copy = new int[nums.length]; diff --git a/checker/tests/index/ArrayCreation.java b/checker/tests/index/ArrayCreation.java deleted file mode 100644 index c2d53cb2f6e4..000000000000 --- a/checker/tests/index/ArrayCreation.java +++ /dev/null @@ -1,11 +0,0 @@ -import org.checkerframework.checker.index.qual.SameLen; - -// Check that creating an array with the length of another -// makes both @SameLen of each other. - -class ArrayCreation { - void test(int[] a, int[] d) { - int[] b = new int[a.length]; - int @SameLen({"a", "b"}) [] c = b; - } -} diff --git a/checker/tests/index/ArrayCreationChecks.java b/checker/tests/index/ArrayCreationChecks.java index 9d97267d5e15..452399e4073c 100644 --- a/checker/tests/index/ArrayCreationChecks.java +++ b/checker/tests/index/ArrayCreationChecks.java @@ -2,7 +2,7 @@ import org.checkerframework.checker.index.qual.*; -class ArrayCreationChecks { +public class ArrayCreationChecks { void test1(@Positive int x, @Positive int y) { int[] newArray = new int[x + y]; diff --git a/checker/tests/index/ArrayCreationTest.java b/checker/tests/index/ArrayCreationTest.java new file mode 100644 index 000000000000..2fa6fd9f0fd6 --- /dev/null +++ b/checker/tests/index/ArrayCreationTest.java @@ -0,0 +1,11 @@ +import org.checkerframework.checker.index.qual.SameLen; + +// Check that creating an array with the length of another +// makes both @SameLen of each other. + +public class ArrayCreationTest { + void test(int[] a, int[] d) { + int[] b = new int[a.length]; + int @SameLen({"a", "b"}) [] c = b; + } +} diff --git a/checker/tests/index/ArrayIntro.java b/checker/tests/index/ArrayIntro.java index 794026a6aa7e..51218711aa24 100644 --- a/checker/tests/index/ArrayIntro.java +++ b/checker/tests/index/ArrayIntro.java @@ -1,7 +1,7 @@ import org.checkerframework.common.value.qual.MinLen; @SuppressWarnings("lowerbound") -class ArrayIntro { +public class ArrayIntro { void test() { int @MinLen(5) [] arr = new int[5]; int a = 9; diff --git a/checker/tests/index/ArrayIntroWithCast.java b/checker/tests/index/ArrayIntroWithCast.java index 201511ab326c..e2068c29fea4 100644 --- a/checker/tests/index/ArrayIntroWithCast.java +++ b/checker/tests/index/ArrayIntroWithCast.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.*; -class ArrayIntroWithCast { +public class ArrayIntroWithCast { void test(String[] a, String[] b) { Object result = new Object[a.length + b.length]; diff --git a/checker/tests/index/ArrayLength.java b/checker/tests/index/ArrayLength.java index 7647d3e91dca..b646fb2de1dd 100644 --- a/checker/tests/index/ArrayLength.java +++ b/checker/tests/index/ArrayLength.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.LTEqLengthOf; -class ArrayLength { +public class ArrayLength { void test() { int[] arr = {1, 2, 3}; @LTEqLengthOf({"arr"}) int a = arr.length; diff --git a/checker/tests/index/ArrayLengthEquality.java b/checker/tests/index/ArrayLengthEquality.java index 571252d73b93..13e09dada30c 100644 --- a/checker/tests/index/ArrayLengthEquality.java +++ b/checker/tests/index/ArrayLengthEquality.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.SameLen; -class ArrayLengthEquality { +public class ArrayLengthEquality { void test(int[] a, int[] b) { if (a.length == b.length) { int @SameLen({"a", "b"}) [] c = a; diff --git a/checker/tests/index/ArrayWrapper.java b/checker/tests/index/ArrayWrapper.java index b77f6daf600f..514bc450d612 100644 --- a/checker/tests/index/ArrayWrapper.java +++ b/checker/tests/index/ArrayWrapper.java @@ -9,6 +9,7 @@ import org.checkerframework.checker.index.qual.LengthOf; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.index.qual.SameLen; +import org.checkerframework.common.value.qual.MinLen; /** ArrayWrapper is a fixed-size generic collection. */ public class ArrayWrapper { @@ -47,4 +48,18 @@ public static void clearIndex3(ArrayWrapper a, @NonNegative in a.set(i, null); } } + + // The following methods are tests that sequence annotations work correctly with + // user-defined sequence types. + + public static Object testSameLen( + @SameLen("#2") ArrayWrapper a, + @SameLen("#1") ArrayWrapper b, + @IndexFor("#1") int i) { + return b.get(i); + } + + public static Object testMinLen(@MinLen(3) ArrayWrapper a) { + return a.get(2); + } } diff --git a/checker/tests/index/ArraysSort.java b/checker/tests/index/ArraysSort.java index 3dba567e27df..d8963d05b843 100644 --- a/checker/tests/index/ArraysSort.java +++ b/checker/tests/index/ArraysSort.java @@ -1,7 +1,8 @@ -import java.util.Arrays; import org.checkerframework.common.value.qual.MinLen; -class ArraysSort { +import java.util.Arrays; + +public class ArraysSort { void sortInt(int @MinLen(10) [] nums) { // Checks the correct handling of the toIndex parameter diff --git a/checker/tests/index/BasicSubsequence.java b/checker/tests/index/BasicSubsequence.java index 3df75870b9aa..e021f25df138 100644 --- a/checker/tests/index/BasicSubsequence.java +++ b/checker/tests/index/BasicSubsequence.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.*; -class BasicSubsequence { +public class BasicSubsequence { // :: error: not.final @HasSubsequence(subsequence = "this", from = "this.x", to = "this.y") int[] b; diff --git a/checker/tests/index/BasicSubsequence2.java b/checker/tests/index/BasicSubsequence2.java index 1d3b6d74e2ef..2b572221c98b 100644 --- a/checker/tests/index/BasicSubsequence2.java +++ b/checker/tests/index/BasicSubsequence2.java @@ -2,7 +2,7 @@ import org.checkerframework.checker.index.qual.IndexFor; import org.checkerframework.checker.index.qual.IndexOrHigh; -class BasicSubsequence2 { +public class BasicSubsequence2 { @HasSubsequence(subsequence = "this", from = "this.start", to = "this.end") int[] array; diff --git a/checker/tests/index/BasicSubsequence3.java b/checker/tests/index/BasicSubsequence3.java index d233862167d1..bb25d8058c11 100644 --- a/checker/tests/index/BasicSubsequence3.java +++ b/checker/tests/index/BasicSubsequence3.java @@ -4,7 +4,7 @@ import org.checkerframework.checker.index.qual.LessThan; @SuppressWarnings("lowerbound") -class BasicSubsequence3 { +public class BasicSubsequence3 { @HasSubsequence(subsequence = "this", from = "this.start", to = "this.end") int[] array; diff --git a/checker/tests/index/BigBinaryExpr.java b/checker/tests/index/BigBinaryExpr.java index 2b6ae87ea9ca..318cdb9cd8f1 100644 --- a/checker/tests/index/BigBinaryExpr.java +++ b/checker/tests/index/BigBinaryExpr.java @@ -1,4 +1,4 @@ -class BigBinaryExpr { +public class BigBinaryExpr { void test() { int i0 = 163; int i1 = 153; diff --git a/checker/tests/index/BinarySearchTest.java b/checker/tests/index/BinarySearchTest.java index 6a79d823d5a3..267901e5b60a 100644 --- a/checker/tests/index/BinarySearchTest.java +++ b/checker/tests/index/BinarySearchTest.java @@ -1,6 +1,7 @@ -import java.util.Arrays; import org.checkerframework.checker.index.qual.*; +import java.util.Arrays; + public class BinarySearchTest { private final long @SameLen("iNameKeys") [] iTransitions; diff --git a/checker/tests/index/BinomialTest.java b/checker/tests/index/BinomialTest.java index 97650379c532..33396da60cc0 100644 --- a/checker/tests/index/BinomialTest.java +++ b/checker/tests/index/BinomialTest.java @@ -1,65 +1,72 @@ import org.checkerframework.checker.index.qual.*; import org.checkerframework.common.value.qual.*; -class BinomialTest { +public class BinomialTest { static final long @MinLen(1) [] factorials = {1L, 1L, 1L * 2}; public static long binomial( - @NonNegative @LTLengthOf("this.factorials") int n, + @NonNegative @LTLengthOf("BinomialTest.factorials") int n, @NonNegative @LessThan("#1 + 1") int k) { return factorials[k]; } - public static void binomial0(@LTLengthOf("this.factorials") int n, @LessThan("#1") int k) { + public static void binomial0( + @LTLengthOf("BinomialTest.factorials") int n, @LessThan("#1") int k) { @LTLengthOf(value = "factorials", offset = "1") int i = k; } - public static void binomial0Error(@LTLengthOf("this.factorials") int n, @LessThan("#1") int k) { + public static void binomial0Error( + @LTLengthOf("BinomialTest.factorials") int n, @LessThan("#1") int k) { // :: error: (assignment.type.incompatible) @LTLengthOf(value = "factorials", offset = "2") int i = k; } - public static void binomial0Weak(@LTLengthOf("this.factorials") int n, @LessThan("#1") int k) { + public static void binomial0Weak( + @LTLengthOf("BinomialTest.factorials") int n, @LessThan("#1") int k) { @LTLengthOf("factorials") int i = k; } - public static void binomial1(@LTLengthOf("this.factorials") int n, @LessThan("#1 + 1") int k) { + public static void binomial1( + @LTLengthOf("BinomialTest.factorials") int n, @LessThan("#1 + 1") int k) { @LTLengthOf("factorials") int i = k; } public static void binomial1Error( - @LTLengthOf("this.factorials") int n, @LessThan("#1 + 1") int k) { + @LTLengthOf("BinomialTest.factorials") int n, @LessThan("#1 + 1") int k) { // :: error: (assignment.type.incompatible) @LTLengthOf(value = "factorials", offset = "1") int i = k; } - public static void binomial2(@LTLengthOf("this.factorials") int n, @LessThan("#1 + 2") int k) { + public static void binomial2( + @LTLengthOf("BinomialTest.factorials") int n, @LessThan("#1 + 2") int k) { @LTLengthOf(value = "factorials", offset = "-1") int i = k; } public static void binomial2Error( - @LTLengthOf("this.factorials") int n, @LessThan("#1 + 2") int k) { + @LTLengthOf("BinomialTest.factorials") int n, @LessThan("#1 + 2") int k) { // :: error: (assignment.type.incompatible) @LTLengthOf(value = "factorials", offset = "0") int i = k; } - public static void binomial_1(@LTLengthOf("this.factorials") int n, @LessThan("#1 - 1") int k) { + public static void binomial_1( + @LTLengthOf("BinomialTest.factorials") int n, @LessThan("#1 - 1") int k) { @LTLengthOf(value = "factorials", offset = "2") int i = k; } public static void binomial_1Error( - @LTLengthOf("this.factorials") int n, @LessThan("#1 - 1") int k) { + @LTLengthOf("BinomialTest.factorials") int n, @LessThan("#1 - 1") int k) { // :: error: (assignment.type.incompatible) @LTLengthOf(value = "factorials", offset = "3") int i = k; } - public static void binomial_2(@LTLengthOf("this.factorials") int n, @LessThan("#1 - 2") int k) { + public static void binomial_2( + @LTLengthOf("BinomialTest.factorials") int n, @LessThan("#1 - 2") int k) { @LTLengthOf(value = "factorials", offset = "3") int i = k; } public static void binomial_2Error( - @LTLengthOf("this.factorials") int n, @LessThan("#1 - 2") int k) { + @LTLengthOf("BinomialTest.factorials") int n, @LessThan("#1 - 2") int k) { // :: error: (assignment.type.incompatible) @LTLengthOf(value = "factorials", offset = "4") int i = k; } diff --git a/checker/tests/index/BitSetLowerBound.java b/checker/tests/index/BitSetLowerBound.java index e5178478fcfb..c08e56fc58a2 100644 --- a/checker/tests/index/BitSetLowerBound.java +++ b/checker/tests/index/BitSetLowerBound.java @@ -1,9 +1,10 @@ // Test case for Issue 185: // https://github.com/typetools/kelloggm/issues/185 -import java.util.BitSet; import org.checkerframework.checker.index.qual.GTENegativeOne; +import java.util.BitSet; + public class BitSetLowerBound { private void m(BitSet b) { diff --git a/checker/tests/index/BottomValTest.java b/checker/tests/index/BottomValTest.java index acb9c9d33c75..51bbd36a9f24 100644 --- a/checker/tests/index/BottomValTest.java +++ b/checker/tests/index/BottomValTest.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.*; import org.checkerframework.common.value.qual.*; -class BottomValTest { +public class BottomValTest { @NonNegative int foo(@BottomVal int bottom) { return bottom; } diff --git a/checker/tests/index/CastArray.java b/checker/tests/index/CastArray.java index 8f4d44a8c194..0ffb750b2228 100644 --- a/checker/tests/index/CastArray.java +++ b/checker/tests/index/CastArray.java @@ -1,4 +1,4 @@ -class CastArray { +public class CastArray { void test(Object a) { int[] b = (int[]) a; } diff --git a/checker/tests/index/CharSequenceTest.java b/checker/tests/index/CharSequenceTest.java index aae089efe2b2..d741b721c137 100644 --- a/checker/tests/index/CharSequenceTest.java +++ b/checker/tests/index/CharSequenceTest.java @@ -1,12 +1,13 @@ // Tests suport for index annotations applied to CharSequence and related indices. -import java.io.IOException; -import java.io.StringWriter; import org.checkerframework.checker.index.qual.IndexFor; import org.checkerframework.checker.index.qual.IndexOrHigh; import org.checkerframework.common.value.qual.MinLen; import org.checkerframework.common.value.qual.StringVal; +import java.io.IOException; +import java.io.StringWriter; + public class CharSequenceTest { // Tests that minlen is correctly applied to CharSequence assigned from String, but not // StringBuilder @@ -15,6 +16,7 @@ void minLenCharSequence() { // :: error: (assignment.type.incompatible) @MinLen(10) CharSequence sb = new StringBuilder("0123456789"); } + // Tests the subSequence method void testSubSequence() { // Local variable used because of https://github.com/kelloggm/checker-framework/issues/165 @@ -35,6 +37,7 @@ void argumentPassing() { // :: error: (argument.type.incompatible) sink(sb, 8); } + // Tests forwardning sequences as CharSequence void agumentForwarding(String s, @IndexOrHigh("#1") int i) { sink(s, i); diff --git a/checker/tests/index/CheckAgainstNegativeOne.java b/checker/tests/index/CheckAgainstNegativeOne.java index 4f39ad77d843..6c61942bfa0e 100644 --- a/checker/tests/index/CheckAgainstNegativeOne.java +++ b/checker/tests/index/CheckAgainstNegativeOne.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.IndexOrHigh; -class CheckAgainstNegativeOne { +public class CheckAgainstNegativeOne { public static String replaceString(String target, String oldStr, String newStr) { if (oldStr.equals("")) { diff --git a/checker/tests/index/CheckNotNull1.java b/checker/tests/index/CheckNotNull1.java index 029a234f2496..afd2b7c418ea 100644 --- a/checker/tests/index/CheckNotNull1.java +++ b/checker/tests/index/CheckNotNull1.java @@ -1,4 +1,4 @@ -class CheckNotNull1 { +public class CheckNotNull1 { T checkNotNull(T ref) { return ref; } diff --git a/checker/tests/index/CheckNotNull2.java b/checker/tests/index/CheckNotNull2.java index 5d8c8acdabe4..8304f2775826 100644 --- a/checker/tests/index/CheckNotNull2.java +++ b/checker/tests/index/CheckNotNull2.java @@ -1,4 +1,4 @@ -class CheckNotNull2 { +public class CheckNotNull2 { T checkNotNull(T ref) { return ref; } diff --git a/checker/tests/index/CombineFacts.java b/checker/tests/index/CombineFacts.java index aa375dbabb02..70a88ccc56e1 100644 --- a/checker/tests/index/CombineFacts.java +++ b/checker/tests/index/CombineFacts.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.LTLengthOf; @SuppressWarnings("lowerbound") -class CombineFacts { +public class CombineFacts { void test(int[] a1) { @LTLengthOf("a1") int len = a1.length - 1; int[] a2 = new int[len]; diff --git a/checker/tests/index/CompareBySubtraction.java b/checker/tests/index/CompareBySubtraction.java index 9a282494a92d..20912bad226f 100644 --- a/checker/tests/index/CompareBySubtraction.java +++ b/checker/tests/index/CompareBySubtraction.java @@ -1,6 +1,6 @@ // @skip-test until fixed. -class CompareBySubtraction { +public class CompareBySubtraction { public int compare(int[] a1, int[] a2) { if (a1 == a2) { return 0; diff --git a/checker/tests/index/CompoundAssignmentCheck.java b/checker/tests/index/CompoundAssignmentCheck.java index 853ce64bd6bd..7518d9c46685 100644 --- a/checker/tests/index/CompoundAssignmentCheck.java +++ b/checker/tests/index/CompoundAssignmentCheck.java @@ -1,4 +1,4 @@ -class CompoundAssignmentCheck { +public class CompoundAssignmentCheck { void test() { int a = 9; a += 5; diff --git a/checker/tests/index/ComputeConst.java b/checker/tests/index/ComputeConst.java new file mode 100644 index 000000000000..851dcdc53dc0 --- /dev/null +++ b/checker/tests/index/ComputeConst.java @@ -0,0 +1,11 @@ +public class ComputeConst { + + public static int hash(long l) { + // If possible, use the value itself. + if (l >= Integer.MIN_VALUE && l <= Integer.MAX_VALUE) { + return (int) l; + } + + return Long.hashCode(l); + } +} diff --git a/checker/tests/index/ConstantArrays.java b/checker/tests/index/ConstantArrays.java index 5c082d8a1b45..c6a56da5ca7e 100644 --- a/checker/tests/index/ConstantArrays.java +++ b/checker/tests/index/ConstantArrays.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.*; -class ConstantArrays { +public class ConstantArrays { void basic_test() { int[] b = new int[4]; @LTLengthOf("b") int[] a = {0, 1, 2, 3}; diff --git a/checker/tests/index/ConstantsIndex.java b/checker/tests/index/ConstantsIndex.java index 9e53627e6060..672b15332b68 100644 --- a/checker/tests/index/ConstantsIndex.java +++ b/checker/tests/index/ConstantsIndex.java @@ -1,6 +1,6 @@ import org.checkerframework.common.value.qual.MinLen; -class ConstantsIndex { +public class ConstantsIndex { void test() { int @MinLen(3) [] arr = {1, 2, 3}; diff --git a/checker/tests/index/DaikonCrash.java b/checker/tests/index/DaikonCrash.java index 7ffd5e67f115..45bf79754eab 100644 --- a/checker/tests/index/DaikonCrash.java +++ b/checker/tests/index/DaikonCrash.java @@ -1,6 +1,7 @@ -import java.util.Arrays; import org.checkerframework.dataflow.qual.Pure; +import java.util.Arrays; + public class DaikonCrash { void method(Object[] a1) { int[] u = union(new int[] {}, new int[] {}); diff --git a/checker/tests/index/DefaultingForEach.java b/checker/tests/index/DefaultingForEach.java new file mode 100644 index 000000000000..5cbe49fddc37 --- /dev/null +++ b/checker/tests/index/DefaultingForEach.java @@ -0,0 +1,22 @@ +// Test case for issue #4248: https://github.com/typetools/checker-framework/issues/4248. +// This test exposed a crash in the original version of the fix. + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.index.qual.Positive; +import org.checkerframework.framework.qual.DefaultQualifier; + +class DefaultForEach { + + @DefaultQualifier(NonNegative.class) + static int[] foo() { + throw new RuntimeException(); + } + + void bar() { + for (Integer p : foo()) { + // :: error: assignment.type.incompatible + @Positive int x = p; + @NonNegative int y = p; + } + } +} diff --git a/checker/tests/index/DivisionTest.java b/checker/tests/index/DivisionTest.java new file mode 100644 index 000000000000..3407f4eef936 --- /dev/null +++ b/checker/tests/index/DivisionTest.java @@ -0,0 +1,8 @@ +import java.util.*; + +public class DivisionTest { + + public static void division() { + System.out.println(1 / (2.0)); + } +} diff --git a/checker/tests/index/EnumValues.java b/checker/tests/index/EnumValues.java index 8e300a0a017c..49452b3fae89 100644 --- a/checker/tests/index/EnumValues.java +++ b/checker/tests/index/EnumValues.java @@ -1,6 +1,6 @@ import org.checkerframework.common.value.qual.*; -class EnumValues { +public class EnumValues { public static enum Direction { NORTH, diff --git a/checker/tests/index/EqualToIndex.java b/checker/tests/index/EqualToIndex.java index 6a2ef1325e76..33aae4b82b25 100644 --- a/checker/tests/index/EqualToIndex.java +++ b/checker/tests/index/EqualToIndex.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.LTEqLengthOf; import org.checkerframework.checker.index.qual.LTLengthOf; -class EqualToIndex { +public class EqualToIndex { static int[] a = {0}; public static void equalToUpper(@LTLengthOf("a") int m, @LTEqLengthOf("a") int r) { diff --git a/checker/tests/index/EqualToTransfer.java b/checker/tests/index/EqualToTransfer.java index 7841a3e7b53d..54d622b32194 100644 --- a/checker/tests/index/EqualToTransfer.java +++ b/checker/tests/index/EqualToTransfer.java @@ -1,6 +1,6 @@ import org.checkerframework.common.value.qual.MinLen; -class EqualToTransfer { +public class EqualToTransfer { void eq_check(int[] a) { if (1 == a.length) { int @MinLen(1) [] b = a; diff --git a/checker/tests/index/GenericAssignment.java b/checker/tests/index/GenericAssignment.java index 18ee0f152c2e..bfb406b3d37a 100644 --- a/checker/tests/index/GenericAssignment.java +++ b/checker/tests/index/GenericAssignment.java @@ -1,9 +1,10 @@ -import java.util.List; import org.checkerframework.checker.index.qual.GTENegativeOne; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.index.qual.Positive; import org.checkerframework.common.value.qual.IntRange; +import java.util.List; + public class GenericAssignment { public void assignNonNegativeList(List<@NonNegative Integer> l) { List<@NonNegative Integer> i = l; // line 10 diff --git a/checker/tests/index/GreaterThanOrEqualTransfer.java b/checker/tests/index/GreaterThanOrEqualTransfer.java index af60fd1ce460..09deb96fbd71 100644 --- a/checker/tests/index/GreaterThanOrEqualTransfer.java +++ b/checker/tests/index/GreaterThanOrEqualTransfer.java @@ -1,6 +1,6 @@ import org.checkerframework.common.value.qual.MinLen; -class GreaterThanOrEqualTransfer { +public class GreaterThanOrEqualTransfer { void gte_check(int[] a) { if (a.length >= 1) { int @MinLen(1) [] b = a; diff --git a/checker/tests/index/GreaterThanTransfer.java b/checker/tests/index/GreaterThanTransfer.java index 04a9628dbf34..dbacd8374efa 100644 --- a/checker/tests/index/GreaterThanTransfer.java +++ b/checker/tests/index/GreaterThanTransfer.java @@ -1,6 +1,6 @@ import org.checkerframework.common.value.qual.MinLen; -class GreaterThanTransfer { +public class GreaterThanTransfer { void gt_check(int[] a) { if (a.length > 0) { int @MinLen(1) [] b = a; diff --git a/checker/tests/index/GuavaPrimitives.java b/checker/tests/index/GuavaPrimitives.java index 5890b286df49..bac19a8055c0 100644 --- a/checker/tests/index/GuavaPrimitives.java +++ b/checker/tests/index/GuavaPrimitives.java @@ -1,6 +1,3 @@ -import java.util.AbstractList; -import java.util.Collections; -import java.util.List; import org.checkerframework.checker.index.qual.HasSubsequence; import org.checkerframework.checker.index.qual.IndexFor; import org.checkerframework.checker.index.qual.IndexOrHigh; @@ -11,6 +8,10 @@ import org.checkerframework.checker.index.qual.Positive; import org.checkerframework.common.value.qual.MinLen; +import java.util.AbstractList; +import java.util.Collections; +import java.util.List; + /** * A simplified version of the Guava primitives classes (such as Bytes, Longs, Shorts, etc.) with * all expected warnings suppressed. @@ -51,9 +52,10 @@ public class GuavaPrimitives extends AbstractList { } @SuppressWarnings( - "index") // these three fields need to be initialized in some order, and any ordering - // leads to the first two issuing errors - since each field is dependent on at - // least one of the others + "index" // these three fields need to be initialized in some order, and any ordering + // leads to the first two issuing errors - since each field is dependent on at least one of the + // others + ) GuavaPrimitives( short @MinLen(1) [] array, @IndexFor("#1") @LessThan("#3") int start, diff --git a/checker/tests/index/IndexConditionalReport.java b/checker/tests/index/IndexConditionalReport.java index b2447654aed4..ef5901705329 100644 --- a/checker/tests/index/IndexConditionalReport.java +++ b/checker/tests/index/IndexConditionalReport.java @@ -1,6 +1,6 @@ // test case for https://github.com/typetools/checker-framework/issues/2345 -class IndexConditionalReport { +public class IndexConditionalReport { public int getI(int len) { for (int i = 0; i < len; i++) { diff --git a/checker/tests/index/IndexForTwoArrays.java b/checker/tests/index/IndexForTwoArrays.java index 1e3a7fff5df3..1afe76eb19a0 100644 --- a/checker/tests/index/IndexForTwoArrays.java +++ b/checker/tests/index/IndexForTwoArrays.java @@ -1,4 +1,4 @@ -class IndexForTwoArrays { +public class IndexForTwoArrays { public int compare(double[] a1, double[] a2) { if (a1 == a2) { diff --git a/checker/tests/index/IndexForTwoArrays2.java b/checker/tests/index/IndexForTwoArrays2.java index 40de32d0f2e2..b1f2d940ccfe 100644 --- a/checker/tests/index/IndexForTwoArrays2.java +++ b/checker/tests/index/IndexForTwoArrays2.java @@ -1,6 +1,6 @@ // Test case for issue #34: https://github.com/kelloggm/checker-framework/issues/34 -class IndexForTwoArrays2 { +public class IndexForTwoArrays2 { public boolean equals(int[] da1, int[] da2) { if (da1.length != da2.length) { diff --git a/checker/tests/index/IntroAdd.java b/checker/tests/index/IntroAdd.java index 02c64c1b3025..42316529ecad 100644 --- a/checker/tests/index/IntroAdd.java +++ b/checker/tests/index/IntroAdd.java @@ -5,6 +5,9 @@ public class IntroAdd { void test(int[] arr) { // :: error: (assignment.type.incompatible) @LTLengthOf({"arr"}) int a = 3; + } + + void test(int[] arr, @LTLengthOf({"#1"}) int a) { // :: error: (assignment.type.incompatible) @LTLengthOf({"arr"}) int c = a + 1; @LTEqLengthOf({"arr"}) int c1 = a + 1; diff --git a/checker/tests/index/IntroAnd.java b/checker/tests/index/IntroAnd.java index 2219a2b37c49..340da58b8aed 100644 --- a/checker/tests/index/IntroAnd.java +++ b/checker/tests/index/IntroAnd.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.*; -class IntroAnd { +public class IntroAnd { void test() { @NonNegative int a = 1 & 0; @NonNegative int b = a & 5; diff --git a/checker/tests/index/IntroShift.java b/checker/tests/index/IntroShift.java index b6e1006609ca..dff7b471fe8a 100644 --- a/checker/tests/index/IntroShift.java +++ b/checker/tests/index/IntroShift.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.NonNegative; -class IntroShift { +public class IntroShift { void test() { @NonNegative int a = 1 >> 1; // :: error: (assignment.type.incompatible) diff --git a/checker/tests/index/IntroSub.java b/checker/tests/index/IntroSub.java index 7cf068e9bd77..778c66107561 100644 --- a/checker/tests/index/IntroSub.java +++ b/checker/tests/index/IntroSub.java @@ -5,6 +5,9 @@ public class IntroSub { void test(int[] arr) { // :: error: (assignment.type.incompatible) @LTLengthOf({"arr"}) int a = 3; + } + + void test(int[] arr, @LTLengthOf({"#1"}) int a) { // :: error: (assignment.type.incompatible) @LTLengthOf({"arr"}) int c = a - (-1); @LTEqLengthOf({"arr"}) int c1 = a - (-1); diff --git a/checker/tests/index/InvalidSubsequence.java b/checker/tests/index/InvalidSubsequence.java index 290635d2ef21..f15b85d8a17a 100644 --- a/checker/tests/index/InvalidSubsequence.java +++ b/checker/tests/index/InvalidSubsequence.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.index.qual.IndexOrHigh; import org.checkerframework.checker.index.qual.LessThan; -class InvalidSubsequence { +public class InvalidSubsequence { // :: error: flowexpr.parse.error :: error: not.final @HasSubsequence(subsequence = "banana", from = "this.from", to = "this.to") int[] a; diff --git a/checker/tests/index/Issue194.java b/checker/tests/index/Issue194.java index e22c4f43d46a..a4788cbf9b91 100644 --- a/checker/tests/index/Issue194.java +++ b/checker/tests/index/Issue194.java @@ -32,6 +32,7 @@ public boolean m(Custom a, Custom b) { return true; } + @SuppressWarnings("anno.on.irrelevant") public void m2(Custom a, Custom b) { if (a.length() != b.length()) { return; diff --git a/checker/tests/index/Issue20.java b/checker/tests/index/Issue20.java index aa0dd9b571f0..f2dc36f942b9 100644 --- a/checker/tests/index/Issue20.java +++ b/checker/tests/index/Issue20.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.LTEqLengthOf; import org.checkerframework.checker.index.qual.LTLengthOf; -class Issue20 { +public class Issue20 { // An issue with LUB that results in losing information when unifying. int[] a, b; diff --git a/checker/tests/index/Issue2029.java b/checker/tests/index/Issue2029.java index eed83f125dd3..d948712a2737 100644 --- a/checker/tests/index/Issue2029.java +++ b/checker/tests/index/Issue2029.java @@ -23,6 +23,8 @@ void LessThanOffsetUpperBound( @NonNegative @LessThan("#3 + 1") int index) { @NonNegative int m = n - k; int[] arr = new int[size]; + // TODO: understand whether this is a false positive + // :: error: (unary.increment.type.incompatible) for (; index < arr.length - 1; index++) { arr[index] = 10; } diff --git a/checker/tests/index/Issue21.java b/checker/tests/index/Issue21.java index 7f651c1d9926..f474d7893f5b 100644 --- a/checker/tests/index/Issue21.java +++ b/checker/tests/index/Issue21.java @@ -1,4 +1,4 @@ -class Issue21 { +public class Issue21 { void test(int[] arr, int[] arr2) { for (int i = 0; i < arr2.length && i < arr.length; i++) { diff --git a/checker/tests/index/Issue2334.java b/checker/tests/index/Issue2334.java index 5eb86d9c1884..c81a8b8115e8 100644 --- a/checker/tests/index/Issue2334.java +++ b/checker/tests/index/Issue2334.java @@ -2,7 +2,7 @@ import org.checkerframework.checker.index.qual.NonNegative; -class Issue2334 { +public class Issue2334 { void hasSideEffect() {} diff --git a/checker/tests/index/Issue2420.java b/checker/tests/index/Issue2420.java index 64f251244393..7ed8cc7dd820 100644 --- a/checker/tests/index/Issue2420.java +++ b/checker/tests/index/Issue2420.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.*; import org.checkerframework.common.value.qual.*; -class Issue2420 { +public class Issue2420 { static void str(String argStr) { if (argStr.isEmpty()) { return; diff --git a/checker/tests/index/Issue2452.java b/checker/tests/index/Issue2452.java new file mode 100644 index 000000000000..02374f96d247 --- /dev/null +++ b/checker/tests/index/Issue2452.java @@ -0,0 +1,40 @@ +// Test case for https://github.com/typetools/checker-framework/issues/2452 + +import org.checkerframework.checker.index.qual.IndexFor; +import org.checkerframework.checker.index.qual.IndexOrHigh; +import org.checkerframework.checker.index.qual.LTEqLengthOf; +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.index.qual.Positive; +import org.checkerframework.common.value.qual.MinLen; + +import java.lang.reflect.Array; + +class Issue2452 { + Object m1(Object[] a1) { + if (Array.getLength(a1) > 0) { + return Array.get(a1, 0); + } else { + return null; + } + } + + void m2() { + int[] arr = {1, 2, 3}; + @LTEqLengthOf({"arr"}) int a = Array.getLength(arr); + } + + void testMinLenSubtractPositive(String @MinLen(10) [] s) { + @Positive int i1 = s.length - 9; + @NonNegative int i0 = Array.getLength(s) - 10; + // :: error: (assignment.type.incompatible) + @NonNegative int im1 = Array.getLength(s) - 11; + } + + void testLessThanLength(String[] s, @IndexOrHigh("#1") int i, @IndexOrHigh("#1") int j) { + if (i < Array.getLength(s)) { + @IndexFor("s") int in = i; + // :: error: (assignment.type.incompatible) + @IndexFor("s") int jn = j; + } + } +} diff --git a/checker/tests/index/Issue2493.java b/checker/tests/index/Issue2493.java index bbd5abb74ece..2a65c9cd9306 100644 --- a/checker/tests/index/Issue2493.java +++ b/checker/tests/index/Issue2493.java @@ -2,8 +2,8 @@ import org.checkerframework.checker.index.qual.*; -class Issue2493 { +public class Issue2493 { public static void test(int a[], int @SameLen("#1") [] b) { - for (@IndexOrHigh("b") int i = 0; i < a.length; i++) ; + for (@IndexOrHigh("b") int i = 0; i < a.length; i++) {} } } diff --git a/checker/tests/index/Issue2494.java b/checker/tests/index/Issue2494.java index f79d99b7ac66..9ad50bca77c2 100644 --- a/checker/tests/index/Issue2494.java +++ b/checker/tests/index/Issue2494.java @@ -20,7 +20,7 @@ public final class Issue2494 { }; static void binomialA( - @NonNegative @LTLengthOf("this.factorials") int n, + @NonNegative @LTLengthOf("Issue2494.factorials") int n, @NonNegative @LessThan("#1 + 1") int k) { @IndexFor("factorials") int j = k; } diff --git a/checker/tests/index/Issue2505.java b/checker/tests/index/Issue2505.java index 90db74096dd3..e20533009ac8 100644 --- a/checker/tests/index/Issue2505.java +++ b/checker/tests/index/Issue2505.java @@ -1,6 +1,6 @@ import org.checkerframework.common.value.qual.MinLen; -class Issue2505 { +public class Issue2505 { public static void warningIfStatement(int @MinLen(1) [] a) { int i = a.length; if (--i >= 0) { diff --git a/checker/tests/index/Issue2613.java b/checker/tests/index/Issue2613.java index 16f5b0a45006..ac47cc6bb5eb 100644 --- a/checker/tests/index/Issue2613.java +++ b/checker/tests/index/Issue2613.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.LTLengthOf; import org.checkerframework.checker.index.qual.LessThan; -class Issue2613 { +public class Issue2613 { private static final String STRING_CONSTANT = "Hello"; diff --git a/checker/tests/index/Issue2629.java b/checker/tests/index/Issue2629.java index 466c37c8a20d..ea16381a0c38 100644 --- a/checker/tests/index/Issue2629.java +++ b/checker/tests/index/Issue2629.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.index.qual.LessThan; -class Issue2629 { +public class Issue2629 { @LessThan("#1 + 1") int test(int a) { return a; } diff --git a/checker/tests/index/Issue3207.java b/checker/tests/index/Issue3207.java index 4cef00ecf164..b2f72a87e2c0 100644 --- a/checker/tests/index/Issue3207.java +++ b/checker/tests/index/Issue3207.java @@ -1,7 +1,5 @@ // Test case for https://tinyurl.com/cfissue/3207 -// @skip-test until the issue is fixed - import org.checkerframework.checker.index.qual.LTLengthOf; import org.checkerframework.common.value.qual.MinLen; diff --git a/checker/tests/index/Issue3224.java b/checker/tests/index/Issue3224.java new file mode 100644 index 000000000000..d37a6606c579 --- /dev/null +++ b/checker/tests/index/Issue3224.java @@ -0,0 +1,41 @@ +// Test case for https://tinyurl.com/cfissue/3224 + +import org.checkerframework.common.value.qual.IntRange; +import org.checkerframework.common.value.qual.MinLen; + +import java.util.Arrays; + +public class Issue3224 { + static class Arrays { + static String[] copyOf(String[] args, int length) { + return args; + } + } + + public static void m1(String @MinLen(1) [] args) { + int i = 4; + String @MinLen(1) [] args2 = java.util.Arrays.copyOf(args, i); + } + + public static void m2(String @MinLen(1) [] args) { + String @MinLen(1) [] args2 = java.util.Arrays.copyOf(args, args.length); + } + + public static void m3(String @MinLen(1) ... args) { + String @MinLen(1) [] args2 = java.util.Arrays.copyOf(args, args.length); + } + + public static void m4(String @MinLen(1) [] args, @IntRange(from = 10, to = 200) int len) { + String @MinLen(1) [] args2 = java.util.Arrays.copyOf(args, len); + } + + public static void m5(String @MinLen(1) [] args, String[] otherArray) { + // :: error: assignment.type.incompatible + String @MinLen(1) [] args2 = java.util.Arrays.copyOf(args, otherArray.length); + } + + public static void m6(String @MinLen(1) [] args) { + // :: error: assignment.type.incompatible + String @MinLen(1) [] args2 = Arrays.copyOf(args, args.length); + } +} diff --git a/checker/tests/index/Issue5471.java b/checker/tests/index/Issue5471.java new file mode 100644 index 000000000000..9f28c2c91501 --- /dev/null +++ b/checker/tests/index/Issue5471.java @@ -0,0 +1,22 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5471. + +import org.checkerframework.checker.index.qual.IndexFor; + +public class Issue5471 { + private static boolean atTheBeginning(@IndexFor("#2") int index, String line) { + return (index == 0); + } + + private static boolean hasDoubleQuestionMarkAtTheBeginning(String line) { + int i = line.indexOf("??"); + if (i != -1) { + return (atTheBeginning(i, line)); + } + return false; + } + + public static void main(String[] args) { + String x = "Hello?World, this is our new program"; + if (hasDoubleQuestionMarkAtTheBeginning(x)) System.out.println("TRUE"); + } +} diff --git a/checker/tests/index/Issue58Minimization.java b/checker/tests/index/Issue58Minimization.java index 5569ba591855..1f8fcb983489 100644 --- a/checker/tests/index/Issue58Minimization.java +++ b/checker/tests/index/Issue58Minimization.java @@ -6,7 +6,7 @@ import org.checkerframework.checker.index.qual.SameLen; import org.checkerframework.common.value.qual.MinLen; -class Issue58Minimization { +public class Issue58Minimization { void test(@GTENegativeOne int x) { int z; diff --git a/checker/tests/index/Issue60.java b/checker/tests/index/Issue60.java index 4b0255cf0c92..eddfb9dc2384 100644 --- a/checker/tests/index/Issue60.java +++ b/checker/tests/index/Issue60.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.index.qual.IndexFor; -class Issue60 { +public class Issue60 { public static int[] fn_compose(@IndexFor("#2") int[] a, int[] b) { int[] result = new int[a.length]; diff --git a/checker/tests/index/IteratorVoid.java b/checker/tests/index/IteratorVoid.java index 541d52a701a7..36f7fc80fdd4 100644 --- a/checker/tests/index/IteratorVoid.java +++ b/checker/tests/index/IteratorVoid.java @@ -1,8 +1,7 @@ import java.util.Iterator; -// If quals are configured incorrectly, there will be an -// incompatible assignment error; this ensures that Void -// is given the Positive type. +// If quals are configured incorrectly, there will be an incompatible assignment error; this ensures +// that Void is given the Positive type. public class IteratorVoid { T next1; diff --git a/checker/tests/index/Kelloggm225.java b/checker/tests/index/Kelloggm225.java index 002456056b46..0bc334b6a53d 100644 --- a/checker/tests/index/Kelloggm225.java +++ b/checker/tests/index/Kelloggm225.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.*; import org.checkerframework.common.value.qual.*; -class Kelloggm225 { +public class Kelloggm225 { void method(int @MinLen(1) [] bar) { foo(bar, 0, bar.length); } diff --git a/checker/tests/index/LBCSubtyping.java b/checker/tests/index/LBCSubtyping.java index 8abda9d3151a..3ef5a1df2dc9 100644 --- a/checker/tests/index/LBCSubtyping.java +++ b/checker/tests/index/LBCSubtyping.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.index.qual.Positive; -class LBCSubtyping { +public class LBCSubtyping { void foo() { diff --git a/checker/tests/index/LTLDivide.java b/checker/tests/index/LTLDivide.java index 8d16cd6bdd08..631c24cfa92b 100644 --- a/checker/tests/index/LTLDivide.java +++ b/checker/tests/index/LTLDivide.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.LTEqLengthOf; import org.checkerframework.checker.index.qual.LTLengthOf; -class LTLDivide { +public class LTLDivide { int[] test(int[] array) { // @LTLengthOf("array") int len = array.length / 2; int len = array.length / 2; diff --git a/checker/tests/index/LTLengthOfPostcondition.java b/checker/tests/index/LTLengthOfPostcondition.java index b89289ea8aad..8ec232b8372b 100644 --- a/checker/tests/index/LTLengthOfPostcondition.java +++ b/checker/tests/index/LTLengthOfPostcondition.java @@ -1,10 +1,11 @@ -import java.util.Arrays; import org.checkerframework.checker.index.qual.EnsuresLTLengthOf; import org.checkerframework.checker.index.qual.EnsuresLTLengthOfIf; import org.checkerframework.checker.index.qual.LTEqLengthOf; import org.checkerframework.checker.index.qual.NonNegative; -class LTLengthOfPostcondition { +import java.util.Arrays; + +public class LTLengthOfPostcondition { Object[] array; diff --git a/checker/tests/index/LengthOfArrayMinusOne.java b/checker/tests/index/LengthOfArrayMinusOne.java index bfb71120f898..e575024e4d73 100644 --- a/checker/tests/index/LengthOfArrayMinusOne.java +++ b/checker/tests/index/LengthOfArrayMinusOne.java @@ -1,4 +1,4 @@ -class LengthOfArrayMinusOne { +public class LengthOfArrayMinusOne { void test(int[] arr) { // :: error: (array.access.unsafe.low) int i = arr[arr.length - 1]; diff --git a/checker/tests/index/LengthOfTest.java b/checker/tests/index/LengthOfTest.java index 7a3c99807239..68f79675aca5 100644 --- a/checker/tests/index/LengthOfTest.java +++ b/checker/tests/index/LengthOfTest.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.*; -class LengthOfTest { +public class LengthOfTest { void foo(int[] a, @LengthOf("#1") int x) { @IndexOrHigh("a") int y = x; // :: error: (assignment.type.incompatible) diff --git a/checker/tests/index/LengthTransfer.java b/checker/tests/index/LengthTransfer.java index 3970c0720741..c7e5bbf63809 100644 --- a/checker/tests/index/LengthTransfer.java +++ b/checker/tests/index/LengthTransfer.java @@ -1,4 +1,4 @@ -class LengthTransfer { +public class LengthTransfer { void exceptional_control_flow(int[] a) { if (a.length == 0) { throw new IllegalArgumentException(); diff --git a/checker/tests/index/LengthTransfer2.java b/checker/tests/index/LengthTransfer2.java index af90aecac78a..b4b9ef91fe57 100644 --- a/checker/tests/index/LengthTransfer2.java +++ b/checker/tests/index/LengthTransfer2.java @@ -2,7 +2,7 @@ import org.checkerframework.common.value.qual.*; -class LengthTransfer2 { +public class LengthTransfer2 { public static void main(String[] args) { if (args.length != 2) { System.err.println("Needs 2 arguments, got " + args.length); diff --git a/checker/tests/index/LengthTransferForMinLen.java b/checker/tests/index/LengthTransferForMinLen.java index 4acc4cd2765a..21d9879f76dc 100644 --- a/checker/tests/index/LengthTransferForMinLen.java +++ b/checker/tests/index/LengthTransferForMinLen.java @@ -1,6 +1,6 @@ import org.checkerframework.common.value.qual.MinLen; -class LengthTransferForMinLen { +public class LengthTransferForMinLen { void exceptional_control_flow(int[] a) { if (a.length == 0) { throw new IllegalArgumentException(); diff --git a/checker/tests/index/LessThanBug.java b/checker/tests/index/LessThanBug.java new file mode 100644 index 000000000000..dd557f22311f --- /dev/null +++ b/checker/tests/index/LessThanBug.java @@ -0,0 +1,15 @@ +import org.checkerframework.checker.index.qual.LessThan; +import org.checkerframework.common.value.qual.IntRange; +import org.checkerframework.common.value.qual.IntVal; + +public class LessThanBug { + + void call() { + bug(30, 1); + } + + void bug(@IntRange(to = 42) int a, @IntVal(1) int c) { + // :: error: (assignment.type.incompatible) + @LessThan("c") int x = a; + } +} diff --git a/checker/tests/index/LessThanFloat.java b/checker/tests/index/LessThanFloat.java new file mode 100644 index 000000000000..9136a5da3645 --- /dev/null +++ b/checker/tests/index/LessThanFloat.java @@ -0,0 +1,62 @@ +import org.checkerframework.checker.index.qual.LessThan; + +public class LessThanFloat { + int bigger; + + @LessThan("bigger") byte b; + + @LessThan("bigger") short s; + + @LessThan("bigger") int i; + + @LessThan("bigger") long l; + + // :: error: (anno.on.irrelevant) + @LessThan("bigger") float f; + + // :: error: (anno.on.irrelevant) + @LessThan("bigger") double d; + + // :: error: (anno.on.irrelevant) + @LessThan("bigger") boolean bool; + + @LessThan("bigger") char c; + + @LessThan("bigger") Byte bBoxed; + + @LessThan("bigger") Short sBoxed; + + @LessThan("bigger") Integer iBoxed; + + @LessThan("bigger") Long lBoxed; + + // :: error: (anno.on.irrelevant) + @LessThan("bigger") Float fBoxed; + + // :: error: (anno.on.irrelevant) + @LessThan("bigger") Double dBoxed; + + // :: error: (anno.on.irrelevant) + @LessThan("bigger") Boolean boolBoxed; + + @LessThan("bigger") Character cBoxed; + + java.lang.@LessThan("bigger") Byte bBoxed2; + + java.lang.@LessThan("bigger") Short sBoxed2; + + java.lang.@LessThan("bigger") Integer iBoxed2; + + java.lang.@LessThan("bigger") Long lBoxed2; + + // :: error: (anno.on.irrelevant) + java.lang.@LessThan("bigger") Float fBoxed2; + + // :: error: (anno.on.irrelevant) + java.lang.@LessThan("bigger") Double dBoxed2; + + // :: error: (anno.on.irrelevant) + java.lang.@LessThan("bigger") Boolean boolBoxed2; + + java.lang.@LessThan("bigger") Character cBoxed2; +} diff --git a/checker/tests/index/LessThanFloatLiteral.java b/checker/tests/index/LessThanFloatLiteral.java new file mode 100644 index 000000000000..5242da9c2027 --- /dev/null +++ b/checker/tests/index/LessThanFloatLiteral.java @@ -0,0 +1,12 @@ +import org.checkerframework.checker.index.qual.LessThan; + +class LessThanFloatLiteral { + void test(int x) { + if (1.0 > x) { + // TODO: It might be nice to handle comparisons against floats, + // but an array index is not generally compared to a float. + // :: error: assignment.type.incompatible + @LessThan("1") int y = x; + } + } +} diff --git a/checker/tests/index/LessThanOrEqualTransfer.java b/checker/tests/index/LessThanOrEqualTransfer.java index ff912850fa58..cfc7efb01d48 100644 --- a/checker/tests/index/LessThanOrEqualTransfer.java +++ b/checker/tests/index/LessThanOrEqualTransfer.java @@ -1,6 +1,6 @@ import org.checkerframework.common.value.qual.MinLen; -class LessThanOrEqualTransfer { +public class LessThanOrEqualTransfer { void lte_check(int[] a) { if (1 <= a.length) { int @MinLen(1) [] b = a; diff --git a/checker/tests/index/LessThanTransfer.java b/checker/tests/index/LessThanTransfer.java deleted file mode 100644 index f1da12adc8f7..000000000000 --- a/checker/tests/index/LessThanTransfer.java +++ /dev/null @@ -1,16 +0,0 @@ -import org.checkerframework.common.value.qual.MinLen; - -class LessThanTransfer { - void lt_check(int[] a) { - if (0 < a.length) { - int @MinLen(1) [] b = a; - } - } - - void lt_bad_check(int[] a) { - if (0 < a.length) { - // :: error: (assignment.type.incompatible) - int @MinLen(2) [] b = a; - } - } -} diff --git a/checker/tests/index/LessThanTransferTest.java b/checker/tests/index/LessThanTransferTest.java new file mode 100644 index 000000000000..bdb55226ea3c --- /dev/null +++ b/checker/tests/index/LessThanTransferTest.java @@ -0,0 +1,16 @@ +import org.checkerframework.common.value.qual.MinLen; + +public class LessThanTransferTest { + void lt_check(int[] a) { + if (0 < a.length) { + int @MinLen(1) [] b = a; + } + } + + void lt_bad_check(int[] a) { + if (0 < a.length) { + // :: error: (assignment.type.incompatible) + int @MinLen(2) [] b = a; + } + } +} diff --git a/checker/tests/index/LessThanValue.java b/checker/tests/index/LessThanValue.java index 3d3a47f17f4d..a59f8117d93b 100644 --- a/checker/tests/index/LessThanValue.java +++ b/checker/tests/index/LessThanValue.java @@ -28,6 +28,8 @@ void lub(int x, int y, @LessThan({"#1", "#2"}) int a, @LessThan("#1") int b) { void transitive(int a, int b, int c) { if (a < b) { if (b < c) { + // TODO: False positive + // :: error: (assignment.type.incompatible) @LessThan("c") int x = a; } } @@ -115,6 +117,7 @@ void count(int count) { newCapacity = Integer.MAX_VALUE; // guaranteed to be >= newCapacity } + // :: error: (return.type.incompatible) return newCapacity; } } diff --git a/checker/tests/index/LessThanZeroArrayLength.java b/checker/tests/index/LessThanZeroArrayLength.java index a61f70113dce..ea0ccd2e11b3 100644 --- a/checker/tests/index/LessThanZeroArrayLength.java +++ b/checker/tests/index/LessThanZeroArrayLength.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.LessThan; -class LessThanZeroArrayLength { +public class LessThanZeroArrayLength { void test(int[] a) { foo(0, a.length); } diff --git a/checker/tests/index/ListAdd.java b/checker/tests/index/ListAdd.java index b251239e4627..51a75b6330de 100644 --- a/checker/tests/index/ListAdd.java +++ b/checker/tests/index/ListAdd.java @@ -5,7 +5,7 @@ // @skip-test until we bring list support back -class ListAdd { +public class ListAdd { List listField; diff --git a/checker/tests/index/ListAddAll.java b/checker/tests/index/ListAddAll.java index 2e76d7c57543..8fed1ddf4d94 100644 --- a/checker/tests/index/ListAddAll.java +++ b/checker/tests/index/ListAddAll.java @@ -4,7 +4,7 @@ // @skip-test until we bring list support back -class ListAddAll { +public class ListAddAll { List listField; List coll; diff --git a/checker/tests/index/ListAddInfiniteLoop.java b/checker/tests/index/ListAddInfiniteLoop.java index 0f7a686f4830..6c9fbd7ceb74 100644 --- a/checker/tests/index/ListAddInfiniteLoop.java +++ b/checker/tests/index/ListAddInfiniteLoop.java @@ -1,6 +1,6 @@ // @skip-test until we bring list support back -class ListAddInfiniteLoop { +public class ListAddInfiniteLoop { void ListLoop(List list) { while (true) { diff --git a/checker/tests/index/ListGet.java b/checker/tests/index/ListGet.java index 178d461e3fe1..e60cea1aef26 100644 --- a/checker/tests/index/ListGet.java +++ b/checker/tests/index/ListGet.java @@ -4,7 +4,7 @@ // @skip-test until we bring list support back -class ListGet { +public class ListGet { List listField; int[] arr = {0}; diff --git a/checker/tests/index/ListIterator.java b/checker/tests/index/ListIterator.java index 23ab03b738fa..e2d85d694132 100644 --- a/checker/tests/index/ListIterator.java +++ b/checker/tests/index/ListIterator.java @@ -4,7 +4,7 @@ // @skip-test until we bring list support back -class ListIterator { +public class ListIterator { List listField; diff --git a/checker/tests/index/ListLowerBound.java b/checker/tests/index/ListLowerBound.java index 92641e5f60c7..fef7cfeacfc8 100644 --- a/checker/tests/index/ListLowerBound.java +++ b/checker/tests/index/ListLowerBound.java @@ -1,9 +1,10 @@ // @skip-test until we bring list support back -import java.util.List; -import java.util.ListIterator; import org.checkerframework.checker.index.qual.GTENegativeOne; import org.checkerframework.checker.index.qual.NonNegative; +import java.util.List; +import java.util.ListIterator; + public class ListLowerBound { private void m(List l) { diff --git a/checker/tests/index/ListRemove.java b/checker/tests/index/ListRemove.java index 80750727a70d..32146eb10bfb 100644 --- a/checker/tests/index/ListRemove.java +++ b/checker/tests/index/ListRemove.java @@ -3,9 +3,10 @@ import org.checkerframework.checker.index.qual.LTLengthOf; // @skip-test until we bring list support back -// @skip-test can't handle until TreeUtils.getMethod has a way to precisly handle method overloading +// @skip-test can't handle until TreeUtils.getMethod has a way to precisely handle method +// overloading -class ListRemove { +public class ListRemove { List listField; diff --git a/checker/tests/index/ListSet.java b/checker/tests/index/ListSet.java index aba477b57a8a..943caac69e9a 100644 --- a/checker/tests/index/ListSet.java +++ b/checker/tests/index/ListSet.java @@ -4,7 +4,7 @@ // @skip-test until we bring list support back -class ListSet { +public class ListSet { List listField; diff --git a/checker/tests/index/ListSupport.java b/checker/tests/index/ListSupport.java index 7d8f7cc7e9f7..de94282ddf22 100644 --- a/checker/tests/index/ListSupport.java +++ b/checker/tests/index/ListSupport.java @@ -4,7 +4,7 @@ // @skip-test until we bring list support back -class ListSupport { +public class ListSupport { void indexOf(List list) { int index = list.indexOf(0); diff --git a/checker/tests/index/ListSupportLBC.java b/checker/tests/index/ListSupportLBC.java index 666b742cac84..4b570c514f70 100644 --- a/checker/tests/index/ListSupportLBC.java +++ b/checker/tests/index/ListSupportLBC.java @@ -1,11 +1,12 @@ -import java.util.ArrayList; import org.checkerframework.checker.index.qual.GTENegativeOne; import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.index.qual.Positive; +import java.util.ArrayList; + // @skip-test until we bring list support back -class ListSupportLBC { +public class ListSupportLBC { void testGet() { diff --git a/checker/tests/index/ListSupportML.java b/checker/tests/index/ListSupportML.java index 83fc71428f70..bbe2e99e3d14 100644 --- a/checker/tests/index/ListSupportML.java +++ b/checker/tests/index/ListSupportML.java @@ -1,9 +1,10 @@ -import java.util.ArrayList; import org.checkerframework.common.value.qual.MinLen; +import java.util.ArrayList; + // @skip-test until we bring list support back -class ListSupportML { +public class ListSupportML { void newListMinLen() { List list = new ArrayList<>(); diff --git a/checker/tests/index/MLEqualTo.java b/checker/tests/index/MLEqualTo.java index 27aa9c9dd21c..642ebbe83094 100644 --- a/checker/tests/index/MLEqualTo.java +++ b/checker/tests/index/MLEqualTo.java @@ -1,6 +1,6 @@ import org.checkerframework.common.value.qual.MinLen; -class MLEqualTo { +public class MLEqualTo { public static void equalToMinLen(int @MinLen(2) [] m, int @MinLen(0) [] r) { if (r == m) { diff --git a/checker/tests/index/MathPlumeClasscastCrash.java b/checker/tests/index/MathPlumeClasscastCrash.java new file mode 100644 index 000000000000..c490963f5abc --- /dev/null +++ b/checker/tests/index/MathPlumeClasscastCrash.java @@ -0,0 +1,16 @@ +// A test that caused a ClassCastException in the UpperBound Checker. Based on a +// function in MathPlume, discovered while minimizing another crash in WPI (hence +// why the function from MathPlume was changed to just return 0 in the first place...). + +import org.checkerframework.checker.index.qual.LessThan; +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.index.qual.PolyUpperBound; + +public final class MathPlumeClasscastCrash { + + @SuppressWarnings("index:return") + public static @NonNegative @LessThan("#2") @PolyUpperBound long modPositive( + long x, @PolyUpperBound long y) { + return 0; + } +} diff --git a/checker/tests/index/MinLenFromPositive.java b/checker/tests/index/MinLenFromPositive.java index 8eaece1b8cf1..7f0cb27af3f9 100644 --- a/checker/tests/index/MinLenFromPositive.java +++ b/checker/tests/index/MinLenFromPositive.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.Positive; import org.checkerframework.common.value.qual.*; -class MinLenFromPositive { +public class MinLenFromPositive { void test(@Positive int x) { int @MinLen(1) [] y = new int[x]; diff --git a/checker/tests/index/MinLenSameLenInteraction.java b/checker/tests/index/MinLenSameLenInteraction.java index d8db871ccf68..3c2b3dfe5cd1 100644 --- a/checker/tests/index/MinLenSameLenInteraction.java +++ b/checker/tests/index/MinLenSameLenInteraction.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.*; -class MinLenSameLenInteraction { +public class MinLenSameLenInteraction { void test(int @SameLen("#2") [] a, int @SameLen("#1") [] b) { if (a.length == 1) { int x = b[0]; diff --git a/checker/tests/index/MinMax.java b/checker/tests/index/MinMax.java index c34472c2611c..8357a70b748d 100644 --- a/checker/tests/index/MinMax.java +++ b/checker/tests/index/MinMax.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.GTENegativeOne; import org.checkerframework.checker.index.qual.Positive; -class MinMax { +public class MinMax { // They call me a power gamer. I stole the test cases from issue 26. void mathmax() { @Positive int i = Math.max(-15, 2); diff --git a/checker/tests/index/MinMaxIndex.java b/checker/tests/index/MinMaxIndex.java index 722d84d2d38d..588d19deace1 100644 --- a/checker/tests/index/MinMaxIndex.java +++ b/checker/tests/index/MinMaxIndex.java @@ -5,17 +5,19 @@ import org.checkerframework.checker.index.qual.IndexFor; import org.checkerframework.checker.index.qual.IndexOrHigh; -class MinMaxIndex { +public class MinMaxIndex { // Both min and max preserve IndexFor void indexFor(char[] array, @IndexFor("#1") int i1, @IndexFor("#1") int i2) { char c = array[Math.max(i1, i2)]; char d = array[Math.min(i1, i2)]; } + // Both min and max preserve IndexOrHigh void indexOrHigh(String str, @IndexOrHigh("#1") int i1, @IndexOrHigh("#1") int i2) { str.substring(Math.max(i1, i2)); str.substring(Math.min(i1, i2)); } + // Combining IndexFor and IndexOrHigh void indexForOrHigh(String str, @IndexFor("#1") int i1, @IndexOrHigh("#1") int i2) { str.substring(Math.max(i1, i2)); @@ -24,6 +26,7 @@ void indexForOrHigh(String str, @IndexFor("#1") int i1, @IndexOrHigh("#1") int i str.charAt(Math.max(i1, i2)); str.charAt(Math.min(i1, i2)); } + // max does not work with different sequences, min does void twoSequences(String str1, String str2, @IndexFor("#1") int i1, @IndexFor("#2") int i2) { // :: error: (argument.type.incompatible) diff --git a/checker/tests/index/NonNegativeCharValue.java b/checker/tests/index/NonNegativeCharValue.java index c5cf600f8850..2c6e65ae056e 100644 --- a/checker/tests/index/NonNegativeCharValue.java +++ b/checker/tests/index/NonNegativeCharValue.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.NonNegative; -class NonNegativeCharValue { +public class NonNegativeCharValue { public static String toString(final @NonNegative Character ch) { return toString(ch.charValue()); } diff --git a/checker/tests/index/NonnegativeChar.java b/checker/tests/index/NonnegativeChar.java index 86cbfbeb3110..e3fe527d4aa0 100644 --- a/checker/tests/index/NonnegativeChar.java +++ b/checker/tests/index/NonnegativeChar.java @@ -1,37 +1,38 @@ -import java.util.ArrayList; import org.checkerframework.checker.index.qual.LowerBoundBottom; import org.checkerframework.checker.index.qual.PolyLowerBound; +import java.util.ArrayList; + public class NonnegativeChar { void foreach(char[] array) { - for (char value : array) ; // line 7 + for (char value : array) {} } char constant() { - return Character.MAX_VALUE; // line 11 + return Character.MAX_VALUE; } char conversion(int i) { - return (char) i; // line 15 + return (char) i; } public void takeList(ArrayList z) {} public void passList() { - takeList(new ArrayList()); // line 20 + takeList(new ArrayList()); } static class CustomList extends ArrayList {} public void passCustomList() { - takeList(new CustomList()); // line 25 + takeList(new CustomList()); } public @LowerBoundBottom char bottomLB(@LowerBoundBottom char c) { - return c; // line 29 + return c; } public @PolyLowerBound char polyLB(@PolyLowerBound char c) { - return c; // line 32 + return c; } } diff --git a/checker/tests/index/NotEqualTransfer.java b/checker/tests/index/NotEqualTransfer.java index ee523a16cc46..24a21afae81c 100644 --- a/checker/tests/index/NotEqualTransfer.java +++ b/checker/tests/index/NotEqualTransfer.java @@ -1,6 +1,6 @@ import org.checkerframework.common.value.qual.MinLen; -class NotEqualTransfer { +public class NotEqualTransfer { void neq_check(int[] a) { if (1 != a.length) { int x = 1; // do nothing. diff --git a/checker/tests/index/ObjectClone.java b/checker/tests/index/ObjectClone.java index 64656257a21e..ab9faaf3b2d2 100644 --- a/checker/tests/index/ObjectClone.java +++ b/checker/tests/index/ObjectClone.java @@ -1,9 +1,10 @@ // Test case for issue 146: https://github.com/kelloggm/checker-framework/issues/146 -import java.util.Arrays; import org.checkerframework.checker.index.qual.*; -class ObjectClone { +import java.util.Arrays; + +public class ObjectClone { void test(int[] a, int @SameLen("#1") [] b) { int @SameLen("a") [] c = b.clone(); diff --git a/checker/tests/index/OffsetExample.java b/checker/tests/index/OffsetExample.java index 4f01562f99a6..39481908a9a7 100644 --- a/checker/tests/index/OffsetExample.java +++ b/checker/tests/index/OffsetExample.java @@ -1,8 +1,9 @@ -import java.util.List; import org.checkerframework.checker.index.qual.IndexFor; import org.checkerframework.checker.index.qual.IndexOrHigh; import org.checkerframework.common.value.qual.MinLen; +import java.util.List; + @SuppressWarnings("lowerbound") public class OffsetExample { void example1(int @MinLen(2) [] a) { diff --git a/checker/tests/index/OneLTL.java b/checker/tests/index/OneLTL.java index 66a4191e15c2..a47bc8fbe8a6 100644 --- a/checker/tests/index/OneLTL.java +++ b/checker/tests/index/OneLTL.java @@ -1,4 +1,4 @@ -class OneLTL { +public class OneLTL { public static boolean sorted(int[] a) { for (int i = 0; i < a.length - 1; i++) { if (a[i + 1] < a[i]) { diff --git a/checker/tests/index/OneOrTwo.java b/checker/tests/index/OneOrTwo.java index 13a5224c9149..8adb3f5a2a67 100644 --- a/checker/tests/index/OneOrTwo.java +++ b/checker/tests/index/OneOrTwo.java @@ -1,6 +1,6 @@ import org.checkerframework.common.value.qual.*; -class OneOrTwo { +public class OneOrTwo { @IntVal({1, 2}) int getOneOrTwo() { return 1; } diff --git a/checker/tests/index/OnlyCheckSubsequenceWhenAssigningToArray.java b/checker/tests/index/OnlyCheckSubsequenceWhenAssigningToArray.java index 0edb268b1a3b..2ec214612e46 100644 --- a/checker/tests/index/OnlyCheckSubsequenceWhenAssigningToArray.java +++ b/checker/tests/index/OnlyCheckSubsequenceWhenAssigningToArray.java @@ -2,7 +2,7 @@ import org.checkerframework.checker.index.qual.IndexFor; import org.checkerframework.checker.index.qual.IndexOrHigh; -class OnlyCheckSubsequenceWhenAssigningToArray { +public class OnlyCheckSubsequenceWhenAssigningToArray { @HasSubsequence(subsequence = "this", from = "this.start", to = "this.end") int[] array; diff --git a/checker/tests/index/OuterThisJavaExpression.java b/checker/tests/index/OuterThisJavaExpression.java new file mode 100644 index 000000000000..9941ec5b55d8 --- /dev/null +++ b/checker/tests/index/OuterThisJavaExpression.java @@ -0,0 +1,59 @@ +// Test case for issue #4558: https://tinyurl.com/cfissue/4558 + +// @skip-test until the issue is fixed + +import org.checkerframework.checker.index.qual.SameLen; + +public abstract class OuterThisJavaExpression { + + String s; + + OuterThisJavaExpression(String s) { + this.s = s; + } + + final class Inner { + + String s = "different from " + OuterThisJavaExpression.this.s; + + @SameLen("s") String f1() { + return s; + } + + @SameLen("s") String f2() { + return this.s; + } + + @SameLen("s") String f3() { + // :: error: (return.type.incompatible) + return OuterThisJavaExpression.this.s; + } + + @SameLen("this.s") String f4() { + return s; + } + + @SameLen("this.s") String f5() { + return this.s; + } + + @SameLen("this.s") String f6() { + // :: error: (return.type.incompatible) + return OuterThisJavaExpression.this.s; + } + + @SameLen("OuterThisJavaExpression.this.s") String f7() { + // :: error: (return.type.incompatible) + return s; + } + + @SameLen("OuterThisJavaExpression.this.s") String f8() { + // :: error: (return.type.incompatible) + return this.s; + } + + @SameLen("OuterThisJavaExpression.this.s") String f9() { + return OuterThisJavaExpression.this.s; + } + } +} diff --git a/checker/tests/index/ParsingBug.java b/checker/tests/index/ParsingBug.java index 953ac9881118..d597ed214325 100644 --- a/checker/tests/index/ParsingBug.java +++ b/checker/tests/index/ParsingBug.java @@ -1,4 +1,4 @@ -class ParsingBug { +public class ParsingBug { void test() { String[] saOrig = new String[] {"foo", "bar"}; Object o1 = do_things((Object) saOrig); diff --git a/checker/tests/index/Pilot2HalfLength.java b/checker/tests/index/Pilot2HalfLength.java index 71c4ea1b6ddd..601a7d5a5644 100644 --- a/checker/tests/index/Pilot2HalfLength.java +++ b/checker/tests/index/Pilot2HalfLength.java @@ -2,7 +2,7 @@ // @skip-test until fixed -class Pilot2HalfLength { +public class Pilot2HalfLength { private static int[] getFirstHalf(int[] array) { int[] firstHalf = new int[array.length / 2]; for (int i = 0; i < firstHalf.length; i++) { diff --git a/checker/tests/index/Pilot3ArrayCreation.java b/checker/tests/index/Pilot3ArrayCreation.java index adf6ac09a230..493b6c36c3d4 100644 --- a/checker/tests/index/Pilot3ArrayCreation.java +++ b/checker/tests/index/Pilot3ArrayCreation.java @@ -1,6 +1,6 @@ // This test case is for issue 44: https://github.com/kelloggm/checker-framework/issues/44 -class Pilot3ArrayCreation { +public class Pilot3ArrayCreation { void test(int[] firstArray, int[] secondArray[]) { int[] newArray = new int[firstArray.length + secondArray.length]; for (int i = 0; i < firstArray.length; i++) { diff --git a/checker/tests/index/Pilot4Subtraction.java b/checker/tests/index/Pilot4Subtraction.java index 46a34cd9a3ed..534fe0ab60dc 100644 --- a/checker/tests/index/Pilot4Subtraction.java +++ b/checker/tests/index/Pilot4Subtraction.java @@ -2,7 +2,7 @@ // @skip-test until bug is fixed -class Pilot4Subtraction { +public class Pilot4Subtraction { private static int[] getSecondHalf(int[] array) { int len = array.length / 2; diff --git a/checker/tests/index/PlumeFail.java b/checker/tests/index/PlumeFail.java index eaa4ee8344d6..1ae9cd296cbb 100644 --- a/checker/tests/index/PlumeFail.java +++ b/checker/tests/index/PlumeFail.java @@ -1,9 +1,10 @@ // Test case affected by eisop Issue 22: // https://github.com/eisop/checker-framework/issues/22 -import java.util.Arrays; import org.checkerframework.common.value.qual.MinLen; +import java.util.Arrays; + public class PlumeFail { void method() { // Workaround by casting. diff --git a/checker/tests/index/PlumeFailMin.java b/checker/tests/index/PlumeFailMin.java index f7368c84a723..bf5a3691e465 100644 --- a/checker/tests/index/PlumeFailMin.java +++ b/checker/tests/index/PlumeFailMin.java @@ -1,6 +1,8 @@ // Test case for eisop Issue 22: // https://github.com/eisop/checker-framework/issues/22 +// @skip-test until the issue is fixed + import org.checkerframework.checker.index.qual.IndexOrHigh; import org.checkerframework.common.value.qual.MinLen; diff --git a/checker/tests/index/PolyLengthTest.java b/checker/tests/index/PolyLengthTest.java index 7bf980fe1c16..379bd367d4ef 100644 --- a/checker/tests/index/PolyLengthTest.java +++ b/checker/tests/index/PolyLengthTest.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.*; import org.checkerframework.common.value.qual.*; -class PolyLengthTest { +public class PolyLengthTest { int @PolyLength [] id(int @PolyLength [] a) { return a; } diff --git a/checker/tests/index/Polymorphic.java b/checker/tests/index/Polymorphic.java index 645509f579db..31f07063b0cf 100644 --- a/checker/tests/index/Polymorphic.java +++ b/checker/tests/index/Polymorphic.java @@ -8,7 +8,7 @@ import org.checkerframework.checker.index.qual.Positive; import org.checkerframework.checker.index.qual.SameLen; -class Polymorphic { +public class Polymorphic { // Identity functions diff --git a/checker/tests/index/Polymorphic2.java b/checker/tests/index/Polymorphic2.java index 686f25b6e82f..9ed046ec2de5 100644 --- a/checker/tests/index/Polymorphic2.java +++ b/checker/tests/index/Polymorphic2.java @@ -7,7 +7,7 @@ import org.checkerframework.checker.index.qual.Positive; import org.checkerframework.checker.index.qual.SameLen; -class Polymorphic2 { +public class Polymorphic2 { public static boolean flag = false; int @PolySameLen [] mergeSameLen(int @PolySameLen [] a, int @PolySameLen [] b) { @@ -26,6 +26,7 @@ void testSameLen(int @SameLen("array1") [] a, int @SameLen("array2") [] b) { @PolyUpperBound int mergeUpperBound(@PolyUpperBound int a, @PolyUpperBound int b) { return flag ? a : b; } + // UpperBound tests void testUpperBound(@LTLengthOf("array1") int a, @LTLengthOf("array2") int b) { int z = mergeUpperBound(a, b); @@ -42,6 +43,7 @@ void testUpperBound2(@LTLengthOf("array1") int a, @LTEqLengthOf("array1") int b) @PolyLowerBound int mergeLowerBound(@PolyLowerBound int a, @PolyLowerBound int b) { return flag ? a : b; } + // LowerBound tests void lbc_id(@NonNegative int n, @Positive int p) { @NonNegative int z = mergeLowerBound(n, p); diff --git a/checker/tests/index/Polymorphic3.java b/checker/tests/index/Polymorphic3.java index 8efa1a7c8e77..5c29323b1c07 100644 --- a/checker/tests/index/Polymorphic3.java +++ b/checker/tests/index/Polymorphic3.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.*; -class Polymorphic3 { +public class Polymorphic3 { // Identity functions diff --git a/checker/tests/index/Polymorphic4.java b/checker/tests/index/Polymorphic4.java index c2dedbcb2039..c1b2fcc41e41 100644 --- a/checker/tests/index/Polymorphic4.java +++ b/checker/tests/index/Polymorphic4.java @@ -2,7 +2,7 @@ // @skip-test until #153 is resolved. -class Polymorphic4 { +public class Polymorphic4 { public static String @PolyValue [] quantify(String @PolyValue [] vars) { diff --git a/checker/tests/index/PredecrementTest.java b/checker/tests/index/PredecrementTest.java index 4713833f9a65..a52ca2cf13df 100644 --- a/checker/tests/index/PredecrementTest.java +++ b/checker/tests/index/PredecrementTest.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.*; import org.checkerframework.common.value.qual.*; -class PredecrementTest { +public class PredecrementTest { public static void warningForLoop(int @MinLen(1) [] a) { for (int i = a.length; --i >= 0; ) { diff --git a/checker/tests/index/PrimitiveWrappers.java b/checker/tests/index/PrimitiveWrappers.java index 27dd7442f378..c07fb338ce22 100644 --- a/checker/tests/index/PrimitiveWrappers.java +++ b/checker/tests/index/PrimitiveWrappers.java @@ -5,7 +5,7 @@ // This test ensures that the checker functions on primitive wrappers in // addition to literal primitives. Primarily it focuses on Integer/int. -class PrimitiveWrappers { +public class PrimitiveWrappers { void int_Integer_access_equivalent(@IndexFor("#3") Integer i, @IndexFor("#3") int j, int[] a) { a[i] = a[j]; diff --git a/checker/tests/index/PrivateSameLen.java b/checker/tests/index/PrivateSameLen.java new file mode 100644 index 000000000000..653bb7601ef6 --- /dev/null +++ b/checker/tests/index/PrivateSameLen.java @@ -0,0 +1,15 @@ +import org.checkerframework.checker.index.qual.SameLen; +import org.checkerframework.dataflow.qual.Pure; + +public class PrivateSameLen { + + @Pure + private @SameLen("#1") String getSameLenString(String in) { + return in; + } + + private void test() { + String in = "foo"; + @SameLen("this.getSameLenString(in)") String myStr = getSameLenString(in); + } +} diff --git a/checker/tests/index/RandomTest.java b/checker/tests/index/RandomTest.java index 5c485fb3be88..85196ace8149 100644 --- a/checker/tests/index/RandomTest.java +++ b/checker/tests/index/RandomTest.java @@ -1,10 +1,12 @@ -import java.util.Random; import org.checkerframework.checker.index.qual.LTLengthOf; +import java.util.Random; + public class RandomTest { void test() { Random rand = new Random(); int[] a = new int[8]; + // :: error: (anno.on.irrelevant) @LTLengthOf("a") double d1 = Math.random() * a.length; @LTLengthOf("a") int deref = (int) (Math.random() * a.length); @LTLengthOf("a") int deref2 = (int) (rand.nextDouble() * a.length); diff --git a/checker/tests/index/RandomTestLBC.java b/checker/tests/index/RandomTestLBC.java index 4d569c148084..ef1fd600e225 100644 --- a/checker/tests/index/RandomTestLBC.java +++ b/checker/tests/index/RandomTestLBC.java @@ -1,12 +1,18 @@ -import java.util.Random; import org.checkerframework.checker.index.qual.NonNegative; +import java.util.Random; + public class RandomTestLBC { void test() { Random rand = new Random(); int[] a = new int[8]; + // Math.random() and Math.nextDouble() are always non-negative, but the Index Checker does + // not reason about floating-point values. + // :: error: (anno.on.irrelevant) @NonNegative double d1 = Math.random() * a.length; + // :: error: (assignment.type.incompatible) @NonNegative int deref = (int) (Math.random() * a.length); + // :: error: (assignment.type.incompatible) @NonNegative int deref2 = (int) (rand.nextDouble() * a.length); @NonNegative int deref3 = rand.nextInt(a.length); } diff --git a/checker/tests/index/RangeIndex.java b/checker/tests/index/RangeIndex.java index 333de5398556..eb705b2db105 100644 --- a/checker/tests/index/RangeIndex.java +++ b/checker/tests/index/RangeIndex.java @@ -1,6 +1,6 @@ import org.checkerframework.common.value.qual.*; -class RangeIndex { +public class RangeIndex { void foo(@IntRange(from = 0, to = 11) int x, int @MinLen(10) [] a) { // :: error: (array.access.unsafe.high.range) int y = a[x]; diff --git a/checker/tests/index/Reassignment.java b/checker/tests/index/Reassignment.java index 22bef7d1df92..0eb46be25989 100644 --- a/checker/tests/index/Reassignment.java +++ b/checker/tests/index/Reassignment.java @@ -1,9 +1,8 @@ -// @skip-test until we solve the underlying issue. The code that caused this to -// pass has been removed, because an incomplete solution that masks the -// problem but still permits some unsoundness is worse than no solution and -// an obvious issue. +// @skip-test until we solve the underlying issue. The code that caused this to pass has been +// removed, because an incomplete solution that masks the problem but still permits some unsoundness +// is worse than no solution and an obvious issue. -class Reassignment { +public class Reassignment { void test(int[] arr, int i) { if (i > 0 && i < arr.length) { arr = new int[0]; diff --git a/checker/tests/index/RefineEq.java b/checker/tests/index/RefineEq.java index 08ad1e05c93c..5c4e13cdcc64 100644 --- a/checker/tests/index/RefineEq.java +++ b/checker/tests/index/RefineEq.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.LTEqLengthOf; import org.checkerframework.checker.index.qual.LTLengthOf; -class RefineEq { +public class RefineEq { int[] arr = {1}; void testLTL(@LTLengthOf("arr") int test) { @@ -28,7 +28,6 @@ void testLTEL(@LTEqLengthOf("arr") int test) { if (test == b) { @LTEqLengthOf("arr") int c = b; - // :: error: (assignment.type.incompatible) @LTLengthOf("arr") int g = b; } else { // :: error: (assignment.type.incompatible) diff --git a/checker/tests/index/RefineGT.java b/checker/tests/index/RefineGT.java index 1847452449e3..a73613c6d7de 100644 --- a/checker/tests/index/RefineGT.java +++ b/checker/tests/index/RefineGT.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.LTEqLengthOf; import org.checkerframework.checker.index.qual.LTLengthOf; -class RefineGT { +public class RefineGT { int[] arr = {1}; void testLTL(@LTLengthOf("arr") int test) { diff --git a/checker/tests/index/RefineGTE.java b/checker/tests/index/RefineGTE.java index df7bb1ff5006..766d11562e26 100644 --- a/checker/tests/index/RefineGTE.java +++ b/checker/tests/index/RefineGTE.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.LTEqLengthOf; import org.checkerframework.checker.index.qual.LTLengthOf; -class RefineGTE { +public class RefineGTE { int[] arr = {1}; void testLTL(@LTLengthOf("arr") int test) { diff --git a/checker/tests/index/RefineLT.java b/checker/tests/index/RefineLT.java index d010001a818c..8412cd7fbfcb 100644 --- a/checker/tests/index/RefineLT.java +++ b/checker/tests/index/RefineLT.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.LTEqLengthOf; import org.checkerframework.checker.index.qual.LTLengthOf; -class RefineLT { +public class RefineLT { int[] arr = {1}; void testLTL(@LTLengthOf("arr") int test, @LTLengthOf("arr") int a, @LTLengthOf("arr") int a3) { diff --git a/checker/tests/index/RefineLTE.java b/checker/tests/index/RefineLTE.java index 0100320c49ea..bc0056aa3c87 100644 --- a/checker/tests/index/RefineLTE.java +++ b/checker/tests/index/RefineLTE.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.LTEqLengthOf; import org.checkerframework.checker.index.qual.LTLengthOf; -class RefineLTE { +public class RefineLTE { int[] arr = {1}; void testLTL(@LTLengthOf("arr") int test) { diff --git a/checker/tests/index/RefineNeq.java b/checker/tests/index/RefineNeq.java index cf8e7fd29fc7..55327d513db1 100644 --- a/checker/tests/index/RefineNeq.java +++ b/checker/tests/index/RefineNeq.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.LTEqLengthOf; import org.checkerframework.checker.index.qual.LTLengthOf; -class RefineNeq { +public class RefineNeq { int[] arr = {1}; void testLTL(@LTLengthOf("arr") int test) { @@ -32,7 +32,6 @@ void testLTEL(@LTEqLengthOf("arr") int test) { } else { @LTEqLengthOf("arr") int c = b; - // :: error: (assignment.type.incompatible) @LTLengthOf("arr") int g = b; } // :: error: (assignment.type.incompatible) diff --git a/checker/tests/index/RefineNeqLength.java b/checker/tests/index/RefineNeqLength.java index 46be6b65952a..d11d3ce2b8d3 100644 --- a/checker/tests/index/RefineNeqLength.java +++ b/checker/tests/index/RefineNeqLength.java @@ -8,7 +8,7 @@ import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.common.value.qual.IntVal; -class RefineNeqLength { +public class RefineNeqLength { void refineNeqLength(int[] array, @IndexOrHigh("#1") int i) { // Refines i <= array.length to i < array.length if (i != array.length) { diff --git a/checker/tests/index/ReflectArray.java b/checker/tests/index/ReflectArray.java index b3d25c9ad690..a974f08d1541 100644 --- a/checker/tests/index/ReflectArray.java +++ b/checker/tests/index/ReflectArray.java @@ -1,7 +1,8 @@ -import java.lang.reflect.Array; import org.checkerframework.common.value.qual.MinLen; -class ReflectArray { +import java.lang.reflect.Array; + +public class ReflectArray { void testNewInstance(int i) { // :: error: (argument.type.incompatible) diff --git a/checker/tests/index/RegexMatcher.java b/checker/tests/index/RegexMatcher.java index 87999ce5cf1d..fae3bcc77458 100644 --- a/checker/tests/index/RegexMatcher.java +++ b/checker/tests/index/RegexMatcher.java @@ -1,16 +1,16 @@ // Test case for Issue panacekcz#8: // https://github.com/panacekcz/checker-framework/issues/8 +import org.checkerframework.checker.index.qual.NonNegative; + import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.checkerframework.checker.index.qual.NonNegative; public class RegexMatcher { static void m(String p, String s) { Matcher matcher = Pattern.compile(p).matcher(s); - // The following line cannot be used as a test, - // because the relation of matcher to p is not tracked, - // so the upper bound is not known. + // The following line cannot be used as a test, because the relation of matcher to p is not + // tracked, so the upper bound is not known. // s.substring(matcher.start(), matcher.end()); diff --git a/checker/tests/index/RepeatLTLengthOf.java b/checker/tests/index/RepeatLTLengthOf.java index 73db3328c7d5..14565c740347 100644 --- a/checker/tests/index/RepeatLTLengthOf.java +++ b/checker/tests/index/RepeatLTLengthOf.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.EnsuresLTLengthOf; import org.checkerframework.checker.index.qual.EnsuresLTLengthOfIf; -class RepeatLTLengthOf { +public class RepeatLTLengthOf { protected String value1; protected String value2; diff --git a/checker/tests/index/RepeatLTLengthOfWithError.java b/checker/tests/index/RepeatLTLengthOfWithError.java index c6e5978765c5..fd8b63a79a2d 100644 --- a/checker/tests/index/RepeatLTLengthOfWithError.java +++ b/checker/tests/index/RepeatLTLengthOfWithError.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.EnsuresLTLengthOf; import org.checkerframework.checker.index.qual.EnsuresLTLengthOfIf; -class RepeatLTLengthOfWithError { +public class RepeatLTLengthOfWithError { protected String value1; protected String value2; diff --git a/checker/tests/index/Return.java b/checker/tests/index/Return.java index 2310f4708e07..008776c851a4 100644 --- a/checker/tests/index/Return.java +++ b/checker/tests/index/Return.java @@ -1,4 +1,4 @@ -class Return { +public class Return { int[] test() { return null; } diff --git a/checker/tests/index/SLSubtyping.java b/checker/tests/index/SLSubtyping.java index 2a3d291a50f8..c99695bd7764 100644 --- a/checker/tests/index/SLSubtyping.java +++ b/checker/tests/index/SLSubtyping.java @@ -2,7 +2,7 @@ // This test checks whether the SameLen type system works as expected. -class SLSubtyping { +public class SLSubtyping { int[] f = {1}; void subtype(int @SameLen("#2") [] a, int[] b) { diff --git a/checker/tests/index/SameLenAssignmentTransfer.java b/checker/tests/index/SameLenAssignmentTransfer.java index ef8db0bcc657..f1fe52fccb41 100644 --- a/checker/tests/index/SameLenAssignmentTransfer.java +++ b/checker/tests/index/SameLenAssignmentTransfer.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.*; -class SameLenAssignmentTransfer { +public class SameLenAssignmentTransfer { void transfer5(int @SameLen("#2") [] a, int[] b) { int[] c = a; for (int i = 0; i < c.length; i++) { // i's type is @LTL("c") diff --git a/checker/tests/index/SameLenEqualsRefinement.java b/checker/tests/index/SameLenEqualsRefinement.java index 0eb6e88416b9..6af7a52c5280 100644 --- a/checker/tests/index/SameLenEqualsRefinement.java +++ b/checker/tests/index/SameLenEqualsRefinement.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.*; import org.checkerframework.dataflow.qual.Pure; -class SameLenEqualsRefinement { +public class SameLenEqualsRefinement { void transfer3(int @SameLen("#2") [] a, int[] b, int[] c) { if (a == c) { for (int i = 0; i < c.length; i++) { // i's type is @LTL("c") diff --git a/checker/tests/index/SameLenIrrelevant.java b/checker/tests/index/SameLenIrrelevant.java new file mode 100644 index 000000000000..103f92aabdac --- /dev/null +++ b/checker/tests/index/SameLenIrrelevant.java @@ -0,0 +1,25 @@ +// Tests that adding an @SameLen annotation to a primitive type is still +// an error. + +// All the errors in this test case are disabled. They were issued when `@SameLen` was restricted +// to arrays and CharSequence, but @SameLen can be written on an arbitrary user-defined type: +// https://checkerframework.org/manual/#index-annotating-fixed-size . + +import org.checkerframework.checker.index.qual.SameLen; + +public class SameLenIrrelevant { + // NO :: error: (anno.on.irrelevant) + public void test(@SameLen("#2") int x, int y) { + // do nothing + } + + // NO :: error: (anno.on.irrelevant) + public void test(@SameLen("#2") double x, double y) { + // do nothing + } + + // NO :: error: (anno.on.irrelevant) + public void test(@SameLen("#2") char x, char y) { + // do nothing + } +} diff --git a/checker/tests/index/SameLenManyArrays.java b/checker/tests/index/SameLenManyArrays.java index f32844c7c3a6..0e0c519c36be 100644 --- a/checker/tests/index/SameLenManyArrays.java +++ b/checker/tests/index/SameLenManyArrays.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.*; import org.checkerframework.dataflow.qual.Pure; -class SameLenManyArrays { +public class SameLenManyArrays { void transfer1(int @SameLen("#2") [] a, int[] b) { int[] c = new int[a.length]; for (int i = 0; i < c.length; i++) { // i's type is @LTL("c") diff --git a/checker/tests/index/SameLenNewArrayWithSameLength.java b/checker/tests/index/SameLenNewArrayWithSameLength.java index f2007c617f44..e4bd9654746a 100644 --- a/checker/tests/index/SameLenNewArrayWithSameLength.java +++ b/checker/tests/index/SameLenNewArrayWithSameLength.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.*; -class SameLenNewArrayWithSameLength { +public class SameLenNewArrayWithSameLength { public void m1(int[] a) { int @SameLen("a") [] b = new int[a.length]; } diff --git a/checker/tests/index/SameLenOnFormalParameter.java b/checker/tests/index/SameLenOnFormalParameter.java index 194cb839f5d2..b8bb360cdf9b 100644 --- a/checker/tests/index/SameLenOnFormalParameter.java +++ b/checker/tests/index/SameLenOnFormalParameter.java @@ -2,7 +2,7 @@ import org.checkerframework.checker.index.qual.*; -class SameLenOnFormalParameter { +public class SameLenOnFormalParameter { public void requiresSameLen1(String x1, @SameLen("#1") String y1) {} public void requiresSameLen2(@SameLen("#2") String x2, String y2) {} diff --git a/checker/tests/index/SameLenOnFormalParameterSimple.java b/checker/tests/index/SameLenOnFormalParameterSimple.java index aa5495380f27..53e11cd1ae1e 100644 --- a/checker/tests/index/SameLenOnFormalParameterSimple.java +++ b/checker/tests/index/SameLenOnFormalParameterSimple.java @@ -2,7 +2,7 @@ import org.checkerframework.checker.index.qual.SameLen; -class SameLenOnFormalParameterSimple { +public class SameLenOnFormalParameterSimple { public void requiresSameLen1(String x1, @SameLen("#1") String y1) {} public void m1(@SameLen("#2") String a1, String b1) { diff --git a/checker/tests/index/SameLenSelf.java b/checker/tests/index/SameLenSelf.java index f3aaa557a075..f4740b26d37b 100644 --- a/checker/tests/index/SameLenSelf.java +++ b/checker/tests/index/SameLenSelf.java @@ -2,7 +2,7 @@ import org.checkerframework.checker.index.qual.*; -class SameLenSelf { +public class SameLenSelf { int @SameLen("this.field") [] field = new int[10]; int @SameLen("field2") [] field2 = new int[10]; int @SameLen("field3") [] field3 = field2; diff --git a/checker/tests/index/SameLenSimpleCase.java b/checker/tests/index/SameLenSimpleCase.java index 812b593a043b..dceaf6dc163d 100644 --- a/checker/tests/index/SameLenSimpleCase.java +++ b/checker/tests/index/SameLenSimpleCase.java @@ -1,4 +1,4 @@ -class SameLenSimpleCase { +public class SameLenSimpleCase { public int compare(int[] a1, int[] a2) { if (a1.length != a2.length) { return a1.length - a2.length; diff --git a/checker/tests/index/SameLenWithObjects.java b/checker/tests/index/SameLenWithObjects.java index b21f6fababf5..60165d4cd9d1 100644 --- a/checker/tests/index/SameLenWithObjects.java +++ b/checker/tests/index/SameLenWithObjects.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.*; -class SameLenWithObjects { +public class SameLenWithObjects { class SimpleCollection { Object[] var_infos; diff --git a/checker/tests/index/SearchIndexTests.java b/checker/tests/index/SearchIndexTests.java index bba7cff5df32..9cfce47efa86 100644 --- a/checker/tests/index/SearchIndexTests.java +++ b/checker/tests/index/SearchIndexTests.java @@ -1,7 +1,8 @@ -import java.util.Arrays; import org.checkerframework.checker.index.qual.*; -class SearchIndexTests { +import java.util.Arrays; + +public class SearchIndexTests { public void test(short[] a, short instant) { int i = Arrays.binarySearch(a, instant); @SearchIndexFor("a") int z = i; diff --git a/checker/tests/index/SimpleCollection.java b/checker/tests/index/SimpleCollection.java index f81da4467880..24a2da1d8ab4 100644 --- a/checker/tests/index/SimpleCollection.java +++ b/checker/tests/index/SimpleCollection.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.*; -class SimpleCollection { +public class SimpleCollection { private int[] values; @IndexOrHigh("values") int size() { diff --git a/checker/tests/index/SimpleTransferAdd.java b/checker/tests/index/SimpleTransferAdd.java index 5dc696592938..38b5a3301834 100644 --- a/checker/tests/index/SimpleTransferAdd.java +++ b/checker/tests/index/SimpleTransferAdd.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.index.qual.Positive; -class SimpleTransferAdd { +public class SimpleTransferAdd { void test() { int bs = -1; // :: error: (assignment.type.incompatible) diff --git a/checker/tests/index/SimpleTransferSub.java b/checker/tests/index/SimpleTransferSub.java index bb4d9b377703..1546726c8e41 100644 --- a/checker/tests/index/SimpleTransferSub.java +++ b/checker/tests/index/SimpleTransferSub.java @@ -1,8 +1,8 @@ import org.checkerframework.checker.index.qual.Positive; -class SimpleTransferSub { +public class SimpleTransferSub { void test() { - // shows a bug in the checker framework. I don't think we can get around this bit... + // shows a bug in the Checker Framework. I don't think we can get around this bit... int bs = 0; // :: error: (assignment.type.incompatible) @Positive int ds = bs--; diff --git a/checker/tests/index/SizeVsLength.java b/checker/tests/index/SizeVsLength.java index b5dcb81514c4..00208a61d3d0 100644 --- a/checker/tests/index/SizeVsLength.java +++ b/checker/tests/index/SizeVsLength.java @@ -2,7 +2,7 @@ import org.checkerframework.checker.index.qual.*; -class SizeVsLength { +public class SizeVsLength { public int[] getArray(@NonNegative int size) { int[] values = new int[size]; diff --git a/checker/tests/index/SpecialTransfersForEquality.java b/checker/tests/index/SpecialTransfersForEquality.java index 203b7aa847c5..2a399df82381 100644 --- a/checker/tests/index/SpecialTransfersForEquality.java +++ b/checker/tests/index/SpecialTransfersForEquality.java @@ -2,7 +2,7 @@ import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.index.qual.Positive; -class SpecialTransfersForEquality { +public class SpecialTransfersForEquality { void gteN1Test(@GTENegativeOne int y) { int[] arr = new int[10]; diff --git a/checker/tests/index/Split.java b/checker/tests/index/Split.java index 46d31b591404..161feb8de480 100644 --- a/checker/tests/index/Split.java +++ b/checker/tests/index/Split.java @@ -1,6 +1,7 @@ -import java.util.regex.Pattern; import org.checkerframework.common.value.qual.MinLen; +import java.util.regex.Pattern; + public class Split { Pattern p = Pattern.compile(".*"); diff --git a/checker/tests/index/StartsEndsWith.java b/checker/tests/index/StartsEndsWith.java index d3fc5aef8d10..781d4c0c0066 100644 --- a/checker/tests/index/StartsEndsWith.java +++ b/checker/tests/index/StartsEndsWith.java @@ -3,11 +3,28 @@ import org.checkerframework.common.value.qual.MinLen; -class StartsEndsWith { +public class StartsEndsWith { + + final String prefix; + + StartsEndsWith(String prefix) { + this.prefix = prefix; + } + + String propertyName(String methodName) { + if (methodName.startsWith(prefix)) { + @SuppressWarnings( + "index") // BUG: https://github.com/typetools/checker-framework/issues/5201 + String result = methodName.substring(prefix.length()); + return result; + } else { + return null; + } + } // This particular test is here rather than in the framework tests because it depends on purity // annotations for these particular JDK methods. - void refineStartsConditional(String str, String prefix) { + static void refineStartsConditional(String str, String prefix) { if (prefix.length() > 10 && str.startsWith(prefix)) { @MinLen(11) String s11 = str; } diff --git a/checker/tests/index/StaticInitializer.java b/checker/tests/index/StaticInitializer.java new file mode 100644 index 000000000000..209f5c92c82b --- /dev/null +++ b/checker/tests/index/StaticInitializer.java @@ -0,0 +1,3 @@ +public final class StaticInitializer { + static final int MAX_SIGNED_POWER_OF_TWO = 1 << (Integer.SIZE - 2); +} diff --git a/checker/tests/index/Stopwatch.java b/checker/tests/index/Stopwatch.java index 3ccb6f36a9f3..6fa60d89d8bb 100644 --- a/checker/tests/index/Stopwatch.java +++ b/checker/tests/index/Stopwatch.java @@ -1,6 +1,7 @@ -import java.text.DecimalFormat; import org.checkerframework.checker.index.qual.IndexFor; +import java.text.DecimalFormat; + public final class Stopwatch { private static final DecimalFormat[] timeFormat = { new DecimalFormat("#.#"), diff --git a/checker/tests/index/StringIndexOf.java b/checker/tests/index/StringIndexOf.java index d262cef47038..003aea18b3a6 100644 --- a/checker/tests/index/StringIndexOf.java +++ b/checker/tests/index/StringIndexOf.java @@ -1,6 +1,6 @@ // Tests using the index returned from String.indexOf -class StringIndexOf { +public class StringIndexOf { public static String remove(String l, String s) { int i = l.indexOf(s); diff --git a/checker/tests/index/StringLenRefinement.java b/checker/tests/index/StringLenRefinement.java index a590f61e2e69..d7556bfa4e4a 100644 --- a/checker/tests/index/StringLenRefinement.java +++ b/checker/tests/index/StringLenRefinement.java @@ -2,7 +2,7 @@ import org.checkerframework.common.value.qual.ArrayLenRange; import org.checkerframework.common.value.qual.StringVal; -class StringLenRefinement { +public class StringLenRefinement { void refineLenRange( @ArrayLenRange(from = 3, to = 10) String range, diff --git a/checker/tests/index/StringLength.java b/checker/tests/index/StringLength.java index b4f491475cf3..ab26225e537b 100644 --- a/checker/tests/index/StringLength.java +++ b/checker/tests/index/StringLength.java @@ -1,6 +1,5 @@ // Tests that String.length() is supported in the same situations as array length -import java.util.Random; import org.checkerframework.checker.index.qual.IndexFor; import org.checkerframework.checker.index.qual.IndexOrHigh; import org.checkerframework.checker.index.qual.LTLengthOf; @@ -9,7 +8,9 @@ import org.checkerframework.checker.index.qual.SameLen; import org.checkerframework.common.value.qual.MinLen; -class StringLength { +import java.util.Random; + +public class StringLength { void testMinLenSubtractPositive(@MinLen(10) String s) { @Positive int i1 = s.length() - 9; @NonNegative int i0 = s.length() - 10; diff --git a/checker/tests/index/StringMethods.java b/checker/tests/index/StringMethods.java index 266374acb258..0c3368d59183 100644 --- a/checker/tests/index/StringMethods.java +++ b/checker/tests/index/StringMethods.java @@ -1,6 +1,6 @@ // Tests for index annotations on string methods in the annotated JDK -class StringMethods { +public class StringMethods { void testCharAt(String s, int i) { // :: error: (argument.type.incompatible) diff --git a/checker/tests/index/StringSameLen.java b/checker/tests/index/StringSameLen.java index 5313e1b271a4..1286990355bf 100644 --- a/checker/tests/index/StringSameLen.java +++ b/checker/tests/index/StringSameLen.java @@ -1,4 +1,4 @@ -class StringSameLen { +public class StringSameLen { public void m(String s) { String t = s; diff --git a/checker/tests/index/StringTokenizerMinLen.java b/checker/tests/index/StringTokenizerMinLen.java index 67934ce78609..d91c481387ee 100644 --- a/checker/tests/index/StringTokenizerMinLen.java +++ b/checker/tests/index/StringTokenizerMinLen.java @@ -3,7 +3,7 @@ import java.util.StringTokenizer; -class StringTokenizerMinLen { +public class StringTokenizerMinLen { void test(String str, String delim, boolean returnDelims) { StringTokenizer st = new StringTokenizer(str, delim, returnDelims); while (st.hasMoreTokens()) { diff --git a/checker/tests/index/SubstringIndexForIrrelevant.java b/checker/tests/index/SubstringIndexForIrrelevant.java new file mode 100644 index 000000000000..0a1ea70b5430 --- /dev/null +++ b/checker/tests/index/SubstringIndexForIrrelevant.java @@ -0,0 +1,14 @@ +import org.checkerframework.checker.index.qual.LTEqLengthOf; +import org.checkerframework.checker.index.qual.SubstringIndexFor; + +public class SubstringIndexForIrrelevant { + + @SuppressWarnings( + "substringindex:return" // https://github.com/kelloggm/checker-framework/issues/206, + // 207, 208 + ) + public static @LTEqLengthOf("#1") @SubstringIndexFor(value = "#1", offset = "#2.length - 1") int + indexOf(boolean[] array, boolean[] target) { + return -1; + } +} diff --git a/checker/tests/index/SubtractionIndex.java b/checker/tests/index/SubtractionIndex.java index 0343cfdebaeb..ddfdfa60cc1c 100644 --- a/checker/tests/index/SubtractionIndex.java +++ b/checker/tests/index/SubtractionIndex.java @@ -3,7 +3,7 @@ // @skip-test until the type system is enriched so it can express either // * N = Grid.length and N-1 = Grid.length-1, or -// * i < N and i <= N-1 +// * i < N and i <= N-1 public class SubtractionIndex { diff --git a/checker/tests/index/TestAgainstLength.java b/checker/tests/index/TestAgainstLength.java index a2bc0d155060..27e754ba7b07 100644 --- a/checker/tests/index/TestAgainstLength.java +++ b/checker/tests/index/TestAgainstLength.java @@ -6,6 +6,7 @@ public class TestAgainstLength { protected int[] values; + /** The number of active elements (equivalently, the first unused index). */ @IndexOrHigh("values") int num_values; diff --git a/checker/tests/index/ToArrayIndex.java b/checker/tests/index/ToArrayIndex.java index 7fe98f428b2e..343d4ba16a87 100644 --- a/checker/tests/index/ToArrayIndex.java +++ b/checker/tests/index/ToArrayIndex.java @@ -1,6 +1,7 @@ -import java.util.ArrayList; import org.checkerframework.common.value.qual.MinLen; +import java.util.ArrayList; + // @skip-test until we bring list support back public class ToArrayIndex { diff --git a/checker/tests/index/TransferMod.java b/checker/tests/index/TransferMod.java index 1e869541521c..a98d62cd76b8 100644 --- a/checker/tests/index/TransferMod.java +++ b/checker/tests/index/TransferMod.java @@ -2,7 +2,7 @@ import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.index.qual.Positive; -class TransferMod { +public class TransferMod { void test() { int aa = -100; diff --git a/checker/tests/index/TypeArrayLengthWithSameLen.java b/checker/tests/index/TypeArrayLengthWithSameLen.java index 21a1571fadbd..a35bed67998d 100644 --- a/checker/tests/index/TypeArrayLengthWithSameLen.java +++ b/checker/tests/index/TypeArrayLengthWithSameLen.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.index.qual.*; -class TypeArrayLengthWithSameLen { +public class TypeArrayLengthWithSameLen { void test(int @SameLen("#2") [] a, int @SameLen("#1") [] b, int[] c) { if (a.length == c.length) { @LTEqLengthOf({"a", "b", "c"}) int x = b.length; diff --git a/checker/tests/index/UBLiteralFlow.java b/checker/tests/index/UBLiteralFlow.java new file mode 100644 index 000000000000..2ffbdbf35a97 --- /dev/null +++ b/checker/tests/index/UBLiteralFlow.java @@ -0,0 +1,188 @@ +import org.checkerframework.checker.index.qual.GTENegativeOne; +import org.checkerframework.checker.index.qual.IndexOrLow; +import org.checkerframework.checker.index.qual.LTLengthOf; + +public class UBLiteralFlow { + + private static @IndexOrLow("#1") int lineStartIndexPartial( + String s, @GTENegativeOne int lineStart) { + int result; + if (lineStart >= s.length()) { + result = -1; + } else { + result = lineStart; + } + return result; + } + + private static @LTLengthOf("#1") int lineStartIndexPartial2( + String s, @GTENegativeOne int lineStart) { + int result; + if (lineStart >= s.length()) { + result = -1; + } else { + result = lineStart; + } + return result; + } + + private static @LTLengthOf(value = "#1", offset = "1") int lineStartIndexPartial3( + String s, @GTENegativeOne int lineStart) { + int result; + if (lineStart >= s.length()) { + result = -1; + } else { + result = lineStart; + } + // :: error: (return.type.incompatible) + return result; + } + + private static @LTLengthOf(value = "#1", offset = "-1") int lineStartIndexPartial4( + String s, @GTENegativeOne int lineStart) { + int result; + if (lineStart >= s.length()) { + result = -1; + } else { + result = lineStart; + } + return result; + } + + /** + * Given a string, return the index of the start of a line, after {@code start}. + * + * @param s the string in which to find the start of a line + * @param start the index at which to start looking for the start of a line + * @return the index of the start of a line, or -1 if no such exists + */ + private static @IndexOrLow("#1") int lineStartIndex(String s, int start) { + if (s.length() == 0) { + return -1; + } + if (start == 0) { + // It doesn't make sense to call this routine with 0, but return 0 anyway. + return 0; + } + if (start > s.length()) { + return -1; + } + // possible line terminators: "\n", "\r\n", "\r". + int newlinePos = s.indexOf("\n", start - 1); + int afterNewline = (newlinePos == -1) ? Integer.MAX_VALUE : newlinePos + 1; + int returnPos1 = s.indexOf("\r\n", start - 2); + int returnPos2 = s.indexOf("\r", start - 1); + int afterReturn1 = (returnPos1 == -1) ? Integer.MAX_VALUE : returnPos1 + 2; + int afterReturn2 = (returnPos2 == -1) ? Integer.MAX_VALUE : returnPos2 + 1; + int lineStart = Math.min(afterNewline, Math.min(afterReturn1, afterReturn2)); + if (lineStart >= s.length()) { + return -1; + } else { + return lineStart; + } + } + + /** + * Given a string, return the index of the start of a line, after {@code start}. + * + * @param s the string in which to find the start of a line + * @param start the index at which to start looking for the start of a line + * @return the index of the start of a line, or -1 if no such exists + */ + private static @LTLengthOf("#1") int lineStartIndex2(String s, int start) { + if (s.length() == 0) { + return -1; + } + if (start == 0) { + // It doesn't make sense to call this routine with 0, but return 0 anyway. + return 0; + } + if (start > s.length()) { + return -1; + } + // possible line terminators: "\n", "\r\n", "\r". + int newlinePos = s.indexOf("\n", start - 1); + int afterNewline = (newlinePos == -1) ? Integer.MAX_VALUE : newlinePos + 1; + int returnPos1 = s.indexOf("\r\n", start - 2); + int returnPos2 = s.indexOf("\r", start - 1); + int afterReturn1 = (returnPos1 == -1) ? Integer.MAX_VALUE : returnPos1 + 2; + int afterReturn2 = (returnPos2 == -1) ? Integer.MAX_VALUE : returnPos2 + 1; + int lineStart = Math.min(afterNewline, Math.min(afterReturn1, afterReturn2)); + if (lineStart >= s.length()) { + return -1; + } else { + return lineStart; + } + } + + /** + * Given a string, return the index of the start of a line, after {@code start}. + * + * @param s the string in which to find the start of a line + * @param start the index at which to start looking for the start of a line + * @return the index of the start of a line, or -1 if no such exists + */ + private static @LTLengthOf(value = "#1", offset = "1") int lineStartIndex3( + String s, int start) { + if (s.length() == 0) { + // :: error: (return.type.incompatible) + return -1; + } + if (start == 0) { + // It doesn't make sense to call this routine with 0, but return 0 anyway. + // :: error: (return.type.incompatible) + return 0; + } + if (start > s.length()) { + return -1; + } + // possible line terminators: "\n", "\r\n", "\r". + int newlinePos = s.indexOf("\n", start - 1); + int afterNewline = (newlinePos == -1) ? Integer.MAX_VALUE : newlinePos + 1; + int returnPos1 = s.indexOf("\r\n", start - 2); + int returnPos2 = s.indexOf("\r", start - 1); + int afterReturn1 = (returnPos1 == -1) ? Integer.MAX_VALUE : returnPos1 + 2; + int afterReturn2 = (returnPos2 == -1) ? Integer.MAX_VALUE : returnPos2 + 1; + int lineStart = Math.min(afterNewline, Math.min(afterReturn1, afterReturn2)); + if (lineStart >= s.length()) { + return -1; + } else { + // :: error: (return.type.incompatible) + return lineStart; + } + } + + /** + * Given a string, return the index of the start of a line, after {@code start}. + * + * @param s the string in which to find the start of a line + * @param start the index at which to start looking for the start of a line + * @return the index of the start of a line, or -1 if no such exists + */ + private static @LTLengthOf(value = "#1", offset = "-1") int lineStartIndex4( + String s, int start) { + if (s.length() == 0) { + return -1; + } + if (start == 0) { + // It doesn't make sense to call this routine with 0, but return 0 anyway. + return 0; + } + if (start > s.length()) { + return -1; + } + // possible line terminators: "\n", "\r\n", "\r". + int newlinePos = s.indexOf("\n", start - 1); + int afterNewline = (newlinePos == -1) ? Integer.MAX_VALUE : newlinePos + 1; + int returnPos1 = s.indexOf("\r\n", start - 2); + int returnPos2 = s.indexOf("\r", start - 1); + int afterReturn1 = (returnPos1 == -1) ? Integer.MAX_VALUE : returnPos1 + 2; + int afterReturn2 = (returnPos2 == -1) ? Integer.MAX_VALUE : returnPos2 + 1; + int lineStart = Math.min(afterNewline, Math.min(afterReturn1, afterReturn2)); + if (lineStart >= s.length()) { + return -1; + } else { + return lineStart; + } + } +} diff --git a/checker/tests/index/VarLteVar.java b/checker/tests/index/VarLteVar.java index 4c09d89a0a01..5059c64e279f 100644 --- a/checker/tests/index/VarLteVar.java +++ b/checker/tests/index/VarLteVar.java @@ -2,7 +2,7 @@ // It is easy to see that: // * i is an index for intermediate // * length <= i (or, at least length <= i+1) -// but I don't see how to verif that length is an index for intermediate. +// but I don't see how to verify that length is an index for intermediate. // @skip-test diff --git a/checker/tests/index/ViewpointAdaptTest.java b/checker/tests/index/ViewpointAdaptTest.java index 048bfedfdf35..a15114fa2d84 100644 --- a/checker/tests/index/ViewpointAdaptTest.java +++ b/checker/tests/index/ViewpointAdaptTest.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.index.qual.LTEqLengthOf; import org.checkerframework.checker.index.qual.LTLengthOf; -class ViewpointAdaptTest { +public class ViewpointAdaptTest { void ListGet( @LTLengthOf("list") int index, @LTEqLengthOf("list") int notIndex, List list) { diff --git a/checker/tests/index/divisionTest.java b/checker/tests/index/divisionTest.java deleted file mode 100644 index d0aae78d8ae7..000000000000 --- a/checker/tests/index/divisionTest.java +++ /dev/null @@ -1,8 +0,0 @@ -import java.util.*; - -class divisionTest { - - public static void division() { - System.out.println(1 / (2.0)); - } -} diff --git a/checker/tests/nullness/init/AnonymousInit.java b/checker/tests/initialization/AnonymousInit.java similarity index 82% rename from checker/tests/nullness/init/AnonymousInit.java rename to checker/tests/initialization/AnonymousInit.java index cb054b1d1d48..fdf72d86fd2e 100644 --- a/checker/tests/nullness/init/AnonymousInit.java +++ b/checker/tests/initialization/AnonymousInit.java @@ -1,9 +1,9 @@ // Ensure field initialization checks for anonymous // classes work. -class AnonymousInit { +public class AnonymousInit { Object o1 = - // :: error: (initialization.fields.uninitialized) new Object() { + // :: error: (initialization.field.uninitialized) Object s; public String toString() { diff --git a/checker/tests/initialization/fbc/CastInit.java b/checker/tests/initialization/CastInit.java similarity index 83% rename from checker/tests/initialization/fbc/CastInit.java rename to checker/tests/initialization/CastInit.java index 14f7666fae79..631b7da1bc43 100644 --- a/checker/tests/initialization/fbc/CastInit.java +++ b/checker/tests/initialization/CastInit.java @@ -1,11 +1,11 @@ import org.checkerframework.checker.initialization.qual.Initialized; import org.checkerframework.checker.initialization.qual.UnknownInitialization; -class CastInit { +public class CastInit { public CastInit() { @UnknownInitialization CastInit t1 = (@UnknownInitialization CastInit) this; - // :: error: (initialization.invalid.cast) + // :: warning: (cast.unsafe) @Initialized CastInit t2 = (@Initialized CastInit) this; } } diff --git a/checker/tests/initialization/fbc/ChainedInitialization.java b/checker/tests/initialization/ChainedInitialization.java similarity index 100% rename from checker/tests/initialization/fbc/ChainedInitialization.java rename to checker/tests/initialization/ChainedInitialization.java diff --git a/checker/tests/initialization/fbc/Commitment.java b/checker/tests/initialization/Commitment.java similarity index 94% rename from checker/tests/initialization/fbc/Commitment.java rename to checker/tests/initialization/Commitment.java index 8f550cc09785..43de35194d61 100644 --- a/checker/tests/initialization/fbc/Commitment.java +++ b/checker/tests/initialization/Commitment.java @@ -29,14 +29,16 @@ public class Commitment { } // :: error: (initialization.invalid.constructor.return.type) + // :: error: (nullness.on.constructor) public @Initialized @NonNull Commitment(boolean i) { a = ""; t = ""; b = ""; } - // :: error: (initialization.invalid.constructor.return.type) - public @Nullable Commitment(char i) { + public + // :: error: (nullness.on.constructor) + @Nullable Commitment(char i) { a = ""; t = ""; b = ""; diff --git a/checker/tests/initialization/fbc/Commitment2.java b/checker/tests/initialization/Commitment2.java similarity index 100% rename from checker/tests/initialization/fbc/Commitment2.java rename to checker/tests/initialization/Commitment2.java diff --git a/checker/tests/initialization/fbc/CommitmentFlow.java b/checker/tests/initialization/CommitmentFlow.java similarity index 88% rename from checker/tests/initialization/fbc/CommitmentFlow.java rename to checker/tests/initialization/CommitmentFlow.java index 17a1dca8e839..e6a4d1d4abff 100644 --- a/checker/tests/initialization/fbc/CommitmentFlow.java +++ b/checker/tests/initialization/CommitmentFlow.java @@ -20,6 +20,6 @@ void foo( local.hashCode(); local = triedAndTrue; - local.hashCode(); // should determine that it is Committed based on flow + local.hashCode(); // should determine that it is Initialized based on flow } } diff --git a/checker/tests/initialization/FBCList.java b/checker/tests/initialization/FBCList.java new file mode 100644 index 000000000000..14f1f5beb748 --- /dev/null +++ b/checker/tests/initialization/FBCList.java @@ -0,0 +1,54 @@ +import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; +import org.checkerframework.checker.initialization.qual.UnderInitialization; +import org.checkerframework.checker.nullness.qual.Nullable; + +// This example is taken from the FBC paper, figure 1 (and has some additional code in main below). +// We made the list generic. +public class FBCList { + @NotOnlyInitialized FBCNode sentinel; + + public FBCList() { + this.sentinel = new FBCNode<>(this); + } + + void insert(@Nullable T data) { + this.sentinel.insertAfter(data); + } + + public static void main() { + FBCList l = new FBCList<>(); + l.insert(1); + l.insert(2); + } +} + +class FBCNode { + @NotOnlyInitialized FBCNode prev; + + @NotOnlyInitialized FBCNode next; + + @NotOnlyInitialized FBCList parent; + + @Nullable T data; + + // for sentinel construction + FBCNode(@UnderInitialization FBCList parent) { + this.parent = parent; + this.prev = this; + this.next = this; + } + + // for data node construction + FBCNode(FBCNode prev, FBCNode next, @Nullable T data) { + this.parent = prev.parent; + this.prev = prev; + this.next = next; + this.data = data; + } + + void insertAfter(@Nullable T data) { + FBCNode n = new FBCNode<>(this, this.next, data); + this.next.prev = n; + this.next = n; + } +} diff --git a/checker/tests/initialization/fbc/FieldSuppressWarnings.java b/checker/tests/initialization/FieldSuppressWarnings.java similarity index 83% rename from checker/tests/initialization/fbc/FieldSuppressWarnings.java rename to checker/tests/initialization/FieldSuppressWarnings.java index feaf8b72161e..7248baa3973d 100644 --- a/checker/tests/initialization/fbc/FieldSuppressWarnings.java +++ b/checker/tests/initialization/FieldSuppressWarnings.java @@ -1,12 +1,12 @@ public class FieldSuppressWarnings { - // :: error: (initialization.fields.uninitialized) static class FieldSuppressWarnings1 { + // :: error: (initialization.field.uninitialized) private Object notInitialized; } static class FieldSuppressWarnings2 { - @SuppressWarnings("initialization.fields.uninitialized") + @SuppressWarnings("initialization.field.uninitialized") private Object notInitializedButSuppressed1; } diff --git a/checker/tests/nullness/init/FieldWithInit.java b/checker/tests/initialization/FieldWithInit.java similarity index 87% rename from checker/tests/nullness/init/FieldWithInit.java rename to checker/tests/initialization/FieldWithInit.java index e47a8f00c336..b42ea03adce1 100644 --- a/checker/tests/nullness/init/FieldWithInit.java +++ b/checker/tests/initialization/FieldWithInit.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.initialization.qual.UnknownInitialization; -class FieldWithInit { +public class FieldWithInit { Object f = foo(); Object foo(@UnknownInitialization FieldWithInit this) { diff --git a/checker/tests/initialization/FlowFbc.java b/checker/tests/initialization/FlowFbc.java new file mode 100644 index 000000000000..e9feac6f8bb8 --- /dev/null +++ b/checker/tests/initialization/FlowFbc.java @@ -0,0 +1,46 @@ +import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class FlowFbc { + + @NonNull String f; + @NotOnlyInitialized @NonNull String g; + + public FlowFbc(String arg) { + // :: error: (dereference.of.nullable) + f.toLowerCase(); + + // We get a dereference.of.nullable error by the Nullness Checker because g may be null, + // as well as a method.invocation.invalid error by the Initialization Checker because g + // is declared as @NotOnlyInitialized and thus may not be @Initialized, + // but toLowerCase()'s receiver type is, by default, @Initialized. + // :: error: (dereference.of.nullable) :: error: (method.invocation.invalid) + g.toLowerCase(); + + f = arg; + g = arg; + foo(); + f.toLowerCase(); + // :: error: (method.invocation.invalid) + g.toLowerCase(); + f = arg; + } + + void test() { + @Nullable String s = null; + s = "a"; + s.toLowerCase(); + } + + void test2(@Nullable String s) { + if (s != null) { + s.toLowerCase(); + } + } + + void foo(@UnknownInitialization FlowFbc this) {} + + // TODO Pure, etc. +} diff --git a/checker/tests/nullness/init/GenericTest12b.java b/checker/tests/initialization/GenericTest12b.java similarity index 95% rename from checker/tests/nullness/init/GenericTest12b.java rename to checker/tests/initialization/GenericTest12b.java index 41d915cfb4fe..6a0cc594a63a 100644 --- a/checker/tests/nullness/init/GenericTest12b.java +++ b/checker/tests/initialization/GenericTest12b.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; import org.checkerframework.checker.nullness.qual.Nullable; -class GenericTest12b { +public class GenericTest12b { class Cell {} class Node { diff --git a/checker/tests/nullness/init/InstanceOf.java b/checker/tests/initialization/InstanceOf.java similarity index 96% rename from checker/tests/nullness/init/InstanceOf.java rename to checker/tests/initialization/InstanceOf.java index 459c65f1237b..6ced9d107e70 100644 --- a/checker/tests/nullness/init/InstanceOf.java +++ b/checker/tests/initialization/InstanceOf.java @@ -13,7 +13,7 @@ Object method() { class OtherPpt extends Ppt {} } -class InstanceOf { +public class InstanceOf { void foo(PptTopLevel.@UnknownInitialization(PptTopLevel.class) Ppt ppt) { // :: error: (method.invocation.invalid) ppt.method(); diff --git a/checker/tests/nullness/init/Issue1044.java b/checker/tests/initialization/Issue1044.java similarity index 88% rename from checker/tests/nullness/init/Issue1044.java rename to checker/tests/initialization/Issue1044.java index f1e5782089c3..8660957d65ac 100644 --- a/checker/tests/nullness/init/Issue1044.java +++ b/checker/tests/initialization/Issue1044.java @@ -5,24 +5,26 @@ import org.checkerframework.checker.nullness.qual.Nullable; public class Issue1044 { - // :: error: (initialization.fields.uninitialized) static class Inner1 { + // :: error: (initialization.field.uninitialized) V f; } - // :: error: (initialization.fields.uninitialized) static class Inner2<@Nullable T extends @Nullable Object> { + // :: error: (initialization.field.uninitialized) @NonNull T f; } static class Inner3 { V f; + // :: error: (initialization.fields.uninitialized) Inner3() {} } static class Inner4<@Nullable T extends @Nullable Object> { @NonNull T f; + // :: error: (initialization.fields.uninitialized) Inner4() {} } @@ -47,13 +49,14 @@ static class Inner8<@Nullable T extends @Nullable Object> { Inner8() {} } - // :: error: (initialization.fields.uninitialized) static class Inner9 { + // :: error: (initialization.field.uninitialized) V f; } static class Inner10 { V f; + // :: error: (initialization.fields.uninitialized) Inner10() {} } diff --git a/checker/tests/nullness/init/Issue1120.java b/checker/tests/initialization/Issue1120.java similarity index 100% rename from checker/tests/nullness/init/Issue1120.java rename to checker/tests/initialization/Issue1120.java diff --git a/checker/tests/nullness/init/Issue1347.java b/checker/tests/initialization/Issue1347.java similarity index 93% rename from checker/tests/nullness/init/Issue1347.java rename to checker/tests/initialization/Issue1347.java index 7a46029c342d..b0e2702e6b10 100644 --- a/checker/tests/nullness/init/Issue1347.java +++ b/checker/tests/initialization/Issue1347.java @@ -1,7 +1,7 @@ // Test case for Issue 1347. // https://github.com/typetools/checker-framework/issues/1347 -class Issue1347 { +public class Issue1347 { T t; T t2; Object o; diff --git a/checker/tests/initialization/Issue3407.java b/checker/tests/initialization/Issue3407.java new file mode 100644 index 000000000000..6bb7c3c1a626 --- /dev/null +++ b/checker/tests/initialization/Issue3407.java @@ -0,0 +1,20 @@ +// @below-java9-jdk-skip-test +public class Issue3407 { + final String foo; + + String getFoo() { + return foo; + } + + Issue3407() { + var anon = + new Object() { + String bar() { + // :: error: (method.invocation.invalid) + return Issue3407.this.getFoo().substring(1); + } + }; + anon.bar(); // / WHOOPS... NPE, `getFoo()` returns `foo` which is still null + this.foo = "Hello world"; + } +} diff --git a/checker/tests/nullness/init/Issue408Init.java b/checker/tests/initialization/Issue408Init.java similarity index 100% rename from checker/tests/nullness/init/Issue408Init.java rename to checker/tests/initialization/Issue408Init.java diff --git a/checker/tests/nullness/init/Issue409.java b/checker/tests/initialization/Issue409.java similarity index 100% rename from checker/tests/nullness/init/Issue409.java rename to checker/tests/initialization/Issue409.java diff --git a/checker/tests/initialization/Issue4567.java b/checker/tests/initialization/Issue4567.java new file mode 100644 index 000000000000..fed5256e684b --- /dev/null +++ b/checker/tests/initialization/Issue4567.java @@ -0,0 +1,11 @@ +import org.checkerframework.checker.initialization.qual.UnderInitialization; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class Issue4567 { + + public Issue4567() { + this(null); + } + + protected Issue4567(final @UnderInitialization @Nullable Object variableScope) {} +} diff --git a/checker/tests/initialization/fbc/Issue556a.java b/checker/tests/initialization/Issue556a.java similarity index 100% rename from checker/tests/initialization/fbc/Issue556a.java rename to checker/tests/initialization/Issue556a.java diff --git a/checker/tests/initialization/Issue556b.java b/checker/tests/initialization/Issue556b.java new file mode 100644 index 000000000000..c0414feb3ef6 --- /dev/null +++ b/checker/tests/initialization/Issue556b.java @@ -0,0 +1,96 @@ +// @skip-test + +// To reproduce the problem, run: +// javac -processor nullness Issue556b.java +// java Issue556b +// and observe that the javac execution issues no warnings but the java execution suffers a null +// pointer exception. + +// Before the constructor is invoked, static initializers and static blocks are executed. This +// suggests that the Initialization Checker can assume that static fields are initialized in the +// constructor. +// +// However, if any user-defined code -- including callbacks such as executions of equals() and +// hashCode() -- appears in a static initializer or static block, then static fields cannot be +// assumed to be initialized within the constructor. + +public class Issue556b { + static class Parent { + private final Object o; + + public Parent(final Object o) { + this.o = o; + } + + @Override + public String toString() { + return o.toString(); + } + } + + static class Child extends Parent { + public static final Child CHILD = new Child(); + private static final Object OBJ = new Object(); + + private Child() { + // This call should not be legal, because at the time that the call occurs, the static + // initializers of Child have not yet finished executing and therefore CHILD and OBJ are + // not necessarily initialized and are not necessarily non-null. + // :: error: (method.invocation.invalid) + super(OBJ); + } + } + + static class Child2 extends Parent { + public static final Child2 CHILD; + private static final Object OBJ; + + static { + CHILD = new Child2(); + OBJ = new Object(); + } + + private Child2() { + // This call should not be legal, because at the time that the call occurs, the static + // initializers of Child have not yet finished executing and therefore CHILD and OBJ are + // not necessarily initialized and are not necessarily non-null. + // :: error: (method.invocation.invalid) + super(OBJ); + } + } + + // Changing the order of the OBJ and CHILD fields prevents a null pointer exception. + static class ChildOk1 extends Parent { + private static final Object OBJ = new Object(); + public static final Child CHILD = new Child(); + + private ChildOk1() { + // This call is legal, because OBJ is non-null at the time of the + // call. That's because OBJ is initialized before CHILD and + // therefore before the call to "new Child()". + super(OBJ); + } + } + + // Changing the order of the OBJ and CHILD field assignments prevents a null pointer exception. + static class ChildOk2 extends Parent { + public static final ChildOk2 CHILD; + private static final Object OBJ; + + static { + OBJ = new Object(); + CHILD = new ChildOk2(); + } + + private ChildOk2() { + // This call is legal, because OBJ is non-null at the time of the + // call. That's because OBJ is initialized before CHILD and + // therefore before the call to "new Child()". + super(OBJ); + } + } + + public static void main(final String[] args) { + System.out.println(Child.CHILD); + } +} diff --git a/checker/tests/initialization/fbc/Issue574.java b/checker/tests/initialization/Issue574.java similarity index 96% rename from checker/tests/initialization/fbc/Issue574.java rename to checker/tests/initialization/Issue574.java index 55845637bcf4..6a26eec4ebf0 100644 --- a/checker/tests/initialization/fbc/Issue574.java +++ b/checker/tests/initialization/Issue574.java @@ -5,12 +5,12 @@ @SuppressWarnings({ // A warning is issued that fields are not initialized in the constructor. // That is expected and it is not what is being verified in this test. - "initialization.fields.uninitialized", + "initialization.field.uninitialized", // Normally @UnknownInitialization is the only initialization annotation allowed on fields. // However, for the purposes of this test, fields must be annotated with @UnderInitialization. "initialization.invalid.field.type" }) -class Issue574 { +public class Issue574 { @UnderInitialization(Object.class) Object o1; @UnderInitialization(String.class) Object o2; diff --git a/checker/tests/nullness/init/Issue779.java b/checker/tests/initialization/Issue779.java similarity index 100% rename from checker/tests/nullness/init/Issue779.java rename to checker/tests/initialization/Issue779.java diff --git a/checker/tests/initialization/fbc/Issue813.java b/checker/tests/initialization/Issue813.java similarity index 96% rename from checker/tests/initialization/fbc/Issue813.java rename to checker/tests/initialization/Issue813.java index 647fc626373c..30689628c143 100644 --- a/checker/tests/initialization/fbc/Issue813.java +++ b/checker/tests/initialization/Issue813.java @@ -5,7 +5,7 @@ import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; import org.checkerframework.checker.initialization.qual.UnderInitialization; -class Issue813 { +public class Issue813 { static interface MyInterface {} static class MyClass { diff --git a/checker/tests/nullness/init/Issue904.java b/checker/tests/initialization/Issue904.java similarity index 97% rename from checker/tests/nullness/init/Issue904.java rename to checker/tests/initialization/Issue904.java index 152f709d860f..19f2b844a00c 100644 --- a/checker/tests/nullness/init/Issue904.java +++ b/checker/tests/initialization/Issue904.java @@ -1,8 +1,6 @@ // Test case for Issue 904: // https://github.com/typetools/checker-framework/issues/904 -// @skip-test - public class Issue904 { final Object mBar; final Runnable mRunnable = diff --git a/checker/tests/initialization/fbc/Issue905.java b/checker/tests/initialization/Issue905.java similarity index 93% rename from checker/tests/initialization/fbc/Issue905.java rename to checker/tests/initialization/Issue905.java index cbf6455b00ca..dda7b6e8d0a5 100644 --- a/checker/tests/initialization/fbc/Issue905.java +++ b/checker/tests/initialization/Issue905.java @@ -7,8 +7,7 @@ public class Issue905 { final Object mBar; Issue905() { - // this should be @UnderInitialization(Object.class), so this call - // should be forbidden. + // this should be @UnderInitialization(Object.class), so this call should be forbidden. // :: error: (method.invocation.invalid) baz(); mBar = ""; diff --git a/checker/tests/initialization/NotOnlyInitializedTest.java b/checker/tests/initialization/NotOnlyInitializedTest.java new file mode 100644 index 000000000000..690de5b4610b --- /dev/null +++ b/checker/tests/initialization/NotOnlyInitializedTest.java @@ -0,0 +1,39 @@ +import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; +import org.checkerframework.checker.initialization.qual.UnderInitialization; + +public class NotOnlyInitializedTest { + + @NotOnlyInitialized NotOnlyInitializedTest f; + NotOnlyInitializedTest g; + + public NotOnlyInitializedTest() { + f = new NotOnlyInitializedTest(); + g = new NotOnlyInitializedTest(); + } + + public NotOnlyInitializedTest(char i) { + // we can store something that is under initialization (like this) in f, but not in g + f = this; + // :: error: (assignment.type.incompatible) + g = this; + } + + static void testDeref(NotOnlyInitializedTest o) { + // o is fully iniatlized, so we can dereference its fields + o.f.toString(); + o.g.toString(); + } + + static void testDeref2(@UnderInitialization NotOnlyInitializedTest o) { + // o is not fully iniatlized, so we cannot dereference its fields. + // We thus get a dereference.of.nullable error by the Nullness Checker for both o.f and o.g. + // For o.f, we also get a method.invocation.invalid error by the Initialization Checker + // because o.f is declared as @NotOnlyInitialized and thus may not be @Initialized, + // but toLowerCase()'s receiver type is, by default, @Initialized. + + // :: error: (dereference.of.nullable) :: error: (method.invocation.invalid) + o.f.toString(); + // :: error: (dereference.of.nullable) + o.g.toString(); + } +} diff --git a/checker/tests/initialization/RawMethodInvocation.java b/checker/tests/initialization/RawMethodInvocation.java new file mode 100644 index 000000000000..8aa9117238d9 --- /dev/null +++ b/checker/tests/initialization/RawMethodInvocation.java @@ -0,0 +1,49 @@ +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.*; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; + +@org.checkerframework.framework.qual.DefaultQualifier(Nullable.class) +public class RawMethodInvocation { + @NonNull String a; + @NonNull String b; + + RawMethodInvocation(boolean constructor_inits_a) { + a = ""; + init_b(); + } + + @EnsuresNonNull("b") + void init_b(@UnknownInitialization RawMethodInvocation this) { + b = ""; + } + + // :: error: (initialization.fields.uninitialized) + RawMethodInvocation(Byte constructor_inits_b) { + init_b(); + } + + // :: error: (initialization.fields.uninitialized) + RawMethodInvocation(byte constructor_inits_b) { + b = ""; + init_b(); + } + + RawMethodInvocation(int constructor_inits_none) { + init_ab(); + } + + @EnsuresNonNull({"a", "b"}) + void init_ab(@UnknownInitialization RawMethodInvocation this) { + a = ""; + b = ""; + } + + RawMethodInvocation(long constructor_escapes_raw) { + a = ""; + // :: error: (method.invocation.invalid) + nonRawMethod(); + b = ""; + } + + void nonRawMethod() {} +} diff --git a/checker/tests/nullness/init/RawTypesInit.java b/checker/tests/initialization/RawTypesInit.java similarity index 98% rename from checker/tests/nullness/init/RawTypesInit.java rename to checker/tests/initialization/RawTypesInit.java index fa7833af5ff4..ec828c2c8d1c 100644 --- a/checker/tests/nullness/init/RawTypesInit.java +++ b/checker/tests/initialization/RawTypesInit.java @@ -5,7 +5,7 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; -class RawTypesInit { +public class RawTypesInit { class Bad { @NonNull String field; @@ -103,9 +103,9 @@ void otherRaw(@UnknownInitialization B this) { } } - // :: error: (initialization.fields.uninitialized) class C extends B { + // :: error: (initialization.field.uninitialized) @NonNull String[] strings; @Override @@ -152,8 +152,8 @@ public AllFieldsInitialized() { public void nonRawMethod() {} } - // :: error: (initialization.fields.uninitialized) class AFSIICell { + // :: error: (initialization.field.uninitialized) AllFieldsSetInInitializer afsii; } @@ -219,6 +219,7 @@ void cast(@UnknownInitialization Object... args) { class RawAfterConstructorBad { Object o; + // :: error: (initialization.fields.uninitialized) RawAfterConstructorBad() {} } @@ -231,6 +232,7 @@ class RawAfterConstructorOK1 { class RawAfterConstructorOK2 { Integer a; + // :: error: (initialization.fields.uninitialized) RawAfterConstructorOK2() {} } diff --git a/checker/tests/initialization/fbc/ReceiverSuperInvocation.java b/checker/tests/initialization/ReceiverSuperInvocation.java similarity index 93% rename from checker/tests/initialization/fbc/ReceiverSuperInvocation.java rename to checker/tests/initialization/ReceiverSuperInvocation.java index 914b7716c93e..6ec0be1e7cb5 100644 --- a/checker/tests/initialization/fbc/ReceiverSuperInvocation.java +++ b/checker/tests/initialization/ReceiverSuperInvocation.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; -class ReceiverSuperInvocation { +public class ReceiverSuperInvocation { void foo(@UnderInitialization(ReceiverSuperInvocation.class) ReceiverSuperInvocation this) {} } diff --git a/checker/tests/initialization/fbc/SimpleFbc.java b/checker/tests/initialization/SimpleFbc.java similarity index 93% rename from checker/tests/initialization/fbc/SimpleFbc.java rename to checker/tests/initialization/SimpleFbc.java index 28986613719e..967608be45cd 100644 --- a/checker/tests/initialization/fbc/SimpleFbc.java +++ b/checker/tests/initialization/SimpleFbc.java @@ -28,10 +28,10 @@ void test() { void test2(@UnknownInitialization @NonNull SimpleFbc t) { // :: error: (assignment.type.incompatible) - @NonNull SimpleFbc a = t.f; + @Initialized @NonNull SimpleFbc a = t.f; } - // check committed-only semantics for fields + // check initialized-only semantics for fields void test3(@UnknownInitialization @NonNull SimpleFbc t) { @Initialized @Nullable SimpleFbc a = t.f; diff --git a/checker/tests/nullness/init/StaticInit.java b/checker/tests/initialization/StaticInit.java similarity index 86% rename from checker/tests/nullness/init/StaticInit.java rename to checker/tests/initialization/StaticInit.java index 5c309e178bfb..64f15adc3969 100644 --- a/checker/tests/nullness/init/StaticInit.java +++ b/checker/tests/initialization/StaticInit.java @@ -2,7 +2,7 @@ // https://github.com/typetools/checker-framework/issues/353 // @skip-test -class StaticInit { +public class StaticInit { static String a; diff --git a/checker/tests/initialization/fbc/Subtyping.java b/checker/tests/initialization/Subtyping.java similarity index 100% rename from checker/tests/initialization/fbc/Subtyping.java rename to checker/tests/initialization/Subtyping.java diff --git a/checker/tests/initialization/Suppression.java b/checker/tests/initialization/Suppression.java new file mode 100644 index 000000000000..4c96654b4531 --- /dev/null +++ b/checker/tests/initialization/Suppression.java @@ -0,0 +1,18 @@ +import org.checkerframework.checker.initialization.qual.UnknownInitialization; + +// This tests that the prefix "initialization" suppresses warnings by the Initialization Checker. +// The test case nullness-initialization/Suppression covers the prefixes used by the +// Nullness Checker. + +public class Suppression { + + Suppression t; + + @SuppressWarnings("initialization.fields.uninitialized") + public Suppression(Suppression arg) {} + + @SuppressWarnings({"initialization"}) + void foo(@UnknownInitialization Suppression arg) { + t = arg; // initialization error + } +} diff --git a/checker/tests/initialization/TestPolyInitialized.java b/checker/tests/initialization/TestPolyInitialized.java new file mode 100644 index 000000000000..0e34a2ec7136 --- /dev/null +++ b/checker/tests/initialization/TestPolyInitialized.java @@ -0,0 +1,48 @@ +import org.checkerframework.checker.initialization.qual.Initialized; +import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; +import org.checkerframework.checker.initialization.qual.PolyInitialized; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; + +public class TestPolyInitialized { + + @NotOnlyInitialized String testStr; + + String test = "test"; + + TestPolyInitialized(@UnknownInitialization String str) { + this.testStr = identity(str); + // :: error: (assignment.type.incompatible) + this.test = identity(str); + } + + @PolyInitialized String identity(@UnknownInitialization TestPolyInitialized this, @PolyInitialized String str) { + return str; + } + + void test1() { + @UnknownInitialization String receiver = identity(testStr); + } + + void test2() { + @Initialized String receiver = identity(test); + } + + @Initialized String test3(@UnknownInitialization String str) { + @UnknownInitialization String localStr = str; + // :: error: (return.type.incompatible) + return identity(str); + } + + @UnknownInitialization String test4(@Initialized String str) { + return identity(str); + } + + @UnknownInitialization(Object.class) String test5(@Initialized String str) { + return identity(str); + } + + @Initialized String test6(@UnknownInitialization(Object.class) String str) { + // :: error: (return.type.incompatible) + return identity(str); + } +} diff --git a/checker/tests/initialization/fbc/TryFinally.java b/checker/tests/initialization/TryFinally.java similarity index 97% rename from checker/tests/initialization/fbc/TryFinally.java rename to checker/tests/initialization/TryFinally.java index 05f53b0254e1..91c8e636ddc5 100644 --- a/checker/tests/initialization/fbc/TryFinally.java +++ b/checker/tests/initialization/TryFinally.java @@ -250,7 +250,8 @@ static String getFoo() { } private String foo; - // :: error: initialization.fields.uninitialized + + // :: error: (initialization.fields.uninitialized) public TestCtnoactionFabsentNonfinal() { try { this.foo = getFoo(); @@ -266,7 +267,8 @@ static String getFoo() { } private String foo; - // :: error: initialization.fields.uninitialized + + // :: error: (initialization.fields.uninitialized) public TestCtnoactionFnoactionNonfinal() { try { this.foo = getFoo(); @@ -352,7 +354,8 @@ static String getFoo() { } private String foo; - // :: error: initialization.fields.uninitialized + + // :: error: (initialization.fields.uninitialized) public TestCenoactionFabsentNonfinal() { try { this.foo = getFoo(); @@ -368,7 +371,8 @@ static String getFoo() { } private String foo; - // :: error: initialization.fields.uninitialized + + // :: error: (initialization.fields.uninitialized) public TestCenoactionFnoactionNonfinal() { try { this.foo = getFoo(); diff --git a/checker/tests/initialization/fbc/TryFinally2.java b/checker/tests/initialization/TryFinally2.java similarity index 99% rename from checker/tests/initialization/fbc/TryFinally2.java rename to checker/tests/initialization/TryFinally2.java index 1a6672e8e3eb..ce57f1d75b1b 100644 --- a/checker/tests/initialization/fbc/TryFinally2.java +++ b/checker/tests/initialization/TryFinally2.java @@ -1,9 +1,10 @@ // Test case for Issue 1500: // https://github.com/typetools/checker-framework/issues/1500 -import java.io.InputStream; import org.checkerframework.checker.nullness.qual.Nullable; +import java.io.InputStream; + public class TryFinally2 { @SuppressWarnings("nullness") // dummy implementation diff --git a/checker/tests/initialization/fbc/TryFinallyBreak.java b/checker/tests/initialization/TryFinallyBreak.java similarity index 99% rename from checker/tests/initialization/fbc/TryFinallyBreak.java rename to checker/tests/initialization/TryFinallyBreak.java index f98dca679317..c78d8fd7b7a9 100644 --- a/checker/tests/initialization/fbc/TryFinallyBreak.java +++ b/checker/tests/initialization/TryFinallyBreak.java @@ -1,7 +1,7 @@ // Test case for Issue 548: // https://github.com/typetools/checker-framework/issues/548 -class TryFinallyBreak { +public class TryFinallyBreak { String testWhile1() { String ans = "x"; while (this.hashCode() > 10000) { diff --git a/checker/tests/initialization/fbc/TryFinallyContinue.java b/checker/tests/initialization/TryFinallyContinue.java similarity index 98% rename from checker/tests/initialization/fbc/TryFinallyContinue.java rename to checker/tests/initialization/TryFinallyContinue.java index 67eb4ce470a7..6b22d5bcb044 100644 --- a/checker/tests/initialization/fbc/TryFinallyContinue.java +++ b/checker/tests/initialization/TryFinallyContinue.java @@ -1,7 +1,7 @@ // Test case for Issue 548: // https://github.com/typetools/checker-framework/issues/548 -class TryFinallyContinue { +public class TryFinallyContinue { String testWhile1() { String ans = "x"; while (true) { diff --git a/checker/tests/initialization/fbc/TypeFrames.java b/checker/tests/initialization/TypeFrames.java similarity index 100% rename from checker/tests/initialization/fbc/TypeFrames.java rename to checker/tests/initialization/TypeFrames.java diff --git a/checker/tests/initialization/fbc/TypeFrames2.java b/checker/tests/initialization/TypeFrames2.java similarity index 100% rename from checker/tests/initialization/fbc/TypeFrames2.java rename to checker/tests/initialization/TypeFrames2.java diff --git a/checker/tests/initialization/TypeFrames3.java b/checker/tests/initialization/TypeFrames3.java new file mode 100644 index 000000000000..1a411dd4af46 --- /dev/null +++ b/checker/tests/initialization/TypeFrames3.java @@ -0,0 +1,24 @@ +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; + +public class TypeFrames3 { + public Object f; + + public TypeFrames3(boolean dummy) { + initF(); + foo(); + } + + public TypeFrames3(int dummy) { + // :: error: (method.invocation.invalid) + foo(); + f = new Object(); + } + + @EnsuresNonNull("this.f") + public void initF(@UnknownInitialization TypeFrames3 this) { + f = new Object(); + } + + public void foo(@UnknownInitialization(TypeFrames3.class) TypeFrames3 this) {} +} diff --git a/checker/tests/initialization/TypeFrames4.java b/checker/tests/initialization/TypeFrames4.java new file mode 100644 index 000000000000..73d947353e1f --- /dev/null +++ b/checker/tests/initialization/TypeFrames4.java @@ -0,0 +1,23 @@ +import org.checkerframework.checker.initialization.qual.UnderInitialization; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; + +public class TypeFrames4 { + public Object f; + + public TypeFrames4(boolean dummy) { + initF(); + @UnderInitialization(TypeFrames4.class) TypeFrames4 a = this; + } + + public TypeFrames4(int dummy) { + // :: error: (assignment.type.incompatible) + @UnderInitialization(TypeFrames4.class) TypeFrames4 a = this; + f = new Object(); + } + + @EnsuresNonNull("this.f") + public void initF(@UnknownInitialization TypeFrames4 this) { + f = new Object(); + } +} diff --git a/checker/tests/initialization/TypeFrames5.java b/checker/tests/initialization/TypeFrames5.java new file mode 100644 index 000000000000..054722a48b9f --- /dev/null +++ b/checker/tests/initialization/TypeFrames5.java @@ -0,0 +1,16 @@ +import org.checkerframework.checker.initialization.qual.Initialized; +import org.checkerframework.checker.initialization.qual.UnderInitialization; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class TypeFrames5 { + public @Nullable Object f; + + public TypeFrames5(boolean dummy) { + @UnderInitialization(TypeFrames5.class) TypeFrames5 a = this; + } + + public TypeFrames5(int dummy) { + // :: error: (assignment.type.incompatible) + @Initialized TypeFrames5 a = this; + } +} diff --git a/checker/tests/initialization/UnboxUninitalizedFieldTest.java b/checker/tests/initialization/UnboxUninitalizedFieldTest.java new file mode 100644 index 000000000000..ad8347ba8264 --- /dev/null +++ b/checker/tests/initialization/UnboxUninitalizedFieldTest.java @@ -0,0 +1,12 @@ +// Test case for eisop issue #297: +// https://github.com/eisop/checker-framework/issues/297 +import org.checkerframework.checker.initialization.qual.UnknownInitialization; + +public class UnboxUninitalizedFieldTest { + @UnknownInitialization Integer n; + + UnboxUninitalizedFieldTest() { + // :: error: (unboxing.of.nullable) + int y = n; + } +} diff --git a/checker/tests/initialization/Uninit.java b/checker/tests/initialization/Uninit.java new file mode 100644 index 000000000000..db69ec25d8b9 --- /dev/null +++ b/checker/tests/initialization/Uninit.java @@ -0,0 +1,4 @@ +public class Uninit { + // :: error: (initialization.field.uninitialized) + Object a; +} diff --git a/checker/tests/nullness/init/Uninit10.java b/checker/tests/initialization/Uninit10.java similarity index 100% rename from checker/tests/nullness/init/Uninit10.java rename to checker/tests/initialization/Uninit10.java diff --git a/checker/tests/nullness/init/Uninit11.java b/checker/tests/initialization/Uninit11.java similarity index 99% rename from checker/tests/nullness/init/Uninit11.java rename to checker/tests/initialization/Uninit11.java index 2bee9da2538a..d4e83fcbb8f2 100644 --- a/checker/tests/nullness/init/Uninit11.java +++ b/checker/tests/initialization/Uninit11.java @@ -1,9 +1,10 @@ -import java.lang.annotation.ElementType; -import java.lang.annotation.Target; import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.Unused; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + @SubtypeOf({}) @Target(ElementType.TYPE_USE) @interface DoesNotUseF {} diff --git a/checker/tests/nullness/init/Uninit12.java b/checker/tests/initialization/Uninit12.java similarity index 88% rename from checker/tests/nullness/init/Uninit12.java rename to checker/tests/initialization/Uninit12.java index 2aa5879c3505..ff778e78713e 100644 --- a/checker/tests/nullness/init/Uninit12.java +++ b/checker/tests/initialization/Uninit12.java @@ -3,9 +3,9 @@ import org.checkerframework.checker.nullness.qual.*; -// :: error: (initialization.static.fields.uninitialized) public class Uninit12 { + // :: error: (initialization.static.field.uninitialized) static Object f; public Uninit12() { diff --git a/checker/tests/nullness/init/Uninit13.java b/checker/tests/initialization/Uninit13.java similarity index 100% rename from checker/tests/nullness/init/Uninit13.java rename to checker/tests/initialization/Uninit13.java diff --git a/checker/tests/nullness/init/Uninit14.java b/checker/tests/initialization/Uninit14.java similarity index 100% rename from checker/tests/nullness/init/Uninit14.java rename to checker/tests/initialization/Uninit14.java diff --git a/checker/tests/nullness/init/Uninit2.java b/checker/tests/initialization/Uninit2.java similarity index 100% rename from checker/tests/nullness/init/Uninit2.java rename to checker/tests/initialization/Uninit2.java diff --git a/checker/tests/nullness/init/Uninit3.java b/checker/tests/initialization/Uninit3.java similarity index 100% rename from checker/tests/nullness/init/Uninit3.java rename to checker/tests/initialization/Uninit3.java diff --git a/checker/tests/nullness/init/Uninit4.java b/checker/tests/initialization/Uninit4.java similarity index 90% rename from checker/tests/nullness/init/Uninit4.java rename to checker/tests/initialization/Uninit4.java index d4f779fcc94e..ac747824df00 100644 --- a/checker/tests/nullness/init/Uninit4.java +++ b/checker/tests/initialization/Uninit4.java @@ -4,8 +4,8 @@ class Mam { Object a = new Object(); } - // :: error: (initialization.fields.uninitialized) class BadSon { + // :: error: (initialization.field.uninitialized) Object b; } diff --git a/checker/tests/initialization/Uninit5.java b/checker/tests/initialization/Uninit5.java new file mode 100644 index 000000000000..a151d5189ff3 --- /dev/null +++ b/checker/tests/initialization/Uninit5.java @@ -0,0 +1,4 @@ +public class Uninit5 { + // :: error: (initialization.field.uninitialized) + String x; +} diff --git a/checker/tests/nullness/init/Uninit6.java b/checker/tests/initialization/Uninit6.java similarity index 100% rename from checker/tests/nullness/init/Uninit6.java rename to checker/tests/initialization/Uninit6.java diff --git a/checker/tests/nullness/init/Uninit7.java b/checker/tests/initialization/Uninit7.java similarity index 100% rename from checker/tests/nullness/init/Uninit7.java rename to checker/tests/initialization/Uninit7.java diff --git a/checker/tests/nullness/init/Uninit8.java b/checker/tests/initialization/Uninit8.java similarity index 100% rename from checker/tests/nullness/init/Uninit8.java rename to checker/tests/initialization/Uninit8.java diff --git a/checker/tests/nullness/init/Uninit9.java b/checker/tests/initialization/Uninit9.java similarity index 100% rename from checker/tests/nullness/init/Uninit9.java rename to checker/tests/initialization/Uninit9.java diff --git a/checker/tests/initialization/fbc/FlowFbc.java b/checker/tests/initialization/fbc/FlowFbc.java deleted file mode 100644 index f2ef184e360d..000000000000 --- a/checker/tests/initialization/fbc/FlowFbc.java +++ /dev/null @@ -1,40 +0,0 @@ -import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; -import org.checkerframework.checker.initialization.qual.UnknownInitialization; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; - -public class FlowFbc { - - @NonNull String f; - @NotOnlyInitialized @NonNull String g; - - public FlowFbc(String arg) { - // :: error: (dereference.of.nullable) - f.toLowerCase(); - // :: error: (dereference.of.nullable) - g.toLowerCase(); - f = arg; - g = arg; - foo(); - f.toLowerCase(); - // :: error: (method.invocation.invalid) - g.toLowerCase(); - f = arg; - } - - void test() { - @Nullable String s = null; - s = "a"; - s.toLowerCase(); - } - - void test2(@Nullable String s) { - if (s != null) { - s.toLowerCase(); - } - } - - void foo(@UnknownInitialization FlowFbc this) {} - - // TODO Pure, etc. -} diff --git a/checker/tests/initialization/fbc/Issue556b.java b/checker/tests/initialization/fbc/Issue556b.java deleted file mode 100644 index 1398c1c220e9..000000000000 --- a/checker/tests/initialization/fbc/Issue556b.java +++ /dev/null @@ -1,101 +0,0 @@ -// @skip-test - -// To reproduce the problem, run: -// javac -processor nullness Issue556b.java -// java Issue556b -// and observe that the javac execution issues no warnings but the java -// execution suffers a null pointer exception. - -// Before the constructor is invoked, static initializers and static blocks -// are executed. This suggests that the Initialization Checker can assume -// that static fields are initialized in the constructor. -// -// However, if any user-defined code -- including callbacks such as -// executions of equals() and hashCode() -- appears in a static initializer -// or static block, then static fields cannot be assumed to be -// initialized within the constructor. - -public class Issue556b { - static class Parent { - private final Object o; - - public Parent(final Object o) { - this.o = o; - } - - @Override - public String toString() { - return o.toString(); - } - } - - static class Child extends Parent { - public static final Child CHILD = new Child(); - private static final Object OBJ = new Object(); - - private Child() { - // This call should not be legal, because at the time that the - // call occurs, the static initializers of Child have not yet - // finished executing and therefore CHILD and OBJ are not - // necessarily initialized and are not necessarily non-null. - // :: error: (method.invocation.invalid) - super(OBJ); - } - } - - static class Child2 extends Parent { - public static final Child2 CHILD; - private static final Object OBJ; - - static { - CHILD = new Child2(); - OBJ = new Object(); - } - - private Child2() { - // This call should not be legal, because at the time that the - // call occurs, the static initializers of Child have not yet - // finished executing and therefore CHILD and OBJ are not - // necessarily initialized and are not necessarily non-null. - // :: error: (method.invocation.invalid) - super(OBJ); - } - } - - // Changing the order of the OBJ and CHILD fields prevents a null pointer - // exception. - static class ChildOk1 extends Parent { - private static final Object OBJ = new Object(); - public static final Child CHILD = new Child(); - - private ChildOk1() { - // This call is legal, because OBJ is non-null at the time of the - // call. That's because OBJ is initialized before CHILD and - // therefore before the call to "new Child()". - super(OBJ); - } - } - - // Changing the order of the OBJ and CHILD field assignments prevents a - // null pointer exception. - static class ChildOk2 extends Parent { - public static final ChildOk2 CHILD; - private static final Object OBJ; - - static { - OBJ = new Object(); - CHILD = new ChildOk2(); - } - - private ChildOk2() { - // This call is legal, because OBJ is non-null at the time of the - // call. That's because OBJ is initialized before CHILD and - // therefore before the call to "new Child()". - super(OBJ); - } - } - - public static void main(final String[] args) { - System.out.println(Child.CHILD); - } -} diff --git a/checker/tests/initialization/fbc/List.java b/checker/tests/initialization/fbc/List.java deleted file mode 100644 index 26464b116dc9..000000000000 --- a/checker/tests/initialization/fbc/List.java +++ /dev/null @@ -1,54 +0,0 @@ -import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; -import org.checkerframework.checker.initialization.qual.UnderInitialization; -import org.checkerframework.checker.nullness.qual.Nullable; - -// This example is taken from the FBC paper, figure 1 (and has some additional code in main below). -// We made the list generic. -public class List { - @NotOnlyInitialized Node sentinel; - - public List() { - this.sentinel = new Node<>(this); - } - - void insert(@Nullable T data) { - this.sentinel.insertAfter(data); - } - - public static void main() { - List l = new List<>(); - l.insert(1); - l.insert(2); - } -} - -class Node { - @NotOnlyInitialized Node prev; - - @NotOnlyInitialized Node next; - - @NotOnlyInitialized List parent; - - @Nullable T data; - - // for sentinel construction - Node(@UnderInitialization List parent) { - this.parent = parent; - this.prev = this; - this.next = this; - } - - // for data node construction - Node(Node prev, Node next, @Nullable T data) { - this.parent = prev.parent; - this.prev = prev; - this.next = next; - this.data = data; - } - - void insertAfter(@Nullable T data) { - Node n = new Node<>(this, this.next, data); - this.next.prev = n; - this.next = n; - } -} diff --git a/checker/tests/initialization/fbc/NotOnlyInitializedTest.java b/checker/tests/initialization/fbc/NotOnlyInitializedTest.java deleted file mode 100644 index 7b5cfb905a10..000000000000 --- a/checker/tests/initialization/fbc/NotOnlyInitializedTest.java +++ /dev/null @@ -1,34 +0,0 @@ -import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; -import org.checkerframework.checker.initialization.qual.UnderInitialization; - -public class NotOnlyInitializedTest { - - @NotOnlyInitialized NotOnlyInitializedTest f; - NotOnlyInitializedTest g; - - public NotOnlyInitializedTest() { - f = new NotOnlyInitializedTest(); - g = new NotOnlyInitializedTest(); - } - - public NotOnlyInitializedTest(char i) { - // we can store something that is under initialization (like this) in f, but not in g - f = this; - // :: error: (assignment.type.incompatible) - g = this; - } - - static void testDeref(NotOnlyInitializedTest o) { - // o is fully iniatlized, so we can dereference its fields - o.f.toString(); - o.g.toString(); - } - - static void testDeref2(@UnderInitialization NotOnlyInitializedTest o) { - // o is not fully iniatlized, so we cannot dereference its fields - // :: error: (dereference.of.nullable) - o.f.toString(); - // :: error: (dereference.of.nullable) - o.g.toString(); - } -} diff --git a/checker/tests/initialization/fbc/Suppression.java b/checker/tests/initialization/fbc/Suppression.java deleted file mode 100644 index 6f578903fe52..000000000000 --- a/checker/tests/initialization/fbc/Suppression.java +++ /dev/null @@ -1,21 +0,0 @@ -import org.checkerframework.checker.initialization.qual.UnknownInitialization; -import org.checkerframework.checker.nullness.qual.NonNull; - -public class Suppression { - - @NonNull Suppression t; - - @SuppressWarnings("initialization.fields.uninitialized") - public Suppression(Suppression arg) {} - - @SuppressWarnings({"fbc", "nullness"}) - void foo(@UnknownInitialization Suppression arg) { - t = arg; // "fbc" error - t = null; // "nullness" error - } - - void test() { - @SuppressWarnings("nullness:assignment.type.incompatible") - @NonNull String s = null; - } -} diff --git a/checker/tests/interning-warnredundantannotations/RedundantAnnotationOnField.java b/checker/tests/interning-warnredundantannotations/RedundantAnnotationOnField.java new file mode 100644 index 000000000000..d2d4b4c8e4e4 --- /dev/null +++ b/checker/tests/interning-warnredundantannotations/RedundantAnnotationOnField.java @@ -0,0 +1,5 @@ +import org.checkerframework.checker.interning.qual.Interned; + +public class RedundantAnnotationOnField { + static final @Interned String A_STRING = "a string"; +} diff --git a/checker/tests/interning-warnredundantannotations/StaticFinalStringDefault.java b/checker/tests/interning-warnredundantannotations/StaticFinalStringDefault.java new file mode 100644 index 000000000000..7e8f33aa4b93 --- /dev/null +++ b/checker/tests/interning-warnredundantannotations/StaticFinalStringDefault.java @@ -0,0 +1,6 @@ +import org.checkerframework.checker.interning.qual.Interned; + +public class StaticFinalStringDefault { + // The default type of str is not @Interned, even though it is later refined to it. + static final @Interned String str = "a"; +} diff --git a/checker/tests/interning/ArrayInitializers.java b/checker/tests/interning/ArrayInitializers.java index 8ca414779413..cb24dfc01816 100644 --- a/checker/tests/interning/ArrayInitializers.java +++ b/checker/tests/interning/ArrayInitializers.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.interning.qual.Interned; -class ArrayInitializers { +public class ArrayInitializers { public static final String STATIC_FIELD = "m"; public static final @Interned String OTHER_FIELD = "n"; diff --git a/checker/tests/interning/Arrays.java b/checker/tests/interning/Arrays.java index d5a97748221b..83fa107f30aa 100644 --- a/checker/tests/interning/Arrays.java +++ b/checker/tests/interning/Arrays.java @@ -1,8 +1,9 @@ -import java.util.ArrayList; -import java.util.List; import org.checkerframework.checker.interning.qual.Interned; import org.checkerframework.checker.interning.qual.PolyInterned; +import java.util.ArrayList; +import java.util.List; + public class Arrays { public static Integer[] arrayclone_simple(Integer[] a_old) { diff --git a/checker/tests/interning/Autoboxing.java b/checker/tests/interning/Autoboxing.java index 320354c94ee8..cc53021822a0 100644 --- a/checker/tests/interning/Autoboxing.java +++ b/checker/tests/interning/Autoboxing.java @@ -1,4 +1,4 @@ -class Autoboxing { +public class Autoboxing { Byte b; Short s; Short sInterned; diff --git a/checker/tests/interning/ClassDefaults.java b/checker/tests/interning/ClassDefaults.java index 41d9a091ae7d..01bc6f33b6eb 100644 --- a/checker/tests/interning/ClassDefaults.java +++ b/checker/tests/interning/ClassDefaults.java @@ -1,12 +1,13 @@ -import java.util.List; import org.checkerframework.checker.interning.qual.Interned; +import java.util.List; + /* * This test case excercises the interaction between class annotations * and method type argument inference. * A previously existing Unqualified annotation wasn't correctly removed. */ -class ClassDefaults { +public class ClassDefaults { @Interned class Test {} public static interface Visitor {} diff --git a/checker/tests/interning/CompileTimeConstants2.java b/checker/tests/interning/CompileTimeConstants2.java index 2a931985138c..f4c725db444e 100644 --- a/checker/tests/interning/CompileTimeConstants2.java +++ b/checker/tests/interning/CompileTimeConstants2.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.interning.qual.Interned; -class CompileTimeConstants2 { +public class CompileTimeConstants2 { @Interned String s1 = "" + ("" + 1); @Interned String s2 = (("" + ("" + 1))); diff --git a/checker/tests/interning/ComplexComparison.java b/checker/tests/interning/ComplexComparison.java index bcf002212526..cc29f2b9e8ee 100644 --- a/checker/tests/interning/ComplexComparison.java +++ b/checker/tests/interning/ComplexComparison.java @@ -1,6 +1,7 @@ -import java.util.Comparator; import org.checkerframework.checker.interning.qual.Interned; +import java.util.Comparator; + public class ComplexComparison { void testInterned() { diff --git a/checker/tests/interning/ConditionalInterning.java b/checker/tests/interning/ConditionalInterning.java index 2462609628ab..342cd77e9814 100644 --- a/checker/tests/interning/ConditionalInterning.java +++ b/checker/tests/interning/ConditionalInterning.java @@ -1,4 +1,4 @@ -class ConditionalInterning { +public class ConditionalInterning { int a, b, c; boolean cmp() { diff --git a/checker/tests/interning/ConstantsInterning.java b/checker/tests/interning/ConstantsInterning.java index d7f7f92782b4..1c4162372144 100644 --- a/checker/tests/interning/ConstantsInterning.java +++ b/checker/tests/interning/ConstantsInterning.java @@ -27,6 +27,7 @@ void foo() { // :: error: (assignment.type.incompatible) is = is + is; is = Constants2.E; + // :: error: (assignment.type.incompatible) is = (String) F; } } diff --git a/checker/tests/interning/Distinct.java b/checker/tests/interning/Distinct.java index 9541599c19e8..4c66f0860e3e 100644 --- a/checker/tests/interning/Distinct.java +++ b/checker/tests/interning/Distinct.java @@ -13,12 +13,12 @@ class Foo {} @InternedDistinct Foo d2; public void testEquals() { - // :: error: not.interned + // :: error: (not.interned) if (f1 == f2) {} - // :: error: not.interned + // :: error: (not.interned) if (f1 == i2) {} if (f1 == d2) {} - // :: error: not.interned + // :: error: (not.interned) if (i1 == f2) {} if (i1 == i2) {} if (i1 == d2) {} @@ -40,7 +40,7 @@ public void testAssignment3() { } public void testAssignment4() { - // :: error: assignment.type.incompatible + // :: error: (assignment.type.incompatible) i1 = f2; } @@ -53,12 +53,12 @@ public void testAssignment6() { } public void testAssignment7() { - // :: error: assignment.type.incompatible + // :: error: (assignment.type.incompatible) d1 = f2; } public void testAssignment8() { - // :: error: assignment.type.incompatible + // :: error: (assignment.type.incompatible) d1 = i2; } diff --git a/checker/tests/interning/DontCrash.java b/checker/tests/interning/DontCrash.java index f318e2721a26..985f882b9ed3 100644 --- a/checker/tests/interning/DontCrash.java +++ b/checker/tests/interning/DontCrash.java @@ -1,11 +1,11 @@ -// This code is illegal (javac issues an error), but nonetheless the -// org.checkerframework.checker shouldn't crash. (Maybe they shouldn't run at all if javac -// issues any errors?) +// This code is illegal (javac issues an error), but nonetheless the org.checkerframework.checker +// shouldn't crash. (Maybe they shouldn't run at all if javac issues any errors?) // @skip-test +import org.checkerframework.checker.interning.qual.Interned; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.interning.qual.Interned; public class DontCrash { diff --git a/checker/tests/interning/Enumerations.java b/checker/tests/interning/Enumerations.java index da22189c8300..40047c45ef71 100644 --- a/checker/tests/interning/Enumerations.java +++ b/checker/tests/interning/Enumerations.java @@ -1,7 +1,6 @@ public class Enumerations { - // All enumeration instances are interned; there should be no need for - // an annotation. + // All enumeration instances are interned; there should be no need for an annotation. enum StudentYear { FRESHMAN, SOPHOMORE, diff --git a/checker/tests/interning/ExpressionsInterning.java b/checker/tests/interning/ExpressionsInterning.java index a079503269a5..e8fe534ae01a 100644 --- a/checker/tests/interning/ExpressionsInterning.java +++ b/checker/tests/interning/ExpressionsInterning.java @@ -44,10 +44,9 @@ public boolean isItTheOne(Foo f) { return THEONE.equals(f); } - // A warning when interned objects are compared via .equals helps me in - // determining whether it is a good idea to convert a given class or - // reference to @Interned -- I can see whether there are places that it - // is compared with .equals, which I might need to examine. + // A warning when interned objects are compared via .equals helps me in determining whether it + // is a good idea to convert a given class or reference to @Interned -- I can see whether there + // are places that it is compared with .equals, which I might need to examine. public boolean dontUseEqualsMethod(@Interned Foo f1, @Interned Foo f2) { // :: warning: (unnecessary.equals) return f1.equals(f2); diff --git a/checker/tests/interning/FieldsImplicits.java b/checker/tests/interning/FieldsImplicits.java index 2b034910e6b7..149e9536822b 100644 --- a/checker/tests/interning/FieldsImplicits.java +++ b/checker/tests/interning/FieldsImplicits.java @@ -1,5 +1,5 @@ /** Tests that a final field annotation is inferred. */ -class FieldsImplicits { +public class FieldsImplicits { final String finalField = "asdf"; static final String finalStaticField = "asdf"; String nonFinalField = "asdf"; diff --git a/checker/tests/interning/FindDistinctTest.java b/checker/tests/interning/FindDistinctTest.java new file mode 100644 index 000000000000..d1ab75cd3d6b --- /dev/null +++ b/checker/tests/interning/FindDistinctTest.java @@ -0,0 +1,31 @@ +import org.checkerframework.checker.interning.qual.FindDistinct; +import org.checkerframework.checker.interning.qual.Interned; +import org.checkerframework.checker.interning.qual.InternedDistinct; + +public class FindDistinctTest { + + public void ok1(@FindDistinct Object o) { + // TODO: The fact that this type-checks is an (undesired) artifact of the current + // implementation of @FindDistinct. + @InternedDistinct Object o2 = o; + } + + public void ok2(@FindDistinct Object findIt, Object other) { + boolean b = findIt == other; + } + + public void useOk1(Object notinterned, @Interned Object interned) { + ok1(notinterned); + ok1(interned); + } + + public void bad1(Object o) { + // :: error: (assignment.type.incompatible) + @InternedDistinct Object o2 = o; + } + + public void bad2(Object findIt, Object other) { + // :: error: (not.interned) + boolean b = findIt == other; + } +} diff --git a/checker/tests/interning/FlowInterning.java b/checker/tests/interning/FlowInterning.java index bdb762179cc4..bd78ffe7a53e 100644 --- a/checker/tests/interning/FlowInterning.java +++ b/checker/tests/interning/FlowInterning.java @@ -16,13 +16,12 @@ public void testAppendingChar() { arg += ' '; // Interning Checker should NOT suggest == here. - if (!arg.equals("")) ; + if (!arg.equals("")) {} } public String[] parse(String args) { - // Split the args string on whitespace boundaries accounting for quoted - // strings. + // Split the args string on whitespace boundaries accounting for quoted strings. args = args.trim(); List arg_list = new ArrayList<>(); String arg = ""; diff --git a/checker/tests/interning/Generics.java b/checker/tests/interning/Generics.java index 07a461a9a44f..5f145af44dec 100644 --- a/checker/tests/interning/Generics.java +++ b/checker/tests/interning/Generics.java @@ -1,3 +1,5 @@ +import org.checkerframework.checker.interning.qual.Interned; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -5,7 +7,6 @@ import java.util.List; import java.util.Map; import java.util.Vector; -import org.checkerframework.checker.interning.qual.Interned; public class Generics { diff --git a/checker/tests/interning/HeuristicsTest.java b/checker/tests/interning/HeuristicsTest.java index b617f7e67599..a5bb85ec0703 100644 --- a/checker/tests/interning/HeuristicsTest.java +++ b/checker/tests/interning/HeuristicsTest.java @@ -1,3 +1,6 @@ +import org.checkerframework.checker.interning.qual.CompareToMethod; +import org.checkerframework.checker.interning.qual.EqualsMethod; + import java.util.Comparator; public class HeuristicsTest implements Comparable { @@ -29,6 +32,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 +123,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/InternMethodTest.java b/checker/tests/interning/InternMethodTest.java index 6878c87b75d6..20ad501590ed 100644 --- a/checker/tests/interning/InternMethodTest.java +++ b/checker/tests/interning/InternMethodTest.java @@ -1,6 +1,7 @@ +import org.checkerframework.checker.interning.qual.Interned; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.interning.qual.Interned; public class InternMethodTest { diff --git a/checker/tests/interning/InternUnbox.java b/checker/tests/interning/InternUnbox.java new file mode 100644 index 000000000000..4139e2ba6eb4 --- /dev/null +++ b/checker/tests/interning/InternUnbox.java @@ -0,0 +1,12 @@ +public class InternUnbox { + void method() { + Boolean leftBoolean = getBooleanValue(); + createBooleanCFValue(!leftBoolean); + } + + private void createBooleanCFValue(boolean b) {} + + private Boolean getBooleanValue() { + return Boolean.FALSE; + } +} diff --git a/checker/tests/interning/InternedClass.java b/checker/tests/interning/InternedClass.java index 7a04b1e856f8..deaab6457e5e 100644 --- a/checker/tests/interning/InternedClass.java +++ b/checker/tests/interning/InternedClass.java @@ -1,10 +1,11 @@ +import org.checkerframework.checker.interning.qual.InternMethod; +import org.checkerframework.checker.interning.qual.Interned; +import org.checkerframework.checker.interning.qual.UnknownInterned; + import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Vector; -import org.checkerframework.checker.interning.qual.InternMethod; -import org.checkerframework.checker.interning.qual.Interned; -import org.checkerframework.checker.interning.qual.UnknownInterned; // The @Interned annotation indicates that much like an enum, all variables // declared of this type are interned (except the constructor return value). @@ -151,6 +152,6 @@ Class[] getSuperClasses(Class c) { void testCast(Object o) { Object i = (InternedClass) o; - if (i == this) ; + if (i == this) {} } } diff --git a/checker/tests/interning/InternedClass2.java b/checker/tests/interning/InternedClass2.java index eefbe8afa51a..b4ff45972ed3 100644 --- a/checker/tests/interning/InternedClass2.java +++ b/checker/tests/interning/InternedClass2.java @@ -1,10 +1,12 @@ -import java.util.HashMap; -import java.util.Map; import org.checkerframework.checker.interning.qual.InternMethod; import org.checkerframework.checker.interning.qual.Interned; +import java.util.HashMap; +import java.util.Map; + public @Interned class InternedClass2 { private final int i; + // @UnknownInterned is the default annotation on constructor results even for @Interned classes. private InternedClass2(int i) { // Type of "this" inside a constructor of an @Interned class is @UnknownInterned. diff --git a/checker/tests/interning/Issue2809.java b/checker/tests/interning/Issue2809.java index 788be873ec3a..64a371352930 100644 --- a/checker/tests/interning/Issue2809.java +++ b/checker/tests/interning/Issue2809.java @@ -4,7 +4,7 @@ import org.checkerframework.checker.interning.qual.Interned; import org.checkerframework.checker.interning.qual.UnknownInterned; -class Issue2809 { +public class Issue2809 { void new1(MyType t, int @Interned [] non) { t.self(new MyType<>(non)); diff --git a/checker/tests/interning/Issue3594.java b/checker/tests/interning/Issue3594.java new file mode 100644 index 000000000000..4d231c061667 --- /dev/null +++ b/checker/tests/interning/Issue3594.java @@ -0,0 +1,10 @@ +public class Issue3594 { + + // Throwable is annotated with @UsesObjectEquals, which is an inherited annotation. + // So, MyThrowable should be treated as @UsesObjectEquals, too. + static class MyThrowable extends Throwable {} + + void use(MyThrowable t, MyThrowable t2) { + boolean b = t == t2; + } +} diff --git a/checker/tests/interning/IterableGenerics.java b/checker/tests/interning/IterableGenerics.java index d2597e44acc5..8123cf74a7a4 100644 --- a/checker/tests/interning/IterableGenerics.java +++ b/checker/tests/interning/IterableGenerics.java @@ -2,10 +2,10 @@ public class IterableGenerics { interface Data extends Iterable {} void typeParam(T t) { - for (String s : t) ; + for (String s : t) {} } void wildcard(Iterable t) { - for (Object a : t.iterator().next()) ; + for (Object a : t.iterator().next()) {} } } diff --git a/checker/tests/interning/NestedGenerics.java b/checker/tests/interning/NestedGenerics.java index a1f05532d0b7..07ab9e8b9795 100644 --- a/checker/tests/interning/NestedGenerics.java +++ b/checker/tests/interning/NestedGenerics.java @@ -1,6 +1,7 @@ -import java.util.List; import org.checkerframework.checker.interning.qual.Interned; +import java.util.List; + public class NestedGenerics { public void test() { diff --git a/checker/tests/interning/Options.java b/checker/tests/interning/Options.java index cef3f08fe71b..e6b6c1c470d0 100644 --- a/checker/tests/interning/Options.java +++ b/checker/tests/interning/Options.java @@ -1,9 +1,10 @@ +import org.checkerframework.checker.interning.qual.*; + import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.interning.qual.*; // Test case lifted from plume.Options -class Options { +public class Options { public void minimal(String s) { String arg = ""; // interned here @@ -23,8 +24,7 @@ public void minimal2(char c) { public String[] otherparse(String args) { - // Split the args string on whitespace boundaries accounting for quoted - // strings. + // Split the args string on whitespace boundaries accounting for quoted strings. args = args.trim(); List arg_list = new ArrayList<>(); String arg = ""; @@ -50,8 +50,7 @@ public String[] otherparse(String args) { public String[] parse(String args) { - // Split the args string on whitespace boundaries accounting for quoted - // strings. + // Split the args string on whitespace boundaries accounting for quoted strings. args = args.trim(); List arg_list = new ArrayList<>(); String arg = ""; diff --git a/checker/tests/interning/OverrideInterned.java b/checker/tests/interning/OverrideInterned.java index 3da93058baf4..ac22ca4bf320 100644 --- a/checker/tests/interning/OverrideInterned.java +++ b/checker/tests/interning/OverrideInterned.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.interning.qual.Interned; -class OverrideInterned { +public class OverrideInterned { // This code is extracted from FreePastry @@ -42,6 +42,7 @@ public class PairwiseStringEqualBad extends TwoSequenceString { public Object check_modified1(String @Interned [] a1) { return new Object(); } + // :: error: (override.param.invalid) public Object check_modified2(@Interned String @Interned [] a1) { return new Object(); diff --git a/checker/tests/interning/Polymorphism.java b/checker/tests/interning/Polymorphism.java index 73d823dd2ff1..f55431a74ff4 100644 --- a/checker/tests/interning/Polymorphism.java +++ b/checker/tests/interning/Polymorphism.java @@ -1,8 +1,9 @@ +import org.checkerframework.checker.interning.qual.*; + import java.lang.ref.WeakReference; import java.util.Date; import java.util.List; import java.util.Map; -import org.checkerframework.checker.interning.qual.*; public class Polymorphism { // Test parameter diff --git a/checker/tests/interning/PrimitivesInterning.java b/checker/tests/interning/PrimitivesInterning.java index 9cac07c1b32a..6f7ab30d1ed1 100644 --- a/checker/tests/interning/PrimitivesInterning.java +++ b/checker/tests/interning/PrimitivesInterning.java @@ -1,6 +1,7 @@ +import org.checkerframework.checker.interning.qual.*; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.interning.qual.*; public class PrimitivesInterning { diff --git a/checker/tests/interning/Raw3.java b/checker/tests/interning/Raw3.java index 076ee491ab6f..509bd3e295e2 100644 --- a/checker/tests/interning/Raw3.java +++ b/checker/tests/interning/Raw3.java @@ -1,6 +1,7 @@ +import org.checkerframework.checker.interning.qual.*; + import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.interning.qual.*; /* * TODO: Make diamond cleverer: @@ -9,7 +10,7 @@ * List<@Interned String> sl = new ArrayList(); * and then the assignment fails. */ -class Raw3 { +public class Raw3 { // We would like behavior that is as similar as possible between the // versions with no raw types and those with raw types. @@ -75,9 +76,9 @@ List bar3(List sl) { class DuoList extends ArrayList {} List bar4(List sl) { - // This line was previously failing because we couldn't adequately infer the - // type of DuoList as a List; it works now, though the future checking of rawtypes - // may be more strict + // This line was previously failing because we couldn't adequately infer the type of + // DuoList as a List; it works now, though the future checking of rawtypes may be more + // strict. // :: warning: [unchecked] unchecked conversion return (DuoList) sl; } diff --git a/checker/tests/interning/StaticInternMethod.java b/checker/tests/interning/StaticInternMethod.java index a1c1a8625f03..07efcc538f65 100644 --- a/checker/tests/interning/StaticInternMethod.java +++ b/checker/tests/interning/StaticInternMethod.java @@ -1,6 +1,7 @@ +import org.checkerframework.checker.interning.qual.*; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.interning.qual.*; public class StaticInternMethod { diff --git a/checker/tests/interning/StringIntern.java b/checker/tests/interning/StringIntern.java index bb503ba5e764..0085d3f0d155 100644 --- a/checker/tests/interning/StringIntern.java +++ b/checker/tests/interning/StringIntern.java @@ -1,6 +1,7 @@ +import org.checkerframework.checker.interning.qual.*; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.interning.qual.*; public class StringIntern { @@ -38,7 +39,8 @@ public void test(@Interned String arg) { internedStr = finalStringInitializedToInterned; // OK // :: error: (assignment.type.incompatible) internedStr = finalString2; // error - @Interned Foo internedFoo = finalFooInitializedToInterned; // OK + // :: error: (assignment.type.incompatible) + @Interned Foo internedFoo = finalFooInitializedToInterned; if (arg == finalStringStatic1) {} // OK // :: error: (not.interned) if (arg == finalStringStatic2) {} // error diff --git a/checker/tests/interning/ThreadUsesObjectEquals.java b/checker/tests/interning/ThreadUsesObjectEquals.java new file mode 100644 index 000000000000..e494d1fd3641 --- /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 883c6ef0d3f8..d878062678bb 100644 --- a/checker/tests/interning/UsesObjectEqualsTest.java +++ b/checker/tests/interning/UsesObjectEqualsTest.java @@ -1,8 +1,9 @@ -import java.util.LinkedList; -import java.util.prefs.*; import org.checkerframework.checker.interning.qual.Interned; import org.checkerframework.checker.interning.qual.UsesObjectEquals; +import java.util.LinkedList; +import java.util.prefs.*; + public class UsesObjectEqualsTest { public @UsesObjectEquals class A { @@ -20,6 +21,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 +77,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-records/LockRecord.java b/checker/tests/lock-records/LockRecord.java new file mode 100644 index 000000000000..86cbe1cbedf9 --- /dev/null +++ b/checker/tests/lock-records/LockRecord.java @@ -0,0 +1,12 @@ +import org.checkerframework.checker.lock.qual.LockingFree; + +import java.util.concurrent.locks.ReentrantLock; + +public record LockRecord(String s, ReentrantLock lock) { + @LockingFree + // :: error: (method.guarantee.violated) + public LockRecord { + // :: error: (method.guarantee.violated) + lock.lock(); + } +} diff --git a/checker/tests/lock-safedefaults/BasicLockTest.java b/checker/tests/lock-safedefaults/BasicLockTest.java index 0346f1a83359..72816357286d 100644 --- a/checker/tests/lock-safedefaults/BasicLockTest.java +++ b/checker/tests/lock-safedefaults/BasicLockTest.java @@ -1,12 +1,17 @@ -import java.util.concurrent.locks.*; import org.checkerframework.checker.lock.qual.*; import org.checkerframework.framework.qual.AnnotatedFor; +import java.util.concurrent.locks.*; + public class BasicLockTest { class MyClass { public Object field; } + Object someValue = new Object(); + + MyClass newMyClass = new MyClass(); + MyClass myUnannotatedMethod(MyClass param) { return param; } @@ -35,7 +40,7 @@ void testFields() { // @GuardedByUnknown (and @GuardedByBottom, but it is unlikely to become the default for // return values on unannotated methods). // :: error: (lock.not.held) :: error: (argument.type.incompatible) - myUnannotatedMethod(o1).field = new Object(); + myUnannotatedMethod(o1).field = someValue; // The second way is less durable because the default for fields is currently @GuardedBy({}) // but could be changed to @GuardedByUnknown. // :: error: (assignment.type.incompatible) :: error: (argument.type.incompatible) @@ -44,10 +49,10 @@ void testFields() { // Now test that an unannotated method behaves as if it's annotated with @MayReleaseLocks lockField.lock(); myAnnotatedMethod2(); - m.field = new Object(); + m.field = someValue; myUnannotatedMethod2(); // :: error: (lock.not.held) - m.field = new Object(); + m.field = someValue; } void unannotatedReleaseLock(ReentrantLock lock) { @@ -56,30 +61,44 @@ void unannotatedReleaseLock(ReentrantLock lock) { @AnnotatedFor("lock") @MayReleaseLocks - void testLocalVariables() { + void testLocalVariables1() { MyClass o2 = new MyClass(), p2; // :: error: (argument.type.incompatible) p2 = myUnannotatedMethod(o2); MyClass o3 = new MyClass(); myAnnotatedMethod(o3); + } + @AnnotatedFor("lock") + @MayReleaseLocks + void testLocalVariables2() { // Now test that an unannotated method behaves as if it's annotated with @MayReleaseLocks final @GuardedBy({}) ReentrantLock lock = new ReentrantLock(); - @GuardedBy("lock") MyClass q = new MyClass(); + @SuppressWarnings("lock:assignment") // prevents flow-sensitive type refinement + @GuardedBy("lock") MyClass q = newMyClass; lock.lock(); myAnnotatedMethod2(); - q.field = new Object(); + q.field = someValue; // Should behave as @MayReleaseLocks, and *should* reset @LockHeld assumption about local // variable lock. myUnannotatedMethod2(); // :: error: (lock.not.held) - q.field = new Object(); + q.field = someValue; + } + + @AnnotatedFor("lock") + @MayReleaseLocks + void testLocalVariables3() { + // Now test that an unannotated method behaves as if it's annotated with @MayReleaseLocks + final @GuardedBy({}) ReentrantLock lock = new ReentrantLock(); + @SuppressWarnings("lock:assignment") // prevents flow-sensitive type refinement + @GuardedBy("lock") MyClass q = newMyClass; lock.lock(); // Should behave as @MayReleaseLocks, and *should* reset @LockHeld assumption about local // variable lock. // :: error: (argument.type.incompatible) unannotatedReleaseLock(lock); // :: error: (lock.not.held) - q.field = new Object(); + q.field = someValue; } } diff --git a/checker/tests/lock/ChapterExamples.java b/checker/tests/lock/ChapterExamples.java index 20305c020f88..f06c1797da68 100644 --- a/checker/tests/lock/ChapterExamples.java +++ b/checker/tests/lock/ChapterExamples.java @@ -1,14 +1,6 @@ -package chapter; -// This test contains the sample code from the Lock Checker manual chapter -// modified to fit testing instead of illustrative purposes, -// and contains other miscellaneous Lock Checker testing. +// This test contains the sample code from the Lock Checker manual chapter modified to fit testing +// instead of illustrative purposes, and contains other miscellaneous Lock Checker testing. -import java.util.AbstractCollection; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.concurrent.locks.ReentrantLock; import org.checkerframework.checker.lock.qual.GuardSatisfied; import org.checkerframework.checker.lock.qual.GuardedBy; import org.checkerframework.checker.lock.qual.GuardedByBottom; @@ -19,7 +11,14 @@ import org.checkerframework.checker.lock.qual.ReleasesNoLocks; import org.checkerframework.checker.nullness.qual.NonNull; -class ChapterExamples { +import java.util.AbstractCollection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.locks.ReentrantLock; + +public class ChapterExamples { // This code crashed when there was a bug before issue 524 was fixed. // An attempt to take the LUB between 'val' in the store with type 'long' // and 'val' in another store with type 'none' resulted in a crash. @@ -267,6 +266,7 @@ void myMethod7() { @GuardedBy("lock") MyClass x = new MyClass(); @GuardedBy("lock") MyClass y = x; // OK, because dereferences of y will require "lock" to be held. + // :: error: (assignment.type.incompatible) @GuardedBy({}) MyClass z = x; // ILLEGAL because dereferences of z do not require "lock" to be held. @@ -404,6 +404,7 @@ void helper4(@GuardedBy("ChapterExamples.myLock") MyClass d) { @ReleasesNoLocks void helper5() {} + // No annotation means @ReleasesNoLocks void helper6() {} @@ -427,10 +428,12 @@ void myMethod2(@GuardedBy("ChapterExamples.myLock") MyClass e) { private @GuardedBy({}) MyClass myField; + int someInt = 1; + // TODO: For now, boxed types are treated as primitive types. This may change in the future. - @SuppressWarnings("deprecation") // new Integer + @SuppressWarnings({"deprecation", "removal"}) // new Integer void unboxing() { - int a = 1; + int a = someInt; // :: error: (immutable.type.guardedby) @GuardedBy("lock") Integer c; synchronized (lock) { @@ -442,29 +445,22 @@ 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. - // :: error: (method.invocation.invalid) - d = b.intValue(); // The de-sugared version does not issue an error. + d = b.intValue(); // The de-sugared version } - c = c + b; // Syntactic sugar for c = new Integer(c.intValue() + b.intValue()). + c = c + b; // Syntactic sugar for c = Integer.valueOf(c.intValue() + b.intValue()). - // Expected, since b and c cannot be @GuardedBy("lock") since they are boxed primitives. - // :: error: (method.invocation.invalid) c = new Integer(c.intValue() + b.intValue()); // The de-sugared version + c = Integer.valueOf(c.intValue() + b.intValue()); // The de-sugared version synchronized (lock) { - c = c + b; // Syntactic sugar for c = new Integer(c.intValue() + b.intValue()). + c = c + b; // Syntactic sugar for c = Integer.valueOf(c.intValue() + b.intValue()). - // Expected, since b and c cannot be @GuardedBy("lock") since they are boxed primitives. - // :: error: (method.invocation.invalid) c = new Integer(c.intValue() + b.intValue()); // The de-sugared version + c = Integer.valueOf(c.intValue() + b.intValue()); // The de-sugared version } - // :: error: (assignment.type.incompatible) a = b; b = c; // OK } @@ -481,18 +477,18 @@ void boxingUnboxing() { @GuardedBy({}) int d; synchronized(lock) { // TODO re-enable this error (assignment.type.incompatible) - d = b; // TODO: This should not result in assignment.type.incompatible because 'b' is actually syntactic sugar for b.intValue(). + d = b; // TODO: This should not result in "assignment.type.incompatible" because 'b' is actually syntactic sugar for b.intValue(). d = b.intValue(); // The de-sugared version does not issue an error. } // TODO re-enable this error (lock.not.held) - c = c + b; // Syntactic sugar for c = new Integer(c.intValue() + b.intValue()), hence 'lock' must be held. + c = c + b; // Syntactic sugar for c = Integer.valueOf(c.intValue() + b.intValue()), hence 'lock' must be held. // TODO re-enable this error (lock.not.held) - c = new Integer(c.intValue() + b.intValue()); // The de-sugared version + c = Integer.valueOf(c.intValue() + b.intValue()); // The de-sugared version synchronized(lock) { - c = c + b; // Syntactic sugar for c = new Integer(c.intValue() + b.intValue()), hence 'lock' must be held. - c = new Integer(c.intValue() + b.intValue()); // The de-sugared version + c = c + b; // Syntactic sugar for c = Integer.valueOf(c.intValue() + b.intValue()), hence 'lock' must be held. + c = Integer.valueOf(c.intValue() + b.intValue()); // The de-sugared version } // TODO re-enable this error (lock.not.held) @@ -569,6 +565,7 @@ public boolean compare(T[] a1, T[] a2) { } private static final Object NULL_KEY = new Object(); + // A guardsatisfied.location.disallowed error is issued for the cast. @SuppressWarnings({"cast.unsafe", "guardsatisfied.location.disallowed"}) private static @GuardSatisfied(1) Object maskNull(@GuardSatisfied(1) Object key) { diff --git a/checker/tests/lock/ClassLiterals.java b/checker/tests/lock/ClassLiterals.java index 512230040583..04564529af67 100644 --- a/checker/tests/lock/ClassLiterals.java +++ b/checker/tests/lock/ClassLiterals.java @@ -2,13 +2,13 @@ import org.checkerframework.checker.lock.qual.Holding; -class ClassLiterals { +public class ClassLiterals { @Holding("ClassLiterals.class") static Object method1() { return new Object(); } - // a class literal may not terminate a flow expression string + // a class literal may not terminate a JavaExpression string @Holding("ClassLiterals") // :: error: (flowexpr.parse.error) static void method2() {} @@ -19,7 +19,7 @@ static void method3() {} @Holding("testpackage.ClassLiterals.class") static void method4() {} - // a class literal may not terminate a flow expression string + // a class literal may not terminate a JavaExpression string @Holding("testpackage.ClassLiterals") // :: error: (flowexpr.parse.error) static void method5() {} diff --git a/checker/tests/lock/ConstructorReturnNPE.java b/checker/tests/lock/ConstructorReturnNPE.java index be6ed74b88ed..2d1be3cfd332 100644 --- a/checker/tests/lock/ConstructorReturnNPE.java +++ b/checker/tests/lock/ConstructorReturnNPE.java @@ -5,7 +5,6 @@ // :: error: (expression.unparsable.type.invalid) @GuardedBy("lock") class ConstructorReturnNPE { - // :: error: (expression.unparsable.type.invalid) :: error: (super.invocation.invalid) - // :: warning: (inconsistent.constructor.type) + // :: error: (expression.unparsable.type.invalid) @GuardedBy("lock") ConstructorReturnNPE() {} } diff --git a/checker/tests/lock/ConstructorsLock.java b/checker/tests/lock/ConstructorsLock.java index 10246a8ecfee..a5e3d5de4750 100644 --- a/checker/tests/lock/ConstructorsLock.java +++ b/checker/tests/lock/ConstructorsLock.java @@ -17,6 +17,7 @@ static class MyClass { static final MyClass unlockedStatic = new MyClass(); @GuardedBy("unlockedStatic") MyClass nonstaticGuardedByStatic = new MyClass(); + // :: error: (expression.unparsable.type.invalid) static @GuardedBy("unlocked") MyClass staticGuardedByNonStatic = new MyClass(); static @GuardedBy("unlockedStatic") MyClass staticGuardedByStatic = new MyClass(); diff --git a/checker/tests/lock/FlowExpressionsTest.java b/checker/tests/lock/FlowExpressionsTest.java index 822158630ec0..3902637684a4 100644 --- a/checker/tests/lock/FlowExpressionsTest.java +++ b/checker/tests/lock/FlowExpressionsTest.java @@ -1,13 +1,19 @@ import org.checkerframework.checker.lock.qual.*; import org.checkerframework.dataflow.qual.Pure; -class FlowExpressionsTest { +public class FlowExpressionsTest { class MyClass { public Object field; } - private final @GuardedBy({""}) MyClass m = new MyClass(); + private final @GuardedBy({""}) MyClass m; + + FlowExpressionsTest() { + m = new MyClass(); + } + // private @GuardedBy({"nonexistentfield"}) MyClass m2; + @Pure private @GuardedBy({""}) MyClass getm() { return m; diff --git a/checker/tests/lock/FullyQualified.java b/checker/tests/lock/FullyQualified.java index 50166892ad0a..23308ea8950a 100644 --- a/checker/tests/lock/FullyQualified.java +++ b/checker/tests/lock/FullyQualified.java @@ -1,8 +1,9 @@ package com.example.mypackage; +import org.checkerframework.checker.lock.qual.GuardedBy; + import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.lock.qual.GuardedBy; public class FullyQualified { public static final @GuardedBy("") List all_classes = new ArrayList<>(); diff --git a/checker/tests/lock/GuardSatisfiedArray.java b/checker/tests/lock/GuardSatisfiedArray.java index 9f21f436f389..9d69fe5fa960 100644 --- a/checker/tests/lock/GuardSatisfiedArray.java +++ b/checker/tests/lock/GuardSatisfiedArray.java @@ -3,9 +3,10 @@ // Test case for Issue #917: // https://github.com/typetools/checker-framework/issues/917 -import java.util.List; import org.checkerframework.checker.lock.qual.GuardSatisfied; +import java.util.List; + public class GuardSatisfiedArray { void foo(@GuardSatisfied Object arg1, @GuardSatisfied Object arg2) {} diff --git a/checker/tests/lock/GuardSatisfiedTest.java b/checker/tests/lock/GuardSatisfiedTest.java index a983e1f7eb76..4d4b40e92bec 100644 --- a/checker/tests/lock/GuardSatisfiedTest.java +++ b/checker/tests/lock/GuardSatisfiedTest.java @@ -83,21 +83,22 @@ void testMethodCall( // Test the receiver type matching a parameter - // Two @GS parameters with no index are incomparable (as is the case for 'this' and 'q') + // Two @GS parameters with no index are incomparable (as is the case for 'this' and 'q'). // :: error: (guardsatisfied.parameters.must.match) methodToCall3(q); // :: error: (guardsatisfied.parameters.must.match) :: error: (lock.not.held) methodToCall3(p); synchronized (lock1) { - // Two @GS parameters with no index are incomparable (as is the case for 'this' and 'q') + // Two @GS parameters with no index are incomparable (as is the case for 'this' and + // 'q'). // :: error: (guardsatisfied.parameters.must.match) methodToCall3(q); // :: error: (guardsatisfied.parameters.must.match) :: error: (lock.not.held) methodToCall3(p); synchronized (lock2) { // Two @GS parameters with no index are incomparable (as is the case for 'this' and - // 'q') + // 'q'). // :: error: (guardsatisfied.parameters.must.match) methodToCall3(q); // :: error: (guardsatisfied.parameters.must.match) @@ -189,10 +190,16 @@ void methodToCall3(@GuardSatisfied(1) GuardSatisfiedTest this, @GuardSatisfied(1 return this; } - final Object lock1 = new Object(), lock2 = new Object(); + final Object lock1 = new Object(); + final Object lock2 = new Object(); + + // This method exists to prevent flow-sensitive refinement. + @GuardedBy({"lock1", "lock2"}) Object guardedByLock1Lock2() { + return new Object(); + } void testAssignment(@GuardSatisfied Object o) { - @GuardedBy({"lock1", "lock2"}) Object p = new Object(); + @GuardedBy({"lock1", "lock2"}) Object p = guardedByLock1Lock2(); // :: error: (lock.not.held) o = p; synchronized (lock1) { @@ -205,16 +212,15 @@ void testAssignment(@GuardSatisfied Object o) { } // Test disallowed @GuardSatisfied locations. - // Whenever a disallowed location can be located within a method return type, - // receiver or parameter, test it there, because it's important to check - // that those are not mistakenly allowed, since annotations - // on method return types, receivers and parameters are allowed. - // By definition, fields and non-parameter local variables cannot be - // in one of these locations on a method declaration, but other - // locations can be. + // Whenever a disallowed location can be located within a method return type, receiver or + // parameter, test it there, because it's important to check that those are not mistakenly + // allowed, since annotations on method return types, receivers and parameters are allowed. By + // definition, fields and non-parameter local variables cannot be in one of these locations on a + // method declaration, but other locations can be. // :: error: (guardsatisfied.location.disallowed) @GuardSatisfied Object field; + // :: error: (guardsatisfied.location.disallowed) void testGuardSatisfiedOnArrayElementAndLocalVariable(@GuardSatisfied Object[] array) { // :: error: (guardsatisfied.location.disallowed) @@ -236,7 +242,7 @@ void testGuardSatisfiedOnArrayOfParameterizedType( void testGuardSatisfiedOnArrayComponentOfParameterizedType( // :: error: (guardsatisfied.location.disallowed) @GuardSatisfied MyParameterizedClass1[] array) {} - }; + } void testGuardSatisfiedOnWildCardExtendsBound( // :: error: (guardsatisfied.location.disallowed) @@ -244,7 +250,7 @@ void testGuardSatisfiedOnWildCardExtendsBound( void testGuardSatisfiedOnWildCardSuperBound( // :: error: (guardsatisfied.location.disallowed) - MyParameterizedClass1 l) {} + MyParameterizedClass1 l) {} @GuardSatisfied(1) Object testGuardSatisfiedOnParameters( @GuardSatisfied GuardSatisfiedTest this, @@ -255,10 +261,13 @@ void testGuardSatisfiedOnWildCardSuperBound( } void testGuardSatisfiedOnArray1(Object @GuardSatisfied [][][] array) {} + // :: error: (guardsatisfied.location.disallowed) void testGuardSatisfiedOnArray2(@GuardSatisfied Object[][][] array) {} + // :: error: (guardsatisfied.location.disallowed) void testGuardSatisfiedOnArray3(Object[] @GuardSatisfied [][] array) {} + // :: error: (guardsatisfied.location.disallowed) void testGuardSatisfiedOnArray4(Object[][] @GuardSatisfied [] array) {} } diff --git a/checker/tests/lock/GuardedByLocalVariable.java b/checker/tests/lock/GuardedByLocalVariable.java index 4775b71f39d3..083067823e42 100644 --- a/checker/tests/lock/GuardedByLocalVariable.java +++ b/checker/tests/lock/GuardedByLocalVariable.java @@ -1,24 +1,30 @@ // Test for Checker Framework issue 795 // https://github.com/typetools/checker-framework/issues/795 +import org.checkerframework.checker.lock.qual.*; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.lock.qual.*; -class GuardedByLocalVariable { +public class GuardedByLocalVariable { public static void localVariableShadowing() { // :: error: (expression.unparsable.type.invalid) @GuardedBy("m0") Object kk; { - final Map m0 = new HashMap<>(); + @SuppressWarnings("assignment") // prevent flow-sensitive type refinement + final Map m0 = someValue(); @GuardedBy("m0") Object k = "key"; - // :: error: (assignment.type.incompatible) + // If the type of kk were legal, this assignment would be illegal because the two + // instances of "m0" would refer to different variables. kk = k; } { - final Map m0 = new HashMap<>(); - // :: error: (assignment.type.incompatible) + @SuppressWarnings( + "assignment.type.incompatible") // prevent flow-sensitive type refinement + final Map m0 = someValue(); + // If the type of kk were legal, this assignment would be illegal because the two + // instances of "m0" would refer to different variables. @GuardedBy("m0") Object k2 = kk; } } @@ -27,4 +33,8 @@ public static void invalidLocalVariable() { // :: error: (expression.unparsable.type.invalid) @GuardedBy("foobar") Object kk; } + + static @GuardedByUnknown Map someValue() { + return new HashMap<>(); + } } diff --git a/checker/tests/lock/Issue152.java b/checker/tests/lock/Issue152.java index 8f5f535ef61e..8606d146ec8c 100644 --- a/checker/tests/lock/Issue152.java +++ b/checker/tests/lock/Issue152.java @@ -29,6 +29,7 @@ class OuterClass { class InnerClass { private final Object lock = new Object(); + // :: error: (assignment.type.incompatible) @GuardedBy("this.lock") Object field2 = field; } diff --git a/checker/tests/lock/Issue2163Lock.java b/checker/tests/lock/Issue2163Lock.java index 86f8ef0d16ba..0b73b90922dd 100644 --- a/checker/tests/lock/Issue2163Lock.java +++ b/checker/tests/lock/Issue2163Lock.java @@ -1,10 +1,10 @@ import org.checkerframework.checker.lock.qual.*; -class Issue2163 { - @GuardedBy Issue2163() {} +public class Issue2163Lock { + @GuardedBy Issue2163Lock() {} void test() { // :: error: (constructor.invocation.invalid) :: error: (guardsatisfied.location.disallowed) - new @GuardSatisfied Issue2163(); + new @GuardSatisfied Issue2163Lock(); } } diff --git a/checker/tests/lock/Issue523.java b/checker/tests/lock/Issue523.java index 06ccbec3ff58..1a29887ddbaa 100644 --- a/checker/tests/lock/Issue523.java +++ b/checker/tests/lock/Issue523.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.lock.qual.*; -class Issue523 { +public class Issue523 { static class MyClass { Object field; } diff --git a/checker/tests/lock/Issue524.java b/checker/tests/lock/Issue524.java index c9f54bb13d94..6a6a43b328ed 100644 --- a/checker/tests/lock/Issue524.java +++ b/checker/tests/lock/Issue524.java @@ -1,12 +1,13 @@ // Test case for Issue 524: // https://github.com/typetools/checker-framework/issues/524 -import java.util.concurrent.locks.ReentrantLock; import org.checkerframework.checker.lock.qual.GuardedBy; +import org.checkerframework.checker.lock.qual.GuardedByUnknown; + +import java.util.concurrent.locks.ReentrantLock; -// WARNING: this test is nondeterministic, and has already been -// minimized - if you modify it by removing what appears to be -// redundant code, it may no longer reproduce the issue or provide +// WARNING: this test is nondeterministic, and has already been minimized - if you modify it by +// removing what appears to be redundant code, it may no longer reproduce the issue or provide // coverage for the issue after a fix for the issue has been made. // About the nondeterminism: @@ -16,17 +17,22 @@ // However even without a fix for issue 524 in place, the test sometimes type checks. // Unfortunately a test case that always fails to typecheck using a Checker Framework build // prior to the fix for issue 524 has not been found. -class Issue524 { +public class Issue524 { class MyClass { public Object field; } + @GuardedByUnknown MyClass someValue() { + return new MyClass(); + } + void testLocalVariables() { @GuardedBy({}) ReentrantLock localLock = new ReentrantLock(); { + @SuppressWarnings("assignment") // prevent flow-sensitive type refinement // :: error: (lock.expression.not.final) - @GuardedBy("localLock") MyClass q = new MyClass(); + @GuardedBy("localLock") MyClass q = someValue(); localLock.lock(); localLock.lock(); // Without a fix for issue 524 in place, the error lock.not.held diff --git a/checker/tests/lock/Issue753.java b/checker/tests/lock/Issue753.java index 31a5e01d1e9f..3a64bc531349 100644 --- a/checker/tests/lock/Issue753.java +++ b/checker/tests/lock/Issue753.java @@ -1,10 +1,11 @@ // Test case for Issue 753: // https://github.com/typetools/checker-framework/issues/753 -import java.util.concurrent.locks.ReentrantLock; import org.checkerframework.checker.lock.qual.*; import org.checkerframework.dataflow.qual.*; +import java.util.concurrent.locks.ReentrantLock; + @SuppressWarnings({ "purity", "contracts.precondition.not.satisfied", diff --git a/checker/tests/lock/Issue804.java b/checker/tests/lock/Issue804.java index 18164cd73d85..122b8a724afc 100644 --- a/checker/tests/lock/Issue804.java +++ b/checker/tests/lock/Issue804.java @@ -1,9 +1,10 @@ // Test case for Issue 804: // https://github.com/typetools/checker-framework/issues/804 -import java.util.concurrent.locks.*; import org.checkerframework.checker.lock.qual.*; +import java.util.concurrent.locks.*; + public class Issue804 extends ReentrantLock { @Holding("this") @MayReleaseLocks diff --git a/checker/tests/lock/Issue805.java b/checker/tests/lock/Issue805.java index 772cb32e0cf5..6a721e772c4f 100644 --- a/checker/tests/lock/Issue805.java +++ b/checker/tests/lock/Issue805.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.lock.qual.Holding; -class Issue805 { +public class Issue805 { @Holding("this.Issue805.class") // :: error: (flowexpr.parse.error) void method() {} diff --git a/checker/tests/lock/ItselfExpressionCases.java b/checker/tests/lock/ItselfExpressionCases.java index 753a792e836f..dc09b416af9a 100644 --- a/checker/tests/lock/ItselfExpressionCases.java +++ b/checker/tests/lock/ItselfExpressionCases.java @@ -2,10 +2,14 @@ import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.dataflow.qual.*; -class ItselfExpressionCases { +public class ItselfExpressionCases { final Object somelock = new Object(); - private final @GuardedBy({""}) MyClass m = new MyClass(); + private @GuardedBy({""}) MyClass guardedBySelf() { + return new MyClass(); + } + + private final @GuardedBy({""}) MyClass m = guardedBySelf(); @Pure private @GuardedBy({""}) MyClass getm() { @@ -89,6 +93,8 @@ public void testCheckPreconditions( // :: error: (contracts.precondition.not.satisfied) o.foo(); synchronized (somelock) { + // o.foo() requires o.somelock is held, not this.somelock. + // :: error: (contracts.precondition.not.satisfied) o.foo(); } } diff --git a/checker/tests/lock/JCIPAnnotations.java b/checker/tests/lock/JCIPAnnotations.java index 059cca542353..57c7fbbc4ae1 100644 --- a/checker/tests/lock/JCIPAnnotations.java +++ b/checker/tests/lock/JCIPAnnotations.java @@ -1,8 +1,9 @@ -import net.jcip.annotations.GuardedBy; import org.checkerframework.checker.lock.qual.GuardSatisfied; import org.checkerframework.checker.lock.qual.Holding; import org.checkerframework.checker.lock.qual.LockingFree; +import net.jcip.annotations.GuardedBy; + // Smoke test for supporting JCIP and Javax annotations. // Note that JCIP and Javax @GuardedBy can only be written on fields and methods. public class JCIPAnnotations { @@ -31,11 +32,13 @@ void methodWithGuardSatisfiedReceiver(@GuardSatisfied MyClass this) {} void testReceivers() { // :: error: (method.invocation.invalid) jcipGuardedField.methodWithUnguardedReceiver(); + // :: error: (method.invocation.invalid) jcipGuardedField.methodWithGuardedReceiver(); // :: error: (lock.not.held) jcipGuardedField.methodWithGuardSatisfiedReceiver(); // :: error: (method.invocation.invalid) javaxGuardedField.methodWithUnguardedReceiver(); + // :: error: (method.invocation.invalid) javaxGuardedField.methodWithGuardedReceiver(); // :: error: (lock.not.held) javaxGuardedField.methodWithGuardSatisfiedReceiver(); diff --git a/checker/tests/lock/LockEffectAnnotations.java b/checker/tests/lock/LockEffectAnnotations.java index aaff1ca97e65..7f4bcddc8170 100644 --- a/checker/tests/lock/LockEffectAnnotations.java +++ b/checker/tests/lock/LockEffectAnnotations.java @@ -1,6 +1,3 @@ -package chapter; - -import java.util.concurrent.locks.ReentrantLock; import org.checkerframework.checker.lock.qual.GuardSatisfied; import org.checkerframework.checker.lock.qual.GuardedBy; import org.checkerframework.checker.lock.qual.GuardedByBottom; @@ -11,6 +8,8 @@ import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.dataflow.qual.SideEffectFree; +import java.util.concurrent.locks.ReentrantLock; + public class LockEffectAnnotations { class MyClass { Object field = new Object(); @@ -139,15 +138,15 @@ void pureMethodWithSynchronizedBlock() { } } + // :: warning: (inconsistent.constructor.type) @GuardedByUnknown class MyClass2 {} - // :: error: (expression.unparsable.type.invalid) :: error: (super.invocation.invalid) - // :: warning: (inconsistent.constructor.type) + // :: error: (expression.unparsable.type.invalid) @GuardedBy("lock") class MyClass3 {} @GuardedBy({}) class MyClass4 {} - // :: error: (guardsatisfied.location.disallowed) :: error: (super.invocation.invalid) - // :: warning: (inconsistent.constructor.type) + + // :: error: (guardsatisfied.location.disallowed) @GuardSatisfied class MyClass5 {} // :: error: (super.invocation.invalid) :: warning: (inconsistent.constructor.type) diff --git a/checker/tests/lock/LockExpressionIsFinal.java b/checker/tests/lock/LockExpressionIsFinal.java index 8c050fe0dbec..23de75eb20fd 100644 --- a/checker/tests/lock/LockExpressionIsFinal.java +++ b/checker/tests/lock/LockExpressionIsFinal.java @@ -1,12 +1,11 @@ -package chapter; - -import java.util.concurrent.locks.ReentrantLock; import org.checkerframework.checker.lock.qual.GuardedBy; import org.checkerframework.checker.lock.qual.GuardedByUnknown; import org.checkerframework.checker.lock.qual.MayReleaseLocks; import org.checkerframework.dataflow.qual.Deterministic; import org.checkerframework.dataflow.qual.Pure; +import java.util.concurrent.locks.ReentrantLock; + public class LockExpressionIsFinal { class MyClass { @@ -204,7 +203,8 @@ void testGuardedByExpressionIsFinal() { @GuardedBy("c1.getFieldPure(b ? c1 : o1, c1)") Object guarded5; @GuardedBy( - "c1.field.field.field.getFieldPure(c1.field, c1.getFieldDeterministic().getFieldPure(c1, c1.field)).field") + "c1.field.field.field.getFieldPure" + + "(c1.field, c1.getFieldDeterministic().getFieldPure(c1, c1.field)).field") Object guarded6; @GuardedBy("c1.field.field.field.getFieldPure2().getFieldDeterministic().field") Object guarded7; @@ -230,9 +230,9 @@ void testGuardedByExpressionIsFinal() { // :: error: (lock.expression.not.final) Object guarded14 = (@GuardedBy("o2") Object) guarded3; - Object guarded15[] = new @GuardedBy("o1") MyClass[3]; + @GuardedBy("o1") Object guarded15[] = new @GuardedBy("o1") MyClass[3]; // :: error: (lock.expression.not.final) - Object guarded16[] = new @GuardedBy("o2") MyClass[3]; + @GuardedBy("o2") Object guarded16[] = new @GuardedBy("o2") MyClass[3]; // Tests that the location of the @GB annotation inside a VariableTree does not matter (i.e. // it does not need to be the leftmost subtree). @@ -282,11 +282,11 @@ void testGuardedByExpressionIsFinal() { return t; } - class MyParameterizedClass1 {}; + class MyParameterizedClass1 {} - MyParameterizedClass1 m1; + MyParameterizedClass1 m1; // :: error: (lock.expression.not.final) - MyParameterizedClass1 m2; + MyParameterizedClass1 m2; MyParameterizedClass1 m3; // :: error: (lock.expression.not.final) @@ -299,8 +299,8 @@ class MyClassContainingALock { } void testItselfFinalLock() { - final @GuardedBy(".finalLock") MyClassContainingALock m = - new MyClassContainingALock(); + @SuppressWarnings("assignment") // prevent flow-sensitive type refinement + final @GuardedBy(".finalLock") MyClassContainingALock m = someValue(); // :: error: (lock.not.held) m.field = new Object(); // Ignore this error: it is expected that an error will be issued for dereferencing 'm' in @@ -315,8 +315,8 @@ void testItselfFinalLock() { } void testItselfNonFinalLock() { - final @GuardedBy(".nonFinalLock") MyClassContainingALock m = - new MyClassContainingALock(); + @SuppressWarnings("assignment") // prevent flow-sensitive type refinement + final @GuardedBy(".nonFinalLock") MyClassContainingALock m = someValue(); // ::error: (lock.not.held) :: error: (lock.expression.not.final) m.field = new Object(); // ::error: (lock.not.held) :: error: (lock.expression.not.final) @@ -324,4 +324,8 @@ void testItselfNonFinalLock() { // :: error: (lock.expression.not.final) m.field = new Object(); } + + @GuardedByUnknown MyClassContainingALock someValue() { + return new MyClassContainingALock(); + } } diff --git a/checker/tests/lock/LockInterfaceTest.java b/checker/tests/lock/LockInterfaceTest.java index db5a33169883..295b832eb705 100644 --- a/checker/tests/lock/LockInterfaceTest.java +++ b/checker/tests/lock/LockInterfaceTest.java @@ -1,13 +1,14 @@ // Test of use of Lock interface -import java.util.Date; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import org.checkerframework.checker.lock.qual.Holding; import org.checkerframework.checker.lock.qual.MayReleaseLocks; import org.checkerframework.checker.lock.qual.ReleasesNoLocks; -class LockInterfaceTest { +import java.util.Date; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class LockInterfaceTest { static final Lock myStaticLock = new ReentrantLock(true); diff --git a/checker/tests/lock/PrimitivesLocking.java b/checker/tests/lock/PrimitivesLocking.java index 5e102157dbe2..2149feb8f259 100644 --- a/checker/tests/lock/PrimitivesLocking.java +++ b/checker/tests/lock/PrimitivesLocking.java @@ -4,7 +4,7 @@ // Note that testing of the immutable.type.guardedby error message is done in TestTreeKinds.java -class PrimitivesLocking { +public class PrimitivesLocking { // @GuardedByName("lock") int primitive = 1; diff --git a/checker/tests/lock/ReturnsNewObjectTest.java b/checker/tests/lock/ReturnsNewObjectTest.java new file mode 100644 index 000000000000..6d780b938ab4 --- /dev/null +++ b/checker/tests/lock/ReturnsNewObjectTest.java @@ -0,0 +1,17 @@ +import org.checkerframework.checker.lock.qual.GuardedBy; +import org.checkerframework.checker.lock.qual.NewObject; + +public class ReturnsNewObjectTest { + @NewObject Object factoryMethod() { + return new Object(); + } + + void m() { + @GuardedBy("this") Object x = factoryMethod(); + } + + void m2() { + String @GuardedBy("this") [] a2 = new String[4]; + String @GuardedBy("this") [] a3 = new String[] {"a", "b", "c"}; + } +} diff --git a/checker/tests/lock/SimpleLockTest.java b/checker/tests/lock/SimpleLockTest.java index e94cb867344f..88222a99c9a1 100644 --- a/checker/tests/lock/SimpleLockTest.java +++ b/checker/tests/lock/SimpleLockTest.java @@ -1,4 +1,5 @@ import org.checkerframework.checker.lock.qual.GuardedBy; +import org.checkerframework.checker.lock.qual.GuardedByUnknown; public class SimpleLockTest { final Object lock1 = new Object(), lock2 = new Object(); @@ -17,7 +18,11 @@ void testMethodCall(@GuardedBy("lock1") SimpleLockTest this) { synchronized (this.lock2) { } - final @GuardedBy("myClass.field") MyClass myClass = new MyClass(); + @SuppressWarnings({ + "assignment", + "method.invocation" + }) // prevent flow-sensitive type refinement + final @GuardedBy("myClass.field") MyClass myClass = someValue(); // :: error: (lock.not.held) synchronized (myClass.field) { } @@ -25,6 +30,10 @@ void testMethodCall(@GuardedBy("lock1") SimpleLockTest this) { } } + @GuardedByUnknown MyClass someValue() { + return new MyClass(); + } + class MyClass { final Object field = new Object(); } diff --git a/checker/tests/lock/Strings.java b/checker/tests/lock/Strings.java index 1f021ef5b14d..c29200e1d47f 100644 --- a/checker/tests/lock/Strings.java +++ b/checker/tests/lock/Strings.java @@ -1,5 +1,3 @@ -package chapter; - import org.checkerframework.checker.lock.qual.GuardSatisfied; import org.checkerframework.checker.lock.qual.GuardedBy; import org.checkerframework.checker.lock.qual.GuardedByBottom; @@ -8,17 +6,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/lock/TestAnon.java b/checker/tests/lock/TestAnon.java index 5750b066207e..0966c61c6416 100644 --- a/checker/tests/lock/TestAnon.java +++ b/checker/tests/lock/TestAnon.java @@ -1,4 +1,4 @@ -class TestAnon { +public class TestAnon { public void foo() { String s = ""; new Object() { diff --git a/checker/tests/lock/TestConcurrentSemantics1.java b/checker/tests/lock/TestConcurrentSemantics1.java index 6c1511cd6540..8636d1662251 100644 --- a/checker/tests/lock/TestConcurrentSemantics1.java +++ b/checker/tests/lock/TestConcurrentSemantics1.java @@ -1,10 +1,9 @@ -package chapter; - -import java.util.concurrent.locks.ReentrantLock; import org.checkerframework.checker.lock.qual.GuardedBy; import org.checkerframework.checker.lock.qual.GuardedByUnknown; -class TestConcurrentSemantics1 { +import java.util.concurrent.locks.ReentrantLock; + +public class TestConcurrentSemantics1 { /* This class tests the following critical scenario. * * Suppose the following lines from method1 are executed on thread A. diff --git a/checker/tests/lock/TestConcurrentSemantics2.java b/checker/tests/lock/TestConcurrentSemantics2.java index db30b7240b2a..73253cf0ad9e 100644 --- a/checker/tests/lock/TestConcurrentSemantics2.java +++ b/checker/tests/lock/TestConcurrentSemantics2.java @@ -1,8 +1,6 @@ -package chapter; - import org.checkerframework.checker.lock.qual.GuardedBy; -class TestConcurrentSemantics2 { +public class TestConcurrentSemantics2 { final Object a = new Object(); final Object b = new Object(); @@ -14,7 +12,7 @@ void method() { // * Context switch to a different thread. // * bar() is called on the other thread. // * Context switch back to this thread. - // o is no longer null and an assignment.type.incompatible error should be issued. + // o is no longer null and an "assignment.type.incompatible" error should be issued. // :: error: (assignment.type.incompatible) @GuardedBy("b") Object o2 = o; } @@ -25,6 +23,7 @@ void bar() { // Test that field assignments do not cause their type to be refined: @GuardedBy("a") Object myObject1 = null; + // :: error: (assignment.type.incompatible) @GuardedBy("b") Object myObject2 = myObject1; } diff --git a/checker/tests/lock/TestTreeKinds.java b/checker/tests/lock/TestTreeKinds.java index be5568f953f4..5d5c2dc72e88 100644 --- a/checker/tests/lock/TestTreeKinds.java +++ b/checker/tests/lock/TestTreeKinds.java @@ -1,8 +1,10 @@ -import java.util.Random; -import java.util.concurrent.locks.ReentrantLock; import org.checkerframework.checker.lock.qual.*; +import org.checkerframework.checker.lock.qual.GuardedByUnknown; import org.checkerframework.dataflow.qual.SideEffectFree; +import java.util.Random; +import java.util.concurrent.locks.ReentrantLock; + public class TestTreeKinds { class MyClass { Object field = new Object(); @@ -15,6 +17,10 @@ Object method(@GuardSatisfied MyClass this) { void method2(@GuardSatisfied MyClass this) {} } + MyClass[] newMyClassArray() { + return new MyClass[3]; + } + @GuardedBy("lock") MyClass m; { @@ -63,15 +69,18 @@ void nonSideEffectFreeMethod() {} @Holding("lock") void requiresLockHeldMethod() {} - MyClass fooArray @GuardedBy("lock") [] = new MyClass[3]; + @SuppressWarnings("assignment") + MyClass @GuardedBy("lock") [] fooArray = new MyClass[3]; - @GuardedBy("lock") MyClass fooArray2[] = new MyClass[3]; + @GuardedBy("lock") MyClass[] fooArray2 = new MyClass[3]; - @GuardedBy("lock") MyClass fooArray3[][] = new MyClass[3][3]; + @SuppressWarnings("assignment") + @GuardedBy("lock") MyClass[][] fooArray3 = new MyClass[3][3]; - MyClass fooArray4 @GuardedBy("lock") [][] = new MyClass[3][3]; + @SuppressWarnings("assignment") + MyClass @GuardedBy("lock") [][] fooArray4 = new MyClass[3][3]; - MyClass fooArray5[] @GuardedBy("lock") [] = new MyClass[3][3]; + MyClass[] @GuardedBy("lock") [] fooArray5 = new MyClass[3][3]; class myClass { int i = 0; @@ -125,7 +134,7 @@ enum myEnumType { @GuardedBy("lock") myEnumType myEnum; void testEnumType() { - // TODO: assignment.type.incompatible is technically correct, but we could + // TODO: "assignment.type.incompatible" is technically correct, but we could // make it friendlier for the user if constant enum values on the RHS // automatically cast to the @GuardedBy annotation of the LHS. // :: error: (assignment.type.incompatible) @@ -149,8 +158,9 @@ void testTreeTypes() { MyClass o = new MyClass(); MyClass f = new MyClass(); - // The following test cases were inspired from annotator.find.ASTPathCriterion.isSatisfiedBy - // in the Annotation File Utilities + // The following test cases were inspired by + // org.checkerframework.afu.annotator.find.ASTPathCriterion.isSatisfiedBy + // in the Annotation File Utilities. // TODO: File a bug for the dataflow issue mentioned in the line below. // TODO: uncomment: Hits a bug in dataflow: @@ -270,7 +280,8 @@ void testTreeTypes() { l = getFooArray5().length; // Test different @GuardedBy(...) present on the element and array locations. - @GuardedBy("lock") MyClass @GuardedBy("lock2") [] array = new MyClass[3]; + @SuppressWarnings("lock:assignment") // prevent flow-sensitive type refinement + @GuardedBy("lock") MyClass @GuardedBy("lock2") [] array = newMyClassArray(); // :: error: (lock.not.held) array[0].field = new Object(); if (lock.isHeldByCurrentThread()) { @@ -363,16 +374,25 @@ void testTreeTypes() { // m2.field.toString(); } + @GuardedBy("lock") MyClass guardedByLock() { + return new MyClass(); + } + + @GuardedByUnknown MyClass someValue() { + return new MyClass(); + } + @MayReleaseLocks public void testLocals() { final ReentrantLock localLock = new ReentrantLock(); - @GuardedBy("localLock") MyClass guardedByLocalLock = new MyClass(); + @SuppressWarnings("assignment") // prevent flow-sensitive refinement + @GuardedBy("localLock") MyClass guardedByLocalLock = someValue(); // :: error: (lock.not.held) guardedByLocalLock.field.toString(); - @GuardedBy("lock") MyClass local = new MyClass(); + @GuardedBy("lock") MyClass local = guardedByLock(); // :: error: (lock.not.held) local.field.toString(); diff --git a/checker/tests/lock/ThisPostCondition.java b/checker/tests/lock/ThisPostCondition.java index 448932937e50..0e31d1ea5ef8 100644 --- a/checker/tests/lock/ThisPostCondition.java +++ b/checker/tests/lock/ThisPostCondition.java @@ -32,7 +32,7 @@ boolean tryLock() { } } -class ThisPostCondition { +public class ThisPostCondition { final MyReentrantLock myLock = new MyReentrantLock(); @GuardedBy("myLock") Bar bar = new Bar(); diff --git a/checker/tests/lock/TypeVarNull.java b/checker/tests/lock/TypeVarNull.java index 555f100af639..a4713a7d8463 100644 --- a/checker/tests/lock/TypeVarNull.java +++ b/checker/tests/lock/TypeVarNull.java @@ -1,3 +1,3 @@ -class TypeVarNull { +public class TypeVarNull { T t = null; } diff --git a/checker/tests/lock/Update.java b/checker/tests/lock/Update.java index 882baa2b1216..66bd10f95d6e 100644 --- a/checker/tests/lock/Update.java +++ b/checker/tests/lock/Update.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.lock.qual.GuardedBy; -class Update { +public class Update { void test() { Object o1 = new Object(); diff --git a/checker/tests/lock/ViewpointAdaptation.java b/checker/tests/lock/ViewpointAdaptation.java index 79df785734b1..a5dc4db90b75 100644 --- a/checker/tests/lock/ViewpointAdaptation.java +++ b/checker/tests/lock/ViewpointAdaptation.java @@ -15,8 +15,6 @@ public class ViewpointAdaptation { public void method1(final String a) { synchronized (a) { - // The expression "a" from the @GuardedBy annotation - // on f is not valid at the declaration site of f. // :: error: (expression.unparsable.type.invalid) f.counter++; } diff --git a/checker/tests/lock/ViewpointAdaptation2.java b/checker/tests/lock/ViewpointAdaptation2.java index d8a017b480fa..a5a878b1a1e2 100644 --- a/checker/tests/lock/ViewpointAdaptation2.java +++ b/checker/tests/lock/ViewpointAdaptation2.java @@ -24,14 +24,18 @@ class Use { @GuardedBy("lockExample1.myLock") Object o1 = lockExample1.locked; @GuardedBy("lockExample1.myLock") Object o2 = lockExample1.locked2; + // :: error: (assignment.type.incompatible) @GuardedBy("myLock") Object o3 = lockExample1.locked; + // :: error: (assignment.type.incompatible) @GuardedBy("this.myLock") Object o4 = lockExample1.locked2; @GuardedBy("lockExample1.myLock") Object oM1 = lockExample1.getLocked(); + // :: error: (assignment.type.incompatible) @GuardedBy("myLock") Object oM2 = lockExample1.getLocked(); + // :: error: (assignment.type.incompatible) @GuardedBy("this.myLock") Object oM3 = lockExample1.getLocked(); diff --git a/checker/tests/mustcall-nolightweightownership/BorrowOnReturn.java b/checker/tests/mustcall-nolightweightownership/BorrowOnReturn.java new file mode 100644 index 000000000000..67a25a1a2973 --- /dev/null +++ b/checker/tests/mustcall-nolightweightownership/BorrowOnReturn.java @@ -0,0 +1,49 @@ +// tests that the Must Call checker respects the Owning and NotOwning annotations on return values. + +import org.checkerframework.checker.mustcall.qual.*; + +class BorrowOnReturn { + @InheritableMustCall("a") + class Foo { + void a() {} + } + + @Owning + Object getOwnedFoo() { + // :: error: (return.type.incompatible) + return new Foo(); + } + + Object getNoAnnoFoo() { + // Treat as owning, so warn + // :: error: (return.type.incompatible) + return new Foo(); + } + + @NotOwning + Object getNotOwningFooWrong() { + // :: error: (return.type.incompatible) + return new Foo(); + } + + Object getNotOwningFooRightButNoNotOwningAnno() { + Foo f = new Foo(); + f.a(); + // This is still an error for now, because it's treated as an owning pointer. TODO: fix this + // kind of FP? + // :: error: (return.type.incompatible) + return f; + } + + @NotOwning + Object getNotOwningFooRight() { + Foo f = new Foo(); + f.a(); + // :: error: (return.type.incompatible) + return f; + } + + @MustCall("a") Object getNotOwningFooRight2() { + return new Foo(); + } +} diff --git a/checker/tests/mustcall-nolightweightownership/NonOwningPolyInteraction.java b/checker/tests/mustcall-nolightweightownership/NonOwningPolyInteraction.java new file mode 100644 index 000000000000..5ad580b4f780 --- /dev/null +++ b/checker/tests/mustcall-nolightweightownership/NonOwningPolyInteraction.java @@ -0,0 +1,45 @@ +// A test that non-owning method parameters are really treated as @MustCall({}) +// wrt polymorphic types. Based on some false positives in Zookeeper. +// +// This version is modified to expect that owning/notowning annotations do nothing, +// for the -AnoLightweightOwnership flag. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class NonOwningPolyInteraction { + void foo(@NotOwning InputStream instream) { + @MustCall({}) BufferedInputStream bis = new BufferedInputStream(instream); + @MustCall({"close"}) BufferedInputStream bis2 = new BufferedInputStream(instream); + } + + void bar(@Owning InputStream instream) { + @MustCall({}) BufferedInputStream bis = new BufferedInputStream(instream); + @MustCall({"close"}) BufferedInputStream bis2 = new BufferedInputStream(instream); + } + + // default anno for params in @NotOwning + void baz(InputStream instream) { + @MustCall({}) BufferedInputStream bis = new BufferedInputStream(instream); + @MustCall({"close"}) BufferedInputStream bis2 = new BufferedInputStream(instream); + } + + NonOwningPolyInteraction(@NotOwning InputStream instream) { + @MustCall({}) BufferedInputStream bis = new BufferedInputStream(instream); + @MustCall({"close"}) BufferedInputStream bis2 = new BufferedInputStream(instream); + } + + // extra param(s) here and on the next constructor because Java requires constructors to have + // different signatures. + NonOwningPolyInteraction(@Owning InputStream instream, int x) { + @MustCall({}) BufferedInputStream bis = new BufferedInputStream(instream); + @MustCall({"close"}) BufferedInputStream bis2 = new BufferedInputStream(instream); + } + + // default anno for params in @NotOwning + NonOwningPolyInteraction(InputStream instream, int x, int y) { + @MustCall({}) BufferedInputStream bis = new BufferedInputStream(instream); + @MustCall({"close"}) BufferedInputStream bis2 = new BufferedInputStream(instream); + } +} diff --git a/checker/tests/mustcall-nolightweightownership/OwningParams.java b/checker/tests/mustcall-nolightweightownership/OwningParams.java new file mode 100644 index 000000000000..4099b1a4434b --- /dev/null +++ b/checker/tests/mustcall-nolightweightownership/OwningParams.java @@ -0,0 +1,13 @@ +// This tests normally tests that parameters (including receiver parameters) marked as @Owning are +// still checked. +// This version is modified for -AnoLightweightOwnership to expect the opposite behavior. + +import org.checkerframework.checker.mustcall.qual.*; + +class OwningParams { + static void o1(@Owning OwningParams o) {} + + void test(@Owning @MustCall({"a"}) OwningParams o) { + o1(o); + } +} diff --git a/checker/tests/mustcall/BinaryInputArchive.java b/checker/tests/mustcall/BinaryInputArchive.java new file mode 100644 index 000000000000..978782c263e5 --- /dev/null +++ b/checker/tests/mustcall/BinaryInputArchive.java @@ -0,0 +1,17 @@ +// Test case based on a false positive that was +// caused by not respecting ownership transfer rules for constructor params. + +import java.io.*; + +class BinaryInputArchive { + + private DataInput in; + + public BinaryInputArchive(DataInput in) { + this.in = in; + } + + public static BinaryInputArchive getArchive(InputStream strm) { + return new BinaryInputArchive(new DataInputStream(strm)); + } +} diff --git a/checker/tests/mustcall/BorrowOnReturn.java b/checker/tests/mustcall/BorrowOnReturn.java new file mode 100644 index 000000000000..fb4327931125 --- /dev/null +++ b/checker/tests/mustcall/BorrowOnReturn.java @@ -0,0 +1,48 @@ +// tests that the Must Call checker respects the Owning and NotOwning annotations on return values. + +import org.checkerframework.checker.mustcall.qual.*; + +class BorrowOnReturn { + @InheritableMustCall("a") + class Foo { + void a() {} + } + + @Owning + Object getOwnedFoo() { + // :: error: (return.type.incompatible) + return new Foo(); + } + + Object getNoAnnoFoo() { + // Treat as owning, so warn + // :: error: (return.type.incompatible) + return new Foo(); + } + + @NotOwning + Object getNotOwningFooWrong() { + // OCC must-call checker will warn about this; MC checker isn't responsible + return new Foo(); + } + + Object getNotOwningFooRightButNoNotOwningAnno() { + Foo f = new Foo(); + f.a(); + // This is still an error for now, because it's treated as an owning pointer. TODO: fix this + // kind of FP? + // :: error: (return.type.incompatible) + return f; + } + + @NotOwning + Object getNotOwningFooRight() { + Foo f = new Foo(); + f.a(); + return f; + } + + @MustCall("a") Object getNotOwningFooRight2() { + return new Foo(); + } +} diff --git a/checker/tests/mustcall/ClassForNameInit.java b/checker/tests/mustcall/ClassForNameInit.java new file mode 100644 index 000000000000..28b83dc344ae --- /dev/null +++ b/checker/tests/mustcall/ClassForNameInit.java @@ -0,0 +1,39 @@ +// Based on a number of false positives in Zookeeper that all use this pattern to reflectively +// initialize a class. -AresolveReflection fixes this version, but most (4/5) of the failures in +// Zookeeper +// persist even with that flag, which also imposes about a 50% perf overhead. So these are now +// expected warnings. + +import java.io.*; +import java.lang.reflect.Constructor; + +class ClassForNameInit { + + public static InputStream inputStreamFactory() throws Exception { + // FYI this code will always fail if you run it, so don't. + // There's no ByteArrayInputStream constructor that takes no arguments. + Class baisClass = Class.forName("java.io.ByteArrayInputStream"); + Object bais = baisClass.getConstructor().newInstance(); + return (InputStream) bais; + } + + public static Object objectFactory() throws Exception { + Class objClass = Class.forName("java.lang.Object"); + Object obj = objClass.getConstructor().newInstance(); + return (Object) obj; + } + + private static Object getAuditLogger(String auditLoggerClass) { + if (auditLoggerClass == null) { + auditLoggerClass = Object.class.getName(); + } + try { + Constructor clientCxnConstructor = + Class.forName(auditLoggerClass).getDeclaredConstructor(); + Object auditLogger = (Object) clientCxnConstructor.newInstance(); + return auditLogger; + } catch (Exception e) { + throw new RuntimeException("Couldn't instantiate " + auditLoggerClass, e); + } + } +} diff --git a/checker/tests/mustcall/CommandResponse.java b/checker/tests/mustcall/CommandResponse.java new file mode 100644 index 000000000000..a58f96a09379 --- /dev/null +++ b/checker/tests/mustcall/CommandResponse.java @@ -0,0 +1,17 @@ +// Based on a false positive in Zookeeper. + +// @above-java17-jdk-skip-test TODO: reinstate, false positives may be due to issue #979 + +import java.util.Map; + +class CommandResponse { + Map data; + + public void putAll(Map m) { + data.putAll(m); + } + + public void putAll2(Map m) { + data.putAll(m); + } +} diff --git a/checker/tests/mustcall/CreatesMustCallForSimple.java b/checker/tests/mustcall/CreatesMustCallForSimple.java new file mode 100644 index 000000000000..097f4101e75c --- /dev/null +++ b/checker/tests/mustcall/CreatesMustCallForSimple.java @@ -0,0 +1,57 @@ +// A simple test that @CreatesMustCallFor works as intended wrt the Must Call Checker. + +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall("a") +class CreatesMustCallForSimple { + + @CreatesMustCallFor + void reset() {} + + @CreatesMustCallFor("this") + void resetThis() {} + + static @MustCall({}) CreatesMustCallForSimple makeNoMC() { + return null; + } + + static void test1() { + CreatesMustCallForSimple cos = makeNoMC(); + @MustCall({}) CreatesMustCallForSimple a = cos; + cos.reset(); + // :: error: assignment.type.incompatible + @MustCall({}) CreatesMustCallForSimple b = cos; + @MustCall("a") CreatesMustCallForSimple c = cos; + } + + static void test2() { + CreatesMustCallForSimple cos = makeNoMC(); + @MustCall({}) CreatesMustCallForSimple a = cos; + cos.resetThis(); + // :: error: assignment.type.incompatible + @MustCall({}) CreatesMustCallForSimple b = cos; + @MustCall("a") CreatesMustCallForSimple c = cos; + } + + static void test3() { + Object cos = makeNoMC(); + @MustCall({}) Object a = cos; + // :: error: createsmustcallfor.target.unparseable + ((CreatesMustCallForSimple) cos).reset(); + // It would be better to issue an assignment incompatible error here, but the + // error above is okay too. + @MustCall({}) Object b = cos; + @MustCall("a") Object c = cos; + } + + // Rewrite of test3 that follows the instructions in the error message. + static void test4() { + Object cos = makeNoMC(); + @MustCall({}) Object a = cos; + CreatesMustCallForSimple r = ((CreatesMustCallForSimple) cos); + r.reset(); + // :: error: assignment.type.incompatible + @MustCall({}) Object b = r; + @MustCall("a") Object c = r; + } +} diff --git a/checker/tests/mustcall/EditLogInputStream.java b/checker/tests/mustcall/EditLogInputStream.java new file mode 100644 index 000000000000..55691c6386c1 --- /dev/null +++ b/checker/tests/mustcall/EditLogInputStream.java @@ -0,0 +1,14 @@ +import java.io.*; +import java.util.*; + +abstract class EditLogInputStream implements Closeable { + public abstract boolean isLocalLog(); +} + +interface JournalSet extends Closeable { + static final Comparator LOCAL_LOG_PREFERENCE_COMPARATOR = + // This is an undesirable false positive that occurs because of the defaulting + // that the Must Call Checker uses for generics. + // :: error: (type.argument.type.incompatible) + Comparator.comparing(EditLogInputStream::isLocalLog).reversed(); +} diff --git a/checker/tests/mustcall/FieldInitializationWithGeneric.java b/checker/tests/mustcall/FieldInitializationWithGeneric.java new file mode 100644 index 000000000000..d3025a86e16a --- /dev/null +++ b/checker/tests/mustcall/FieldInitializationWithGeneric.java @@ -0,0 +1,9 @@ +// based on a false positive I found in Zookeeper + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +class FieldInitializationWithGeneric { + private Set activeObservers = + Collections.newSetFromMap(new ConcurrentHashMap()); +} diff --git a/checker/tests/mustcall/FileDescriptors.java b/checker/tests/mustcall/FileDescriptors.java new file mode 100644 index 000000000000..5586abd420c9 --- /dev/null +++ b/checker/tests/mustcall/FileDescriptors.java @@ -0,0 +1,19 @@ +// A test for some issues related to the getFD() method in RandomAccessFile. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class FileDescriptors { + void test(@Owning RandomAccessFile r) throws Exception { + @MustCall("close") FileDescriptor fd = r.getFD(); + // :: error: assignment.type.incompatible + @MustCall({}) FileDescriptor fd2 = r.getFD(); + } + + void test2(@Owning RandomAccessFile r) throws Exception { + @MustCall("close") FileInputStream f = new FileInputStream(r.getFD()); + // :: error: assignment.type.incompatible + @MustCall({}) FileInputStream f2 = new FileInputStream(r.getFD()); + } +} diff --git a/checker/tests/mustcall/InferTypeArgs.java b/checker/tests/mustcall/InferTypeArgs.java new file mode 100644 index 000000000000..7563486e2778 --- /dev/null +++ b/checker/tests/mustcall/InferTypeArgs.java @@ -0,0 +1,21 @@ +// A test case from the CF's all-systems tests that fails with the MC checker unless an +// implicit upper bound is made explicit. + +class CFAbstractValue> {} + +class CFAbstractAnalysis> {} + +class GenericAnnotatedTypeFactoryMustCallTest< + Value extends CFAbstractValue, FlowAnalysis extends CFAbstractAnalysis> { + + protected FlowAnalysis createFlowAnalysis() { + FlowAnalysis result = invokeConstructorFor(); + return result; + } + + // The difference between this version of this test and the all-systems version is the "extends + // Object" on the next line. + public static T invokeConstructorFor() { + return null; + } +} diff --git a/checker/tests/mustcall/InheritableMustCallEmpty.java b/checker/tests/mustcall/InheritableMustCallEmpty.java new file mode 100644 index 000000000000..113c69efaac4 --- /dev/null +++ b/checker/tests/mustcall/InheritableMustCallEmpty.java @@ -0,0 +1,26 @@ +// A simple test for @InheritableMustCall({}). + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +public class InheritableMustCallEmpty { + + @InheritableMustCall({}) + // :: error: inconsistent.mustcall.subtype + class NoObligationCloseable implements Closeable { + @Override + public void close() throws IOException { + // no resource, nothing to do + } + } + + @InheritableMustCall() + // :: error: inconsistent.mustcall.subtype + class NoObligationCloseable2 implements Closeable { + @Override + public void close() throws IOException { + // no resource, nothing to do + } + } +} diff --git a/checker/tests/mustcall/ListOfMustCall.java b/checker/tests/mustcall/ListOfMustCall.java new file mode 100644 index 000000000000..5a8fa58ca915 --- /dev/null +++ b/checker/tests/mustcall/ListOfMustCall.java @@ -0,0 +1,40 @@ +// A test that checks that parameterized classes in the JDK don't cause false positives +// when they are used with an @MustCall-annotated class. +// Currently, two undesirable errors are issued on this test case, because it is not currently +// possible to add an item with a non-empty must call type to a list without an error. +// It is possible to call other List methods by using List +// (or equivalently List, etc.). +// We should revisit this test if we ever revisit the idea of adding support for owning generics. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.util.*; + +@InheritableMustCall("a") +class ListOfMustCall { + static void test(ListOfMustCall lm) { + // :: error: (type.argument.type.incompatible) + List l = new ArrayList<>(); + // add(E e) takes an object of the type argument's type + l.add(lm); + // remove(Object e) takes an object + l.remove(lm); + } + + static void test2(ListOfMustCall lm) { + // :: error: (type.argument.type.incompatible) + List<@MustCall("a") ListOfMustCall> l = new ArrayList<>(); + l.add(lm); + l.remove(lm); + } + + static void test3(ListOfMustCall lm) { + List l = new ArrayList<>(); + l.remove(lm); + } + + static void test4(ListOfMustCall lm) { + List l = new ArrayList<>(); + l.remove(lm); + } +} diff --git a/checker/tests/mustcall/LogTheSocket.java b/checker/tests/mustcall/LogTheSocket.java new file mode 100644 index 000000000000..ddc235a9dc70 --- /dev/null +++ b/checker/tests/mustcall/LogTheSocket.java @@ -0,0 +1,77 @@ +// This test case was intended to simulate the code below, which issued +// a false positive at the call to LOG.warn() because the socket is @MustCall("close"): +// +// synchronized void closeSockets() { +// for (ServerSocket serverSocket : serverSockets) { +// if (!serverSocket.isClosed()) { +// try { +// serverSocket.close(); +// } catch (IOException e) { +// LOG.warn("Ignoring unexpected exception during close {}", serverSocket, e); +// } +// } +// } +// } +// + +// This test is also coincidentally a test case for +// https://github.com/typetools/checker-framework/pull/3867. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.IOException; +import java.net.ServerSocket; +import java.nio.channels.SocketChannel; + +class LogTheSocket { + + @NotOwning ServerSocket s; + + @MustCall("") Object s2; + + void testAssign(@Owning ServerSocket s1) { + s = s1; + // :: error: assignment.type.incompatible + s2 = s1; + } + + void logVarargs(String s, Object... objects) {} + + void logNoVarargs(String s, Object object) {} + + void test(ServerSocket serverSocket) { + if (!serverSocket.isClosed()) { + try { + serverSocket.close(); + } catch (IOException e) { + logVarargs("Ignoring unexpected exception during close {}", serverSocket, e); + } + } + } + + void test2(ServerSocket serverSocket) { + if (!serverSocket.isClosed()) { + try { + serverSocket.close(); + } catch (IOException e) { + logNoVarargs("Ignoring unexpected exception during close {}", serverSocket); + } + } + } + + // This is (mostly) copied from ACSocketTest; under a previous implementation of the + // ownership-transfer scheme, + // it caused false positive warnings from the Must Call checker. + SocketChannel createSock() throws IOException { + SocketChannel sock; + sock = SocketChannel.open(); + sock.configureBlocking(false); + sock.socket().setSoLinger(false, -1); + sock.socket().setTcpNoDelay(true); + return sock; + } + + void testPrintln(ServerSocket s) { + System.out.println(s); + } +} diff --git a/checker/tests/mustcall/MapWrap.java b/checker/tests/mustcall/MapWrap.java new file mode 100644 index 000000000000..52f792c691ff --- /dev/null +++ b/checker/tests/mustcall/MapWrap.java @@ -0,0 +1,22 @@ +// A test for a class that wraps a map. I found a similar example in Zookeeper that causes false +// positives. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.util.HashMap; + +class MapWrap { + HashMap impl = new HashMap(); + + String remove(E e) { + // remove should permit any object: its signature is remove(Object key), *not* remove(E key) + String old = impl.remove(e); + return old; + } + + String remove2(@MustCall({}) E e) { + // remove should permit any object: its signature is remove(Object key), *not* remove(E key) + String old = impl.remove(e); + return old; + } +} diff --git a/checker/tests/mustcall/MustCallAliasImpl.java b/checker/tests/mustcall/MustCallAliasImpl.java new file mode 100644 index 000000000000..e2f87781f7a1 --- /dev/null +++ b/checker/tests/mustcall/MustCallAliasImpl.java @@ -0,0 +1,20 @@ +// A simple test that MustCallAlias annotations in source code don't issue +// bogus annotations.on.use errors. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +public class MustCallAliasImpl implements Closeable { + + @Owning final Closeable foo; + + public @MustCallAlias MustCallAliasImpl(@MustCallAlias Closeable foo) { + this.foo = foo; + } + + @Override + public void close() throws IOException { + this.foo.close(); + } +} diff --git a/checker/tests/mustcall/MustCallSubtypingTest.java b/checker/tests/mustcall/MustCallSubtypingTest.java new file mode 100644 index 000000000000..affa13cb52b5 --- /dev/null +++ b/checker/tests/mustcall/MustCallSubtypingTest.java @@ -0,0 +1,132 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5760 . + +// @skip-test until the bug is fixed + +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.MustCallUnknown; + +public class MustCallSubtypingTest { + + @MustCall({"toString"}) String foo(@MustCall({"hashCode"}) String arg) { + // :: (return) + return arg; + } + + @MustCall({}) String mcEmpty; + + @MustCall({"hashCode"}) String mcHashCode; + + @MustCall({"toString"}) String mcToString; + + @MustCallUnknown String mcUnknown; + + void clientSetMcEmpty() { + mcEmpty = mcHashCode; + mcEmpty = mcToString; + mcEmpty = mcUnknown; + } + + void clientSetMcHashCode() { + mcHashCode = mcEmpty; + mcHashCode = mcToString; + mcHashCode = mcUnknown; + } + + void clientSetMcToString() { + mcToString = mcEmpty; + mcToString = mcHashCode; + mcToString = mcUnknown; + } + + void clientSetMcUnknown() { + mcUnknown = mcEmpty; + mcUnknown = mcHashCode; + mcUnknown = mcToString; + } + + void requiresMustCallEmptyObject(@MustCall({}) Object o) {} + + void requiresMustCallHashCodeObject(@MustCall({"hashCode"}) Object o) {} + + void requiresMustCallToStringObject(@MustCall({"toString"}) Object o) {} + + void requiresMustCallUnknownObject(@MustCallUnknown Object o) {} + + void requiresMustCallEmptyString(@MustCall({}) String s) {} + + void requiresMustCallHashCodeString(@MustCall({"hashCode"}) String s) {} + + void requiresMustCallToStringString(@MustCall({"toString"}) String s) {} + + void requiresMustCallUnknownString(@MustCallUnknown String s) {} + + void client(Integer i, Integer[] ia) { + requiresMustCallEmptyObject(i); + requiresMustCallEmptyObject(ia); + // :: (argument) + requiresMustCallEmptyObject(mcHashCode); + // :: (argument) + requiresMustCallEmptyObject(mcToString); + requiresMustCallEmptyObject(mcEmpty); + // :: (argument) + requiresMustCallEmptyObject(mcUnknown); + + // :: (argument) + requiresMustCallEmptyString(mcHashCode); + // :: (argument) + requiresMustCallEmptyString(mcToString); + requiresMustCallEmptyString(mcEmpty); + // :: (argument) + requiresMustCallEmptyString(mcUnknown); + + requiresMustCallHashCodeObject(i); + requiresMustCallHashCodeObject(ia); + requiresMustCallHashCodeObject(mcHashCode); + // :: (argument) + requiresMustCallHashCodeObject(mcToString); + requiresMustCallHashCodeObject(mcEmpty); + // :: (argument) + requiresMustCallHashCodeObject(mcUnknown); + + requiresMustCallHashCodeString(mcHashCode); + // :: (argument) + requiresMustCallHashCodeString(mcToString); + requiresMustCallHashCodeString(mcEmpty); + // :: (argument) + requiresMustCallHashCodeString(mcUnknown); + + requiresMustCallToStringObject(i); + requiresMustCallToStringObject(ia); + // :: (argument) + requiresMustCallToStringObject(mcHashCode); + requiresMustCallToStringObject(mcToString); + requiresMustCallToStringObject(mcEmpty); + // :: (argument) + requiresMustCallToStringObject(mcUnknown); + + // :: (argument) + requiresMustCallToStringString(mcHashCode); + requiresMustCallToStringString(mcToString); + requiresMustCallToStringString(mcEmpty); + // :: (argument) + requiresMustCallToStringString(mcUnknown); + + requiresMustCallUnknownObject(i); + requiresMustCallUnknownObject(ia); + // :: (argument) + requiresMustCallUnknownObject(mcHashCode); + // :: (argument) + requiresMustCallUnknownObject(mcToString); + // :: (argument) + requiresMustCallUnknownObject(mcEmpty); + requiresMustCallUnknownObject(mcUnknown); + + // :: (argument) + requiresMustCallUnknownString(mcHashCode); + // :: (argument) + requiresMustCallUnknownString(mcToString); + // :: (argument) + requiresMustCallUnknownString(mcEmpty); + requiresMustCallUnknownString(mcUnknown); + } +} diff --git a/checker/tests/mustcall/MyDataInputStream.java b/checker/tests/mustcall/MyDataInputStream.java new file mode 100644 index 000000000000..4cbdf61c3954 --- /dev/null +++ b/checker/tests/mustcall/MyDataInputStream.java @@ -0,0 +1,23 @@ +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +public class MyDataInputStream extends DataInputStream { + public MyDataInputStream(InputStream in) { + super(in); + } + + public void readFully(long position, ByteBuffer buf) throws IOException { + if (in instanceof ByteBufferPositionedReadable) { + ((ByteBufferPositionedReadable) in).readFully(position, buf); + } else { + throw new UnsupportedOperationException( + "Byte-buffer pread unsupported by " + in.getClass().getCanonicalName()); + } + } + + interface ByteBufferPositionedReadable { + void readFully(long position, ByteBuffer buf) throws IOException; + } +} diff --git a/checker/tests/mustcall/NonOwningPolyInteraction.java b/checker/tests/mustcall/NonOwningPolyInteraction.java new file mode 100644 index 000000000000..b8183f412640 --- /dev/null +++ b/checker/tests/mustcall/NonOwningPolyInteraction.java @@ -0,0 +1,44 @@ +// A test that non-owning method parameters are really treated as @MustCall({}) +// wrt polymorphic types. Based on some false positives in Zookeeper. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class NonOwningPolyInteraction { + void foo(@NotOwning InputStream instream) { + @MustCall({}) BufferedInputStream bis = new BufferedInputStream(instream); + @MustCall({"close"}) BufferedInputStream bis2 = new BufferedInputStream(instream); + } + + void bar(@Owning InputStream instream) { + // :: error: assignment.type.incompatible + @MustCall({}) BufferedInputStream bis = new BufferedInputStream(instream); + @MustCall({"close"}) BufferedInputStream bis2 = new BufferedInputStream(instream); + } + + // default anno for params in @NotOwning + void baz(InputStream instream) { + @MustCall({}) BufferedInputStream bis = new BufferedInputStream(instream); + @MustCall({"close"}) BufferedInputStream bis2 = new BufferedInputStream(instream); + } + + NonOwningPolyInteraction(@NotOwning InputStream instream) { + @MustCall({}) BufferedInputStream bis = new BufferedInputStream(instream); + @MustCall({"close"}) BufferedInputStream bis2 = new BufferedInputStream(instream); + } + + // extra param(s) here and on the next constructor because Java requires constructors to have + // different signatures. + NonOwningPolyInteraction(@Owning InputStream instream, int x) { + // :: error: assignment.type.incompatible + @MustCall({}) BufferedInputStream bis = new BufferedInputStream(instream); + @MustCall({"close"}) BufferedInputStream bis2 = new BufferedInputStream(instream); + } + + // default anno for params in @NotOwning + NonOwningPolyInteraction(InputStream instream, int x, int y) { + @MustCall({}) BufferedInputStream bis = new BufferedInputStream(instream); + @MustCall({"close"}) BufferedInputStream bis2 = new BufferedInputStream(instream); + } +} diff --git a/checker/tests/mustcall/NotInheritableMustCallOnClassError.java b/checker/tests/mustcall/NotInheritableMustCallOnClassError.java new file mode 100644 index 000000000000..12e053868b72 --- /dev/null +++ b/checker/tests/mustcall/NotInheritableMustCallOnClassError.java @@ -0,0 +1,11 @@ +// A test that checks that writing @MustCall instead of @InheritableMustCall +// on a class declaration generates an error, as discussed in +// https://github.com/typetools/checker-framework/issues/5181. + +import org.checkerframework.checker.mustcall.qual.*; + +@MustCall("foo") +// :: warning: mustcall.not.inheritable +public class NotInheritableMustCallOnClassError { + public void foo() {} +} diff --git a/checker/tests/mustcall/NotInheritableMustCallOnFinalClassNoError.java b/checker/tests/mustcall/NotInheritableMustCallOnFinalClassNoError.java new file mode 100644 index 000000000000..d60849ae05f4 --- /dev/null +++ b/checker/tests/mustcall/NotInheritableMustCallOnFinalClassNoError.java @@ -0,0 +1,8 @@ +// A test that checks that writing @MustCall instead of @InheritableMustCall +// on a class declaration does not generate an error when the class is final. + +import org.checkerframework.checker.mustcall.qual.*; + +@MustCall("foo") public final class NotInheritableMustCallOnFinalClassNoError { + public void foo() {} +} diff --git a/checker/tests/mustcall/NullOutputStreamTest.java b/checker/tests/mustcall/NullOutputStreamTest.java new file mode 100644 index 000000000000..6ece05181455 --- /dev/null +++ b/checker/tests/mustcall/NullOutputStreamTest.java @@ -0,0 +1,12 @@ +// @below-java11-jdk-skip-test OutputStream.nullOutputStream() was introduced in JDK 11. + +import org.checkerframework.checker.mustcall.qual.MustCall; + +import java.io.OutputStream; + +class NullOutputStreamTest { + + void m() { + @MustCall() OutputStream nullOS = OutputStream.nullOutputStream(); + } +} diff --git a/checker/tests/mustcall/NullableTransfer.java b/checker/tests/mustcall/NullableTransfer.java new file mode 100644 index 000000000000..d716f8137a33 --- /dev/null +++ b/checker/tests/mustcall/NullableTransfer.java @@ -0,0 +1,19 @@ +// A test that the must-call type of an object tested against null +// is always empty. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class NullableTransfer { + + void test(@Owning InputStream is) { + if (is == null) { + @MustCall({}) InputStream is2 = is; + } else { + // :: error: assignment.type.incompatible + @MustCall({}) InputStream is3 = is; + @MustCall("close") InputStream is4 = is; + } + } +} diff --git a/checker/tests/mustcall/OwningMustCallNothing.java b/checker/tests/mustcall/OwningMustCallNothing.java new file mode 100644 index 000000000000..2035a60b9c5f --- /dev/null +++ b/checker/tests/mustcall/OwningMustCallNothing.java @@ -0,0 +1,67 @@ +import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.Owning; + +import java.io.Closeable; + +@InheritableMustCall({}) +// :: error: (inconsistent.mustcall.subtype) +public class OwningMustCallNothing implements Closeable { + + protected @Owning AnnotationClassLoader loader; + + @CreatesMustCallFor("this") + private void loadTypeAnnotationsFromQualDir() { + if (loader != null) { + loader.close(); + } + loader = createAnnotationClassLoader(); + } + + AnnotationClassLoader createAnnotationClassLoader() { + return null; + } + + public void close() {} +} + +// :: error: (inconsistent.mustcall.subtype) +@MustCall({}) class OwningMustCallNothing2 implements Closeable { + + protected @Owning AnnotationClassLoader loader; + + @CreatesMustCallFor("this") + private void loadTypeAnnotationsFromQualDir() { + if (loader != null) { + loader.close(); + } + loader = createAnnotationClassLoader(); + } + + AnnotationClassLoader createAnnotationClassLoader() { + return null; + } + + public void close() {} +} + +@InheritableMustCall("close") +// :: error: (declaration.inconsistent.with.extends.clause) +class SubclassMustCallClose1 extends OwningMustCallNothing {} + +// :: error: (declaration.inconsistent.with.extends.clause) +@MustCall("close") class SubclassMustCallClose2 extends OwningMustCallNothing {} + +@InheritableMustCall("close") +// :: error: (declaration.inconsistent.with.extends.clause) +class SubclassMustCallClose3 extends OwningMustCallNothing2 {} + +// :: error: (declaration.inconsistent.with.extends.clause) +@MustCall("close") class SubclassMustCallClose4 extends OwningMustCallNothing2 {} + +@InheritableMustCall({}) // Don't check whether AnnotationClassLoaders are closed. +// :: error: (inconsistent.mustcall.subtype) +class AnnotationClassLoader implements Closeable { + public void close() {} +} diff --git a/checker/tests/mustcall/OwningParams.java b/checker/tests/mustcall/OwningParams.java new file mode 100644 index 000000000000..5430d509f3c2 --- /dev/null +++ b/checker/tests/mustcall/OwningParams.java @@ -0,0 +1,13 @@ +// Tests that parameters marked as @Owning are still checked. + +import org.checkerframework.checker.mustcall.qual.*; + +class OwningParams { + static void o1(@Owning OwningParams o) {} + + void test(@Owning @MustCall({"a"}) OwningParams o, @Owning OwningParams p) { + // :: error: argument.type.incompatible + o1(o); + o1(p); + } +} diff --git a/checker/tests/mustcall/PlumeUtilRequiredAnnotations.java b/checker/tests/mustcall/PlumeUtilRequiredAnnotations.java new file mode 100644 index 000000000000..f5fdedb0d5da --- /dev/null +++ b/checker/tests/mustcall/PlumeUtilRequiredAnnotations.java @@ -0,0 +1,60 @@ +// 6/1/2023: We changed the defaulting rules despite the issues described below, because we +// discovered a soundness bug that demonstrated that the only reason the other defaulting scheme +// didn't produce a lot of false positive errors was a mistake in our implementation: +// both defaulting schemes produce (different) false positives, but defaulting to @MustCall({}) on +// the upper bound of type variables limits the extra warnings to code that actually uses resources, +// which is desirable. As part of this change, the expected warnings in this test case were removed, +// but I have preserved their locations with "a type.argument error used to be issued here". +// The following comment is the original note attached to this test case: + +// This is a test case that shows off some places in plume-util where +// annotations were required, even though we'd have preferred the defaulting +// rules to result in those annotations being the defaults. +// See the discussion on https://github.com/kelloggm/object-construction-checker/pull/363 +// and https://github.com/plume-lib/plume-util/pull/126 for more details, especially +// on why changing the default isn't feasible. + +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.*; + +class PlumeUtilRequiredAnnotations { + // In the real version of this code, there is only one type parameter. + // T is the unannotated version of the parameter - i.e., what it was before + // we first ran the Must Call Checker. S is the annotated version. Adding the + // annotation itself is immaterial - what's important is that the bound + // must be explicit rather than implicit (see that the eqR field never issue errors, + // just like the eqS fields). + class MultiRandSelector { + // a type.argument error used to be issued here. + private Partitioner eqT; + private Partitioner eqS; + private Partitioner eqR; + + // Adding annotations to the definition of Partitioner doesn't fix this problem: + // a type.argument error used to be issued here. + private Partitioner2 eqT2; + private Partitioner2 eqS2; + private Partitioner2 eqR2; + + // But removing the explicit bounds on Partitioner does (not feasible in this case, though, + // because of the @Nullable annotations): + private Partitioner3 eqT3; + private Partitioner3 eqS3; + private Partitioner3 eqR3; + } + + interface Partitioner { + CLASS assignToBucket(ELEMENT obj); + } + + interface Partitioner2< + ELEMENT extends @Nullable @MustCall Object, CLASS extends @Nullable @MustCall Object> { + CLASS assignToBucket(ELEMENT obj); + } + + interface Partitioner3 { + CLASS assignToBucket(ELEMENT obj); + } +} diff --git a/checker/tests/mustcall/PolyMustCallDifferentNames.java b/checker/tests/mustcall/PolyMustCallDifferentNames.java new file mode 100644 index 000000000000..b3aa76b12f0b --- /dev/null +++ b/checker/tests/mustcall/PolyMustCallDifferentNames.java @@ -0,0 +1,76 @@ +// Tests for @PolyMustCall and @MustCallAlias where the must-call method of the return type has +// a different name than the must-call method of the parameter type. + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.*; + +class PolyMustCallDifferentNames { + + @InheritableMustCall("a") + static class Wrapped { + void a() {} + } + + @InheritableMustCall("b") + static class Wrapper1 { + private final @Owning Wrapped field; + + public @PolyMustCall Wrapper1(@PolyMustCall Wrapped w) { + // we get this error since we only have a field-assignment special case for + // @MustCallAlias, not @PolyMustCall + // :: error: (assignment.type.incompatible) + this.field = w; + } + + @EnsuresCalledMethods( + value = {"this.field"}, + methods = {"a"}) + void b() { + this.field.a(); + } + } + + static @PolyMustCall Wrapper1 getWrapper1(@PolyMustCall Wrapped w) { + return new Wrapper1(w); + } + + @InheritableMustCall("c") + static class Wrapper2 { + private final @Owning Wrapped field; + + public @MustCallAlias Wrapper2(@MustCallAlias Wrapped w) { + this.field = w; + } + + @EnsuresCalledMethods( + value = {"this.field"}, + methods = {"a"}) + void c() { + this.field.a(); + } + } + + static @MustCallAlias Wrapper2 getWrapper2(@MustCallAlias Wrapped w) { + return new Wrapper2(w); + } + + static void test1() { + @MustCall("a") Wrapped x = new Wrapped(); + @MustCall("b") Wrapper1 w1 = new Wrapper1(x); + @MustCall("b") Wrapper1 w2 = getWrapper1(x); + // :: error: (assignment.type.incompatible) + @MustCall("a") Wrapper1 w3 = new Wrapper1(x); + // :: error: (assignment.type.incompatible) + @MustCall("a") Wrapper1 w4 = getWrapper1(x); + } + + static void test2() { + @MustCall("a") Wrapped x = new Wrapped(); + @MustCall("c") Wrapper2 w1 = new Wrapper2(x); + @MustCall("c") Wrapper2 w2 = getWrapper2(x); + // :: error: (assignment.type.incompatible) + @MustCall("a") Wrapper2 w3 = new Wrapper2(x); + // :: error: (assignment.type.incompatible) + @MustCall("a") Wrapper2 w4 = getWrapper2(x); + } +} diff --git a/checker/tests/mustcall/PolyTests.java b/checker/tests/mustcall/PolyTests.java new file mode 100644 index 000000000000..4b9863630523 --- /dev/null +++ b/checker/tests/mustcall/PolyTests.java @@ -0,0 +1,42 @@ +// Unit tests for the poly annotation. + +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall("close") +class PolyTests { + static @PolyMustCall Object id(@PolyMustCall Object obj) { + return obj; + } + + static void test1(@Owning @MustCall("close") Object o) { + @MustCall("close") Object o1 = id(o); + // :: error: assignment.type.incompatible + @MustCall({}) Object o2 = id(o); + } + + static void test2(@Owning @MustCall({}) Object o) { + @MustCall("close") Object o1 = id(o); + @MustCall({}) Object o2 = id(o); + } + + // These sort of constructors will always appear in stub files and are unverifiable for now. + @SuppressWarnings("mustcall:annotations.on.use") + @PolyMustCall PolyTests(@PolyMustCall Object obj) {} + + static void test3(@Owning @MustCall({"close"}) Object o) { + @MustCall("close") Object o1 = new PolyTests(o); + // :: error: assignment.type.incompatible + @MustCall({}) Object o2 = new PolyTests(o); + } + + static void test4(@Owning @MustCall({}) Object o) { + @MustCall("close") Object o1 = new PolyTests(o); + @MustCall({}) Object o2 = new PolyTests(o); + } + + static void testArbitary(@Owning PolyTests p) { + @MustCall("close") Object o1 = p; + // :: error: assignment.type.incompatible + @MustCall({}) Object o2 = p; + } +} diff --git a/checker/tests/mustcall/SimpleException.java b/checker/tests/mustcall/SimpleException.java new file mode 100644 index 000000000000..d6cf38f2e517 --- /dev/null +++ b/checker/tests/mustcall/SimpleException.java @@ -0,0 +1,15 @@ +// A test that throwing and catching exceptions doesn't cause false positives. + +class SimpleException { + void thrower() throws Exception { + throw new RuntimeException("some exception"); + } + + void test() { + try { + thrower(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/checker/tests/mustcall/SimpleSocketField.java b/checker/tests/mustcall/SimpleSocketField.java new file mode 100644 index 000000000000..a6a4e3c30ff3 --- /dev/null +++ b/checker/tests/mustcall/SimpleSocketField.java @@ -0,0 +1,22 @@ +// a test that sockets in fields are considered @MustCall("close") + +import org.checkerframework.checker.mustcall.qual.MustCall; + +import java.net.Socket; + +class SimpleSocketField { + Socket mySock = new Socket(); + + SimpleSocketField() throws Exception { + @MustCall("close") Socket s = mySock; + // This assignment is safe, because the only possible value of mySock here is the + // unconnected socket in the field initializer. + @MustCall({}) Socket s1 = mySock; + } + + void test() { + @MustCall("close") Socket s = mySock; + // :: error: assignment.type.incompatible + @MustCall({}) Socket s1 = mySock; + } +} diff --git a/checker/tests/mustcall/SimpleStreamExample.java b/checker/tests/mustcall/SimpleStreamExample.java new file mode 100644 index 000000000000..8bb103f64f5f --- /dev/null +++ b/checker/tests/mustcall/SimpleStreamExample.java @@ -0,0 +1,9 @@ +// Based on a false positive in Zookeeper + +import java.util.*; + +class SimpleStreamExample { + static void test(List s) { + s.stream().filter(str -> str == null); + } +} diff --git a/checker/tests/mustcall/SocketBufferedReader.java b/checker/tests/mustcall/SocketBufferedReader.java new file mode 100644 index 000000000000..40c9fdee0eaa --- /dev/null +++ b/checker/tests/mustcall/SocketBufferedReader.java @@ -0,0 +1,24 @@ +// a test for missing mustcall propagation that might have caused a false positive? + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.net.*; + +class SocketBufferedReader { + void test(String address, int port) { + try { + Socket socket = new Socket(address, 80); + PrintStream out = new PrintStream(socket.getOutputStream()); + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + @MustCall("close") BufferedReader reader = in; + // :: error: assignment.type.incompatible + @MustCall({}) BufferedReader reader2 = in; + in.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/checker/tests/mustcall/StreamBool.java b/checker/tests/mustcall/StreamBool.java new file mode 100644 index 000000000000..6826763d9c60 --- /dev/null +++ b/checker/tests/mustcall/StreamBool.java @@ -0,0 +1,11 @@ +// A test case for a false positive in hfds. + +import java.io.InputStream; + +class StreamBool { + InputStream stream; + + boolean isActive() { + return stream != null; + } +} diff --git a/checker/tests/mustcall/StringSort.java b/checker/tests/mustcall/StringSort.java new file mode 100644 index 000000000000..bfb10cfd0e75 --- /dev/null +++ b/checker/tests/mustcall/StringSort.java @@ -0,0 +1,10 @@ +// Another false positive I found in Zookeeper. + +import java.util.*; + +class StringSort { + public static void sort() { + List myList = new ArrayList(); + Collections.sort(myList); + } +} diff --git a/checker/tests/mustcall/Subtype0.java b/checker/tests/mustcall/Subtype0.java new file mode 100644 index 000000000000..cfd9950a337f --- /dev/null +++ b/checker/tests/mustcall/Subtype0.java @@ -0,0 +1,61 @@ +// A test that the @InheritedMustCall declaration annotation works correctly. + +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall("a") +public class Subtype0 { + + public class Subtype1 extends Subtype0 { + void m1() {} + } + + public class Subtype2 extends Subtype1 {} + + static void test( + @Owning Subtype0 s0, + @Owning Subtype1 s1, + @Owning Subtype2 s2, + @Owning Subtype3 s3, + @Owning Subtype4 s4) { + // :: error: assignment.type.incompatible + @MustCall({}) Object obj1 = s0; + @MustCall({"a"}) Object obj2 = s0; + + // :: error: assignment.type.incompatible + @MustCall({}) Object obj3 = s1; + @MustCall({"a"}) Object obj4 = s1; + + // :: error: assignment.type.incompatible + @MustCall({}) Object obj5 = s2; + @MustCall({"a"}) Object obj6 = s2; + + @MustCall({}) Object obj7 = s3; + @MustCall({"a"}) Object obj8 = s3; + + @MustCall({}) Object obj9 = s4; + @MustCall({"a"}) Object obj10 = s4; + } + + @MustCall({}) + // :: error: inconsistent.mustcall.subtype :: error: super.invocation.invalid + public class Subtype3 extends Subtype0 {} + + @InheritableMustCall({}) + // :: error: inconsistent.mustcall.subtype :: error: super.invocation.invalid + public class Subtype4 extends Subtype0 {} + + @MustCall({"a"}) public class Subtype5 extends Subtype0 {} + + @InheritableMustCall({"a"}) + public class Subtype6 extends Subtype0 {} + + public class Container { + Subtype0 in; + + void test() { + if (in instanceof Subtype1) { + ((Subtype1) in).m1(); + } + } + } +} diff --git a/checker/tests/mustcall/Subtyping.java b/checker/tests/mustcall/Subtyping.java new file mode 100644 index 000000000000..df785f446e0a --- /dev/null +++ b/checker/tests/mustcall/Subtyping.java @@ -0,0 +1,57 @@ +// simple subtyping test for the MustCall annotation + +import org.checkerframework.checker.mustcall.qual.*; + +class Subtyping { + + Object unannotatedObj; + + void test_act(@Owning @MustCallUnknown Object o) { + @MustCallUnknown Object act = o; + // :: error: assignment.type.incompatible + @MustCall("close") Object file = o; + // :: error: assignment.type.incompatible + @MustCall({"close", "read"}) Object f2 = o; + // :: error: assignment.type.incompatible + @MustCall({}) Object notAfile = o; + // :: error: assignment.type.incompatible + unannotatedObj = o; + } + + void test_close(@Owning @MustCall("close") Object o) { + @MustCallUnknown Object act = o; + @MustCall("close") Object file = o; + @MustCall({"close", "read"}) Object f2 = o; + // :: error: assignment.type.incompatible + @MustCall({}) Object notAfile = o; + // :: error: assignment.type.incompatible + unannotatedObj = o; + } + + void test_close_read(@Owning @MustCall({"close", "read"}) Object o) { + @MustCallUnknown Object act = o; + // :: error: assignment.type.incompatible + @MustCall("close") Object file = o; + @MustCall({"close", "read"}) Object f2 = o; + // :: error: assignment.type.incompatible + @MustCall({}) Object notAfile = o; + // :: error: assignment.type.incompatible + unannotatedObj = o; + } + + void test_blank(@Owning @MustCall({}) Object o) { + @MustCallUnknown Object act = o; + @MustCall("close") Object file = o; + @MustCall({"close", "read"}) Object f2 = o; + @MustCall({}) Object notAfile = o; + unannotatedObj = o; + } + + void test_unannotated(@Owning Object o) { + @MustCallUnknown Object act = o; + @MustCall("close") Object file = o; + @MustCall({"close", "read"}) Object f2 = o; + @MustCall({}) Object notAfile = o; + unannotatedObj = o; + } +} diff --git a/checker/tests/mustcall/SystemInOut.java b/checker/tests/mustcall/SystemInOut.java new file mode 100644 index 000000000000..145c86a56e23 --- /dev/null +++ b/checker/tests/mustcall/SystemInOut.java @@ -0,0 +1,15 @@ +// A test that the checker doesn't ask you to close System.in, System.out, or System.err. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.util.Scanner; + +class SystemInOut { + void test() { + @MustCall({}) InputStream in = System.in; + @MustCall({}) OutputStream out = System.out; + @MustCall({}) OutputStream err = System.err; + @MustCall({}) Scanner sysIn = new Scanner(System.in); + } +} diff --git a/checker/tests/mustcall/ToStringOnSocket.java b/checker/tests/mustcall/ToStringOnSocket.java new file mode 100644 index 000000000000..3a93f9becc09 --- /dev/null +++ b/checker/tests/mustcall/ToStringOnSocket.java @@ -0,0 +1,18 @@ +// A test for a false positive I found in Zookeeper. Sockets are must-close, but the +// result of calling toString on them shouldn't be! + +import java.net.Socket; + +class ToStringOnSocket { + void log(String string) { + System.out.println(string); + } + + void test(Socket socket) { + log("bad socket: " + socket); + } + + void test2(Socket socket) { + log("bad socket: " + socket.toString()); + } +} diff --git a/checker/tests/mustcall/TryWithResourcesCrash.java b/checker/tests/mustcall/TryWithResourcesCrash.java new file mode 100644 index 000000000000..f9a52f434776 --- /dev/null +++ b/checker/tests/mustcall/TryWithResourcesCrash.java @@ -0,0 +1,31 @@ +// A test case for a crash while checking hfds. + +import java.io.Closeable; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +class TryWithResourcesCrash { + void test(FileSystem fs, byte[] bytes, String path) throws IOException { + try (FSDataOutputStream out = fs.createFile(path).overwrite(true).build()) { + out.write(bytes); + } + } + + class FSDataOutputStream extends DataOutputStream { + FSDataOutputStream(OutputStream os) { + super(os); + } + } + + abstract class FSDataOutputStreamBuilder< + S extends FSDataOutputStream, B extends FSDataOutputStreamBuilder> { + abstract S build(); + + abstract B overwrite(boolean b); + } + + abstract class FileSystem implements Closeable { + abstract FSDataOutputStreamBuilder createFile(String s); + } +} diff --git a/checker/tests/mustcall/TryWithResourcesSimple.java b/checker/tests/mustcall/TryWithResourcesSimple.java new file mode 100644 index 000000000000..d71be20c9531 --- /dev/null +++ b/checker/tests/mustcall/TryWithResourcesSimple.java @@ -0,0 +1,52 @@ +// A test that try-with-resources variables are always @MustCall({"close"}). + +import org.checkerframework.checker.mustcall.qual.MustCall; + +import java.io.*; +import java.net.*; + +public class TryWithResourcesSimple { + static void test(String address, int port) { + try (Socket socket = new Socket(address, port)) { + @MustCall({"close"}) Object s = socket; + } catch (Exception e) { + + } + } + + @SuppressWarnings("mustcall:annotations.on.use") + public static @MustCall({"close", "myMethod"}) Socket getFancySocket() { + return null; + } + + void test_fancy_sock(String address, int port) { + // This is illegal, because getFancySock()'s return type has another MC method beyond + // "close", + // which is the only MC method for Socket itself. + try (Socket socket = getFancySocket()) { + // :: error: (assignment.type.incompatible) + @MustCall({"close"}) Object s = socket; + } catch (Exception e) { + + } + } + + static void test_poly(String address, int port) { + try (Socket socket = new Socket(address, port)) { + // getChannel is @MustCallAlias (= poly) with the socket, so it should also be + // @MC({"close"}) + @MustCall({"close"}) Object s = socket.getChannel(); + } catch (Exception e) { + + } + } + + static void test_two_mca_variables(String address, int port) { + try (Socket socket = new Socket(address, port); + InputStream in = socket.getInputStream()) { + @MustCall({"close"}) Object s = in; + } catch (Exception e) { + + } + } +} diff --git a/checker/tests/mustcall/TypeArgs.java b/checker/tests/mustcall/TypeArgs.java new file mode 100644 index 000000000000..180001e919e6 --- /dev/null +++ b/checker/tests/mustcall/TypeArgs.java @@ -0,0 +1,91 @@ +import org.checkerframework.checker.mustcall.qual.MustCall; + +public class TypeArgs { + + static class A {} + + static class B extends A {} + + public void f1(Generic real, Generic other, boolean flag) { + f2(flag ? real : other); + } + + <@MustCall({"carly"}) Q extends @MustCall({"carly"}) Object> void f2( + Generic parm) {} + + interface Generic {} + + void m3( + @MustCall({}) Object a, + @MustCall({"foo"}) Object b, + @MustCall({"bar"}) Object c, + @MustCall({"foo", "bar"}) Object d) { + requireNothing1(a); + requireNothing2(a); + requireNothing1(b); + requireNothing2(b); + requireNothing1(c); + requireNothing2(c); + requireNothing1(d); + requireNothing2(d); + + requireFoo1(a); + requireFoo2(a); + requireFoo1(b); + requireFoo2(b); + requireFoo1(c); + requireFoo2(c); + requireFoo1(d); + requireFoo2(d); + + requireBar1(a); + requireBar2(a); + requireBar1(b); + requireBar2(b); + requireBar1(c); + requireBar2(c); + requireBar1(d); + requireBar2(d); + + requireFooBar1(a); + requireFooBar2(a); + requireFooBar1(b); + requireFooBar2(b); + requireFooBar1(c); + requireFooBar2(c); + requireFooBar1(d); + requireFooBar2(d); + } + + public static T requireNothing1(T obj) { + return obj; + } + + public static @MustCall({}) T requireNothing2(@MustCall({}) T obj) { + return obj; + } + + public static T requireFoo1(T obj) { + return obj; + } + + public static @MustCall({"foo"}) T requireFoo2(@MustCall({"foo"}) T obj) { + return obj; + } + + public static T requireBar1(T obj) { + return obj; + } + + public static @MustCall({"bar"}) T requireBar2(@MustCall({"bar"}) T obj) { + return obj; + } + + public static T requireFooBar1(T obj) { + return obj; + } + + public static @MustCall({"foo", "bar"}) T requireFooBar2(@MustCall({"foo", "bar"}) T obj) { + return obj; + } +} diff --git a/checker/tests/nullness-asserts/NonNullMapValue.java b/checker/tests/nullness-asserts/NonNullMapValue.java index 41721fb5a7b1..aa86bc5b8ce7 100644 --- a/checker/tests/nullness-asserts/NonNullMapValue.java +++ b/checker/tests/nullness-asserts/NonNullMapValue.java @@ -1,3 +1,8 @@ +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.KeyFor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + import java.io.PrintStream; import java.util.Date; import java.util.HashMap; @@ -6,10 +11,6 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; -import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; -import org.checkerframework.checker.nullness.qual.KeyFor; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; public class NonNullMapValue { @@ -20,26 +21,11 @@ public class NonNullMapValue { // explicitly because it is the annotation we are talking about.) // HashMap myMap; // - // However, the get method's declaration is misleading (in the context of - // the nullness type system), since it can always return null no matter - // whether the map values are non-null: + // However, the get method's declaration is misleading (in the context of the nullness type + // system), since it can always return null no matter whether the map values are non-null: // V get(Object key) { ... return null; } - // - // Here are potential solutions: - // * Forbid declaring values as non-null. This is the wrong approach. - // (It would also be hard to express syntactically.) - // * The checker could recognize a special new annotation on the return - // value of get, indicating that its return type isn't merely inferred - // from the generic type, but is always nullable. (This special new - // annotations could even be "@Nullable". A different annotation may - // be better, becase in general we would like to issue an error - // message when someone applies an annotation to a generic type - // parameter.) - // Additionally, to reduce the number of false positive warnings caused - // by the fact that get's return value is nullable: - // * Build a more specialized sophisticated flow analysis that checks - // that the passed key to Map.containsKey() is either checked against - // Map.containsKey() or Map.keySet(). + // The Nullness Checker does not use the signature as written. It has hard-coded rules for the + // get() method. It checks that the passed key to has type @KeyFor("theMap"). Map myMap; @@ -186,9 +172,9 @@ void testAnd(MyMap map, MyMap map2) { if (map.containsKey(KEY)) { map.get(KEY).toString(); } - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (map.containsKey(KEY2) && map.get(KEY2).toString() != null) {} - // :: error: (dereference.of.nullable) :: warning: (known.nonnull) + // :: error: (dereference.of.nullable) :: warning: (nulltest.redundant) if (map2.containsKey(KEY2) && map2.get(KEY2).toString() != null) {} } @@ -196,7 +182,7 @@ void testAndWithIllegalMapAnnotation(MyMap2 map) { if (map.containsKey(KEY)) { map.get(KEY).toString(); } - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (map.containsKey(KEY2) && map.get(KEY2).toString() != null) { // do nothing } @@ -204,11 +190,10 @@ void testAndWithIllegalMapAnnotation(MyMap2 map) { interface MyMap2 { @org.checkerframework.dataflow.qual.Pure - // This annotation is not legal on containsKey in general. - // If the Map is declared as (say) Map, - // then get returns a nullable value. We really want to say that if - // containsKey returns non-null, then get returns V rather than - // @Nullable V, but I don't know how to say that. + // This annotation is not legal on containsKey in general. If the Map is declared as (say) + // Map, then get returns a nullable value. We really want to say + // that if containsKey returns non-null, then get returns V rather than @Nullable V, but I + // don't know how to say that. @EnsuresNonNullIf(result = true, expression = "get(#1)") public abstract boolean containsKey(@Nullable Object a1); diff --git a/checker/tests/nullness-asserts/TestAssumeAssertionsAreEnabled.java b/checker/tests/nullness-asserts/TestAssumeAssertionsAreEnabled.java index 003f3a52caa6..3d639182b473 100644 --- a/checker/tests/nullness-asserts/TestAssumeAssertionsAreEnabled.java +++ b/checker/tests/nullness-asserts/TestAssumeAssertionsAreEnabled.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; -class TestAssumeAssertionsAreEnabled { +public class TestAssumeAssertionsAreEnabled { void foo(@Nullable String s1, @Nullable String s2) { // :: error: (dereference.of.nullable) diff --git a/checker/tests/nullness-assumeassertions/TestAssumeAssertionsAreDisabled.java b/checker/tests/nullness-assumeassertions/TestAssumeAssertionsAreDisabled.java index ae15c04e9b1c..37b2fb419d60 100644 --- a/checker/tests/nullness-assumeassertions/TestAssumeAssertionsAreDisabled.java +++ b/checker/tests/nullness-assumeassertions/TestAssumeAssertionsAreDisabled.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; -class TestAssumeAssertionsAreDisabled { +public class TestAssumeAssertionsAreDisabled { void foo(@Nullable String s1, @Nullable String s2) { diff --git a/checker/tests/nullness-assumeinitialized/AssumeInitTest.java b/checker/tests/nullness-assumeinitialized/AssumeInitTest.java new file mode 100644 index 000000000000..f6155ad083bb --- /dev/null +++ b/checker/tests/nullness-assumeinitialized/AssumeInitTest.java @@ -0,0 +1,29 @@ +import org.checkerframework.checker.initialization.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +public class AssumeInitTest { + + AssumeInitTest f; + + public AssumeInitTest(String arg) {} + + void test() { + @NonNull String s = "234"; + // :: error: (assignment.type.incompatible) + s = null; + } + + void test2(@UnknownInitialization @NonNull AssumeInitTest t) { + @Initialized @NonNull AssumeInitTest a = t.f; + } + + void simplestTestEver() { + @NonNull String a = "abc"; + + // :: error: (assignment.type.incompatible) + a = null; + + // :: error: (assignment.type.incompatible) + @NonNull String b = null; + } +} diff --git a/checker/tests/nullness-assumekeyfor/AssumeKeyForTest.java b/checker/tests/nullness-assumekeyfor/AssumeKeyForTest.java new file mode 100644 index 000000000000..3c5fa2911e9a --- /dev/null +++ b/checker/tests/nullness-assumekeyfor/AssumeKeyForTest.java @@ -0,0 +1,52 @@ +import org.checkerframework.checker.nullness.qual.KeyFor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class AssumeKeyForTest { + + void m1(Map m, String k) { + @NonNull Integer x = m.get(k); + } + + void m1b(HashMap m, String k) { + @NonNull Integer x = m.get(k); + } + + void m2(Map m, String k) { + @Nullable Integer x = m.get(k); + } + + void m3(Map m, String k) { + // :: error: (assignment.type.incompatible) + @NonNull Integer x = m.get(k); + } + + void m4(Map m, String k) { + @Nullable Integer x = m.get(k); + } + + void m5(Map m, @KeyFor("#1") String k) { + @NonNull Integer x = m.get(k); + } + + void m6(Map m, @KeyFor("#1") String k) { + @Nullable Integer x = m.get(k); + } + + void m7(Map m, @KeyFor("#1") String k) { + // :: error: (assignment.type.incompatible) + @NonNull Integer x = m.get(k); + } + + void m7b(HashMap m, @KeyFor("#1") String k) { + // :: error: (assignment.type.incompatible) + @NonNull Integer x = m.get(k); + } + + void m8(Map m, @KeyFor("#1") String k) { + @Nullable Integer x = m.get(k); + } +} diff --git a/checker/tests/nullness-checkcastelementtype/Issue1315.java b/checker/tests/nullness-checkcastelementtype/Issue1315.java index 3fb1afe1d52a..5c8b4ed68ab6 100644 --- a/checker/tests/nullness-checkcastelementtype/Issue1315.java +++ b/checker/tests/nullness-checkcastelementtype/Issue1315.java @@ -16,6 +16,7 @@ T test1(@Nullable Object p) { // :: warning: (cast.unsafe) return (T) p; } + // The Nullness Checker should not issue a cast.unsafe warning, // but the KeyFor Checker does, so suppress that warning. @SuppressWarnings({"unchecked", "keyfor:cast.unsafe"}) diff --git a/checker/tests/nullness-enclosingexpr/NullnessEnclosingExprTest.java b/checker/tests/nullness-enclosingexpr/NullnessEnclosingExprTest.java new file mode 100644 index 000000000000..d698e742b782 --- /dev/null +++ b/checker/tests/nullness-enclosingexpr/NullnessEnclosingExprTest.java @@ -0,0 +1,39 @@ +import org.checkerframework.checker.initialization.qual.Initialized; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; + +class NullnessEnclosingExprTest { + class InnerWithImplicitEnclosingExpression { + // There is no possible NPE and therefore no expected error. + InnerWithImplicitEnclosingExpression() { + NullnessEnclosingExprTest.this.f.hashCode(); + } + } + + class InnerWithInitializedEnclosingExpression { + // The default type of enclosing expression is same as InnerWithImplicitEnclosingExpression, + // we just make it explicit for testing. + InnerWithInitializedEnclosingExpression( + @Initialized NullnessEnclosingExprTest NullnessEnclosingExprTest.this) {} + } + + class InnerWithUnknownInitializationEnclosingExpression { + InnerWithUnknownInitializationEnclosingExpression( + @UnknownInitialization NullnessEnclosingExprTest NullnessEnclosingExprTest.this) { + // This should also never lead to an NPE, because that dereference should produce an + // type error. + // See Issue https://github.com/eisop/checker-framework/issues/412. + NullnessEnclosingExprTest.this.f.hashCode(); + } + } + + NullnessEnclosingExprTest() { + // :: error: (enclosingexpr.type.incompatible) + this.new InnerWithImplicitEnclosingExpression(); + // :: error: (enclosingexpr.type.incompatible) + this.new InnerWithInitializedEnclosingExpression(); + this.new InnerWithUnknownInitializationEnclosingExpression(); + f = "a"; + } + + Object f; +} diff --git a/checker/tests/nullness-extra/Bug109_B.java b/checker/tests/nullness-extra/Bug109_B.java index 3bb75bda3ca2..b4beca96922e 100644 --- a/checker/tests/nullness-extra/Bug109_B.java +++ b/checker/tests/nullness-extra/Bug109_B.java @@ -1,4 +1,4 @@ -class Bug109_B extends Bug109_A { +public class Bug109_B extends Bug109_A { public Bug109_B() { // Accessing field one causes NPE // at org.checkerframework.checker.nullness.MapGetHeuristics.handle diff --git a/checker/tests/nullness-extra/Makefile b/checker/tests/nullness-extra/Makefile index b599d8afa945..6f493a631f27 100644 --- a/checker/tests/nullness-extra/Makefile +++ b/checker/tests/nullness-extra/Makefile @@ -1,5 +1,5 @@ # Tests that are currently passing -PASSING_TESTS = Bug109 compat issue265 issue594 issue607 multiple-errors package-anno shorthand +PASSING_TESTS = Bug109 compat issue265 issue594 issue607 multiple-errors package-anno shorthand issue3597 issue5174 ifeq (,$(findstring 1.8,$(shell javac -version))) # issue309 and issue502 fail with Java 11 because of differences between Java 8 and Java 11 bytecode. # TODO: issue559 should work with an annotated jdk11. @@ -53,5 +53,11 @@ shorthand: issue607: $(MAKE) -C issue607 +issue3597: + $(MAKE) -C issue3597 + +issue5174: + $(MAKE) -C issue5174 + # All tests: passing and failing .PHONY: all skipped ${PASSING_TESTS} diff --git a/checker/tests/nullness-extra/compat/CompatTest.java b/checker/tests/nullness-extra/compat/CompatTest.java index 079c81b9becc..2241d42030d9 100644 --- a/checker/tests/nullness-extra/compat/CompatTest.java +++ b/checker/tests/nullness-extra/compat/CompatTest.java @@ -1,4 +1,5 @@ import lib.Lib; + import org.checkerframework.checker.nullness.qual.NonNull; public class CompatTest { diff --git a/checker/tests/nullness-extra/compat/Expected.txt b/checker/tests/nullness-extra/compat/Expected.txt index 18e2d419b8b1..7353ed619f2e 100644 --- a/checker/tests/nullness-extra/compat/Expected.txt +++ b/checker/tests/nullness-extra/compat/Expected.txt @@ -1,6 +1,6 @@ -CompatTest.java:6: error: [assignment.type.incompatible] incompatible types in assignment. +CompatTest.java:7: error: [assignment.type.incompatible] incompatible types in assignment. @NonNull Object o = Lib.maybeGetObject(); ^ - found : @Initialized @Nullable Object - required: @UnknownInitialization @NonNull Object + found : @Nullable Object + required: @NonNull Object 1 error diff --git a/checker/tests/nullness-extra/compat/Makefile b/checker/tests/nullness-extra/compat/Makefile index 59a34133ea92..d2dd485bfc30 100644 --- a/checker/tests/nullness-extra/compat/Makefile +++ b/checker/tests/nullness-extra/compat/Makefile @@ -2,5 +2,5 @@ all: rm -f Out.txt CompatTest.class lib/Lib.class javax/annotation/Nullable.class - $(JAVAC) -processor org.checkerframework.checker.nullness.NullnessChecker lib/Lib.java javax/annotation/Nullable.java CompatTest.java > Out.txt 2>&1 || true + -$(JAVAC) -processor org.checkerframework.checker.nullness.NullnessChecker lib/Lib.java javax/annotation/Nullable.java CompatTest.java > Out.txt 2>&1 diff -u Expected.txt Out.txt diff --git a/checker/tests/nullness-extra/issue265/Delta.java b/checker/tests/nullness-extra/issue265/Delta.java index b62a66c0f119..f8b67139c700 100644 --- a/checker/tests/nullness-extra/issue265/Delta.java +++ b/checker/tests/nullness-extra/issue265/Delta.java @@ -1,6 +1,6 @@ import java.util.List; -class Delta { +public class Delta { List field; Delta(List field) { diff --git a/checker/tests/nullness-extra/issue309/Issue309.java b/checker/tests/nullness-extra/issue309/Issue309.java index 9975d15d164c..2c3a1029d3de 100644 --- a/checker/tests/nullness-extra/issue309/Issue309.java +++ b/checker/tests/nullness-extra/issue309/Issue309.java @@ -1,6 +1,6 @@ import lib.Lib; -class Issue309 { +public class Issue309 { void bar() { Lib.foo(); } diff --git a/checker/tests/nullness-extra/issue348/Issue348.java b/checker/tests/nullness-extra/issue348/Issue348.java index 4969b63d1d90..f7dd6b828233 100644 --- a/checker/tests/nullness-extra/issue348/Issue348.java +++ b/checker/tests/nullness-extra/issue348/Issue348.java @@ -1,6 +1,6 @@ import lib.Lib; -class Issue348 { +public class Issue348 { void test() { Lib lib = new Lib(); diff --git a/checker/tests/nullness-extra/issue3597/Makefile b/checker/tests/nullness-extra/issue3597/Makefile new file mode 100644 index 000000000000..a25e91b8725a --- /dev/null +++ b/checker/tests/nullness-extra/issue3597/Makefile @@ -0,0 +1,8 @@ +.PHONY: all + +all: clean + $(JAVAC) testpkg/Issue3597B.java + $(JAVAC) -Astubs=issue3597.astub -processor Nullness -sourcepath : -cp . testpkg/Issue3597A.java + +clean: + rm -f testpkg/*.class diff --git a/checker/tests/nullness-extra/issue3597/issue3597.astub b/checker/tests/nullness-extra/issue3597/issue3597.astub new file mode 100644 index 000000000000..d10cf642f52e --- /dev/null +++ b/checker/tests/nullness-extra/issue3597/issue3597.astub @@ -0,0 +1,7 @@ +package testpkg; + +import org.checkerframework.checker.nullness.qual.Nullable; + +class Issue3597B { + Object f(); +} diff --git a/checker/tests/nullness-extra/issue3597/testpkg/Issue3597A.java b/checker/tests/nullness-extra/issue3597/testpkg/Issue3597A.java new file mode 100644 index 000000000000..0a9be45d12ca --- /dev/null +++ b/checker/tests/nullness-extra/issue3597/testpkg/Issue3597A.java @@ -0,0 +1,7 @@ +package testpkg; + +public class Issue3597A { + void f() { + System.err.println(new Issue3597B().f().toString()); + } +} diff --git a/checker/tests/nullness-extra/issue3597/testpkg/Issue3597B.java b/checker/tests/nullness-extra/issue3597/testpkg/Issue3597B.java new file mode 100644 index 000000000000..b365430b09f2 --- /dev/null +++ b/checker/tests/nullness-extra/issue3597/testpkg/Issue3597B.java @@ -0,0 +1,9 @@ +package testpkg; + +import org.checkerframework.checker.nullness.qual.Nullable; + +public class Issue3597B { + @Nullable Object f() { + return new Object(); + } +} diff --git a/checker/tests/nullness-extra/issue502/Issue502.java b/checker/tests/nullness-extra/issue502/Issue502.java index 21576cc30fb1..a39f3ff37499 100644 --- a/checker/tests/nullness-extra/issue502/Issue502.java +++ b/checker/tests/nullness-extra/issue502/Issue502.java @@ -1,7 +1,7 @@ // Test case for Issue 502: // https://github.com/typetools/checker-framework/issues/502 -class Issue502 { +public class Issue502 { @Override public String toString() { return ""; diff --git a/checker/tests/nullness-extra/issue5174/Issue5174.java b/checker/tests/nullness-extra/issue5174/Issue5174.java new file mode 100644 index 000000000000..a234c2bb7604 --- /dev/null +++ b/checker/tests/nullness-extra/issue5174/Issue5174.java @@ -0,0 +1,69 @@ +// Test case for Issue 5174: +// https://github.com/typetools/checker-framework/issues/5174 + +class Issue5174Super { + S methodInner(S in) { + return in; + } + + S f; + static Object sf = ""; + + Issue5174Super(S f) { + this.f = f; + } +} + +class Issue5174Sub extends Issue5174Super { + Issue5174Sub(T f) { + super(f); + } + + void accMethImpl(T in) { + Object o = methodInner(in); + } + + void accMethExpl(T in) { + Object o = this.methodInner(in); + } + + void accFieldImpl() { + Object o = f; + } + + void accFieldExpl() { + Object o = this.f; + } + + void accStaticField() { + Object o; + o = sf; + o = Issue5174Sub.sf; + o = Issue5174Super.sf; + } + + class SubNested { + void nestedaccMethImpl(T in) { + Object o = methodInner(in); + } + + void nestedaccMethExpl(T in) { + Object o = Issue5174Sub.this.methodInner(in); + } + + void nestedaccFieldImpl() { + Object o = f; + } + + void nestedaccFieldExpl() { + Object o = Issue5174Sub.this.f; + } + + void nestedaccStaticField() { + Object o; + o = sf; + o = Issue5174Sub.sf; + o = Issue5174Super.sf; + } + } +} diff --git a/checker/tests/nullness-extra/issue5174/Issue5174.out b/checker/tests/nullness-extra/issue5174/Issue5174.out new file mode 100644 index 000000000000..d4144a111ab4 --- /dev/null +++ b/checker/tests/nullness-extra/issue5174/Issue5174.out @@ -0,0 +1,1702 @@ +2 -> 3 +3 -> 0 + +2: +Before: InitializationStore#3( + initialized fields = []) +~~~~~~~~~ + + +3: +Before: InitializationStore#3( + initialized fields = []) +~~~~~~~~~ +"" [ StringLiteral ] > CFAV{@Initialized, String} +(this).sf = "" [ Assignment ] > CFAV{@Initialized, String} + +0: +Before: InitializationStore#4( + Issue5174Super.sf > CFAV{@Initialized, String} + initialized fields = [sf]) +~~~~~~~~~ + +7 -> 8 +8 -> 5 + +7: +Before: InitializationStore#8( + in > CFAV{, S} + Issue5174Super.sf > CFAV{@Initialized, Object} + this.f > CFAV{, S} + initialized fields = [f, sf]) +~~~~~~~~~ + + +8: +Before: InitializationStore#8( + in > CFAV{, S} + Issue5174Super.sf > CFAV{@Initialized, Object} + this.f > CFAV{, S} + initialized fields = [f, sf]) +~~~~~~~~~ +in [ LocalVariable ] > CFAV{, S} +return in [ Return ] + +5: +Before: InitializationStore#9( + in > CFAV{, S} + Issue5174Super.sf > CFAV{@Initialized, Object} + this.f > CFAV{, S} + initialized fields = [f, sf]) +~~~~~~~~~ + +13 -> 14 +14 -> 15 +15 -> 16 +15 -> 12 +15 -> 12 +16 -> 11 + +13: +Before: InitializationStore#13( + f > CFAV{, S} + Issue5174Super.sf > CFAV{@Initialized, Object} + initialized fields = [sf]) +~~~~~~~~~ + + +14: +Before: InitializationStore#13( + f > CFAV{, S} + Issue5174Super.sf > CFAV{@Initialized, Object} + initialized fields = [sf]) +~~~~~~~~~ +(this) [ ImplicitThis ] +(this). [ MethodAccess ] > CFAV{@UnderInitialization, Issue5174Super} + +15: +Before: InitializationStore#14( + f > CFAV{, S} + Issue5174Super.sf > CFAV{@Initialized, Object} + initialized fields = [sf]) +~~~~~~~~~ +(this).() [ MethodInvocation ] > CFAV{@UnderInitialization, Object} + +16: +Before: InitializationStore#15( + f > CFAV{, S} + Issue5174Super.sf > CFAV{@Initialized, Object} + this.() > CFAV{@UnderInitialization, Object} + initialized fields = [sf]) +~~~~~~~~~ +expression statement super() [ ExpressionStatement ] +this [ ExplicitThis ] > CFAV{@UnderInitialization, Issue5174Super} +f [ LocalVariable ] > CFAV{, S} +this.f [ FieldAccess ] +this.f = f [ Assignment ] > CFAV{, S} +expression statement this.f = f [ ExpressionStatement ] + +12: +Before: InitializationStore#16( + f > CFAV{, S} + Issue5174Super.sf > CFAV{@Initialized, Object} + initialized fields = [sf]) +~~~~~~~~~ + + +11: +Before: InitializationStore#19( + f > CFAV{, S} + Issue5174Super.sf > CFAV{@Initialized, Object} + this.f > CFAV{, S} + initialized fields = [f, sf]) +~~~~~~~~~ + +2 -> 3 +3 -> 0 + +2: +Before: NullnessNoInitStore#27( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +3: +Before: NullnessNoInitStore#27( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +"" [ StringLiteral ] > NV{@NonNull, String, poly nn/n=f/f} +(this).sf = "" [ Assignment ] > NV{@NonNull, String, poly nn/n=f/f} + +0: +Before: NullnessNoInitStore#28( + Issue5174Super.sf > NV{@NonNull, String, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + +7 -> 8 +8 -> 5 + +7: +Before: NullnessNoInitStore#32( + in > NV{, S, poly nn/n=f/f} + Issue5174Super.sf > NV{@NonNull, Object, poly nn/n=f/f} + this.f > NV{, S, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +8: +Before: NullnessNoInitStore#32( + in > NV{, S, poly nn/n=f/f} + Issue5174Super.sf > NV{@NonNull, Object, poly nn/n=f/f} + this.f > NV{, S, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +in [ LocalVariable ] > NV{, S, poly nn/n=f/f} +return in [ Return ] > NV{@NonNull, boolean, poly nn/n=f/f} + +5: +Before: NullnessNoInitStore#33( + in > NV{, S, poly nn/n=f/f} + Issue5174Super.sf > NV{@NonNull, Object, poly nn/n=f/f} + this.f > NV{, S, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + +13 -> 14 +14 -> 15 +15 -> 16 +15 -> 12 +15 -> 12 +16 -> 11 + +13: +Before: NullnessNoInitStore#37( + f > NV{, S, poly nn/n=f/f} + Issue5174Super.sf > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +14: +Before: NullnessNoInitStore#37( + f > NV{, S, poly nn/n=f/f} + Issue5174Super.sf > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +(this) [ ImplicitThis ] +(this). [ MethodAccess ] > NV{@NonNull, Issue5174Super, poly nn/n=f/f} + +15: +Before: NullnessNoInitStore#38( + f > NV{, S, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Super, poly nn/n=f/f} + Issue5174Super.sf > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +(this).() [ MethodInvocation ] > NV{@NonNull, Object, poly nn/n=f/f} + +16: +Before: NullnessNoInitStore#39( + f > NV{, S, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Super, poly nn/n=f/f} + Issue5174Super.sf > NV{@NonNull, Object, poly nn/n=f/f} + this.() > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +expression statement super() [ ExpressionStatement ] +this [ ExplicitThis ] > NV{@NonNull, Issue5174Super, poly nn/n=f/f} +f [ LocalVariable ] > NV{, S, poly nn/n=f/f} +this.f [ FieldAccess ] +this.f = f [ Assignment ] > NV{, S, poly nn/n=f/f} +expression statement this.f = f [ ExpressionStatement ] + +12: +Before: NullnessNoInitStore#40( + f > NV{, S, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Super, poly nn/n=f/f} + Issue5174Super.sf > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +11: +Before: NullnessNoInitStore#43( + f > NV{, S, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Super, poly nn/n=f/f} + Issue5174Super.sf > NV{@NonNull, Object, poly nn/n=f/f} + this.f > NV{, S, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + +20 -> 21 +21 -> 22 +22 -> 23 +22 -> 19 +22 -> 19 +23 -> 18 + +20: +Before: InitializationStore#53( + f > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + + +21: +Before: InitializationStore#53( + f > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +(this) [ ImplicitThis ] +(this). [ MethodAccess ] > CFAV{@UnderInitialization(Issue5174Sub.class), Issue5174Sub} +f [ LocalVariable ] > CFAV{, T} + +22: +Before: InitializationStore#54( + f > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +(this).(f) [ MethodInvocation ] > CFAV{@UnderInitialization, Issue5174Super} + +23: +Before: InitializationStore#55( + f > CFAV{, T} + initialized fields = [f, sf]) +~~~~~~~~~ +expression statement super(f) [ ExpressionStatement ] + +19: +Before: InitializationStore#58( + f > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + + +18: +Before: InitializationStore#61( + f > CFAV{, T} + initialized fields = [f, sf]) +~~~~~~~~~ + +27 -> 28 +28 -> 29 +29 -> 30 +29 -> 26 +29 -> 26 +30 -> 25 + +27: +Before: InitializationStore#68( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + + +28: +Before: InitializationStore#68( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +o [ VariableDeclaration ] +(this) [ ImplicitThis ] +(this).methodInner [ MethodAccess ] +in [ LocalVariable ] > CFAV{, T} + +29: +Before: InitializationStore#69( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +(this).methodInner(in) [ MethodInvocation ] > CFAV{, T} + +30: +Before: InitializationStore#70( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +o = (this).methodInner(in) [ Assignment ] > CFAV{, T} + +26: +Before: InitializationStore#71( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + + +25: +Before: InitializationStore#74( + in > CFAV{, T} + o > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + +34 -> 35 +35 -> 36 +36 -> 37 +36 -> 33 +36 -> 33 +37 -> 32 + +34: +Before: InitializationStore#81( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + + +35: +Before: InitializationStore#81( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +o [ VariableDeclaration ] +this [ ExplicitThis ] > CFAV{@Initialized, Issue5174Sub} +this.methodInner [ MethodAccess ] +in [ LocalVariable ] > CFAV{, T} + +36: +Before: InitializationStore#82( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +this.methodInner(in) [ MethodInvocation ] > CFAV{, T} + +37: +Before: InitializationStore#83( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +o = this.methodInner(in) [ Assignment ] > CFAV{, T} + +33: +Before: InitializationStore#84( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + + +32: +Before: InitializationStore#87( + in > CFAV{, T} + o > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + +41 -> 42 +42 -> 39 + +41: +Before: InitializationStore#94( + initialized fields = []) +~~~~~~~~~ + + +42: +Before: InitializationStore#94( + initialized fields = []) +~~~~~~~~~ +o [ VariableDeclaration ] +(this) [ ImplicitThis ] +(this).f [ FieldAccess ] > CFAV{, T} +o = (this).f [ Assignment ] > CFAV{, T} + +39: +Before: InitializationStore#95( + o > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + +46 -> 47 +47 -> 44 + +46: +Before: InitializationStore#99( + initialized fields = []) +~~~~~~~~~ + + +47: +Before: InitializationStore#99( + initialized fields = []) +~~~~~~~~~ +o [ VariableDeclaration ] +this [ ExplicitThis ] > CFAV{@Initialized, Issue5174Sub} +this.f [ FieldAccess ] > CFAV{, T} +o = this.f [ Assignment ] > CFAV{, T} + +44: +Before: InitializationStore#100( + o > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + +51 -> 52 +52 -> 53 +53 -> 54 +53 -> 50 +53 -> 50 +53 -> 50 +53 -> 50 +54 -> 55 +55 -> 56 +55 -> 50 +55 -> 50 +55 -> 50 +55 -> 50 +56 -> 57 +57 -> 58 +57 -> 50 +57 -> 50 +57 -> 50 +57 -> 50 +58 -> 49 + +51: +Before: InitializationStore#104( + initialized fields = []) +~~~~~~~~~ + + +52: +Before: InitializationStore#104( + initialized fields = []) +~~~~~~~~~ +o [ VariableDeclaration ] +o [ LocalVariable ] + +53: +Before: InitializationStore#105( + initialized fields = []) +~~~~~~~~~ +Issue5174Super [ ClassName ] + +54: +Before: InitializationStore#106( + initialized fields = []) +~~~~~~~~~ +Issue5174Super.sf [ FieldAccess ] > CFAV{@Initialized, Object} +o = Issue5174Super.sf [ Assignment ] > CFAV{@Initialized, Object} +expression statement o = sf [ ExpressionStatement ] +o [ LocalVariable ] + +50: +Before: InitializationStore#107( + initialized fields = []) +~~~~~~~~~ + + +55: +Before: InitializationStore#114( + o > CFAV{@Initialized, Object} + initialized fields = []) +~~~~~~~~~ +Issue5174Sub [ ClassName ] > CFAV{@Initialized, Issue5174Sub} + +56: +Before: InitializationStore#115( + o > CFAV{@Initialized, Object} + initialized fields = []) +~~~~~~~~~ +Issue5174Sub.sf [ FieldAccess ] > CFAV{@Initialized, Object} +o = Issue5174Sub.sf [ Assignment ] > CFAV{@Initialized, Object} +expression statement o = Issue5174Sub.sf [ ExpressionStatement ] +o [ LocalVariable ] + +57: +Before: InitializationStore#124( + o > CFAV{@Initialized, Object} + initialized fields = []) +~~~~~~~~~ +Issue5174Super [ ClassName ] > CFAV{@Initialized, Issue5174Super} + +58: +Before: InitializationStore#125( + o > CFAV{@Initialized, Object} + initialized fields = []) +~~~~~~~~~ +Issue5174Super.sf [ FieldAccess ] > CFAV{@Initialized, Object} +o = Issue5174Super.sf [ Assignment ] > CFAV{@Initialized, Object} +expression statement o = Issue5174Super.sf [ ExpressionStatement ] + +49: +Before: InitializationStore#134( + o > CFAV{@Initialized, Object} + initialized fields = []) +~~~~~~~~~ + +62 -> 63 +63 -> 64 +64 -> 65 +64 -> 61 +64 -> 61 +65 -> 60 + +62: +Before: InitializationStore#145( + initialized fields = []) +~~~~~~~~~ + + +63: +Before: InitializationStore#145( + initialized fields = []) +~~~~~~~~~ +(this) [ ImplicitThis ] +(this). [ MethodAccess ] > CFAV{@UnderInitialization(Issue5174Sub.SubNested.class), SubNested} + +64: +Before: InitializationStore#146( + initialized fields = []) +~~~~~~~~~ +(this).() [ MethodInvocation ] > CFAV{@UnderInitialization, Object} + +65: +Before: InitializationStore#147( + this.() > CFAV{@UnderInitialization, Object} + initialized fields = []) +~~~~~~~~~ +expression statement super() [ ExpressionStatement ] + +61: +Before: InitializationStore#148( + initialized fields = []) +~~~~~~~~~ + + +60: +Before: InitializationStore#151( + this.() > CFAV{@UnderInitialization, Object} + initialized fields = []) +~~~~~~~~~ + +69 -> 70 +70 -> 71 +71 -> 72 +71 -> 68 +71 -> 68 +72 -> 67 + +69: +Before: InitializationStore#158( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + + +70: +Before: InitializationStore#158( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +o [ VariableDeclaration ] +(this) [ ImplicitThis ] +(this).methodInner [ MethodAccess ] +in [ LocalVariable ] > CFAV{, T} + +71: +Before: InitializationStore#159( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +(this).methodInner(in) [ MethodInvocation ] > CFAV{, T} + +72: +Before: InitializationStore#160( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +o = (this).methodInner(in) [ Assignment ] > CFAV{, T} + +68: +Before: InitializationStore#161( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + + +67: +Before: InitializationStore#164( + in > CFAV{, T} + o > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + +76 -> 77 +77 -> 78 +78 -> 80 +78 -> 75 +78 -> 75 +78 -> 75 +78 -> 75 +80 -> 82 +80 -> 75 +82 -> 83 +82 -> 75 +83 -> 84 +84 -> 85 +84 -> 75 +84 -> 75 +85 -> 74 + +76: +Before: InitializationStore#171( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + + +77: +Before: InitializationStore#171( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +o [ VariableDeclaration ] + +78: +Before: InitializationStore#172( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +Issue5174Sub [ ClassName ] > CFAV{@Initialized, Issue5174Sub} + +80: +Before: InitializationStore#173( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +Issue5174Sub.this [ FieldAccess ] > CFAV{@Initialized, Issue5174Sub} + +75: +Before: InitializationStore#174( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + + +82: +Before: InitializationStore#181( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +Issue5174Sub.this.methodInner [ MethodAccess ] + +83: +Before: InitializationStore#184( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +in [ LocalVariable ] > CFAV{, T} + +84: +Before: InitializationStore#187( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +Issue5174Sub.this.methodInner(in) [ MethodInvocation ] > CFAV{, T} + +85: +Before: InitializationStore#188( + in > CFAV{, T} + initialized fields = []) +~~~~~~~~~ +o = Issue5174Sub.this.methodInner(in) [ Assignment ] > CFAV{, T} + +74: +Before: InitializationStore#193( + in > CFAV{, T} + o > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + +89 -> 90 +90 -> 87 + +89: +Before: InitializationStore#204( + initialized fields = []) +~~~~~~~~~ + + +90: +Before: InitializationStore#204( + initialized fields = []) +~~~~~~~~~ +o [ VariableDeclaration ] +(this) [ ImplicitThis ] +(this).f [ FieldAccess ] > CFAV{, T} +o = (this).f [ Assignment ] > CFAV{, T} + +87: +Before: InitializationStore#205( + o > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + +94 -> 95 +95 -> 96 +96 -> 98 +96 -> 93 +96 -> 93 +96 -> 93 +96 -> 93 +98 -> 100 +98 -> 93 +100 -> 101 +100 -> 93 +101 -> 92 + +94: +Before: InitializationStore#209( + initialized fields = []) +~~~~~~~~~ + + +95: +Before: InitializationStore#209( + initialized fields = []) +~~~~~~~~~ +o [ VariableDeclaration ] + +96: +Before: InitializationStore#210( + initialized fields = []) +~~~~~~~~~ +Issue5174Sub [ ClassName ] > CFAV{@Initialized, Issue5174Sub} + +98: +Before: InitializationStore#211( + initialized fields = []) +~~~~~~~~~ +Issue5174Sub.this [ FieldAccess ] > CFAV{@Initialized, Issue5174Sub} + +93: +Before: InitializationStore#212( + initialized fields = []) +~~~~~~~~~ + + +100: +Before: InitializationStore#219( + initialized fields = []) +~~~~~~~~~ +Issue5174Sub.this.f [ FieldAccess ] > CFAV{, T} + +101: +Before: InitializationStore#222( + initialized fields = []) +~~~~~~~~~ +o = Issue5174Sub.this.f [ Assignment ] > CFAV{, T} + +92: +Before: InitializationStore#225( + o > CFAV{, T} + initialized fields = []) +~~~~~~~~~ + +105 -> 106 +106 -> 107 +107 -> 108 +107 -> 104 +107 -> 104 +107 -> 104 +107 -> 104 +108 -> 109 +109 -> 110 +109 -> 104 +109 -> 104 +109 -> 104 +109 -> 104 +110 -> 111 +111 -> 112 +111 -> 104 +111 -> 104 +111 -> 104 +111 -> 104 +112 -> 103 + +105: +Before: InitializationStore#234( + initialized fields = []) +~~~~~~~~~ + + +106: +Before: InitializationStore#234( + initialized fields = []) +~~~~~~~~~ +o [ VariableDeclaration ] +o [ LocalVariable ] + +107: +Before: InitializationStore#235( + initialized fields = []) +~~~~~~~~~ +Issue5174Super [ ClassName ] + +108: +Before: InitializationStore#236( + initialized fields = []) +~~~~~~~~~ +Issue5174Super.sf [ FieldAccess ] > CFAV{@Initialized, Object} +o = Issue5174Super.sf [ Assignment ] > CFAV{@Initialized, Object} +expression statement o = sf [ ExpressionStatement ] +o [ LocalVariable ] + +104: +Before: InitializationStore#237( + initialized fields = []) +~~~~~~~~~ + + +109: +Before: InitializationStore#244( + o > CFAV{@Initialized, Object} + initialized fields = []) +~~~~~~~~~ +Issue5174Sub [ ClassName ] > CFAV{@Initialized, Issue5174Sub} + +110: +Before: InitializationStore#245( + o > CFAV{@Initialized, Object} + initialized fields = []) +~~~~~~~~~ +Issue5174Sub.sf [ FieldAccess ] > CFAV{@Initialized, Object} +o = Issue5174Sub.sf [ Assignment ] > CFAV{@Initialized, Object} +expression statement o = Issue5174Sub.sf [ ExpressionStatement ] +o [ LocalVariable ] + +111: +Before: InitializationStore#254( + o > CFAV{@Initialized, Object} + initialized fields = []) +~~~~~~~~~ +Issue5174Super [ ClassName ] > CFAV{@Initialized, Issue5174Super} + +112: +Before: InitializationStore#255( + o > CFAV{@Initialized, Object} + initialized fields = []) +~~~~~~~~~ +Issue5174Super.sf [ FieldAccess ] > CFAV{@Initialized, Object} +o = Issue5174Super.sf [ Assignment ] > CFAV{@Initialized, Object} +expression statement o = Issue5174Super.sf [ ExpressionStatement ] + +103: +Before: InitializationStore#264( + o > CFAV{@Initialized, Object} + initialized fields = []) +~~~~~~~~~ + +20 -> 21 +21 -> 22 +22 -> 23 +22 -> 19 +22 -> 19 +23 -> 18 + +20: +Before: NullnessNoInitStore#275( + f > NV{, T, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +21: +Before: NullnessNoInitStore#275( + f > NV{, T, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +(this) [ ImplicitThis ] +(this). [ MethodAccess ] > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} +f [ LocalVariable ] > NV{, T, poly nn/n=f/f} + +22: +Before: NullnessNoInitStore#276( + f > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +(this).(f) [ MethodInvocation ] > NV{@NonNull, Issue5174Super, poly nn/n=f/f} + +23: +Before: NullnessNoInitStore#277( + f > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +expression statement super(f) [ ExpressionStatement ] + +19: +Before: NullnessNoInitStore#278( + f > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +18: +Before: NullnessNoInitStore#281( + f > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + +27 -> 28 +28 -> 29 +29 -> 30 +29 -> 26 +29 -> 26 +30 -> 25 + +27: +Before: NullnessNoInitStore#288( + in > NV{, T, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +28: +Before: NullnessNoInitStore#288( + in > NV{, T, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o [ VariableDeclaration ] +(this) [ ImplicitThis ] +(this).methodInner [ MethodAccess ] +in [ LocalVariable ] > NV{, T, poly nn/n=f/f} + +29: +Before: NullnessNoInitStore#289( + in > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +(this).methodInner(in) [ MethodInvocation ] > NV{, T, poly nn/n=f/f} + +30: +Before: NullnessNoInitStore#290( + in > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o = (this).methodInner(in) [ Assignment ] > NV{, T, poly nn/n=f/f} + +26: +Before: NullnessNoInitStore#291( + in > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +25: +Before: NullnessNoInitStore#294( + in > NV{, T, poly nn/n=f/f} + o > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + +34 -> 35 +35 -> 36 +36 -> 37 +36 -> 33 +36 -> 33 +37 -> 32 + +34: +Before: NullnessNoInitStore#301( + in > NV{, T, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +35: +Before: NullnessNoInitStore#301( + in > NV{, T, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o [ VariableDeclaration ] +this [ ExplicitThis ] > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} +this.methodInner [ MethodAccess ] +in [ LocalVariable ] > NV{, T, poly nn/n=f/f} + +36: +Before: NullnessNoInitStore#302( + in > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +this.methodInner(in) [ MethodInvocation ] > NV{, T, poly nn/n=f/f} + +37: +Before: NullnessNoInitStore#303( + in > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o = this.methodInner(in) [ Assignment ] > NV{, T, poly nn/n=f/f} + +33: +Before: NullnessNoInitStore#304( + in > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +32: +Before: NullnessNoInitStore#307( + in > NV{, T, poly nn/n=f/f} + o > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + +41 -> 42 +42 -> 39 + +41: +Before: NullnessNoInitStore#314( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +42: +Before: NullnessNoInitStore#314( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o [ VariableDeclaration ] +(this) [ ImplicitThis ] +(this).f [ FieldAccess ] > NV{, T, poly nn/n=f/f} +o = (this).f [ Assignment ] > NV{, T, poly nn/n=f/f} + +39: +Before: NullnessNoInitStore#315( + o > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + +46 -> 47 +47 -> 44 + +46: +Before: NullnessNoInitStore#319( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +47: +Before: NullnessNoInitStore#319( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o [ VariableDeclaration ] +this [ ExplicitThis ] > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} +this.f [ FieldAccess ] > NV{, T, poly nn/n=f/f} +o = this.f [ Assignment ] > NV{, T, poly nn/n=f/f} + +44: +Before: NullnessNoInitStore#320( + o > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + +51 -> 52 +52 -> 53 +53 -> 54 +53 -> 50 +53 -> 50 +53 -> 50 +53 -> 50 +54 -> 55 +55 -> 56 +55 -> 50 +55 -> 50 +55 -> 50 +55 -> 50 +56 -> 57 +57 -> 58 +57 -> 50 +57 -> 50 +57 -> 50 +57 -> 50 +58 -> 49 + +51: +Before: NullnessNoInitStore#324( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +52: +Before: NullnessNoInitStore#324( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o [ VariableDeclaration ] +o [ LocalVariable ] + +53: +Before: NullnessNoInitStore#325( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Super [ ClassName ] + +54: +Before: NullnessNoInitStore#326( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Super.sf [ FieldAccess ] > NV{@NonNull, Object, poly nn/n=f/f} +o = Issue5174Super.sf [ Assignment ] > NV{@NonNull, Object, poly nn/n=f/f} +expression statement o = sf [ ExpressionStatement ] +o [ LocalVariable ] + +50: +Before: NullnessNoInitStore#327( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +55: +Before: NullnessNoInitStore#334( + o > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Sub [ ClassName ] > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + +56: +Before: NullnessNoInitStore#335( + o > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Sub.sf [ FieldAccess ] > NV{@NonNull, Object, poly nn/n=f/f} +o = Issue5174Sub.sf [ Assignment ] > NV{@NonNull, Object, poly nn/n=f/f} +expression statement o = Issue5174Sub.sf [ ExpressionStatement ] +o [ LocalVariable ] + +57: +Before: NullnessNoInitStore#344( + o > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Super [ ClassName ] > NV{@NonNull, Issue5174Super, poly nn/n=f/f} + +58: +Before: NullnessNoInitStore#345( + o > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Super.sf [ FieldAccess ] > NV{@NonNull, Object, poly nn/n=f/f} +o = Issue5174Super.sf [ Assignment ] > NV{@NonNull, Object, poly nn/n=f/f} +expression statement o = Issue5174Super.sf [ ExpressionStatement ] + +49: +Before: NullnessNoInitStore#354( + o > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + +62 -> 63 +63 -> 64 +64 -> 65 +64 -> 61 +64 -> 61 +65 -> 60 + +62: +Before: NullnessNoInitStore#365( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +63: +Before: NullnessNoInitStore#365( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +(this) [ ImplicitThis ] +(this). [ MethodAccess ] > NV{@NonNull, SubNested, poly nn/n=f/f} + +64: +Before: NullnessNoInitStore#366( + this > NV{@NonNull, SubNested, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +(this).() [ MethodInvocation ] > NV{@NonNull, Object, poly nn/n=f/f} + +65: +Before: NullnessNoInitStore#367( + this > NV{@NonNull, SubNested, poly nn/n=f/f} + this.() > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +expression statement super() [ ExpressionStatement ] + +61: +Before: NullnessNoInitStore#368( + this > NV{@NonNull, SubNested, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +60: +Before: NullnessNoInitStore#371( + this > NV{@NonNull, SubNested, poly nn/n=f/f} + this.() > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + +69 -> 70 +70 -> 71 +71 -> 72 +71 -> 68 +71 -> 68 +72 -> 67 + +69: +Before: NullnessNoInitStore#378( + in > NV{, T, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +70: +Before: NullnessNoInitStore#378( + in > NV{, T, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o [ VariableDeclaration ] +(this) [ ImplicitThis ] +(this).methodInner [ MethodAccess ] +in [ LocalVariable ] > NV{, T, poly nn/n=f/f} + +71: +Before: NullnessNoInitStore#379( + in > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +(this).methodInner(in) [ MethodInvocation ] > NV{, T, poly nn/n=f/f} + +72: +Before: NullnessNoInitStore#380( + in > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o = (this).methodInner(in) [ Assignment ] > NV{, T, poly nn/n=f/f} + +68: +Before: NullnessNoInitStore#381( + in > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +67: +Before: NullnessNoInitStore#384( + in > NV{, T, poly nn/n=f/f} + o > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + +76 -> 77 +77 -> 78 +78 -> 80 +78 -> 75 +78 -> 75 +78 -> 75 +78 -> 75 +80 -> 82 +80 -> 75 +82 -> 83 +82 -> 75 +83 -> 84 +84 -> 85 +84 -> 75 +84 -> 75 +85 -> 74 + +76: +Before: NullnessNoInitStore#391( + in > NV{, T, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +77: +Before: NullnessNoInitStore#391( + in > NV{, T, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o [ VariableDeclaration ] + +78: +Before: NullnessNoInitStore#392( + in > NV{, T, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Sub [ ClassName ] > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + +80: +Before: NullnessNoInitStore#393( + in > NV{, T, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Sub.this [ FieldAccess ] > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + +75: +Before: NullnessNoInitStore#410( + in > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +82: +Before: NullnessNoInitStore#401( + in > NV{, T, poly nn/n=f/f} + Issue5174Sub.class > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Sub.this.methodInner [ MethodAccess ] + +83: +Before: NullnessNoInitStore#404( + in > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + Issue5174Sub.class > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +in [ LocalVariable ] > NV{, T, poly nn/n=f/f} + +84: +Before: NullnessNoInitStore#407( + in > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + Issue5174Sub.class > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Sub.this.methodInner(in) [ MethodInvocation ] > NV{, T, poly nn/n=f/f} + +85: +Before: NullnessNoInitStore#408( + in > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + Issue5174Sub.class > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o = Issue5174Sub.this.methodInner(in) [ Assignment ] > NV{, T, poly nn/n=f/f} + +74: +Before: NullnessNoInitStore#413( + in > NV{, T, poly nn/n=f/f} + o > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + Issue5174Sub.class > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + +89 -> 90 +90 -> 87 + +89: +Before: NullnessNoInitStore#424( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +90: +Before: NullnessNoInitStore#424( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o [ VariableDeclaration ] +(this) [ ImplicitThis ] +(this).f [ FieldAccess ] > NV{, T, poly nn/n=f/f} +o = (this).f [ Assignment ] > NV{, T, poly nn/n=f/f} + +87: +Before: NullnessNoInitStore#425( + o > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + +94 -> 95 +95 -> 96 +96 -> 98 +96 -> 93 +96 -> 93 +96 -> 93 +96 -> 93 +98 -> 100 +98 -> 93 +100 -> 101 +100 -> 93 +101 -> 92 + +94: +Before: NullnessNoInitStore#429( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +95: +Before: NullnessNoInitStore#429( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o [ VariableDeclaration ] + +96: +Before: NullnessNoInitStore#430( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Sub [ ClassName ] > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + +98: +Before: NullnessNoInitStore#431( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Sub.this [ FieldAccess ] > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + +93: +Before: NullnessNoInitStore#432( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +100: +Before: NullnessNoInitStore#439( + Issue5174Sub.class > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Sub.this.f [ FieldAccess ] > NV{, T, poly nn/n=f/f} + +101: +Before: NullnessNoInitStore#442( + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + Issue5174Sub.class > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o = Issue5174Sub.this.f [ Assignment ] > NV{, T, poly nn/n=f/f} + +92: +Before: NullnessNoInitStore#445( + o > NV{, T, poly nn/n=f/f} + this > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + Issue5174Sub.class > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + +105 -> 106 +106 -> 107 +107 -> 108 +107 -> 104 +107 -> 104 +107 -> 104 +107 -> 104 +108 -> 109 +109 -> 110 +109 -> 104 +109 -> 104 +109 -> 104 +109 -> 104 +110 -> 111 +111 -> 112 +111 -> 104 +111 -> 104 +111 -> 104 +111 -> 104 +112 -> 103 + +105: +Before: NullnessNoInitStore#454( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +106: +Before: NullnessNoInitStore#454( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +o [ VariableDeclaration ] +o [ LocalVariable ] + +107: +Before: NullnessNoInitStore#455( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Super [ ClassName ] + +108: +Before: NullnessNoInitStore#456( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Super.sf [ FieldAccess ] > NV{@NonNull, Object, poly nn/n=f/f} +o = Issue5174Super.sf [ Assignment ] > NV{@NonNull, Object, poly nn/n=f/f} +expression statement o = sf [ ExpressionStatement ] +o [ LocalVariable ] + +104: +Before: NullnessNoInitStore#457( + + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + + +109: +Before: NullnessNoInitStore#464( + o > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Sub [ ClassName ] > NV{@NonNull, Issue5174Sub, poly nn/n=f/f} + +110: +Before: NullnessNoInitStore#465( + o > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Sub.sf [ FieldAccess ] > NV{@NonNull, Object, poly nn/n=f/f} +o = Issue5174Sub.sf [ Assignment ] > NV{@NonNull, Object, poly nn/n=f/f} +expression statement o = Issue5174Sub.sf [ ExpressionStatement ] +o [ LocalVariable ] + +111: +Before: NullnessNoInitStore#474( + o > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Super [ ClassName ] > NV{@NonNull, Issue5174Super, poly nn/n=f/f} + +112: +Before: NullnessNoInitStore#475( + o > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ +Issue5174Super.sf [ FieldAccess ] > NV{@NonNull, Object, poly nn/n=f/f} +o = Issue5174Super.sf [ Assignment ] > NV{@NonNull, Object, poly nn/n=f/f} +expression statement o = Issue5174Super.sf [ ExpressionStatement ] + +103: +Before: NullnessNoInitStore#484( + o > NV{@NonNull, Object, poly nn/n=f/f} + isPolyNullNonNull = false + isPolyNullNull = false) +~~~~~~~~~ + diff --git a/checker/tests/nullness-extra/issue5174/Makefile b/checker/tests/nullness-extra/issue5174/Makefile new file mode 100644 index 000000000000..b046ecc2bc27 --- /dev/null +++ b/checker/tests/nullness-extra/issue5174/Makefile @@ -0,0 +1,8 @@ +.PHONY: all + +all: clean + $(JAVAC) -processor org.checkerframework.checker.nullness.NullnessChecker -Acfgviz=org.checkerframework.dataflow.cfg.visualize.StringCFGVisualizer -AassumeKeyFor Issue5174.java > Out.txt 2>&1 || true + diff -u Issue5174.out Out.txt + +clean: + rm -f *.class Out.txt diff --git a/checker/tests/nullness-extra/issue559/Expected.txt b/checker/tests/nullness-extra/issue559/Expected.txt index 59bbf4f8b806..9ec9e713e530 100644 --- a/checker/tests/nullness-extra/issue559/Expected.txt +++ b/checker/tests/nullness-extra/issue559/Expected.txt @@ -1,8 +1,8 @@ -warning: StubParser: in file myjdk.astub at line 6 ignored existing annotations on type: T[ extends @Initialized @Nullable Object super @Initialized @Nullable Void] +warning: AnnotationFileParser: in file myjdk.astub at line 6 ignored existing annotations on type: T[ extends @Initialized @Nullable Object super @Initialized @Nullable Void] Issue559.java:10: error: [argument.type.incompatible] incompatible types in argument. o.orElse(null); ^ found : null - required: @Initialized @NonNull String + required: @NonNull String 1 error 1 warning diff --git a/checker/tests/nullness-extra/issue559/Issue559.java b/checker/tests/nullness-extra/issue559/Issue559.java index 848d799db72f..158dc6fa3842 100644 --- a/checker/tests/nullness-extra/issue559/Issue559.java +++ b/checker/tests/nullness-extra/issue559/Issue559.java @@ -3,7 +3,7 @@ import java.util.Optional; -class Issue559 { +public class Issue559 { void bar(Optional o) { // With myjdk.astub the following should fail with an // argument.type.incompatible error. diff --git a/checker/tests/nullness-extra/issue559/Makefile b/checker/tests/nullness-extra/issue559/Makefile index db0d6b8fe3f9..f7f60ad4e2eb 100644 --- a/checker/tests/nullness-extra/issue559/Makefile +++ b/checker/tests/nullness-extra/issue559/Makefile @@ -1,7 +1,7 @@ .PHONY: all all: clean - $(JAVAC) -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=myjdk.astub -AstubWarnIfOverwritesBytecode Issue559.java > Out.txt 2>&1 || true + -$(JAVAC) -processor org.checkerframework.checker.nullness.NullnessChecker -Astubs=myjdk.astub -AstubWarnIfOverwritesBytecode Issue559.java > Out.txt 2>&1 diff -u Expected.txt Out.txt clean: diff --git a/checker/tests/nullness-extra/issue594/Expected.txt b/checker/tests/nullness-extra/issue594/Expected.txt index 6e53aaea884d..a6ec8f6a1dd3 100644 --- a/checker/tests/nullness-extra/issue594/Expected.txt +++ b/checker/tests/nullness-extra/issue594/Expected.txt @@ -1,6 +1,6 @@ Issue594.java:17: error: [return.type.incompatible] incompatible types in return. return result; ^ - type of expression: T[ extends @Initialized @Nullable Object super @Initialized @Nullable Void] - method return type: T[ extends @Initialized @Nullable Object super @Initialized @NonNull Void] + type of expression: T[ extends @Nullable Object super @Nullable Void] + method return type: T[ extends @Nullable Object super @NonNull Void] 1 error diff --git a/checker/tests/nullness-extra/issue594/Makefile b/checker/tests/nullness-extra/issue594/Makefile index 9445d4bfb960..f142dc3a13fd 100644 --- a/checker/tests/nullness-extra/issue594/Makefile +++ b/checker/tests/nullness-extra/issue594/Makefile @@ -1,8 +1,8 @@ .PHONY: all all: clean - $(JAVAC) -processor org.checkerframework.checker.nullness.NullnessChecker Issue594.java > Out.txt 2>&1 || true - diff Expected.txt Out.txt + -$(JAVAC) -processor org.checkerframework.checker.nullness.NullnessChecker Issue594.java > Out.txt 2>&1 + diff -u Expected.txt Out.txt clean: rm -f Issue594.class Out.txt diff --git a/checker/tests/nullness-extra/issue607/Issue607.java b/checker/tests/nullness-extra/issue607/Issue607.java index 3be6545a1bd6..c441fc5784fd 100644 --- a/checker/tests/nullness-extra/issue607/Issue607.java +++ b/checker/tests/nullness-extra/issue607/Issue607.java @@ -1,4 +1,4 @@ -class Issue607 extends Issue607SuperClass { +public class Issue607 extends Issue607SuperClass { static String simpleString = "a"; Issue607() { diff --git a/checker/tests/nullness-extra/multiple-errors/Expected.txt b/checker/tests/nullness-extra/multiple-errors/Expected.txt index 8c9210bcc5f8..8601eacd73a5 100644 --- a/checker/tests/nullness-extra/multiple-errors/Expected.txt +++ b/checker/tests/nullness-extra/multiple-errors/Expected.txt @@ -1,17 +1,14 @@ -C1.java:1: error: [initialization.fields.uninitialized] the constructor does not initialize fields: o -public class C1 { - ^ +C1.java:2: error: [initialization.field.uninitialized] the default constructor does not initialize field o + Object o; + ^ C2.java:2: error: [assignment.type.incompatible] incompatible types in assignment. Object o = null; ^ - found : null - required: @Initialized @NonNull Object + found : null (NullType) + required: @NonNull Object C3.java:4: error: cannot find symbol void bad(XXX p) { ^ symbol: class XXX location: class C3b -C4.java:3: error: [dereference.of.nullable] dereference of possibly-null reference p - p.toString(); - ^ -4 errors +3 errors diff --git a/checker/tests/nullness-extra/multiple-errors/Makefile b/checker/tests/nullness-extra/multiple-errors/Makefile index 5bb4e1d369d8..859a673b192f 100644 --- a/checker/tests/nullness-extra/multiple-errors/Makefile +++ b/checker/tests/nullness-extra/multiple-errors/Makefile @@ -2,5 +2,5 @@ all: rm -f Out.txt - $(JAVAC) -processor org.checkerframework.checker.nullness.NullnessChecker *.java > Out.txt 2>&1 || true + -$(JAVAC) -processor org.checkerframework.checker.nullness.NullnessChecker *.java > Out.txt 2>&1 diff -u Expected.txt Out.txt diff --git a/checker/tests/nullness-extra/package-anno/Makefile b/checker/tests/nullness-extra/package-anno/Makefile index bf468b88a581..ca64ef71722c 100644 --- a/checker/tests/nullness-extra/package-anno/Makefile +++ b/checker/tests/nullness-extra/package-anno/Makefile @@ -2,5 +2,5 @@ all: rm -f Out.txt - $(JAVAC) -processor org.checkerframework.checker.nullness.NullnessChecker test/*.java > Out.txt 2>&1 || true + -$(JAVAC) -processor org.checkerframework.checker.nullness.NullnessChecker test/*.java > Out.txt 2>&1 diff -u Expected.txt Out.txt diff --git a/checker/tests/nullness-extra/package-anno/test/PackageAnnotationTest.java b/checker/tests/nullness-extra/package-anno/test/PackageAnnotationTest.java index 0e5606955034..87991eae4ff2 100644 --- a/checker/tests/nullness-extra/package-anno/test/PackageAnnotationTest.java +++ b/checker/tests/nullness-extra/package-anno/test/PackageAnnotationTest.java @@ -1,6 +1,6 @@ package test; -class PackageAnnotationTest { +public class PackageAnnotationTest { // Allowed because of package annotation. Object f = null; } diff --git a/checker/tests/nullness-extra/shorthand/Makefile b/checker/tests/nullness-extra/shorthand/Makefile index 6fb7f9095072..3d6808e427ea 100644 --- a/checker/tests/nullness-extra/shorthand/Makefile +++ b/checker/tests/nullness-extra/shorthand/Makefile @@ -4,20 +4,20 @@ all: nullnessOnly nullnessRegex nullnessBad nonsense nullnessOnly: rm -f Out.txt - $(JAVAC) -processor NullnessChecker -XDrawDiagnostics -Anomsgtext -Awarns NullnessRegexWithErrors.java > Out.txt 2>&1 || true + -$(JAVAC) -processor NullnessChecker -XDrawDiagnostics -Anomsgtext -Awarns NullnessRegexWithErrors.java > Out.txt 2>&1 diff -u NullnessOnlyExpected.txt Out.txt nullnessRegex: rm -f Out.txt - $(JAVAC) -processor NullnessChecker,RegexChecker -XDrawDiagnostics -Anomsgtext -Awarns NullnessRegexWithErrors.java > Out.txt 2>&1 || true + -$(JAVAC) -processor NullnessChecker,RegexChecker -XDrawDiagnostics -Anomsgtext -Awarns NullnessRegexWithErrors.java > Out.txt 2>&1 diff -u NullnessRegexExpected.txt Out.txt nullnessBad: rm -f Out.txt - $(JAVAC) -processor nullness.NullnessChecker -XDrawDiagnostics -Anomsgtext -Awarns NullnessRegexWithErrors.java > Out.txt 2>&1 || true + -$(JAVAC) -processor nullness.NullnessChecker -XDrawDiagnostics -Anomsgtext -Awarns NullnessRegexWithErrors.java > Out.txt 2>&1 diff -u NullnessBadExpected.txt Out.txt nonsense: rm -f Out.txt - $(JAVAC) -processor NonsenseChecker -XDrawDiagnostics -Anomsgtext -Awarns NullnessRegexWithErrors.java > Out.txt 2>&1 || true + -$(JAVAC) -processor NonsenseChecker -XDrawDiagnostics -Anomsgtext -Awarns NullnessRegexWithErrors.java > Out.txt 2>&1 diff -u NonsenseExpected.txt Out.txt diff --git a/checker/tests/nullness-extra/shorthand/NullnessRegexWithErrors.java b/checker/tests/nullness-extra/shorthand/NullnessRegexWithErrors.java index 7a151c7984a0..d9bc93ab5ad0 100644 --- a/checker/tests/nullness-extra/shorthand/NullnessRegexWithErrors.java +++ b/checker/tests/nullness-extra/shorthand/NullnessRegexWithErrors.java @@ -2,7 +2,7 @@ import java.util.regex.Pattern; -class NullnessRegexWithErrors { +public class NullnessRegexWithErrors { String str = "(str"; void context() { diff --git a/checker/tests/nullness-extra/shorthand/README b/checker/tests/nullness-extra/shorthand/README index a406bbc63ce4..1269808a102e 100644 --- a/checker/tests/nullness-extra/shorthand/README +++ b/checker/tests/nullness-extra/shorthand/README @@ -1,5 +1,5 @@ This is a test of the "checker shorthand" feature. -See: https://checkerframework.org/manual/#shorthand-for-checkers +See: https://eisop.github.io/cf/manual/#shorthand-for-checkers This test is not Nullness specific. We have placed this test in this location because it has two preconditions: diff --git a/checker/tests/nullness-genericwildcardlib/GwiParent.java b/checker/tests/nullness-genericwildcardlib/GwiParent.java index e469bc2a8984..6fde1b3e7c21 100644 --- a/checker/tests/nullness-genericwildcardlib/GwiParent.java +++ b/checker/tests/nullness-genericwildcardlib/GwiParent.java @@ -1,6 +1,6 @@ // Library for issue #511: https://github.com/typetools/checker-framework/issues/511 -abstract class GwiParent { +public abstract class GwiParent { abstract void syntaxError(Recognizer recognizer); } diff --git a/checker/tests/nullness/AssignmentDuringInitialization.java b/checker/tests/nullness-initialization/AssignmentDuringInitialization.java similarity index 88% rename from checker/tests/nullness/AssignmentDuringInitialization.java rename to checker/tests/nullness-initialization/AssignmentDuringInitialization.java index 1530b4a8a892..fc0623c71c69 100644 --- a/checker/tests/nullness/AssignmentDuringInitialization.java +++ b/checker/tests/nullness-initialization/AssignmentDuringInitialization.java @@ -28,9 +28,9 @@ public AssignmentDuringInitialization() { } public void goodBehavior() { - // this isn't a constructor or initializer - // the receiver of this method should already be initialized - // and therefore f1 and f2 should already be initialized + // This isn't a constructor or initializer. + // The receiver of this method should already be initialized + // and therefore f1 and f2 should already be initialized. f5 = f6; f6 = f5; f6.toString(); // No exception here diff --git a/checker/tests/nullness-initialization/EisopIssue635.java b/checker/tests/nullness-initialization/EisopIssue635.java new file mode 100644 index 000000000000..d8416628d570 --- /dev/null +++ b/checker/tests/nullness-initialization/EisopIssue635.java @@ -0,0 +1,22 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +class EisopIssue635 { + + private @Nullable Runnable r; + + private void f() { + // No crash without this assignment first. + r = null; + r = + new Runnable() { + @Override + public void run() { + if (r != this) { + return; + } + // No crash without this call. + f(); + } + }; + } +} diff --git a/checker/tests/nullness-initialization/EnumFieldUninit.java b/checker/tests/nullness-initialization/EnumFieldUninit.java new file mode 100644 index 000000000000..2b8f2ad9ba9b --- /dev/null +++ b/checker/tests/nullness-initialization/EnumFieldUninit.java @@ -0,0 +1,23 @@ +enum EnumFieldUninit { + DUMMY; + + // :: error: (assignment.type.incompatible) + public static String s = null; + + // :: error: (initialization.static.field.uninitialized) + public static String u; + + static String[] arrayInit = new String[] {}; + + static String[] arrayInitInBlock; + + static { + arrayInitInBlock = new String[] {}; + } + + // :: error: (assignment.type.incompatible) + static String[] arrayInitToNull = null; + + // :: error: (initialization.static.field.uninitialized) + static String[] arrayUninit; +} diff --git a/checker/tests/nullness/FieldInit.java b/checker/tests/nullness-initialization/FieldInit.java similarity index 82% rename from checker/tests/nullness/FieldInit.java rename to checker/tests/nullness-initialization/FieldInit.java index 657a3108b5ee..eb1ec8d773d6 100644 --- a/checker/tests/nullness/FieldInit.java +++ b/checker/tests/nullness-initialization/FieldInit.java @@ -1,5 +1,3 @@ -import org.checkerframework.checker.nullness.qual.*; - public class FieldInit { // :: error: (argument.type.incompatible) :: error: (method.invocation.invalid) String f = init(this); diff --git a/checker/tests/nullness-initialization/FinalClass.java b/checker/tests/nullness-initialization/FinalClass.java new file mode 100644 index 000000000000..6408858942cd --- /dev/null +++ b/checker/tests/nullness-initialization/FinalClass.java @@ -0,0 +1,45 @@ +// Test case for EISOP issue 610: +// https://github.com/eisop/checker-framework/issues/610 + +// The issue was that receivers of class type `A` whose fields are all initialized were sometimes +// considered `@UnderInitialization(A.class)` instead of `@Initialized` even when `A` was final. + +import org.checkerframework.checker.initialization.qual.Initialized; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +final class EisopIssue610_1 { + @MonotonicNonNull String s; + + EisopIssue610_1() { + init(); + } + + void init() {} +} + +final class EisopIssue610_2 { + @Nullable String s; + + EisopIssue610_2() { + init(); + } + + void init() {} +} + +final class EisopIssue610_3 { + @MonotonicNonNull String s; + + EisopIssue610_3() { + @Initialized EisopIssue610_3 other = this; + } +} + +final class EisopIssue610_4 { + @Nullable String s; + + EisopIssue610_4() { + @Initialized EisopIssue610_4 other = this; + } +} diff --git a/checker/tests/nullness-initialization/FinalClassLambda.java b/checker/tests/nullness-initialization/FinalClassLambda.java new file mode 100644 index 000000000000..f5f6afc4cd33 --- /dev/null +++ b/checker/tests/nullness-initialization/FinalClassLambda.java @@ -0,0 +1,101 @@ +// Test case for EISOP issues #640 and #641: +// https://github.com/eisop/checker-framework/issues/640 +// https://github.com/eisop/checker-framework/issues/641 + +import org.checkerframework.checker.nullness.qual.Nullable; + +final class FinalClassLambda1 { + @Nullable String s; + + FinalClassLambda1() { + use(this::init); + } + + void init() {} + + static void use(Runnable r) {} +} + +final class FinalClassLambda2 extends FinalClassLambda2Base { + @Nullable String s; + + FinalClassLambda2() { + use(() -> init()); + use( + new Runnable() { + @Override + public void run() { + init(); + } + }); + } + + void init() {} +} + +class FinalClassLambda2Base { + void use(Runnable r) {} +} + +final class FinalClassLambda3 { + String s; + + FinalClassLambda3() { + s = "hello"; + use(this::init); + } + + void init() {} + + static void use(Runnable r) {} +} + +final class FinalClassLambda4 extends FinalClassLambda2Base { + String s; + + FinalClassLambda4() { + s = "world"; + use(() -> init()); + use( + new Runnable() { + @Override + public void run() { + init(); + } + }); + } + + void init() {} +} + +// Not a final class, but uses same name for consistency. +class FinalClassLambda5 extends FinalClassLambda2Base { + String s; + + FinalClassLambda5() { + s = "hello"; + // :: error: (method.invocation.invalid) + use( + // :: error: (methodref.receiver.bound.invalid) + this::init); + } + + FinalClassLambda5(int dummy) { + s = "world"; + // :: error: (method.invocation.invalid) + use( + // :: error: (method.invocation.invalid) + () -> init()); + // :: error: (method.invocation.invalid) + use( + new Runnable() { + @Override + public void run() { + // :: error: (method.invocation.invalid) + init(); + } + }); + } + + void init() {} +} diff --git a/checker/tests/nullness/FlowConstructor.java b/checker/tests/nullness-initialization/FlowConstructor.java similarity index 100% rename from checker/tests/nullness/FlowConstructor.java rename to checker/tests/nullness-initialization/FlowConstructor.java diff --git a/checker/tests/nullness/FlowConstructor2.java b/checker/tests/nullness-initialization/FlowConstructor2.java similarity index 100% rename from checker/tests/nullness/FlowConstructor2.java rename to checker/tests/nullness-initialization/FlowConstructor2.java diff --git a/checker/tests/nullness/FlowInitialization.java b/checker/tests/nullness-initialization/FlowInitialization.java similarity index 100% rename from checker/tests/nullness/FlowInitialization.java rename to checker/tests/nullness-initialization/FlowInitialization.java diff --git a/checker/tests/nullness/Initializer.java b/checker/tests/nullness-initialization/Initializer.java similarity index 91% rename from checker/tests/nullness/Initializer.java rename to checker/tests/nullness-initialization/Initializer.java index 7ee126f04b61..7a08abd1d956 100644 --- a/checker/tests/nullness/Initializer.java +++ b/checker/tests/nullness-initialization/Initializer.java @@ -3,7 +3,7 @@ import org.checkerframework.framework.qual.EnsuresQualifier; import org.checkerframework.framework.qual.EnsuresQualifierIf; -class Initializer { +public class Initializer { public String a; public String b = "abc"; @@ -13,6 +13,7 @@ class Initializer { public String d = (""); + // :: error: (initialization.fields.uninitialized) public Initializer() { // :: error: (assignment.type.incompatible) a = null; @@ -26,11 +27,13 @@ public Initializer(boolean foo) {} public Initializer(int foo) { a = ""; c = ""; + f = ""; } public Initializer(float foo) { setField(); c = ""; + f = ""; } public Initializer(double foo) { @@ -38,6 +41,7 @@ public Initializer(double foo) { a = ""; } c = ""; + f = ""; } // :: error: (initialization.fields.uninitialized) @@ -59,7 +63,7 @@ public boolean setFieldMaybe(@UnknownInitialization Initializer this) { return true; } - String f = ""; + String f; void t1(@UnknownInitialization Initializer this) { // :: error: (dereference.of.nullable) @@ -71,7 +75,8 @@ void t1(@UnknownInitialization Initializer this) { class SubInitializer extends Initializer { - String f = ""; + // :: error: (initialization.field.uninitialized) + String f; void subt1(@UnknownInitialization(Initializer.class) SubInitializer this) { fieldF.toString(); diff --git a/checker/tests/nullness/Issue1096.java b/checker/tests/nullness-initialization/Issue1096.java similarity index 82% rename from checker/tests/nullness/Issue1096.java rename to checker/tests/nullness-initialization/Issue1096.java index 2c38a970b322..70b354fd6506 100644 --- a/checker/tests/nullness/Issue1096.java +++ b/checker/tests/nullness-initialization/Issue1096.java @@ -32,13 +32,13 @@ void doNullable(@UnknownInitialization PreCond this) { } void foo(@UnknownInitialization PreCond this) { - // Receiver is not fully initialized, so raise error + // The receiver is not fully initialized, so raise an error. // :: error: (contracts.precondition.not.satisfied) early(); } void bar() { - // Receiver is initialized, so non-null field f is definitely non-null + // The receiver is initialized, so non-null field f is definitely non-null. early(); // Nullable fields stay nullable // :: error: (contracts.precondition.not.satisfied) @@ -48,13 +48,13 @@ void bar() { class User { void foo(@UnknownInitialization PreCond pc) { - // Receiver is not fully initialized, so raise error + // The receiver is not fully initialized, so raise an error. // :: error: (contracts.precondition.not.satisfied) pc.early(); } void bar(PreCond pc) { - // Receiver is initialized, so non-null field f is definitely non-null + // The receiver is initialized, so non-null field f is definitely non-null. pc.early(); // Nullable fields stay nullable // :: error: (contracts.precondition.not.satisfied) diff --git a/checker/tests/nullness-initialization/Issue1590.java b/checker/tests/nullness-initialization/Issue1590.java new file mode 100644 index 000000000000..326db23408fa --- /dev/null +++ b/checker/tests/nullness-initialization/Issue1590.java @@ -0,0 +1,16 @@ +@SuppressWarnings("initialization") +public class Issue1590 { + + private String a; + + public Issue1590() { + // valid because of suppressed warnings + init(); + // :: error: (dereference.of.nullable) + a.length(); + } + + public void init() { + a = "gude"; + } +} diff --git a/checker/tests/nullness-initialization/Issue1590a.java b/checker/tests/nullness-initialization/Issue1590a.java new file mode 100644 index 000000000000..f4de70f5f5ad --- /dev/null +++ b/checker/tests/nullness-initialization/Issue1590a.java @@ -0,0 +1,16 @@ +// This test case shows that @SuppressWarnings("initialization.") has no effect +@SuppressWarnings("initialization.") +public class Issue1590a { + + private String a; + + // :: error: (initialization.fields.uninitialized) + public Issue1590a() { + // :: error: (method.invocation.invalid) + init(); + } + + public void init() { + a = "gude"; + } +} diff --git a/checker/tests/nullness/Issue261.java b/checker/tests/nullness-initialization/Issue261.java similarity index 78% rename from checker/tests/nullness/Issue261.java rename to checker/tests/nullness-initialization/Issue261.java index 737a95df28ac..34630eafeae3 100644 --- a/checker/tests/nullness/Issue261.java +++ b/checker/tests/nullness-initialization/Issue261.java @@ -1,9 +1,10 @@ // Test case for Issue 261 // https://github.com/typetools/checker-framework/issues/261 -class Issue261 { +public class Issue261 { boolean b; - // :: error: (initialization.fields.uninitialized) + class Flag { + // :: error: (initialization.field.uninitialized) T value; } diff --git a/checker/tests/nullness/Issue345.java b/checker/tests/nullness-initialization/Issue345.java similarity index 100% rename from checker/tests/nullness/Issue345.java rename to checker/tests/nullness-initialization/Issue345.java diff --git a/checker/tests/nullness-initialization/Issue354.java b/checker/tests/nullness-initialization/Issue354.java new file mode 100644 index 000000000000..f0047696739a --- /dev/null +++ b/checker/tests/nullness-initialization/Issue354.java @@ -0,0 +1,19 @@ +public class Issue354 { + + String a; + + { + Object o = + new Object() { + @Override + public String toString() { + // :: error: (dereference.of.nullable) + return a.toString(); + } + }.toString(); + + // This is needed to avoid the initialization.fields.uninitialized warning. + // The NPE still occurs + a = ""; + } +} diff --git a/checker/tests/nullness/Issue400.java b/checker/tests/nullness-initialization/Issue400.java similarity index 77% rename from checker/tests/nullness/Issue400.java rename to checker/tests/nullness-initialization/Issue400.java index 7ff5b1b1562a..a66051580d13 100644 --- a/checker/tests/nullness/Issue400.java +++ b/checker/tests/nullness-initialization/Issue400.java @@ -2,11 +2,12 @@ import java.util.Collection; public class Issue400 { - // :: error: (initialization.fields.uninitialized) final class YYPair { + // :: error: (initialization.field.uninitialized) T first; + // :: error: (initialization.field.uninitialized) V second; - }; + } class YY { public Collection> getX() { diff --git a/checker/tests/nullness/Issue408.java b/checker/tests/nullness-initialization/Issue408.java similarity index 96% rename from checker/tests/nullness/Issue408.java rename to checker/tests/nullness-initialization/Issue408.java index c6d2f358dd71..bec43dddbbe9 100644 --- a/checker/tests/nullness/Issue408.java +++ b/checker/tests/nullness-initialization/Issue408.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; -class Issue408 { +public class Issue408 { static class Bar { Bar() { doIssue408(); diff --git a/checker/tests/nullness/KeyForValidation.java b/checker/tests/nullness-initialization/KeyForValidation.java similarity index 94% rename from checker/tests/nullness/KeyForValidation.java rename to checker/tests/nullness-initialization/KeyForValidation.java index b533f3f2ef65..41d74c24e3b1 100644 --- a/checker/tests/nullness/KeyForValidation.java +++ b/checker/tests/nullness-initialization/KeyForValidation.java @@ -1,15 +1,17 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; -import org.checkerframework.checker.nullness.qual.*; -@SuppressWarnings("fields.uninitialized") public class KeyForValidation { // :: error: (expression.unparsable.type.invalid) + // :: error: (initialization.static.field.uninitialized) static @KeyFor("this") Object f; + // :: error: (initialization.field.uninitialized) @KeyFor("this") Object g; // :: error: (expression.unparsable.type.invalid) @@ -28,8 +30,10 @@ void m4(@KeyFor("#1") Object p, Map m) {} void m4(@KeyFor("#2") String p, Map m) {} // :: error: (expression.unparsable.type.invalid) + // :: error: (initialization.field.uninitialized) @KeyFor("INVALID") Object h; + // :: error: (initialization.field.uninitialized) @KeyFor("f") Object i; void foo(Object p) { @@ -44,28 +48,36 @@ void foo(Object p) { // :: error: (expression.unparsable.type.invalid) void foo2(@KeyFor("ALSOBAD") Object o) {} + // :: error: (expression.unparsable.type.invalid) void foo3(@KeyFor("ALSOBAD") Object[] o) {} + // :: error: (expression.unparsable.type.invalid) void foo4(Map<@KeyFor("ALSOBAD") Object, Object> o) {} + // :: error: (expression.unparsable.type.invalid) @KeyFor("ALSOBAD") Object[] foo5() { throw new RuntimeException(); } + // :: error: (expression.unparsable.type.invalid) @KeyFor("ALSOBAD") Object foo6() { throw new RuntimeException(); } + // :: error: (expression.unparsable.type.invalid) Map<@KeyFor("ALSOBAD") Object, Object> foo7() { throw new RuntimeException(); } + // :: error: (expression.unparsable.type.invalid) <@KeyFor("ALSOBAD") T> void foo8() { throw new RuntimeException(); } + // :: error: (expression.unparsable.type.invalid) <@KeyForBottom T extends @KeyFor("ALSOBAD") Object> void foo9() {} + // :: error: (expression.unparsable.type.invalid) void foo10(@KeyFor("ALSOBAD") KeyForValidation this) {} @@ -86,26 +98,33 @@ public void test(Set<@KeyFor("BAD") String> keySet) { // :: error: (expression.unparsable.type.invalid) static void bar2(@KeyFor("this.instanceField") Object o) {} + // :: error: (expression.unparsable.type.invalid) static void bar3(@KeyFor("this.instanceField") Object[] o) {} + // :: error: (expression.unparsable.type.invalid) static void bar4(Map<@KeyFor("this.instanceField") Object, Object> o) {} + // :: error: (expression.unparsable.type.invalid) static @KeyFor("this.instanceField") Object[] bar5() { throw new RuntimeException(); } + // :: error: (expression.unparsable.type.invalid) static @KeyFor("this.instanceField") Object bar6() { throw new RuntimeException(); } + // :: error: (expression.unparsable.type.invalid) static Map<@KeyFor("this.instanceField") Object, Object> bar7() { throw new RuntimeException(); } + // :: error: (expression.unparsable.type.invalid) static <@KeyFor("this.instanceField") T> void bar8() { throw new RuntimeException(); } + // :: error: (expression.unparsable.type.invalid) static <@KeyForBottom T extends @KeyFor("this.instanceField") Object> void bar9() {} diff --git a/checker/tests/nullness/Listener.java b/checker/tests/nullness-initialization/Listener.java similarity index 100% rename from checker/tests/nullness/Listener.java rename to checker/tests/nullness-initialization/Listener.java diff --git a/checker/tests/nullness/MethodInvocation.java b/checker/tests/nullness-initialization/MethodInvocation.java similarity index 89% rename from checker/tests/nullness/MethodInvocation.java rename to checker/tests/nullness-initialization/MethodInvocation.java index 5d9cf0fcbbaf..5625f95e9caa 100644 --- a/checker/tests/nullness/MethodInvocation.java +++ b/checker/tests/nullness-initialization/MethodInvocation.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.initialization.qual.*; import org.checkerframework.checker.nullness.qual.*; -class MethodInvocation { +public class MethodInvocation { String s; @@ -15,7 +15,7 @@ public MethodInvocation() { public MethodInvocation(boolean p) { // :: error: (method.invocation.invalid) - a(); // still not okay to be committed + a(); // still not okay to be initialized s = "abc"; } diff --git a/checker/tests/nullness/MultiConstructorInit.java b/checker/tests/nullness-initialization/MultiConstructorInit.java similarity index 94% rename from checker/tests/nullness/MultiConstructorInit.java rename to checker/tests/nullness-initialization/MultiConstructorInit.java index 512a4e1192b6..782a476533b4 100644 --- a/checker/tests/nullness/MultiConstructorInit.java +++ b/checker/tests/nullness-initialization/MultiConstructorInit.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.initialization.qual.*; import org.checkerframework.checker.nullness.qual.*; -class MultiConstructorInit { +public class MultiConstructorInit { String a; diff --git a/checker/tests/nullness-initialization/ObjectArrayParam.java b/checker/tests/nullness-initialization/ObjectArrayParam.java new file mode 100644 index 000000000000..cf70acde51dd --- /dev/null +++ b/checker/tests/nullness-initialization/ObjectArrayParam.java @@ -0,0 +1,12 @@ +import org.checkerframework.checker.initialization.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +class ObjectArrayParam { + void test(@UnknownInitialization Object... args) { + for (Object obj : args) { + boolean isClass = obj instanceof Class; + // :: warning: (cast.unsafe) + @Initialized @NonNull Class clazz = (isClass ? (@Initialized @NonNull Class) obj : obj.getClass()); + } + } +} diff --git a/checker/tests/nullness-initialization/ObjectListParam.java b/checker/tests/nullness-initialization/ObjectListParam.java new file mode 100644 index 000000000000..c2c28d0ad7e2 --- /dev/null +++ b/checker/tests/nullness-initialization/ObjectListParam.java @@ -0,0 +1,15 @@ +import org.checkerframework.checker.initialization.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +import java.util.List; + +class ObjectListParam { + // :: error: type.argument.type.incompatible + void test(List<@UnknownInitialization Object> args) { + for (Object obj : args) { + boolean isClass = obj instanceof Class; + // :: warning: (cast.unsafe) + @Initialized Class clazz = (isClass ? (@Initialized Class) obj : obj.getClass()); + } + } +} diff --git a/checker/tests/nullness/PrivateMethodUnknownInit.java b/checker/tests/nullness-initialization/PrivateMethodUnknownInit.java similarity index 100% rename from checker/tests/nullness/PrivateMethodUnknownInit.java rename to checker/tests/nullness-initialization/PrivateMethodUnknownInit.java diff --git a/checker/tests/nullness/Raw2.java b/checker/tests/nullness-initialization/Raw2.java similarity index 97% rename from checker/tests/nullness/Raw2.java rename to checker/tests/nullness-initialization/Raw2.java index 7c7fef3cfbdc..f9b46115c77d 100644 --- a/checker/tests/nullness/Raw2.java +++ b/checker/tests/nullness-initialization/Raw2.java @@ -1,8 +1,9 @@ import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.nullness.qual.*; -class Raw2 { +public class Raw2 { private @NonNull Object field; + // :: error: (initialization.fields.uninitialized) public Raw2(int i) { this.method(this); diff --git a/checker/tests/nullness/RawField.java b/checker/tests/nullness-initialization/RawField.java similarity index 93% rename from checker/tests/nullness/RawField.java rename to checker/tests/nullness-initialization/RawField.java index 29720829b444..e6be359f3d30 100644 --- a/checker/tests/nullness/RawField.java +++ b/checker/tests/nullness-initialization/RawField.java @@ -42,7 +42,8 @@ public void parse_or_usage() { class MultiVersionControl { - @SuppressWarnings("fbc") // see https://github.com/typetools/checker-framework/issues/223 + @SuppressWarnings( + "initialization") // see https://github.com/typetools/checker-framework/issues/223 public void parseArgs(@UnknownInitialization MultiVersionControl this) { Options options = new Options(this); options.parse_or_usage(); diff --git a/checker/tests/nullness-initialization/RawMethodInvocation.java b/checker/tests/nullness-initialization/RawMethodInvocation.java new file mode 100644 index 000000000000..761d16893d3e --- /dev/null +++ b/checker/tests/nullness-initialization/RawMethodInvocation.java @@ -0,0 +1,39 @@ +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.*; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; + +@org.checkerframework.framework.qual.DefaultQualifier(Nullable.class) +public class RawMethodInvocation { + Object a; + Object b; + + RawMethodInvocation(boolean constructor_inits_a) { + a = 1; + init_b(); + } + + @EnsuresNonNull("b") + void init_b(@UnknownInitialization RawMethodInvocation this) { + b = 2; + } + + RawMethodInvocation(int constructor_inits_none) { + init_ab(); + } + + @EnsuresNonNull({"a", "b"}) + void init_ab(@UnknownInitialization RawMethodInvocation this) { + a = 1; + b = 2; + } + + RawMethodInvocation(long constructor_escapes_raw) { + a = 1; + // this call is not valid, this is still raw + // :: error: (method.invocation.invalid) + nonRawMethod(); + b = 2; + } + + void nonRawMethod() {} +} diff --git a/checker/tests/nullness/RawTypesBounded.java b/checker/tests/nullness-initialization/RawTypesBounded.java similarity index 99% rename from checker/tests/nullness/RawTypesBounded.java rename to checker/tests/nullness-initialization/RawTypesBounded.java index af5e4793a4d4..1edb3c1f12ee 100644 --- a/checker/tests/nullness/RawTypesBounded.java +++ b/checker/tests/nullness-initialization/RawTypesBounded.java @@ -4,7 +4,7 @@ import org.checkerframework.checker.nullness.qual.*; @org.checkerframework.framework.qual.DefaultQualifier(Nullable.class) -class RawTypesBounded { +public class RawTypesBounded { class Bad { @NonNull String field; diff --git a/checker/tests/nullness/Simple2.java b/checker/tests/nullness-initialization/Simple2.java similarity index 100% rename from checker/tests/nullness/Simple2.java rename to checker/tests/nullness-initialization/Simple2.java diff --git a/checker/tests/nullness/StaticInitialization.java b/checker/tests/nullness-initialization/StaticInitialization.java similarity index 100% rename from checker/tests/nullness/StaticInitialization.java rename to checker/tests/nullness-initialization/StaticInitialization.java diff --git a/checker/tests/nullness-initialization/StaticInitializer.java b/checker/tests/nullness-initialization/StaticInitializer.java new file mode 100644 index 000000000000..6c6b5182a88e --- /dev/null +++ b/checker/tests/nullness-initialization/StaticInitializer.java @@ -0,0 +1,60 @@ +import org.checkerframework.checker.initialization.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +public class StaticInitializer { + + public static String a; + // :: error: (initialization.static.field.uninitialized) + public static String b; + + static { + a = ""; + } + + public StaticInitializer() {} +} + +class StaticInitializer2 { + // :: error: (initialization.static.field.uninitialized) + public static String a; + // :: error: (initialization.static.field.uninitialized) + public static String b; +} + +class StaticInitializer3 { + public static String a = ""; +} + +class StaticInitializer4 { + public static String a = ""; + public static String b; + + static { + b = ""; + } +} + +class StaticInitializer5 { + public static String a = ""; + + static { + a.toString(); + } + + public static String b = ""; +} + +class StaticInitializer6 { + public static String a = ""; + + public static String b; + + static { + // TODO error expected. See #556. + b.toString(); + } + + static { + b = ""; + } +} diff --git a/checker/tests/nullness/SuperConstructorInit.java b/checker/tests/nullness-initialization/SuperConstructorInit.java similarity index 90% rename from checker/tests/nullness/SuperConstructorInit.java rename to checker/tests/nullness-initialization/SuperConstructorInit.java index 762d0a7489bf..aa863fb05f91 100644 --- a/checker/tests/nullness/SuperConstructorInit.java +++ b/checker/tests/nullness-initialization/SuperConstructorInit.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class SuperConstructorInit { +public class SuperConstructorInit { String a; @@ -10,6 +10,7 @@ public SuperConstructorInit() { public static class B extends SuperConstructorInit { String b; + // :: error: (initialization.fields.uninitialized) public B() { super(); diff --git a/checker/tests/nullness-initialization/Suppression.java b/checker/tests/nullness-initialization/Suppression.java new file mode 100644 index 000000000000..93ea66a4f306 --- /dev/null +++ b/checker/tests/nullness-initialization/Suppression.java @@ -0,0 +1,24 @@ +public class Suppression { + + Object f; + + @SuppressWarnings("nullnessnoinit") + void test() { + String a = null; + a.toString(); + } + + @SuppressWarnings("initialization") + void test2() { + String a = null; + // :: error: (dereference.of.nullable) + a.toString(); + } + + @SuppressWarnings("nullness") + Suppression() {} + + @SuppressWarnings("nullnessnoinit") + // :: error: (initialization.fields.uninitialized) + Suppression(int dummy) {} +} diff --git a/checker/tests/nullness-initialization/ThisNodeTest.java b/checker/tests/nullness-initialization/ThisNodeTest.java new file mode 100644 index 000000000000..1bb39b13975c --- /dev/null +++ b/checker/tests/nullness-initialization/ThisNodeTest.java @@ -0,0 +1,21 @@ +import org.checkerframework.checker.initialization.qual.*; +import org.checkerframework.checker.nullness.qual.*; + +public class ThisNodeTest { + public ThisNodeTest() { + new Object() { + void test() { + @UnderInitialization ThisNodeTest l1 = ThisNodeTest.this; + // :: error: (assignment.type.incompatible) + @Initialized ThisNodeTest l2 = ThisNodeTest.this; + + // :: error: (method.invocation.invalid) + ThisNodeTest.this.foo(); + // :: error: (method.invocation.invalid) + foo(); + } + }; + } + + void foo() {} +} diff --git a/checker/tests/nullness/Throwing.java b/checker/tests/nullness-initialization/Throwing.java similarity index 100% rename from checker/tests/nullness/Throwing.java rename to checker/tests/nullness-initialization/Throwing.java diff --git a/checker/tests/nullness-initialization/TryCatch.java b/checker/tests/nullness-initialization/TryCatch.java new file mode 100644 index 000000000000..6d8d9032849e --- /dev/null +++ b/checker/tests/nullness-initialization/TryCatch.java @@ -0,0 +1,42 @@ +import org.checkerframework.checker.nullness.qual.*; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +class EntryReader { + public EntryReader() throws IOException {} +} + +public class TryCatch { + void constructorException() throws IOException { + List file_errors = new ArrayList<>(); + try { + new EntryReader(); + } catch (FileNotFoundException e) { + file_errors.add(e); + } + } + + void unreachableCatch(String[] xs) { + String t = ""; + t.toString(); + try { + } catch (Throwable e) { + // Note that this code is dead. + // :: error: (dereference.of.nullable) :: error: (method.invocation.invalid) + t.toString(); + } + } + + void noClassDefFoundError(@Nullable Object x) { + try { + Class cls = EntryReader.class; + } catch (NoClassDefFoundError e) { + if (x != null) { + // OK + x.toString(); + } + } + } +} diff --git a/checker/tests/nullness/TwoStaticInitBlocks.java b/checker/tests/nullness-initialization/TwoStaticInitBlocks.java similarity index 95% rename from checker/tests/nullness/TwoStaticInitBlocks.java rename to checker/tests/nullness-initialization/TwoStaticInitBlocks.java index dd5d865fb3f6..18873846461b 100644 --- a/checker/tests/nullness/TwoStaticInitBlocks.java +++ b/checker/tests/nullness-initialization/TwoStaticInitBlocks.java @@ -2,7 +2,7 @@ // this is a test-case for initialization that covers multiple initializer blocks, field // initializers and a few other things -class TwoStaticInitBlocks { +public class TwoStaticInitBlocks { String f2; String f1 = (f2 = ""); diff --git a/checker/tests/nullness/ValidType.java b/checker/tests/nullness-initialization/ValidType.java similarity index 93% rename from checker/tests/nullness/ValidType.java rename to checker/tests/nullness-initialization/ValidType.java index 9943cdaef9c6..f98a5efb3ce5 100644 --- a/checker/tests/nullness/ValidType.java +++ b/checker/tests/nullness-initialization/ValidType.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.initialization.qual.*; import org.checkerframework.checker.nullness.qual.*; -class ValidType { +public class ValidType { void t1() { // :: error: (type.invalid.conflicting.annos) diff --git a/checker/tests/nullness/VarInfoName.java b/checker/tests/nullness-initialization/VarInfoName.java similarity index 95% rename from checker/tests/nullness/VarInfoName.java rename to checker/tests/nullness-initialization/VarInfoName.java index 8b54122e6b13..63c15569aa6e 100644 --- a/checker/tests/nullness/VarInfoName.java +++ b/checker/tests/nullness-initialization/VarInfoName.java @@ -8,7 +8,7 @@ public abstract static class BooleanAndVisitor extends Visitor { private boolean result; public BooleanAndVisitor(VarInfoName name) { - // :: error: (argument.type.incompatible) :: warning: (known.nonnull) + // :: error: (argument.type.incompatible) :: warning: (nulltest.redundant) result = (name.accept(this) != null); } } diff --git a/checker/tests/nullness/Wellformed.java b/checker/tests/nullness-initialization/Wellformed.java similarity index 94% rename from checker/tests/nullness/Wellformed.java rename to checker/tests/nullness-initialization/Wellformed.java index 6c211de9c15b..e7a0a59733a0 100644 --- a/checker/tests/nullness/Wellformed.java +++ b/checker/tests/nullness-initialization/Wellformed.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class Wellformed { +public class Wellformed { // :: error: (type.invalid.conflicting.annos) @NonNull @Nullable Object f = null; @@ -10,9 +10,11 @@ class Gen1a {} class Gen1b { // :: error: (type.invalid.conflicting.annos) void m(T p) {} + // :: error: (type.invalid.conflicting.annos) <@NonNull @Nullable T> void m2(T p) {} } + // :: error: (type.invalid.conflicting.annos) class Gen1c<@NonNull @Nullable TTT> {} @@ -40,8 +42,8 @@ class Gen3b { } } - // :: error: (initialization.fields.uninitialized) class Gen4 { + // :: error: (initialization.field.uninitialized) @NonNull T f; @NonNull T get() { @@ -58,8 +60,10 @@ class Gen5b extends Gen5a<@Nullable Object> {} class Gen5c extends Gen5a<@Nullable S> {} class Gen6a {} + // :: error: (type.argument.type.incompatible) class Gen6b extends Gen6a<@Nullable Object> {} + // :: error: (type.argument.type.incompatible) class Gen6c extends Gen6a<@Nullable S> {} } diff --git a/checker/tests/nullness/generics/AnnotatedGenerics.java b/checker/tests/nullness-initialization/generics/AnnotatedGenerics.java similarity index 90% rename from checker/tests/nullness/generics/AnnotatedGenerics.java rename to checker/tests/nullness-initialization/generics/AnnotatedGenerics.java index 06ad4564775c..3c2f038d9e89 100644 --- a/checker/tests/nullness/generics/AnnotatedGenerics.java +++ b/checker/tests/nullness-initialization/generics/AnnotatedGenerics.java @@ -1,10 +1,10 @@ import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.dataflow.qual.*; -class AnnotatedGenerics { +public class AnnotatedGenerics { public static void testNullableTypeVariable() { - // :: error: (initialization.fields.uninitialized) class Test { + // :: error: (initialization.field.uninitialized) T f; @Nullable T get() { @@ -13,7 +13,7 @@ class Test { } Test> l = new Test<>(); // :: error: (iterating.over.nullable) - for (String s : l.get()) ; + for (String s : l.get()) {} } public static void testNonNullTypeVariable() { @@ -23,9 +23,9 @@ class Test { } } Test<@Nullable Iterable> l = new Test<>(); - for (String s : l.get()) ; + for (String s : l.get()) {} Test> n = new Test<>(); - for (String s : n.get()) ; + for (String s : n.get()) {} } static class MyClass implements MyIterator<@Nullable T> { diff --git a/checker/tests/nullness/generics/AnnotatedGenerics2.java b/checker/tests/nullness-initialization/generics/AnnotatedGenerics2.java similarity index 93% rename from checker/tests/nullness/generics/AnnotatedGenerics2.java rename to checker/tests/nullness-initialization/generics/AnnotatedGenerics2.java index 5c92da56683e..c229d4d0638a 100644 --- a/checker/tests/nullness/generics/AnnotatedGenerics2.java +++ b/checker/tests/nullness-initialization/generics/AnnotatedGenerics2.java @@ -1,12 +1,13 @@ import org.checkerframework.checker.nullness.qual.*; -class AnnotatedGenerics2 { +public class AnnotatedGenerics2 { // Top-level class to ensure that both classes are processed. - // :: error: (initialization.fields.uninitialized) class AnnotatedGenerics2Nble { + // :: error: (initialization.field.uninitialized) @NonNull T myFieldNN; @Nullable T myFieldNble; + // :: error: (initialization.field.uninitialized) T myFieldT; /* TODO: This test case gets affected by flow inference. @@ -68,10 +69,11 @@ void params(@NonNull T myParamNN, @Nullable T myParamNble, T myParamT) { } } - // :: error: (initialization.fields.uninitialized) class AnnotatedGenerics2NN { + // :: error: (initialization.field.uninitialized) @NonNull T myFieldNN; @Nullable T myFieldNble; + // :: error: (initialization.field.uninitialized) T myFieldT; /* TODO: This test case gets affected by flow inference. diff --git a/checker/tests/nullness/generics/GenericBoundsExplicit.java b/checker/tests/nullness-initialization/generics/GenericBoundsExplicit.java similarity index 77% rename from checker/tests/nullness/generics/GenericBoundsExplicit.java rename to checker/tests/nullness-initialization/generics/GenericBoundsExplicit.java index fa9b86f8efc5..cb5843ebe06e 100644 --- a/checker/tests/nullness/generics/GenericBoundsExplicit.java +++ b/checker/tests/nullness-initialization/generics/GenericBoundsExplicit.java @@ -2,9 +2,9 @@ import org.checkerframework.checker.nullness.qual.*; -@SuppressWarnings("initialization.fields.uninitialized") -class GenericBoundsExplicit<@NonNull T extends @Nullable Object> { +public class GenericBoundsExplicit<@NonNull T extends @Nullable Object> { + @SuppressWarnings("initialization.field.uninitialized") T t; public void method() { @@ -19,10 +19,11 @@ public static void doSomething() { } } -@SuppressWarnings("initialization.fields.uninitialized") class GenericBoundsExplicit2<@NonNull TT extends @Nullable Object> { @Nullable TT tt1; + // :: error: (initialization.field.uninitialized) @NonNull TT tt2; + // :: error: (initialization.field.uninitialized) TT tt3; public void context() { @@ -36,7 +37,7 @@ public void context() { } } -@SuppressWarnings("initialization.fields.uninitialized") +@SuppressWarnings("initialization.field.uninitialized") class GenericBoundsExplicit3<@NonNull TTT extends @NonNull Object> { @Nullable TTT ttt1; @NonNull TTT ttt2; diff --git a/checker/tests/nullness/generics/Issue314.java b/checker/tests/nullness-initialization/generics/Issue314.java similarity index 90% rename from checker/tests/nullness/generics/Issue314.java rename to checker/tests/nullness-initialization/generics/Issue314.java index 71c3a5b15d3b..419cabb048ba 100644 --- a/checker/tests/nullness/generics/Issue314.java +++ b/checker/tests/nullness-initialization/generics/Issue314.java @@ -1,11 +1,12 @@ // Test case for Issue 314: // https://github.com/typetools/checker-framework/issues/314 -import java.util.List; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -class Issue314 { +import java.util.List; + +public class Issue314 { List m1(List<@NonNull T> l1) { return l1; } @@ -22,7 +23,6 @@ class Also { { // :: error: (assignment.type.incompatible) f1 = f2; - // :: error: (assignment.type.incompatible) f2 = f1; } } diff --git a/checker/tests/nullness-initialization/generics/Issue783c.java b/checker/tests/nullness-initialization/generics/Issue783c.java new file mode 100644 index 000000000000..9838d5759a0f --- /dev/null +++ b/checker/tests/nullness-initialization/generics/Issue783c.java @@ -0,0 +1,12 @@ +public class Issue783c { + // :: error: (initialization.field.uninitialized) + private T val; + + public void set(T val) { + this.val = val; + } + + public T get() { + return val; + } +} diff --git a/checker/tests/nullness/generics/NullableLUB.java b/checker/tests/nullness-initialization/generics/NullableLUB.java similarity index 94% rename from checker/tests/nullness/generics/NullableLUB.java rename to checker/tests/nullness-initialization/generics/NullableLUB.java index f69bea8c4e70..87d88d7d384b 100644 --- a/checker/tests/nullness/generics/NullableLUB.java +++ b/checker/tests/nullness-initialization/generics/NullableLUB.java @@ -4,8 +4,8 @@ * type variables with non-type variables. The error did not previously * get raised, leading to a missed NPE. */ -// :: error: (initialization.fields.uninitialized) public class NullableLUB { + // :: error: (initialization.field.uninitialized) T t; @Nullable T nt; diff --git a/checker/tests/nullness/generics/WellformedBounds.java b/checker/tests/nullness-initialization/generics/WellformedBounds.java similarity index 88% rename from checker/tests/nullness/generics/WellformedBounds.java rename to checker/tests/nullness-initialization/generics/WellformedBounds.java index d4ebc11f28c6..1f09132efd18 100644 --- a/checker/tests/nullness/generics/WellformedBounds.java +++ b/checker/tests/nullness-initialization/generics/WellformedBounds.java @@ -1,8 +1,8 @@ import org.checkerframework.checker.nullness.qual.*; -// Field f needs to be set, b/c the upper bound is @Initialized -// :: error: (initialization.fields.uninitialized) class Param { + // Field f needs to be set, because the upper bound is @Initialized + // :: error: (initialization.field.uninitialized) T f; void foo() { diff --git a/checker/tests/nullness-initialization/java8/lambda/LambdaInit.java b/checker/tests/nullness-initialization/java8/lambda/LambdaInit.java new file mode 100644 index 000000000000..562ecdc9f957 --- /dev/null +++ b/checker/tests/nullness-initialization/java8/lambda/LambdaInit.java @@ -0,0 +1,201 @@ +// Test field initialization +// fields, initializers, static initializers, constructors. + +import org.checkerframework.checker.nullness.qual.*; + +interface FunctionInit { + R apply(T t); +} + +interface Consumer { + void consume(T t); +} + +public class LambdaInit { + String f1; + String f2 = ""; + @Nullable String f3 = ""; + + String f1b; + FunctionInit ff0 = + s -> { + // :: error: (dereference.of.nullable) + f1.toString(); + // :: error: (dereference.of.nullable) + f1b.toString(); + f2.toString(); + // :: error: (dereference.of.nullable) + f3.toString(); + return ""; + }; + // Test field value refinement after initializer. f1b should still be @Nullable in the lambda. + Object o1 = f1b = ""; + + String f4; + + { + f3 = ""; + f4 = ""; + FunctionInit ff0 = + s -> { + // :: error: (dereference.of.nullable) + f1.toString(); + f2.toString(); + // :: error: (dereference.of.nullable) + f3.toString(); + f4.toString(); + return ""; + }; + } + + String f5; + + @SuppressWarnings("initialization.fields.uninitialized") // f1 is not initialized + LambdaInit() { + f5 = ""; + FunctionInit ff0 = + s -> { + // :: error: (dereference.of.nullable) + f1.toString(); + f2.toString(); + // :: error: (dereference.of.nullable) + f3.toString(); + f5.toString(); + return ""; + }; + } + + // Test for https://github.com/typetools/checker-framework/issues/5194 . + Object o = + new Object() { + @Override + public String toString() { + // BUG: this should not yield a warning. + // :: error: (dereference.of.nullable) + f1.toString(); + f2.toString(); + return ""; + } + }; + + // Works! + void method() { + FunctionInit ff0 = + s -> { + f1.toString(); + f2.toString(); + // :: error: (dereference.of.nullable) + f3.toString(); + return ""; + }; + } + + // Test for nested + class Nested { + FunctionInit ff0 = + s -> { + f1.toString(); + f2.toString(); + // :: error: (dereference.of.nullable) + f3.toString(); + return ""; + }; + + String f4; + + { + f3 = ""; + f4 = ""; + FunctionInit ff0 = + s -> { + f1.toString(); + f2.toString(); + // :: error: (dereference.of.nullable) + f3.toString(); + f4.toString(); + return ""; + }; + } + + String f5; + + Nested() { + f5 = ""; + FunctionInit ff0 = + s -> { + f1.toString(); + f2.toString(); + // :: error: (dereference.of.nullable) + f3.toString(); + f5.toString(); + return ""; + }; + } + + void method() { + FunctionInit ff0 = + s -> { + f1.toString(); + f2.toString(); + // :: error: (dereference.of.nullable) + f3.toString(); + return ""; + }; + } + } + + // Test for nested in a lambda + Consumer func = + s -> { + Consumer ff0 = + s2 -> { + // :: error: (dereference.of.nullable) + f1.toString(); + f2.toString(); + // :: error: (dereference.of.nullable) + f3.toString(); + }; + }; + + // Tests for static initializers. + // :: error: (initialization.static.field.uninitialized) + static String sf1; + static String sf2 = ""; + static @Nullable String sf3 = ""; + static String sf1b; + static FunctionInit sff0 = + s -> { + + // This is an issue with static initializers in general + // // :: error: (dereference.of.nullable) + sf1.toString(); + // This is an issue with static initializers in general + // // :: error: (dereference.of.nullable) + sf1b.toString(); + sf2.toString(); + // :: error: (dereference.of.nullable) + sf3.toString(); + return ""; + }; + // Test field value refinement after initializer. f1b should still be null. + static Object so1 = sf1b = ""; + + static String sf4; + + static { + sf3 = ""; + sf4 = ""; + FunctionInit sff0 = + s -> { + + // This is an issue with static initializers in general + // // :: error: (dereference.of.nullable) + sf1.toString(); + sf2.toString(); + // :: error: (dereference.of.nullable) + sf3.toString(); + sf4.toString(); + return ""; + }; + } +} diff --git a/checker/tests/nullness/java8/lambda/ReceiversLambda.java b/checker/tests/nullness-initialization/java8/lambda/ReceiversLambda.java similarity index 91% rename from checker/tests/nullness/java8/lambda/ReceiversLambda.java rename to checker/tests/nullness-initialization/java8/lambda/ReceiversLambda.java index 65c9495bd3d1..5f43e8df2d11 100644 --- a/checker/tests/nullness/java8/lambda/ReceiversLambda.java +++ b/checker/tests/nullness-initialization/java8/lambda/ReceiversLambda.java @@ -18,10 +18,12 @@ class ReceiverTest { // :: error: (method.invocation.invalid) FunctionRT f2 = s -> super.toString(); + // :: error: (nullness.on.receiver) void context1(@NonNull ReceiverTest this) { SupplierR s = () -> this; } + // :: error: (nullness.on.receiver) void context2(@Nullable ReceiverTest this) { // TODO: This is bug that is not specific to lambdas // https://github.com/typetools/checker-framework/issues/352 diff --git a/checker/tests/nullness-javac-errors/BadCast1.java b/checker/tests/nullness-javac-errors/BadCast1.java index 06f503646d69..2d9cf3c1bbaf 100644 --- a/checker/tests/nullness-javac-errors/BadCast1.java +++ b/checker/tests/nullness-javac-errors/BadCast1.java @@ -1,4 +1,4 @@ -class BadCast1 { +public class BadCast1 { public void m() { // :: error: illegal start of type :: error: not a statement (@NonNull) ""; diff --git a/checker/tests/nullness-javac-errors/BadCast2.java b/checker/tests/nullness-javac-errors/BadCast2.java index fdded88f7a5c..174f85cd789c 100644 --- a/checker/tests/nullness-javac-errors/BadCast2.java +++ b/checker/tests/nullness-javac-errors/BadCast2.java @@ -1,4 +1,4 @@ -class BadCast2 { +public class BadCast2 { public static void main(String[] args) { // :: error: illegal start of type String example = (@NonNull) ""; diff --git a/checker/tests/nullness-javadoc/JavadocJdkAnnotations.java b/checker/tests/nullness-javadoc/JavadocJdkAnnotations.java index e0e24665fb6d..2eeb33b2349b 100644 --- a/checker/tests/nullness-javadoc/JavadocJdkAnnotations.java +++ b/checker/tests/nullness-javadoc/JavadocJdkAnnotations.java @@ -1,13 +1,17 @@ import com.sun.javadoc.Doc; + import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +// @above-java11-jdk-skip-test com.sun.javadoc.Doc doesn't exist above 11. public class JavadocJdkAnnotations { @Nullable Object f = null; @SuppressWarnings("removal") void testPureAnnotation(Doc d) { + // This tests that @Pure and @SideEffectFree annotations are read. + f = "non-null value"; d.isIncluded(); @NonNull Object x = f; diff --git a/checker/tests/nullness-nodelombok/UnsoundnessTest.java b/checker/tests/nullness-nodelombok/UnsoundnessTest.java new file mode 100644 index 000000000000..2264637f10b1 --- /dev/null +++ b/checker/tests/nullness-nodelombok/UnsoundnessTest.java @@ -0,0 +1,17 @@ +// An example of an unsoundness that occurs when running the Nullness Checker +// on Lombok'd code without running delombok first. + +@lombok.Builder +class UnsoundnessTest { + @lombok.NonNull Object foo; + @lombok.NonNull Object bar; + + static void test() { + // An error should be issued here, but the code has not been delombok'd. + // If the CF and Lombok are ever able to work in the same invocation of javac + // (i.e. without delomboking first), then this error should be changed back to an + // expected error by re-adding the leading "::". + // error: (assignment.type.incompatible) + builder().foo(null).build(); + } +} diff --git a/checker/tests/nullness-nullmarked/parentandchildpackage/NotNullMarkedBecauseChildPackage.java b/checker/tests/nullness-nullmarked/parentandchildpackage/NotNullMarkedBecauseChildPackage.java new file mode 100644 index 000000000000..7813d480d7bf --- /dev/null +++ b/checker/tests/nullness-nullmarked/parentandchildpackage/NotNullMarkedBecauseChildPackage.java @@ -0,0 +1,7 @@ +package parent.child; + +import org.jspecify.annotations.Nullable; + +public class NotNullMarkedBecauseChildPackage { + void foo(NotNullMarkedBecauseChildPackage<@Nullable String> d) {} +} diff --git a/checker/tests/nullness-nullmarked/parentandchildpackage/package-info.java b/checker/tests/nullness-nullmarked/parentandchildpackage/package-info.java new file mode 100644 index 000000000000..b46c46a61c07 --- /dev/null +++ b/checker/tests/nullness-nullmarked/parentandchildpackage/package-info.java @@ -0,0 +1,2 @@ +@org.jspecify.annotations.NullMarked +package parent; diff --git a/checker/tests/nullness-nullmarked/singlepackage/NullMarkedBecausePackageIs.java b/checker/tests/nullness-nullmarked/singlepackage/NullMarkedBecausePackageIs.java new file mode 100644 index 000000000000..0ac4b19cf17d --- /dev/null +++ b/checker/tests/nullness-nullmarked/singlepackage/NullMarkedBecausePackageIs.java @@ -0,0 +1,8 @@ +package other; + +import org.jspecify.annotations.Nullable; + +public class NullMarkedBecausePackageIs { + // :: error: (type.argument.type.incompatible) + void foo(NullMarkedBecausePackageIs<@Nullable String> d) {} +} diff --git a/checker/tests/nullness-nullmarked/singlepackage/package-info.java b/checker/tests/nullness-nullmarked/singlepackage/package-info.java new file mode 100644 index 000000000000..1624ea9aa122 --- /dev/null +++ b/checker/tests/nullness-nullmarked/singlepackage/package-info.java @@ -0,0 +1,2 @@ +@org.jspecify.annotations.NullMarked +package other; diff --git a/checker/tests/nullness-permitClearProperty/PermitClearProperty.java b/checker/tests/nullness-permitClearProperty/PermitClearProperty.java index 48449344430b..487decaca717 100644 --- a/checker/tests/nullness-permitClearProperty/PermitClearProperty.java +++ b/checker/tests/nullness-permitClearProperty/PermitClearProperty.java @@ -1,9 +1,10 @@ // Same code (but different expected errors) as test PreventClearProperty.java . -import java.util.Properties; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.common.value.qual.StringVal; +import java.util.Properties; + public class PermitClearProperty { static final @StringVal("line.separator") String LINE_SEPARATOR = "line.separator"; diff --git a/checker/tests/nullness-records/BasicRecord.java b/checker/tests/nullness-records/BasicRecord.java new file mode 100644 index 000000000000..0b2c793b76d1 --- /dev/null +++ b/checker/tests/nullness-records/BasicRecord.java @@ -0,0 +1,14 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java16-jdk-skip-test +public record BasicRecord(String str) { + + public static BasicRecord makeNonNull(String s) { + return new BasicRecord(s); + } + + public static BasicRecord makeNull(@Nullable String s) { + // :: error: (argument.type.incompatible) + return new BasicRecord(s); + } +} diff --git a/checker/tests/nullness-records/BasicRecordCanon.java b/checker/tests/nullness-records/BasicRecordCanon.java new file mode 100644 index 000000000000..6785c8036000 --- /dev/null +++ b/checker/tests/nullness-records/BasicRecordCanon.java @@ -0,0 +1,16 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java16-jdk-skip-test +public record BasicRecordCanon(String str) { + + public static BasicRecordCanon makeNonNull(String s) { + return new BasicRecordCanon(s); + } + + public static BasicRecordCanon makeNull(@Nullable String s) { + // :: error: (argument.type.incompatible) + return new BasicRecordCanon(s); + } + + public BasicRecordCanon {} +} diff --git a/checker/tests/nullness-records/BasicRecordNullable.java b/checker/tests/nullness-records/BasicRecordNullable.java new file mode 100644 index 000000000000..1c1064b3e114 --- /dev/null +++ b/checker/tests/nullness-records/BasicRecordNullable.java @@ -0,0 +1,31 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java16-jdk-skip-test +public record BasicRecordNullable(@Nullable String str) { + + public static BasicRecordNullable makeNonNull(String s) { + return new BasicRecordNullable(s); + } + + public static BasicRecordNullable makeNull(@Nullable String s) { + return new BasicRecordNullable(s); + } + + public @Nullable String getStringFromField() { + return str; + } + + public @Nullable String getStringFromMethod() { + return str(); + } + + public String getStringFromFieldErr() { + // :: error: (return.type.incompatible) + return str; + } + + public String getStringFromMethodErr() { + // :: error: (return.type.incompatible) + return str(); + } +} diff --git a/checker/tests/nullness-records/DefaultQualRecord.java b/checker/tests/nullness-records/DefaultQualRecord.java new file mode 100644 index 000000000000..781c1da2ab30 --- /dev/null +++ b/checker/tests/nullness-records/DefaultQualRecord.java @@ -0,0 +1,62 @@ +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; + +class StandardQualClass { + // :: error: (assignment.type.incompatible) + public static String s = null; + // :: error: (initialization.static.field.uninitialized) + public static String u; +} + +@DefaultQualifier(Nullable.class) +class DefaultQualClass { + public static String s = null; + public static String u; +} + +interface StandardQualInterface { + // :: error: (assignment.type.incompatible) + public static String s = null; +} + +@DefaultQualifier(Nullable.class) +interface DefaultQualInterface { + public static String s = null; +} + +enum StandardQualEnum { + DUMMY; + // :: error: (assignment.type.incompatible) + public static String s = null; + // :: error: (initialization.static.field.uninitialized) + public static String u; +} + +@DefaultQualifier(Nullable.class) +enum DefaultQualEnum { + DUMMY; + public static String s = null; + public static String u; +} + +record StandardQualRecord(String m) { + // :: error: (assignment.type.incompatible) + public static String s = null; + // :: error: (initialization.static.field.uninitialized) + public static String u; + + StandardQualRecord { + // :: error: (assignment.type.incompatible) + m = null; + } +} + +@DefaultQualifier(Nullable.class) +record DefaultQualRecord(String m) { + public static String s = null; + public static String u; + + DefaultQualRecord { + m = null; + } +} diff --git a/checker/tests/nullness-records/GenericPair.java b/checker/tests/nullness-records/GenericPair.java new file mode 100644 index 000000000000..13e5a863ba38 --- /dev/null +++ b/checker/tests/nullness-records/GenericPair.java @@ -0,0 +1,11 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java16-jdk-skip-test +public record GenericPair(K key, V value) { + + public static void foo() { + GenericPair p = new GenericPair<>("k", null); + // :: error: (dereference.of.nullable) + p.value().toString(); + } +} diff --git a/checker/tests/nullness-records/Issue5200.java b/checker/tests/nullness-records/Issue5200.java new file mode 100644 index 000000000000..4044bf61b0a3 --- /dev/null +++ b/checker/tests/nullness-records/Issue5200.java @@ -0,0 +1,25 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5200 + +import org.checkerframework.checker.nullness.qual.Nullable; + +class Test { + record Foo(@Nullable String bar) { + @Nullable String baz() { + return Math.random() > 0.5 ? bar : null; + } + } + + void main() { + checkEmpty(new Foo("")); + } + + void checkEmpty(Foo foo) { + if (foo.bar() != null && !foo.bar().isEmpty()) { + System.out.println("ok"); + } + // :: error: (dereference.of.nullable) + if (foo.baz() != null && !foo.baz().isEmpty()) { + System.out.println("not ok"); + } + } +} diff --git a/checker/tests/nullness-records/LocalRecords.java b/checker/tests/nullness-records/LocalRecords.java new file mode 100644 index 000000000000..64cf96399da4 --- /dev/null +++ b/checker/tests/nullness-records/LocalRecords.java @@ -0,0 +1,12 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java16-jdk-skip-test +public class LocalRecords { + public static void foo() { + record L(String key, @Nullable Integer value) {} + L a = new L("one", 1); + L b = new L("i", null); + // :: error: (argument.type.incompatible) + L c = new L(null, 6); + } +} diff --git a/checker/tests/nullness-records/NestedRecordTest.java b/checker/tests/nullness-records/NestedRecordTest.java new file mode 100644 index 000000000000..bc9847081137 --- /dev/null +++ b/checker/tests/nullness-records/NestedRecordTest.java @@ -0,0 +1,98 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java16-jdk-skip-test + +public class NestedRecordTest { + + static @NonNull String nn = "foo"; + static @Nullable String nble = null; + static @NonNull String nn2 = "foo"; + static @Nullable String nble2 = null; + + public static class Nested { + public record NPerson(String familyName, @Nullable String maidenName) {} + + void nclient() { + Nested.NPerson np1 = new Nested.NPerson(nn, nn); + Nested.NPerson np2 = new Nested.NPerson(nn, nble); + // :: error: (argument.type.incompatible) + Nested.NPerson np3 = new Nested.NPerson(nble, nn); + // :: error: (argument.type.incompatible) + Nested.NPerson np4 = new Nested.NPerson(nble, nble); + Inner.IPerson ip1 = new Inner.IPerson(nn, nn); + Inner.IPerson ip2 = new Inner.IPerson(nn, nble); + // :: error: (argument.type.incompatible) + Inner.IPerson ip3 = new Inner.IPerson(nble, nn); + // :: error: (argument.type.incompatible) + Inner.IPerson ip4 = new Inner.IPerson(nble, nble); + + nn2 = np2.familyName(); + nble2 = np2.familyName(); + // :: error: (assignment.type.incompatible) + nn2 = np2.maidenName(); + nble2 = np2.maidenName(); + nn2 = ip2.familyName(); + nble2 = ip2.familyName(); + // :: error: (assignment.type.incompatible) + nn2 = ip2.maidenName(); + nble2 = ip2.maidenName(); + } + } + + public class Inner { + public record IPerson(String familyName, @Nullable String maidenName) {} + + void iclient() { + Nested.NPerson np1 = new Nested.NPerson(nn, nn); + Nested.NPerson np2 = new Nested.NPerson(nn, nble); + // :: error: (argument.type.incompatible) + Nested.NPerson np3 = new Nested.NPerson(nble, nn); + // :: error: (argument.type.incompatible) + Nested.NPerson np4 = new Nested.NPerson(nble, nble); + Inner.IPerson ip1 = new Inner.IPerson(nn, nn); + Inner.IPerson ip2 = new Inner.IPerson(nn, nble); + // :: error: (argument.type.incompatible) + Inner.IPerson ip3 = new Inner.IPerson(nble, nn); + // :: error: (argument.type.incompatible) + Inner.IPerson ip4 = new Inner.IPerson(nble, nble); + + nn2 = np2.familyName(); + nble2 = np2.familyName(); + // :: error: (assignment.type.incompatible) + nn2 = np2.maidenName(); + nble2 = np2.maidenName(); + nn2 = ip2.familyName(); + nble2 = ip2.familyName(); + // :: error: (assignment.type.incompatible) + nn2 = ip2.maidenName(); + nble2 = ip2.maidenName(); + } + } + + void client() { + Nested.NPerson np1 = new Nested.NPerson(nn, nn); + Nested.NPerson np2 = new Nested.NPerson(nn, nble); + // :: error: (argument.type.incompatible) + Nested.NPerson np3 = new Nested.NPerson(nble, nn); + // :: error: (argument.type.incompatible) + Nested.NPerson np4 = new Nested.NPerson(nble, nble); + Inner.IPerson ip1 = new Inner.IPerson(nn, nn); + Inner.IPerson ip2 = new Inner.IPerson(nn, nble); + // :: error: (argument.type.incompatible) + Inner.IPerson ip3 = new Inner.IPerson(nble, nn); + // :: error: (argument.type.incompatible) + Inner.IPerson ip4 = new Inner.IPerson(nble, nble); + + nn2 = np2.familyName(); + nble2 = np2.familyName(); + // :: error: (assignment.type.incompatible) + nn2 = np2.maidenName(); + nble2 = np2.maidenName(); + nn2 = ip2.familyName(); + nble2 = ip2.familyName(); + // :: error: (assignment.type.incompatible) + nn2 = ip2.maidenName(); + nble2 = ip2.maidenName(); + } +} diff --git a/checker/tests/nullness-records/NormalizingRecord.java b/checker/tests/nullness-records/NormalizingRecord.java new file mode 100644 index 000000000000..f15b7c434c8d --- /dev/null +++ b/checker/tests/nullness-records/NormalizingRecord.java @@ -0,0 +1,60 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java16-jdk-skip-test + +public class NormalizingRecord {} + +// TODO: Nest the rest of the file within NormalizingRecord when that doesn't crash the Checker +// Framework. + +record NormalizingRecord1(@Nullable String s) { + NormalizingRecord1(String s) { + if (s.equals("")) { + this.s = null; + } else { + this.s = s; + } + } +} + +record NormalizingRecord2(String s) { + NormalizingRecord2(@Nullable String s) { + if (s == null) { + s = ""; + } + this.s = s; + } +} + +record NormalizingRecordIllegalConstructor1(String s) { + NormalizingRecordIllegalConstructor1(@Nullable String s) { + // :: error: (assignment.type.incompatible) + this.s = s; + } +} + +record NormalizingRecordIllegalConstructor2(@Nullable String s) { + NormalizingRecordIllegalConstructor2(String s) { + if (s.equals("")) { + // The formal parametr type is @NonNull, so this assignment to it is illegal. + // :: error: (assignment.type.incompatible) + s = null; + } + this.s = s; + } +} + +class Client { + + // :: error: (argument.type.incompatible) + NormalizingRecord1 nr1_1 = new NormalizingRecord1(null); + NormalizingRecord1 nr1_2 = new NormalizingRecord1(""); + NormalizingRecord1 nr1_3 = new NormalizingRecord1("hello"); + @Nullable String nble = nr1_2.s(); + + NormalizingRecord2 nr2_1 = new NormalizingRecord2(null); + NormalizingRecord2 nr2_2 = new NormalizingRecord2(""); + NormalizingRecord2 nr2_3 = new NormalizingRecord2("hello"); + @NonNull String nn = nr2_1.s(); +} diff --git a/checker/tests/nullness-records/RecordPurity.java b/checker/tests/nullness-records/RecordPurity.java new file mode 100644 index 000000000000..f6a866d10f30 --- /dev/null +++ b/checker/tests/nullness-records/RecordPurity.java @@ -0,0 +1,92 @@ +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.Pure; + +// @below-java17-jdk-skip-test +record RecordPurity(@Nullable String first, @Nullable String second) { + public String checkNullnessOfFields() { + // :: error: (dereference.of.nullable) + return first.toString() + " " + second.toString(); + } + + public String checkNullnessOfAccessors() { + // :: error: (dereference.of.nullable) + return first().toString() + " " + second().toString(); + } + + public String checkPurityOfFields() { + if (first == null || second == null) return ""; + else return "" + first.length() + second.length(); + } + + public static String checkPurityOfDefaultAccessor(RecordPurity r) { + if (r.first() == null || r.second() == null) return ""; + else return "" + r.first().length() + " " + r.second().length(); + } + + public String checkPurityOfDefaultAccessorSelf() { + if (first() == null || second() == null) return ""; + else return "" + first().length() + " " + second().length(); + } + + public String checkPurityOfDefaultAccessorSelf2() { + if (first() == null) return ""; + if (second() == null) return ""; + + return "" + first().length() + " " + second().length(); + } + + public String checkPurityOfDefaultAccessorSelfFirst() { + if (first() == null) return ""; + else return "" + "".length() + first().length(); + } + + public String checkPurityOfDefaultAccessorSelfFirst2() { + if (first() == null) return ""; + else return "" + "".length() + first().length() + first().length(); + } + + @Pure + public @Nullable String pureMethod() { + return ""; + } + + @Pure + public @Nullable String pureMethod2() { + return null; + } + + public String checkPurityOfAccessor4() { + if (pureMethod() == null) return ""; + else return "" + pureMethod().toString(); + } + + public String checkPurityOfAccessor5() { + if (pureMethod() == null || pureMethod2() == null) return ""; + else return "" + pureMethod().length() + pureMethod2().length(); + } + + // An unrelated non-pure method of same name: + public @Nullable String first(java.util.List ss) { + return ss.isEmpty() ? null : ss.get(0); + } + + // An unrelated pure method of same name: + @Pure + public @Nullable String second(java.util.List ss) { + return ss.isEmpty() ? null : ss.get(1); + } + + public String checkPurityOfImpureMethod() { + java.util.List ss = java.util.List.of(); + if (first(ss) == null) return ""; + else + // :: error: (dereference.of.nullable) + return "" + "".length() + first(ss).length(); + } + + public String checkPurityOfPureMethod() { + java.util.List ss = java.util.List.of(); + if (second(ss) == null) return ""; + else return "" + "".length() + second(ss).length(); + } +} diff --git a/checker/tests/nullness-records/RecordPurityGeneric.java b/checker/tests/nullness-records/RecordPurityGeneric.java new file mode 100644 index 000000000000..e139599a8b0e --- /dev/null +++ b/checker/tests/nullness-records/RecordPurityGeneric.java @@ -0,0 +1,47 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java17-jdk-skip-test +record RecordPurityGeneric(A a, B b) { + public String checkNullnessOfFields() { + // :: error: (dereference.of.nullable) + return a.toString() + " " + b.toString(); + } + + public String checkNullnessOfAccessors() { + // :: error: (dereference.of.nullable) + return a().toString() + " " + b().toString(); + } + + public static String checkNullnessOfFields( + RecordPurityGeneric<@Nullable String, @Nullable String> r) { + // :: error: (dereference.of.nullable) + return r.a.toString() + " " + r.b.toString(); + } + + public static String checkNullnessOfAccessors( + RecordPurityGeneric<@Nullable String, @Nullable String> r) { + // :: error: (dereference.of.nullable) + return r.a().toString() + " " + r.b().toString(); + } + + public String checkPurityOfFields() { + if (a == null || b == null) return ""; + else return a.toString() + " " + b.toString(); + } + + public String checkPurityOfFields(RecordPurityGeneric<@Nullable String, @Nullable String> r) { + if (r.a == null || r.b == null) return ""; + else return r.a.toString() + " " + r.b.toString(); + } + + public static String checkPurityOfDefaultAccessor( + RecordPurityGeneric<@Nullable String, @Nullable String> r) { + if (r.a() == null || r.b() == null) return ""; + else return r.a().toString() + " " + r.b().toString(); + } + + public String checkPurityOfDefaultAccessorSelf() { + if (a() == null || b() == null) return ""; + else return a().toString() + " " + b().toString(); + } +} diff --git a/checker/tests/nullness-records/RecordPurityOverride.java b/checker/tests/nullness-records/RecordPurityOverride.java new file mode 100644 index 000000000000..422ef1b4fd76 --- /dev/null +++ b/checker/tests/nullness-records/RecordPurityOverride.java @@ -0,0 +1,38 @@ +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.Pure; + +record RecordPurityOverride(@Nullable String pure, @Nullable String impure) { + @Pure + public @Nullable String pure() { + return pure; + } + + // Note: not @Pure + public @Nullable String impure() { + return impure; + } + + public String checkPurityOfFields() { + if (pure == null || impure == null) return ""; + else return pure.toString() + " " + impure.toString(); + } + + public String checkPurityOfAccessor1() { + if (pure() == null || impure() == null) return ""; + else + // :: error: (dereference.of.nullable) + return pure().toString() + " " + impure().toString(); + } + + public String checkPurityOfAccessor2() { + if (pure() == null) return ""; + else return pure().toString(); + } + + public String checkPurityOfAccessor3() { + if (impure() == null) return ""; + else + // :: error: (dereference.of.nullable) + return impure().toString(); + } +} diff --git a/checker/tests/nullness-reflection/NullnessReflectionExampleTest.java b/checker/tests/nullness-reflection/NullnessReflectionExampleTest.java index 0c25b2fdc4f6..efe11600cbf4 100644 --- a/checker/tests/nullness-reflection/NullnessReflectionExampleTest.java +++ b/checker/tests/nullness-reflection/NullnessReflectionExampleTest.java @@ -1,8 +1,9 @@ -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.common.reflection.qual.MethodVal; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + /** Example used in the reflection resolution section of the Checker Framework manual. */ public class NullnessReflectionExampleTest { @NonNull Location getCurrentLocation() { @@ -11,8 +12,11 @@ public class NullnessReflectionExampleTest { } String getCurrentCity() - throws NoSuchMethodException, SecurityException, IllegalAccessException, - IllegalArgumentException, InvocationTargetException { + throws NoSuchMethodException, + SecurityException, + IllegalAccessException, + IllegalArgumentException, + InvocationTargetException { @MethodVal( className = "NullnessReflectionExampleTest", methodName = "getCurrentLocation", diff --git a/checker/tests/nullness-reflection/NullnessReflectionResolutionTest.java b/checker/tests/nullness-reflection/NullnessReflectionResolutionTest.java index 36cd1f882e10..34c011a5fe26 100644 --- a/checker/tests/nullness-reflection/NullnessReflectionResolutionTest.java +++ b/checker/tests/nullness-reflection/NullnessReflectionResolutionTest.java @@ -1,8 +1,9 @@ -import java.lang.reflect.Method; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.reflection.qual.MethodVal; +import java.lang.reflect.Method; + /** Testing that reflection resolution uses more precise annotations for the Nullness Checker. */ public class NullnessReflectionResolutionTest { @NonNull Object returnNonNull() { diff --git a/checker/tests/nullness-reflection/VoidTest.java b/checker/tests/nullness-reflection/VoidTest.java index ec85c0127848..a6d88928b062 100644 --- a/checker/tests/nullness-reflection/VoidTest.java +++ b/checker/tests/nullness-reflection/VoidTest.java @@ -1,4 +1,4 @@ -class VoidTest { +public class VoidTest { Class test() { return void.class; diff --git a/checker/tests/nullness-safedefaultsbytecode/AnnotatedJdkTest.java b/checker/tests/nullness-safedefaultsbytecode/AnnotatedJdkTest.java new file mode 100644 index 000000000000..8fd522c1d314 --- /dev/null +++ b/checker/tests/nullness-safedefaultsbytecode/AnnotatedJdkTest.java @@ -0,0 +1,16 @@ +import org.checkerframework.checker.nullness.qual.*; + +import java.util.HashMap; +import java.util.Set; + +// There should be no warnings for the following operations +// if the annotated JDK is loaded properly +public class AnnotatedJdkTest { + String toStringTest(Object v) { + return v.toString(); + } + + Set<@KeyFor("#1") String> keySetTest(HashMap map) { + return map.keySet(); + } +} diff --git a/checker/tests/nullness-safedefaultsbytecode/BytecodeDefaultsTest.java b/checker/tests/nullness-safedefaultsbytecode/BytecodeDefaultsTest.java index d27a28057dff..b2265edc9aa1 100644 --- a/checker/tests/nullness-safedefaultsbytecode/BytecodeDefaultsTest.java +++ b/checker/tests/nullness-safedefaultsbytecode/BytecodeDefaultsTest.java @@ -3,7 +3,7 @@ // Tests that the unchecked bytecode defaults option does not // affect defaulting nor suppress errors in source code. -class BytecodeDefaultsTest { +public class BytecodeDefaultsTest { void f() { g(""); } diff --git a/checker/tests/nullness-safedefaultssourcecode/BasicSafeDefaultsTest.java b/checker/tests/nullness-safedefaultssourcecode/BasicSafeDefaultsTest.java index af5346017373..a12e7ebc23aa 100644 --- a/checker/tests/nullness-safedefaultssourcecode/BasicSafeDefaultsTest.java +++ b/checker/tests/nullness-safedefaultssourcecode/BasicSafeDefaultsTest.java @@ -5,7 +5,7 @@ // -AuseConservativeDefaultsForUncheckedCode=source is supplied. @AnnotatedFor("nullness") -class BasicSafeDefaultsTest { +public class BasicSafeDefaultsTest { void m1() { @NonNull Object x1 = SdfuscLib.unannotated(); diff --git a/checker/tests/nullness-safedefaultssourcecode/Issue3449.java b/checker/tests/nullness-safedefaultssourcecode/Issue3449.java new file mode 100644 index 000000000000..417721ecb0a4 --- /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-safedefaultssourcecode/PrimitiveClassLiteral.java b/checker/tests/nullness-safedefaultssourcecode/PrimitiveClassLiteral.java new file mode 100644 index 000000000000..1d70bcd18c47 --- /dev/null +++ b/checker/tests/nullness-safedefaultssourcecode/PrimitiveClassLiteral.java @@ -0,0 +1,33 @@ +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.AnnotatedFor; + +@AnnotatedFor("nullness") +public class PrimitiveClassLiteral { + private static @Nullable Class unwrapPrimitive(Class c) { + if (c == Byte.class) { + return byte.class; + } + if (c == Character.class) { + return char.class; + } + if (c == Short.class) { + return short.class; + } + if (c == Integer.class) { + return int.class; + } + if (c == Long.class) { + return long.class; + } + if (c == Float.class) { + return float.class; + } + if (c == Double.class) { + return double.class; + } + if (c == Boolean.class) { + return boolean.class; + } + return c; + } +} diff --git a/checker/tests/nullness-stubfile/Issue4598.java b/checker/tests/nullness-stubfile/Issue4598.java new file mode 100644 index 000000000000..3b4d568d5e5f --- /dev/null +++ b/checker/tests/nullness-stubfile/Issue4598.java @@ -0,0 +1,16 @@ +// Test case for Issue #4598 + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Objects; + +public class Issue4598 { + + final @Nullable Object d = null; + + public Object foo() { + Objects.requireNonNull(d, "destination"); + // :: error: (return.type.incompatible) + return d; + } +} diff --git a/checker/tests/nullness-stubfile/NullnessStubfileMerge.java b/checker/tests/nullness-stubfile/NullnessStubfileMerge.java index fc26d492c87c..6139ac75eb83 100644 --- a/checker/tests/nullness-stubfile/NullnessStubfileMerge.java +++ b/checker/tests/nullness-stubfile/NullnessStubfileMerge.java @@ -1,6 +1,8 @@ -// warning: StubParser: Package-private method notReal(String) not found in type java.lang.String -// warning: StubParser: Type not found: java.lang.NotARealClass -// warning: StubParser: Type not found: not.real.NotARealClassInNotRealPackage +// warning: stubfile1.astub:(line 16,col 6): package-private method notReal(String) not found in +// type java.lang.String +// warning: stubfile1.astub:(line 19,col 1): type not found: java.lang.NotARealClass +// warning: stubfile1.astub:(line 20,col 1): package not found: not.real +// warning: stubfile1.astub:(line 21,col 1): type not found: not.real.NotARealClassInNotRealPackage import org.checkerframework.checker.nullness.qual.*; diff --git a/checker/tests/nullness-stubfile/requireNonNull.astub b/checker/tests/nullness-stubfile/requireNonNull.astub new file mode 100644 index 000000000000..d8b3debe88b6 --- /dev/null +++ b/checker/tests/nullness-stubfile/requireNonNull.astub @@ -0,0 +1,15 @@ +package java.util; + +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public final class Objects { + + // `@EnsuresNonNull({})` overrides `@EnsuresNonNull("#1")` in the annotated JDK. + + @EnsuresNonNull({}) + public static T requireNonNull(@Nullable T obj); + + @EnsuresNonNull({}) + public static T requireNonNull(@Nullable T obj, String message); +} diff --git a/checker/tests/nullness-warnredundantannotations/AnnoOnTypeVariableCrashCase.java b/checker/tests/nullness-warnredundantannotations/AnnoOnTypeVariableCrashCase.java new file mode 100644 index 000000000000..1f04a655db49 --- /dev/null +++ b/checker/tests/nullness-warnredundantannotations/AnnoOnTypeVariableCrashCase.java @@ -0,0 +1,7 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +public class AnnoOnTypeVariableCrashCase { + @Nullable T test() { + return (@Nullable T) null; + } +} diff --git a/checker/tests/nullness-warnredundantannotations/RedundantAnnoWithDefaultQualifier.java b/checker/tests/nullness-warnredundantannotations/RedundantAnnoWithDefaultQualifier.java new file mode 100644 index 000000000000..1f541b093919 --- /dev/null +++ b/checker/tests/nullness-warnredundantannotations/RedundantAnnoWithDefaultQualifier.java @@ -0,0 +1,19 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; + +@DefaultQualifier(Nullable.class) +public class RedundantAnnoWithDefaultQualifier { + + // :: warning: (redundant.anno) + void foo(@Nullable String message) {} + + // :: warning: (redundant.anno) + @Nullable Integer foo() { + return 5; + } + + void bar(String p) {} + + void baz(@NonNull String p) {} +} diff --git a/checker/tests/nullness-warnredundantannotations/RedundantAnnotation.java b/checker/tests/nullness-warnredundantannotations/RedundantAnnotation.java new file mode 100644 index 000000000000..c7cc62fe0062 --- /dev/null +++ b/checker/tests/nullness-warnredundantannotations/RedundantAnnotation.java @@ -0,0 +1,91 @@ +import org.checkerframework.checker.nullness.qual.*; + +import java.io.InputStream; +import java.util.List; + +/* +Check for redundant annotations in the following locations +(+ means that CF correctly reports an expected warning in this location, + - means that CF doesn't report an expected warning in this location, mostly because the compiler + doesn't store all explicit annotations in the underlying TypeMirrors): + +1. Field + +2. Local Variable + +3. Parameter (includes Receiver Parameter) + +4. Exception Parameter + +5. Resource Variable + +6. Return Type + +7. Enum Constant - (cannot get explicit annotations here) +8. Constructor Result - (cannot get explicit annotations here) +9. Wildcard upper bound and lower bound - (cannot get explicit annotations here) +10. Type Parameter upper bound and lower bound - (cannot get explicit annotations here) +11. Type (class, interface or Enum) - (cannot get explicit annotations here) +12. TypeCast - (cannot get explicit annotations here) +13. InstanceOf - (cannot get explicit annotations here) +14. Object Creation - (cannot get explicit annotations here) +15. Component Type - (cannot get explicit annotations here) +*/ + +@NonNull class RedundantAnnotation< + // TODO :: warning: (redundant.anno) + T extends @Nullable Object> { + + enum InnerEnum { + // TODO :: warning: (redundant.anno) + // :: error: (nullness.on.enum) + @NonNull EXPLICIT, + IMPLICIT, + } + + // :: warning: (redundant.anno) + @NonNull Object f; + + // :: warning: (redundant.anno) + @NonNull Integer foo(InputStream arg) { + // :: warning: (redundant.anno) + @Nullable Object local; + return Integer.valueOf(1); + } + + // :: warning: (redundant.anno) + void foo2(@NonNull Integer i) {} + + // TODO :: warning: (redundant.anno) + // :: error: (nullness.on.constructor) + @NonNull RedundantAnnotation() { + f = new Object(); + } + + // :: error: (nullness.on.receiver) + // :: warning: (redundant.anno) + void bar(@NonNull RedundantAnnotation this, InputStream arg) throws Exception { + // :: warning: (redundant.anno) + try (@Nullable InputStream in = arg) { + + // :: warning: (redundant.anno) + // :: warning: (nullness.on.exception.parameter) + } catch (@NonNull Exception e) { + + } + + // TODO :: warning: (redundant.anno) warning on the upper bound + List l; + + // TODO :: warning: (redundant.anno) warning on the lower bound + // :: error: (type.invalid.super.wildcard) + List l2; + + Object obj = null; + // TODO :: warning: (redundant.anno) for the typecast + String x = (@Nullable String) obj; + + // TODO :: warning: (redundant.anno) for the instanceof + // :: error: (instanceof.nullable) + boolean b = x instanceof @Nullable String; + + // TODO :: warning: (redundant.anno) on the component type + @NonNull String[] strs; + // TODO :: warning: (redundant.anno) on the component type + strs = new @NonNull String[10]; + } +} diff --git a/checker/tests/nullness-warnredundantannotations/RedundantAnnotationOptions.java b/checker/tests/nullness-warnredundantannotations/RedundantAnnotationOptions.java new file mode 100644 index 000000000000..97ec97291f15 --- /dev/null +++ b/checker/tests/nullness-warnredundantannotations/RedundantAnnotationOptions.java @@ -0,0 +1,20 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +class RedundantAnnotationOptions { + private static @Nullable T safeGetAnnotation( + Field f, Class annotationClass) { + @Nullable T annotation; + try { + @Nullable T cast = f.getAnnotation((Class<@NonNull T>) annotationClass); + annotation = cast; + } catch (Exception e) { + annotation = null; + } + + return annotation; + } +} diff --git a/checker/tests/nullness/AnnotatedJdkEqualsTest.java b/checker/tests/nullness/AnnotatedJdkEqualsTest.java index 47376c0f5ca1..d5ea25699aae 100644 --- a/checker/tests/nullness/AnnotatedJdkEqualsTest.java +++ b/checker/tests/nullness/AnnotatedJdkEqualsTest.java @@ -4,16 +4,15 @@ import java.net.URL; -class AnnotatedJdkEqualsTest { +public class AnnotatedJdkEqualsTest { void foo(URL u) { // As of this writing, the annotated JDK does not contain a URL.java file // for the java.net.URL class. // Nonetheless, the following code should type-check. - // This could be handled via inheritance of annotations from - // superclasses either during JDK creation or during type-checking. - // It would be impractical to manually annotate every method in the - // entire JDK: it would be too labor-intensive and there would be - // certain to be some oversights. + // This could be handled via inheritance of annotations from superclasses either during JDK + // creation or during type-checking. It would be impractical to manually annotate every + // method in the entire JDK: it would be too labor-intensive and there would be certain to + // be some oversights. u.equals(null); } } diff --git a/checker/tests/nullness/AnnotatedJdkTest.java b/checker/tests/nullness/AnnotatedJdkTest.java index d914f7a77225..54267f3d2fba 100644 --- a/checker/tests/nullness/AnnotatedJdkTest.java +++ b/checker/tests/nullness/AnnotatedJdkTest.java @@ -1,10 +1,11 @@ // Test case for issue 370: https://github.com/typetools/checker-framework/issues/370 +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.Arrays; import java.util.List; -import org.checkerframework.checker.nullness.qual.Nullable; -class AnnotatedJdkTest { +public class AnnotatedJdkTest { // This code should type-check because of the annotated JDK, which contains: // class Arrays { // public static List asList(T... a); diff --git a/checker/tests/nullness/AnnotatedSupertype.java b/checker/tests/nullness/AnnotatedSupertype.java new file mode 100644 index 000000000000..33b67fa2c082 --- /dev/null +++ b/checker/tests/nullness/AnnotatedSupertype.java @@ -0,0 +1,19 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.io.Serializable; + +public class AnnotatedSupertype { + + class NullableSupertype + // :: error: (nullness.on.supertype) + extends @Nullable Object + // :: error: (nullness.on.supertype) + implements @Nullable Serializable {} + + @NonNull class NonNullSupertype + // :: error: (nullness.on.supertype) + extends @NonNull Object + // :: error: (nullness.on.supertype) + implements @NonNull Serializable {} +} diff --git a/checker/tests/nullness/ArrayCreationNullable.java b/checker/tests/nullness/ArrayCreationNullable.java index 2ef6e33afadf..3a8ca4533f71 100644 --- a/checker/tests/nullness/ArrayCreationNullable.java +++ b/checker/tests/nullness/ArrayCreationNullable.java @@ -93,10 +93,8 @@ void testIntegerArray(@NonNull Integer @NonNull [] p) { ints[0].toString(); } - // The component type of zero-length arrays can - // be non-null - they will always generate - // IndexOutOfBoundsExceptions, but are usually just - // used for the type, e.g. in List.toArray. + // The component type of zero-length arrays can be non-null - they will always generate + // IndexOutOfBoundsExceptions, but are usually just used for the type, e.g. in List.toArray. void testLengthZero() { @NonNull Object @NonNull [] objs; objs = new Object[0]; diff --git a/checker/tests/nullness/ArrayIndex.java b/checker/tests/nullness/ArrayIndex.java index b25bf6769d1d..6be82600be4d 100644 --- a/checker/tests/nullness/ArrayIndex.java +++ b/checker/tests/nullness/ArrayIndex.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; -class ArrayIndex { +public class ArrayIndex { void foo(@Nullable Object[] a, int i) { if (a[i] != null) { a[i].hashCode(); diff --git a/checker/tests/nullness/ArrayInitBug.java b/checker/tests/nullness/ArrayInitBug.java index 1bae579a5728..19fbb8d7fe73 100644 --- a/checker/tests/nullness/ArrayInitBug.java +++ b/checker/tests/nullness/ArrayInitBug.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class ArrayInitBug { +public class ArrayInitBug { @Nullable Object @Nullable [] aa; diff --git a/checker/tests/nullness/ArrayLazyNN.java b/checker/tests/nullness/ArrayLazyNN.java index bc5e4eca56a6..8054f1d23a15 100644 --- a/checker/tests/nullness/ArrayLazyNN.java +++ b/checker/tests/nullness/ArrayLazyNN.java @@ -3,9 +3,9 @@ /* Use @MonotonicNonNull as component type to ensure that null can never be * assigned into a component. Then, after a single iteration over the array, * we can be sure that all elements are non-null. - * TODO: support for (i=0; i < a.lenght.... and change component type to non-null. + * TODO: support for (i=0; i < a.length.... and change component type to non-null. */ -class ArrayLazyNN { +public class ArrayLazyNN { void test1() { @MonotonicNonNull Object[] o1 = new @MonotonicNonNull Object[10]; o1[0] = new Object(); diff --git a/checker/tests/nullness/ArrayNew.java b/checker/tests/nullness/ArrayNew.java index 6a7232ebb5b3..3afa35fd1ca9 100644 --- a/checker/tests/nullness/ArrayNew.java +++ b/checker/tests/nullness/ArrayNew.java @@ -1,7 +1,8 @@ -import java.util.Collection; import org.checkerframework.checker.nullness.qual.*; -class ArrayNew { +import java.util.Collection; + +public class ArrayNew { void m(Collection seq1) { Integer[] seq1_array = new @NonNull Integer[] {5}; Integer[] seq2_array = seq1.toArray(new @NonNull Integer[0]); diff --git a/checker/tests/nullness/ArrayRefs.java b/checker/tests/nullness/ArrayRefs.java index c63a8d028f7d..934c6ecc8be6 100644 --- a/checker/tests/nullness/ArrayRefs.java +++ b/checker/tests/nullness/ArrayRefs.java @@ -1,6 +1,7 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.util.Arrays; import java.util.List; -import org.checkerframework.checker.nullness.qual.*; public class ArrayRefs { diff --git a/checker/tests/nullness/AssertAfter2.java b/checker/tests/nullness/AssertAfter2.java index 469bf2905ae7..1cdaebb9e990 100644 --- a/checker/tests/nullness/AssertAfter2.java +++ b/checker/tests/nullness/AssertAfter2.java @@ -1,8 +1,9 @@ -import java.util.HashMap; -import java.util.List; import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import java.util.HashMap; +import java.util.List; + public class AssertAfter2 { public class Graph { diff --git a/checker/tests/nullness/AssertIfClient.java b/checker/tests/nullness/AssertIfClient.java index 13ef63c91ff9..1a2e808c270e 100644 --- a/checker/tests/nullness/AssertIfClient.java +++ b/checker/tests/nullness/AssertIfClient.java @@ -49,10 +49,9 @@ boolean rpcResponseReceived() { return response != null; } - // Returns non-null if the response has been received, null otherwise; - // but an @AssertNonNullIfNonNull annotation would states the converse, - // that if the result is non-null then the response hs been received. - // See rpcResponseReceived. + // Returns non-null if the response has been received, null otherwise; but an + // @AssertNonNullIfNonNull annotation would states the converse, that if the result is non-null + // then the response hs been received. See rpcResponseReceived. @Pure @Nullable Object rpcResponse() { return response; diff --git a/checker/tests/nullness/AssertMethodTest.java b/checker/tests/nullness/AssertMethodTest.java new file mode 100644 index 000000000000..c8bd1b813263 --- /dev/null +++ b/checker/tests/nullness/AssertMethodTest.java @@ -0,0 +1,87 @@ +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.AssertMethod; +import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.dataflow.qual.SideEffectFree; + +public class AssertMethodTest { + @interface Anno { + Class value(); + } + + @AssertMethod + @SideEffectFree + void assertMethod(boolean b) { + if (!b) { + throw new RuntimeException(); + } + } + + void test1(@Nullable Object o) { + assertMethod(o != null); + o.toString(); + } + + @Nullable Object getO() { + return null; + } + + void test2() { + assertMethod(getO() != null); + // :: error: dereference.of.nullable + getO().toString(); // error + } + + @Pure + @Nullable Object getPureO() { + return ""; + } + + void test3() { + assertMethod(getPureO() != null); + getPureO().toString(); + } + + @Nullable Object field = null; + + void test4() { + assertMethod(field != null); + field.toString(); + } + + String getError() { + field = null; + return "error"; + } + + @AssertMethod(value = RuntimeException.class, parameter = 2) + @SideEffectFree + void assertMethod(Object p, boolean b, Object error) { + if (!b) { + throw new RuntimeException(); + } + } + + void test5() { + assertMethod(getError(), field != null, getError()); + // :: error: dereference.of.nullable + field.toString(); // error + } + + void test5b() { + assertMethod(getError(), field != null, ""); + field.toString(); + } + + @AssertMethod(isAssertFalse = true) + @SideEffectFree + void assertFalse(boolean b) { + if (b) { + throw new RuntimeException(); + } + } + + void test6() { + assertFalse(field == null); + field.toString(); + } +} diff --git a/checker/tests/nullness/AssertNonNullIfNonNullTest.java b/checker/tests/nullness/AssertNonNullIfNonNullTest.java index 51c01fd6b8fc..e6a957ae63f7 100644 --- a/checker/tests/nullness/AssertNonNullIfNonNullTest.java +++ b/checker/tests/nullness/AssertNonNullIfNonNullTest.java @@ -3,8 +3,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.qual.Pure; -// Re-enable when @AssertNonNullIfNonNull checking is enhanced -// @skip-test +// @skip-test Re-enable when @AssertNonNullIfNonNull checking is enhanced public class AssertNonNullIfNonNullTest { diff --git a/checker/tests/nullness/AssertTwice.java b/checker/tests/nullness/AssertTwice.java index 094fb0a78da3..cfad53777d63 100644 --- a/checker/tests/nullness/AssertTwice.java +++ b/checker/tests/nullness/AssertTwice.java @@ -17,7 +17,7 @@ private void assertTwiceWithUse() { String methodDeclaration = null; assert methodDeclaration != null : "@AssumeAssertion(nullness)"; methodDeclaration.toString(); - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) assert methodDeclaration != null; methodDeclaration = null; } diff --git a/checker/tests/nullness/Asserts.java b/checker/tests/nullness/Asserts.java index fc05711d2a51..3e4db381dac0 100644 --- a/checker/tests/nullness/Asserts.java +++ b/checker/tests/nullness/Asserts.java @@ -37,7 +37,7 @@ boolean pairwiseEqual(boolean @Nullable [] seq1, boolean @Nullable [] seq2) { if (!sameLength(seq1, seq2)) { return false; } - if (ne(seq1[0], seq2[0])) ; + if (ne(seq1[0], seq2[0])) {} return true; } @@ -58,13 +58,13 @@ void testAssertBad(boolean @Nullable [] seq1, boolean @Nullable [] seq2) { assert sameLength(seq1, seq2); // the @EnsuresNonNullIf is not taken from the assert, as it doesn't contain "nullness" // :: error: (accessing.nullable) - if (seq1[0]) ; + if (seq1[0]) {} } void testAssertGood(boolean @Nullable [] seq1, boolean @Nullable [] seq2) { assert sameLength(seq1, seq2) : "@AssumeAssertion(nullness)"; // The explanation contains "nullness" and we therefore take the additional assumption - if (seq1[0]) ; + if (seq1[0]) {} } void testAssertAnd(@Nullable Object o) { diff --git a/checker/tests/nullness/BinaryOp.java b/checker/tests/nullness/BinaryOp.java index 5f3eda7a0dc7..64a96590dc83 100644 --- a/checker/tests/nullness/BinaryOp.java +++ b/checker/tests/nullness/BinaryOp.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.initialization.qual.*; import org.checkerframework.checker.nullness.qual.*; -class BinaryOp { +public class BinaryOp { void test(@UnknownInitialization Object obj) { throw new Error("" + obj); } diff --git a/checker/tests/nullness/BinarySearch.java b/checker/tests/nullness/BinarySearch.java index 4aa04ab31cd0..d1be308f32f2 100644 --- a/checker/tests/nullness/BinarySearch.java +++ b/checker/tests/nullness/BinarySearch.java @@ -1,12 +1,15 @@ -import org.checkerframework.checker.nullness.qual.*; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; -// Allow searching through nullable arrays components -// and for nullable keys. -class BinarySearch { +// Searching through nullable array components +// and for nullable keys is forbidden. +public class BinarySearch { @Nullable Object @NonNull [] arr = {"a", "b", null}; void search(@Nullable Object key) { + // :: error: (argument.type.incompatible) int res = java.util.Arrays.binarySearch(arr, key); + // :: error: (argument.type.incompatible) res = java.util.Arrays.binarySearch(arr, 0, 4, key); } } diff --git a/checker/tests/nullness/Bug102.java b/checker/tests/nullness/Bug102.java index a4f97caf95fa..c178b8d653e9 100644 --- a/checker/tests/nullness/Bug102.java +++ b/checker/tests/nullness/Bug102.java @@ -13,6 +13,7 @@ void bug2() { m(c); } + // :: error: (invalid.polymorphic.qualifier) <@org.checkerframework.checker.nullness.qual.PolyNull S> void m( final C<@org.checkerframework.checker.nullness.qual.PolyNull String> a) {} } diff --git a/checker/tests/nullness/Bug103.java b/checker/tests/nullness/Bug103.java index 9eb271a33b3a..64f984d645be 100644 --- a/checker/tests/nullness/Bug103.java +++ b/checker/tests/nullness/Bug103.java @@ -14,7 +14,7 @@ public class Bug103 extends HR { // Crazy: replace IG.C with IG.C+"" and it compiles // Crazy: remove final and it compiles // Crazy: replace with new String[22] and it compiles - // Crazy: reduce to less than 5 distinct values and it compiles (replace IG.D with IG.C) + // Crazy: reduce to less than 5 distinct values and it compiles (replace IG.D with IG.C) final String[] ids = { IG.C, IG.C, IG.C, IG.C, IG.C, IG.C, IG.C, IG.C, IG.C, IG.C, IG.C, IG.C, IG.C, IG.C, IG.C, IG.C, IG.C, IG.C, IG.D, IG.E, IG.F, IG.G diff --git a/checker/tests/nullness/CallSuper.java b/checker/tests/nullness/CallSuper.java index 25e5ab490ef9..6f3ecbb97890 100644 --- a/checker/tests/nullness/CallSuper.java +++ b/checker/tests/nullness/CallSuper.java @@ -1,6 +1,7 @@ -import java.io.*; import org.checkerframework.checker.nullness.qual.*; +import java.io.*; + class MyFilterInputStream { MyFilterInputStream(InputStream in) {} } diff --git a/checker/tests/nullness/CastsNullness.java b/checker/tests/nullness/CastsNullness.java index ab24f7dc523b..cb5deb34eece 100644 --- a/checker/tests/nullness/CastsNullness.java +++ b/checker/tests/nullness/CastsNullness.java @@ -96,7 +96,7 @@ void m() { } void testSafeCasts() { - // :: error: (type.invalid.annotations.on.use) + // :: error: (nullness.on.primitive) Integer x = (@Nullable int) 1; } } diff --git a/checker/tests/nullness/ChainAssignment.java b/checker/tests/nullness/ChainAssignment.java index 891ac587dffb..45557c6c4af1 100644 --- a/checker/tests/nullness/ChainAssignment.java +++ b/checker/tests/nullness/ChainAssignment.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; -class ChainAssignment { +public class ChainAssignment { @Nullable Object a; @Nullable Object b; Object x = new Object(); diff --git a/checker/tests/nullness/CompoundAssign.java b/checker/tests/nullness/CompoundAssign.java index b8ce8d273a5e..e9cf4075d09b 100644 --- a/checker/tests/nullness/CompoundAssign.java +++ b/checker/tests/nullness/CompoundAssign.java @@ -1,4 +1,4 @@ -class CompoundAssign { +public class CompoundAssign { void m(String args) { String arg = ""; for (int ii = 0; ii < args.length(); ii++) { diff --git a/checker/tests/nullness/ConditionalPolyNull.java b/checker/tests/nullness/ConditionalPolyNull.java new file mode 100644 index 000000000000..69cad88a08a2 --- /dev/null +++ b/checker/tests/nullness/ConditionalPolyNull.java @@ -0,0 +1,40 @@ +// Test case for EISOP issue #519 +// https://github.com/eisop/checker-framework/issues/519 +import org.checkerframework.checker.nullness.qual.PolyNull; + +class ConditionalPolyNull { + @PolyNull String toLowerCaseA(@PolyNull String text) { + return text == null ? null : text.toLowerCase(); + } + + @PolyNull String toLowerCaseB(@PolyNull String text) { + return text != null ? text.toLowerCase() : null; + } + + @PolyNull String toLowerCaseC(@PolyNull String text) { + // :: error: (dereference.of.nullable) + // :: error: (return.type.incompatible) + return text == null ? text.toLowerCase() : null; + } + + @PolyNull String toLowerCaseD(@PolyNull String text) { + // :: error: (return.type.incompatible) + // :: error: (dereference.of.nullable) + return text != null ? null : text.toLowerCase(); + } + + @PolyNull String foo(@PolyNull String param) { + if (param != null) { + // @PolyNull is really @NonNull, so change + // the type of param to @NonNull. + return param.toString(); + } + if (param == null) { + // @PolyNull is really @Nullable, so change + // the type of param to @Nullable. + param = null; + return null; + } + return param; + } +} diff --git a/checker/tests/nullness/ConstructorPostcondition.java b/checker/tests/nullness/ConstructorPostcondition.java new file mode 100644 index 000000000000..3068540e5d04 --- /dev/null +++ b/checker/tests/nullness/ConstructorPostcondition.java @@ -0,0 +1,22 @@ +import org.checkerframework.checker.nullness.qual.*; + +public class ConstructorPostcondition { + + class Box { + @Nullable Object f; + } + + @EnsuresNonNull("#1.f") + // :: error: (contracts.postcondition.not.satisfied) + ConstructorPostcondition(Box b) {} + + @EnsuresNonNull("#1.f") + ConstructorPostcondition(Box b, Object o) { + b.f = o; + } + + void foo(Box b) { + ConstructorPostcondition x = new ConstructorPostcondition(b, "x"); + b.f.hashCode(); + } +} diff --git a/checker/tests/nullness/CopyOfArray.java b/checker/tests/nullness/CopyOfArray.java index 22b3ca168ec3..f93a99161630 100644 --- a/checker/tests/nullness/CopyOfArray.java +++ b/checker/tests/nullness/CopyOfArray.java @@ -1,13 +1,14 @@ -import java.util.Arrays; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Arrays; + public class CopyOfArray { - protected void makeCopy(Object[] args) { - // :: error: (assignment.type.incompatible) - Object[] copy = Arrays.copyOf(args, args.length); - } + protected void makeCopy(Object[] args, int i) { + Object[] copyExact1 = Arrays.copyOf(args, args.length); + @Nullable Object[] copyExact2 = Arrays.copyOf(args, args.length); - protected void makeCopyGood(Object[] args) { - @Nullable Object @Nullable [] copy = Arrays.copyOf(args, args.length); + // :: error: (assignment.type.incompatible) + Object[] copyInexact1 = Arrays.copyOf(args, i); + @Nullable Object[] copyInexact2 = Arrays.copyOf(args, i); } } diff --git a/checker/tests/nullness/DaikonEnhancedFor.java b/checker/tests/nullness/DaikonEnhancedFor.java new file mode 100644 index 000000000000..a0534eb45718 --- /dev/null +++ b/checker/tests/nullness/DaikonEnhancedFor.java @@ -0,0 +1,31 @@ +// Based on a false positive encountered in Daikon related to common CFGs +// by the KeyFor checker. + +import org.checkerframework.checker.nullness.qual.*; + +import java.util.*; + +class DaikonEnhancedFor { + + @SuppressWarnings("nullness") + Map> cmap = null; + + @SuppressWarnings("nullness") + Object[] getObjects() { + return null; + } + + void process(@KeyFor("this.cmap") Object super_c) { + @SuppressWarnings("keyfor") // the loop below makes all these keys to cmap + @KeyFor("this.cmap") Object[] clazzes = getObjects(); + // go through all of the classes and intialize the map + for (Object cd : clazzes) { + cmap.put(cd, new TreeSet<@KeyFor("cmap") Object>()); + } + // go through the list again and put in the derived class information + for (Object cd : clazzes) { + Set<@KeyFor("this.cmap") Object> derived = cmap.get(super_c); + derived.add(cd); + } + } +} diff --git a/checker/tests/nullness/DaikonEnhancedForNoThis.java b/checker/tests/nullness/DaikonEnhancedForNoThis.java new file mode 100644 index 000000000000..e4f954ade275 --- /dev/null +++ b/checker/tests/nullness/DaikonEnhancedForNoThis.java @@ -0,0 +1,33 @@ +// Based on a false positive encountered in Daikon related to common CFGs +// for subcheckers. This shows the original version of the Daikon code, +// before it was modified to avoid missing standardization. See DaikonEnhancedFor.java +// for the "fixed" version. There are no longer expected errors in this test. + +import org.checkerframework.checker.nullness.qual.*; + +import java.util.*; + +class DaikonEnhancedForNoThis { + + @SuppressWarnings("nullness") + Map> cmap = null; + + @SuppressWarnings("nullness") + Object[] getObjects() { + return null; + } + + void process(@KeyFor("this.cmap") Object super_c) { + @SuppressWarnings("keyfor") // the loop below makes all these keys to cmap + @KeyFor("cmap") Object[] clazzes = getObjects(); + // go through all of the classes and intialize the map + for (Object cd : clazzes) { + cmap.put(cd, new TreeSet<@KeyFor("cmap") Object>()); + } + // go through the list again and put in the derived class information + for (Object cd : clazzes) { + Set<@KeyFor("this.cmap") Object> derived = cmap.get(super_c); + derived.add(cd); + } + } +} diff --git a/checker/tests/nullness/DefaultAnnotation.java b/checker/tests/nullness/DefaultAnnotation.java index a14f8a4d188a..c983c260110f 100644 --- a/checker/tests/nullness/DefaultAnnotation.java +++ b/checker/tests/nullness/DefaultAnnotation.java @@ -1,9 +1,10 @@ -import java.util.Iterator; -import java.util.List; import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.framework.qual.DefaultQualifier; import org.checkerframework.framework.qual.TypeUseLocation; +import java.util.Iterator; +import java.util.List; + public class DefaultAnnotation { public void testNoDefault() { diff --git a/checker/tests/nullness/DefaultInterface.java b/checker/tests/nullness/DefaultInterface.java index 22c765be8814..d5751902de40 100644 --- a/checker/tests/nullness/DefaultInterface.java +++ b/checker/tests/nullness/DefaultInterface.java @@ -7,7 +7,7 @@ interface Foo { } @DefaultQualifier(org.checkerframework.checker.nullness.qual.NonNull.class) -class DefaultInterface { +public class DefaultInterface { public void test() { diff --git a/checker/tests/nullness/DefaultLoops.java b/checker/tests/nullness/DefaultLoops.java index 6f8851badd26..5eafbcc39863 100644 --- a/checker/tests/nullness/DefaultLoops.java +++ b/checker/tests/nullness/DefaultLoops.java @@ -1,6 +1,7 @@ -import java.util.LinkedList; import org.checkerframework.checker.nullness.qual.*; +import java.util.LinkedList; + class MyTS extends LinkedList {} public class DefaultLoops { diff --git a/checker/tests/nullness/DefaultingForEach.java b/checker/tests/nullness/DefaultingForEach.java new file mode 100644 index 000000000000..923aace8ba46 --- /dev/null +++ b/checker/tests/nullness/DefaultingForEach.java @@ -0,0 +1,45 @@ +// Test case for issue #4248: https://github.com/typetools/checker-framework/issues/4248 + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; + +import java.util.List; + +class DefaultForEach { + @DefaultQualifier(Nullable.class) + Object @NonNull [] foo() { + return new Object[] {null}; + } + + void bar() { + for (Object p : foo()) { + // :: error: dereference.of.nullable + p.toString(); + } + } + + @DefaultQualifier(Nullable.class) + @NonNull List foo2() { + throw new RuntimeException(); + } + + void bar2() { + for (Object p : foo2()) { + // :: error: (dereference.of.nullable) + p.toString(); + } + } + + double[][] foo3() { + throw new RuntimeException(); + } + + void bar3() { + for (double[] pa : foo3()) { + for (Double p : pa) { + p.toString(); + } + } + } +} diff --git a/checker/tests/nullness/DefaultsNullness.java b/checker/tests/nullness/DefaultsNullness.java index 9f61e04c795f..0c21ec58cc57 100644 --- a/checker/tests/nullness/DefaultsNullness.java +++ b/checker/tests/nullness/DefaultsNullness.java @@ -13,7 +13,7 @@ void test(@UnknownInitialization DefaultsNullness para, @Initialized DefaultsNul DefaultsNullness d; d = null; // null okay (default == @Nullable) - d = comm; // committed okay (default == @Initialized) + d = comm; // initialized okay (default == @Initialized) d.hashCode(); } } diff --git a/checker/tests/nullness/DotClass.java b/checker/tests/nullness/DotClass.java index a1cac7b9f32a..058d19c9aa36 100644 --- a/checker/tests/nullness/DotClass.java +++ b/checker/tests/nullness/DotClass.java @@ -1,7 +1,8 @@ -import java.lang.annotation.Annotation; import org.checkerframework.checker.nullness.qual.NonNull; -class DotClass { +import java.lang.annotation.Annotation; + +public class DotClass { void test() { doStuff(NonNull.class); diff --git a/checker/tests/nullness/EisopIssue308.java b/checker/tests/nullness/EisopIssue308.java new file mode 100644 index 000000000000..b5e613a8d6f5 --- /dev/null +++ b/checker/tests/nullness/EisopIssue308.java @@ -0,0 +1,16 @@ +// Test case for eisop issue #308: +// https://github.com/eisop/checker-framework/issues/308 + +class EisopIssue308Other { + abstract class Inner implements Runnable {} +} + +class EisopIssue308 { + EisopIssue308Other other = new EisopIssue308Other(); + + EisopIssue308Other.Inner foo() { + return other.new Inner() { + public void run() {} + }; + } +} diff --git a/checker/tests/nullness/EmptyConstructor.java b/checker/tests/nullness/EmptyConstructor.java index 4db946bb2df7..c37bd11c1173 100644 --- a/checker/tests/nullness/EmptyConstructor.java +++ b/checker/tests/nullness/EmptyConstructor.java @@ -3,7 +3,7 @@ import org.checkerframework.dataflow.qual.*; -class SuperClass { +public class SuperClass { static int count = 0; public SuperClass() { @@ -16,9 +16,8 @@ public SuperClass() { // side-effect-free method // public EmptyConstructor() {} // ^ -// because there's no obvious call. The message key should be changed to -// one whose message is "call to non-side-effect-free superclass -// constructor not allowed in side-effect-free constructor" +// because there's no obvious call. The message key should be changed to one whose message is "call +// to non-side-effect-free superclass constructor not allowed in side-effect-free constructor" public class EmptyConstructor extends SuperClass { @SideEffectFree diff --git a/checker/tests/nullness/EnsuresKeyForOverriding.java b/checker/tests/nullness/EnsuresKeyForOverriding.java new file mode 100644 index 000000000000..9d87eb34d715 --- /dev/null +++ b/checker/tests/nullness/EnsuresKeyForOverriding.java @@ -0,0 +1,24 @@ +import org.checkerframework.checker.nullness.qual.EnsuresKeyFor; + +import java.util.Map; + +public class EnsuresKeyForOverriding { + static class MyClass { + Object field = new Object(); + } + + MyClass o = new MyClass(); + + @EnsuresKeyFor(value = "#1.field", map = "#2") + void method(MyClass o, Map map) { + map.put(o.field, "Hello"); + } + + static class SubEnsuresKeyForOverriding extends EnsuresKeyForOverriding { + @Override + @EnsuresKeyFor(value = "#1.field", map = "#2") + void method(MyClass q, Map subMap) { + super.method(q, subMap); + } + } +} diff --git a/checker/tests/nullness/EnsuresNonNullIfTest.java b/checker/tests/nullness/EnsuresNonNullIfTest.java index 16fdd513dd5d..a7b88dd07928 100644 --- a/checker/tests/nullness/EnsuresNonNullIfTest.java +++ b/checker/tests/nullness/EnsuresNonNullIfTest.java @@ -30,7 +30,7 @@ public static void fromDirNeg(File1 dbdir) { // TODO: Have a way of saying the property holds no matter what value is used in a given expression. -class File1 { +public class File1 { @EnsuresNonNullIf( result = true, expression = { diff --git a/checker/tests/nullness/EnumStaticBlock.java b/checker/tests/nullness/EnumStaticBlock.java index c203092df501..dc056ffd9c14 100644 --- a/checker/tests/nullness/EnumStaticBlock.java +++ b/checker/tests/nullness/EnumStaticBlock.java @@ -1,7 +1,7 @@ import java.util.ArrayList; import java.util.List; -class EnumStaticBlock { +public class EnumStaticBlock { public enum Section { ME, OTHER; diff --git a/checker/tests/nullness/Enums.java b/checker/tests/nullness/Enums.java deleted file mode 100644 index 5fcf8e6ff61c..000000000000 --- a/checker/tests/nullness/Enums.java +++ /dev/null @@ -1,21 +0,0 @@ -import org.checkerframework.checker.nullness.qual.*; - -public class Enums { - - enum MyEnum { - A, - B, - C, - D - } - // :: error: (assignment.type.incompatible) - MyEnum myEnum = null; // invalid - @Nullable MyEnum myNullableEnum = null; - - void testLocalEnum() { - // Enums are allowed to be null: no error here. - MyEnum myNullableEnum = null; - // :: error: (assignment.type.incompatible) - @NonNull MyEnum myEnum = null; // invalid - } -} diff --git a/checker/tests/nullness/EnumsNullness.java b/checker/tests/nullness/EnumsNullness.java new file mode 100644 index 000000000000..203884ba4fdb --- /dev/null +++ b/checker/tests/nullness/EnumsNullness.java @@ -0,0 +1,39 @@ +import org.checkerframework.checker.nullness.qual.*; + +public class EnumsNullness { + + enum MyEnum { + A, + B, + C, + D + } + + // :: error: (assignment.type.incompatible) + MyEnum myEnum = null; // invalid + @Nullable MyEnum myNullableEnum = null; + + void testLocalEnum() { + // Enums are allowed to be null: no error here. + MyEnum myNullableEnum = null; + // :: error: (assignment.type.incompatible) + @NonNull MyEnum myEnum = null; // invalid + } + + enum EnumBadAnnos { + A, + // :: error: (nullness.on.enum) + @NonNull B, + // :: error: (nullness.on.enum) + @Nullable C, + D; + + public static final EnumBadAnnos A2 = A; + public static final @NonNull EnumBadAnnos B2 = B; + public static final @Nullable EnumBadAnnos C2 = C; + + @Nullable String method() { + return null; + } + } +} diff --git a/checker/tests/nullness/ExceptionParam.java b/checker/tests/nullness/ExceptionParam.java index 00e495dc74b8..bcbdbf32430b 100644 --- a/checker/tests/nullness/ExceptionParam.java +++ b/checker/tests/nullness/ExceptionParam.java @@ -4,7 +4,7 @@ /** Exception parameters are non-null, even if the default is nullable. */ @DefaultQualifier(org.checkerframework.checker.nullness.qual.Nullable.class) -class ExceptionParam { +public class ExceptionParam { void exc1() { try { } catch (AssertionError e) { @@ -14,6 +14,7 @@ void exc1() { void exc2() { try { + // :: warning: (nullness.on.exception.parameter) } catch (@NonNull AssertionError e) { @NonNull Object o = e; } @@ -21,7 +22,7 @@ void exc2() { void exc3() { try { - // :: error: (type.invalid.annotations.on.use) + // :: warning: (nullness.on.exception.parameter) } catch (@Nullable AssertionError e) { @NonNull Object o = e; } diff --git a/checker/tests/nullness/Exceptions.java b/checker/tests/nullness/Exceptions.java index c77d600f90c4..8e77f9457a71 100644 --- a/checker/tests/nullness/Exceptions.java +++ b/checker/tests/nullness/Exceptions.java @@ -12,7 +12,7 @@ void nonnullExceptionParam(@NonNull Exception m) { void exception(@Nullable Exception m) { try { - + throwException(); } catch (Exception e) { e.getClass(); // :: error: (dereference.of.nullable) @@ -37,6 +37,7 @@ void throwException() { void reassignException() { try { + throwException(); } catch (RuntimeException e) { // :: error: (assignment.type.incompatible) e = null; diff --git a/checker/tests/nullness/ExpressionsNullness.java b/checker/tests/nullness/ExpressionsNullness.java index 4076d5415be9..824c42b72330 100644 --- a/checker/tests/nullness/ExpressionsNullness.java +++ b/checker/tests/nullness/ExpressionsNullness.java @@ -1,3 +1,6 @@ +import org.checkerframework.checker.nullness.qual.*; +import org.checkerframework.framework.qual.DefaultQualifier; + import java.io.*; import java.util.Date; import java.util.HashMap; @@ -6,8 +9,6 @@ import java.util.List; import java.util.Set; import java.util.regex.*; -import org.checkerframework.checker.nullness.qual.*; -import org.checkerframework.framework.qual.DefaultQualifier; @DefaultQualifier(NonNull.class) public class ExpressionsNullness { diff --git a/checker/tests/nullness/FenumExplicit.java b/checker/tests/nullness/FenumExplicit.java new file mode 100644 index 000000000000..7290269205d3 --- /dev/null +++ b/checker/tests/nullness/FenumExplicit.java @@ -0,0 +1,23 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5165 + +// @skip-test until the bug is fixed + +import org.checkerframework.checker.nullness.qual.Nullable; + +class EnumExplicit { + + public static enum EnumWithMethod { + VALUE { + @Override + public void call(@Nullable String string) { + // Null string is acceptable in this function. + } + }; + + public abstract void call(String string); + } + + public static void main(String[] args) { + EnumWithMethod.VALUE.call(null); + } +} diff --git a/checker/tests/nullness/FieldWithAnnotatedLambda.java b/checker/tests/nullness/FieldWithAnnotatedLambda.java new file mode 100644 index 000000000000..80151cb9f77d --- /dev/null +++ b/checker/tests/nullness/FieldWithAnnotatedLambda.java @@ -0,0 +1,18 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.function.Function; + +class FieldWithAnnotatedLambda { + Function f1 = (@Nullable Object in) -> in; + + class Outer { + class Inner {} + } + + Function f2 = (Outer.@Nullable Inner in) -> in; + + // Lambda parameter is defaulted to be @NonNull, raising an error + // for the lambda parameter type. + // :: error: (lambda.param.type.incompatible) + Function f3 = (Outer.Inner in) -> in; +} diff --git a/checker/tests/nullness/FinalFields.java b/checker/tests/nullness/FinalFields.java index e5e6645512c9..e385837b5526 100644 --- a/checker/tests/nullness/FinalFields.java +++ b/checker/tests/nullness/FinalFields.java @@ -2,18 +2,18 @@ class Upper { @Nullable String fs = "NonNull init"; - final @Nullable String ffs = "NonNull init"; + private final @Nullable String ffs = "NonNull init"; void access() { // Error, because non-final field type is not refined // :: error: (dereference.of.nullable) fs.hashCode(); - // Final field in the same class is refined + // private final field is refined ffs.hashCode(); } } -class FinalFields { +public class FinalFields { public void foo(Upper u) { // Error, because final field in different class is not refined // :: error: (dereference.of.nullable) @@ -35,13 +35,13 @@ public void local() { class Lower { @Nullable String fs = "NonNull init, too"; - final @Nullable String ffs = "NonNull init, too"; + private final @Nullable String ffs = "NonNull init, too"; void access() { // Error, because non-final field type is not refined // :: error: (dereference.of.nullable) fs.hashCode(); - // Final field in the same class is refined + // private final field is refined ffs.hashCode(); } } diff --git a/checker/tests/nullness/FinalVar.java b/checker/tests/nullness/FinalVar.java index 7d23965b0ed7..1aba0708f943 100644 --- a/checker/tests/nullness/FinalVar.java +++ b/checker/tests/nullness/FinalVar.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; -class FinalVar { +public class FinalVar { public Object pptIterator() { // Only test with (effectively) final variables; Java only permits final or diff --git a/checker/tests/nullness/FinalVar2.java b/checker/tests/nullness/FinalVar2.java index 66b186f3db98..af05348ff8c0 100644 --- a/checker/tests/nullness/FinalVar2.java +++ b/checker/tests/nullness/FinalVar2.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.nullness.qual.*; -class FinalVar2 { +public class FinalVar2 { static Object method1(@Nullable Object arg) { final Object tmp = arg; diff --git a/checker/tests/nullness/FinalVar3.java b/checker/tests/nullness/FinalVar3.java index 47084b22f198..0a024047ec63 100644 --- a/checker/tests/nullness/FinalVar3.java +++ b/checker/tests/nullness/FinalVar3.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class FinalVar3 { +public class FinalVar3 { static Object method1(@Nullable Object arg) { final Object tmp = arg; diff --git a/checker/tests/nullness/FlowCompound.java b/checker/tests/nullness/FlowCompound.java index 3a97aa6b488a..67846105552e 100644 --- a/checker/tests/nullness/FlowCompound.java +++ b/checker/tests/nullness/FlowCompound.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class FlowCompound { +public class FlowCompound { @org.checkerframework.dataflow.qual.Pure public boolean equals(@Nullable Object o) { diff --git a/checker/tests/nullness/FlowCompoundConcatenation.java b/checker/tests/nullness/FlowCompoundConcatenation.java index e81d0734cbb9..8ab7c81a7a6c 100644 --- a/checker/tests/nullness/FlowCompoundConcatenation.java +++ b/checker/tests/nullness/FlowCompoundConcatenation.java @@ -1,4 +1,4 @@ -class FlowCompoundConcatenation { +public class FlowCompoundConcatenation { static String getNonNullString() { return ""; } diff --git a/checker/tests/nullness/FlowConditions.java b/checker/tests/nullness/FlowConditions.java index d1ea8217addf..bef7c35fa56f 100644 --- a/checker/tests/nullness/FlowConditions.java +++ b/checker/tests/nullness/FlowConditions.java @@ -1,10 +1,11 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; -import org.checkerframework.checker.nullness.qual.*; -class FlowConditions { +public class FlowConditions { void m(@Nullable Object x, @Nullable Object y) { if (x == null || y == null) { // :: error: (dereference.of.nullable) diff --git a/checker/tests/nullness/FlowExpressionParsingBug.java b/checker/tests/nullness/FlowExpressionParsingBug.java index deee2bf3f0be..b2acf73c9765 100644 --- a/checker/tests/nullness/FlowExpressionParsingBug.java +++ b/checker/tests/nullness/FlowExpressionParsingBug.java @@ -1,10 +1,11 @@ -import javax.swing.JMenuBar; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.RequiresNonNull; +import javax.swing.JMenuBar; + public abstract class FlowExpressionParsingBug { - //// Check that flow expressions with explicit and implicit 'this' work + //// Check that JavaExpressions with explicit and implicit 'this' work protected @Nullable JMenuBar menuBar = null; @@ -14,7 +15,7 @@ public void addFavorite() {} @RequiresNonNull("this.menuBar") public void addFavorite1() {} - //// Check flow expressions for static fields with different ways to access the field + //// Check JavaExpressions for static fields with different ways to access the field static @Nullable String i = null; diff --git a/checker/tests/nullness/FlowField.java b/checker/tests/nullness/FlowField.java index ded9925d5228..9e2a29d08cd6 100644 --- a/checker/tests/nullness/FlowField.java +++ b/checker/tests/nullness/FlowField.java @@ -27,7 +27,7 @@ void test1() { nonnull.toString(); } - static final String nonnull = new String(); + private static final String nonnull = new String(); class A { protected String field = null; diff --git a/checker/tests/nullness/FlowLoop.java b/checker/tests/nullness/FlowLoop.java index 1e25ba1b3a8b..6f91788a7ea3 100644 --- a/checker/tests/nullness/FlowLoop.java +++ b/checker/tests/nullness/FlowLoop.java @@ -112,14 +112,14 @@ void testBreak(@Nullable Object o) { void testSimpleNull() { String r1 = null; - while (r1 != null) ; + while (r1 != null) {} // :: error: (dereference.of.nullable) r1.toString(); // error } void testMulticheckNull() { String r1 = null; - while (r1 != null && r1.equals("m")) ; + while (r1 != null && r1.equals("m")) {} // :: error: (dereference.of.nullable) r1.toString(); // error } diff --git a/checker/tests/nullness/FlowNegation.java b/checker/tests/nullness/FlowNegation.java index b358929d149b..cc3716447a46 100644 --- a/checker/tests/nullness/FlowNegation.java +++ b/checker/tests/nullness/FlowNegation.java @@ -9,7 +9,7 @@ void testSimpleValid() { void testCase1() { String s = "m"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (s != null) { } else { // nothing to do @@ -19,7 +19,7 @@ void testCase1() { void testCase2() { String s = "m"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (s == null) { } else { // nothing to do @@ -29,7 +29,7 @@ void testCase2() { void testInvalidCase1() { String s = "m"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (s != null) { s = null; } else { @@ -41,7 +41,7 @@ void testInvalidCase1() { void testInvalidCase2() { String s = "m"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (s != null) { // nothing to do } else { @@ -58,21 +58,21 @@ void testSimpleValidTernary() { void testTernaryCase1() { String s = "m"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) Object m = (s != null) ? "m" : "n"; s.toString(); } void testTernaryCase2() { String s = "m"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) Object m = (s == null) ? "m" : "n"; s.toString(); } void testTernaryInvalidCase1() { String s = "m"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) Object m = (s != null) ? (s = null) : "n"; // :: error: (dereference.of.nullable) s.toString(); // error @@ -80,7 +80,7 @@ void testTernaryInvalidCase1() { void testTernaryInvalidCase2() { String s = "m"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) Object m = (s != null) ? "m" : (s = null); // :: error: (dereference.of.nullable) s.toString(); // error diff --git a/checker/tests/nullness/FlowNonThis.java b/checker/tests/nullness/FlowNonThis.java index f438c8fee8fa..22dbf3c145b2 100644 --- a/checker/tests/nullness/FlowNonThis.java +++ b/checker/tests/nullness/FlowNonThis.java @@ -1,7 +1,8 @@ -import java.io.*; import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import java.io.*; + public class FlowNonThis { @Nullable String c; diff --git a/checker/tests/nullness/FlowNullness.java b/checker/tests/nullness/FlowNullness.java index 6554fc3c951b..308e39cec1ec 100644 --- a/checker/tests/nullness/FlowNullness.java +++ b/checker/tests/nullness/FlowNullness.java @@ -6,7 +6,7 @@ public void testIf() { String str = "foo"; @NonNull String a; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (str != null) { a = str; } @@ -20,7 +20,7 @@ public void testIfNoBlock() { String str = "foo"; @NonNull String a; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (str != null) { a = str; } @@ -34,7 +34,7 @@ public void testElse() { String str = "foo"; @NonNull String a; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (str == null) { testAssert(); } else { @@ -50,7 +50,7 @@ public void testElseNoBlock() { String str = "foo"; @NonNull String a; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (str == null) { testAssert(); } else { @@ -65,7 +65,7 @@ public void testElseNoBlock() { public void testReturnIf() { String str = "foo"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (str == null) { testAssert(); return; @@ -81,7 +81,7 @@ public void testReturnIf() { public void testReturnElse() { String str = "foo"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (str != null) { testAssert(); } else { @@ -98,7 +98,7 @@ public void testReturnElse() { public void testThrowIf() { String str = "foo"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (str == null) { testAssert(); throw new RuntimeException("foo"); @@ -114,7 +114,7 @@ public void testThrowIf() { public void testThrowElse() { String str = "foo"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (str != null) { testAssert(); } else { @@ -131,7 +131,7 @@ public void testThrowElse() { public void testAssert() { String str = "foo"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) assert str != null; @NonNull String a = str; @@ -144,7 +144,7 @@ public void testAssert() { public void testWhile() { String str = "foo"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) while (str != null) { @NonNull String a = str; break; @@ -184,7 +184,7 @@ public void testNew() { public void testExit() { String str = "foo"; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (str == null) { System.exit(0); } @@ -278,8 +278,7 @@ public boolean equals(@Nullable Object o) { void while_set_and_test(@Nullable String s) { String line; - // imagine "s" is "reader.readLine()" (but avoid use of libraries - // in unit tests) + // imagine "s" is "reader.readLine()" (but avoid use of libraries in unit tests) while ((line = s) != null) { line.trim(); } @@ -326,7 +325,7 @@ void test() { public void add_modified(double[] a, int count) { // System.out.println ("common: " + ArraysMDE.toString (a)); - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (a == null) { return; } else if (intersect == null) { diff --git a/checker/tests/nullness/FlowSelf.java b/checker/tests/nullness/FlowSelf.java index 55a7d1f5eaf2..10ba4e5d3a43 100644 --- a/checker/tests/nullness/FlowSelf.java +++ b/checker/tests/nullness/FlowSelf.java @@ -1,13 +1,13 @@ import org.checkerframework.checker.nullness.qual.*; -class FlowSelf { +public class FlowSelf { void test(@Nullable String s) { if (s == null) { return; } - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) assert s != null; s = s.substring(1); diff --git a/checker/tests/nullness/ForEachMin.java b/checker/tests/nullness/ForEachMin.java index fc7b2c3d6b87..a3a27b9842f3 100644 --- a/checker/tests/nullness/ForEachMin.java +++ b/checker/tests/nullness/ForEachMin.java @@ -1,6 +1,7 @@ +import org.checkerframework.checker.nullness.qual.NonNull; + import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.nullness.qual.NonNull; class MyTop { List children = new ArrayList<>(); @@ -14,7 +15,7 @@ void init_hierarchy_new() { @NonNull Object o1 = ppt.children; - for (String rel : ppt.children) ; + for (String rel : ppt.children) {} @NonNull Object o2 = ppt.children; } diff --git a/checker/tests/nullness/FullyQualifiedAnnotation.java b/checker/tests/nullness/FullyQualifiedAnnotation.java index 7b2f10ac621e..438a9f2d6cf0 100644 --- a/checker/tests/nullness/FullyQualifiedAnnotation.java +++ b/checker/tests/nullness/FullyQualifiedAnnotation.java @@ -1,6 +1,6 @@ import java.util.Iterator; -class FullyQualifiedAnnotation { +public class FullyQualifiedAnnotation { void client1(Iterator i) { @SuppressWarnings("nullness") diff --git a/checker/tests/nullness/GeneralATFStore.java b/checker/tests/nullness/GeneralATFStore.java index 58da8c00500d..e282e3a8502c 100644 --- a/checker/tests/nullness/GeneralATFStore.java +++ b/checker/tests/nullness/GeneralATFStore.java @@ -1,6 +1,5 @@ // Test case for a mysterious compilation failure. -// The underlying reason was that the GeneralATF -// tried storing defaulted declaration annotations. +// The underlying reason was that the GeneralATF tried storing defaulted declaration annotations. import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/checker/tests/nullness/GetConstantStr.java b/checker/tests/nullness/GetConstantStr.java index 269c766a9c26..d40a7a3be9c2 100644 --- a/checker/tests/nullness/GetConstantStr.java +++ b/checker/tests/nullness/GetConstantStr.java @@ -1,6 +1,6 @@ public class GetConstantStr { public static void get_constant_str(Object obj) { - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) assert obj != null; } } diff --git a/checker/tests/nullness/GetInterfacesPurity.java b/checker/tests/nullness/GetInterfacesPurity.java new file mode 100644 index 000000000000..1241779c4b17 --- /dev/null +++ b/checker/tests/nullness/GetInterfacesPurity.java @@ -0,0 +1,11 @@ +import org.checkerframework.dataflow.qual.Pure; + +public class GetInterfacesPurity { + + @Pure + public static boolean isSubtypeTestMethod(Class sub, Class sup) { + // :: error: (purity.not.deterministic.call) + Class[] interfaces = sub.getInterfaces(); + return interfaces.length == 0; + } +} diff --git a/checker/tests/nullness/GetProperty.java b/checker/tests/nullness/GetProperty.java index 247638994be3..9c27c55e40f1 100644 --- a/checker/tests/nullness/GetProperty.java +++ b/checker/tests/nullness/GetProperty.java @@ -1,6 +1,7 @@ -import java.util.Properties; import org.checkerframework.checker.nullness.qual.*; +import java.util.Properties; + public class GetProperty { @NonNull Object nno = new Object(); diff --git a/checker/tests/nullness/GetRefArg.java b/checker/tests/nullness/GetRefArg.java index 95f6c418edaf..2f398aca44a7 100644 --- a/checker/tests/nullness/GetRefArg.java +++ b/checker/tests/nullness/GetRefArg.java @@ -1,10 +1,11 @@ -import java.lang.reflect.*; import org.checkerframework.checker.nullness.qual.*; +import java.lang.reflect.*; + public class GetRefArg { private void get_ref_arg(Constructor constructor) throws Exception { Object val = constructor.newInstance(); - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) assert val != null; } } diff --git a/checker/tests/nullness/HierarchicalInit.java b/checker/tests/nullness/HierarchicalInit.java index 8b3f341b4829..eb85da392752 100644 --- a/checker/tests/nullness/HierarchicalInit.java +++ b/checker/tests/nullness/HierarchicalInit.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class HierarchicalInit { +public class HierarchicalInit { String a; diff --git a/checker/tests/nullness/ImplementInterface.java b/checker/tests/nullness/ImplementInterface.java index 6fcebb115083..610c165307fd 100644 --- a/checker/tests/nullness/ImplementInterface.java +++ b/checker/tests/nullness/ImplementInterface.java @@ -4,7 +4,7 @@ interface TestInterface { public char @Nullable [] getChars(); } -class ImplementInterface implements TestInterface { +public class ImplementInterface implements TestInterface { @Override public char @Nullable [] getChars() { return null; diff --git a/checker/tests/nullness/Imports2.java b/checker/tests/nullness/Imports2.java index 64de7cf4d2a7..655b5225514a 100644 --- a/checker/tests/nullness/Imports2.java +++ b/checker/tests/nullness/Imports2.java @@ -1,5 +1,3 @@ -// import org.checkerframework.checker.nullness.qual.EnsuresNonNull; - public class Imports2 { void call() { java.util.Arrays.asList("m", 1); diff --git a/checker/tests/nullness/InferListParam.java b/checker/tests/nullness/InferListParam.java index 8020d79657b0..981823e98d85 100644 --- a/checker/tests/nullness/InferListParam.java +++ b/checker/tests/nullness/InferListParam.java @@ -1,7 +1,7 @@ import java.util.Collections; import java.util.List; -class InferListParam { +public class InferListParam { List fieldValues; InferListParam() { diff --git a/checker/tests/nullness/InferNullType.java b/checker/tests/nullness/InferNullType.java index c7be64e36dfa..8f9eacbab8ba 100644 --- a/checker/tests/nullness/InferNullType.java +++ b/checker/tests/nullness/InferNullType.java @@ -1,5 +1,5 @@ // Version of framework/tests/all-systems/InferNullType.java with expected Nullness Checker warnings -class InferNullType { +public class InferNullType { T toInfer(T input) { return input; diff --git a/checker/tests/nullness/InferTypeArgsConditionalExpression.java b/checker/tests/nullness/InferTypeArgsConditionalExpression.java new file mode 100644 index 000000000000..b0103be21301 --- /dev/null +++ b/checker/tests/nullness/InferTypeArgsConditionalExpression.java @@ -0,0 +1,19 @@ +// Used to cause crash similar to the one reported in #579 +// https://github.com/typetools/checker-framework/issues/579 +// Issue 579 test case is in checker/tests/nullness/java8/Issue579.java +// A similar test case appears in +// checker-framework/framework/tests/all-systems/InferTypeArgsConditionalExpression.java + +import org.checkerframework.checker.nullness.qual.NonNull; + +public class InferTypeArgsConditionalExpression { + + public void foo(Generic real, Generic other, boolean flag) { + // :: error: (type.argument.type.incompatible) + bar(flag ? real : other); + } + + <@NonNull Q extends @NonNull Object> void bar(Generic parm) {} + + interface Generic {} +} diff --git a/checker/tests/nullness/InferTypeArgsCondtionalExpression.java b/checker/tests/nullness/InferTypeArgsCondtionalExpression.java deleted file mode 100644 index 9dbd21d1ab23..000000000000 --- a/checker/tests/nullness/InferTypeArgsCondtionalExpression.java +++ /dev/null @@ -1,19 +0,0 @@ -// Used to cause crash similar to the one reported in #579 -// https://github.com/typetools/checker-framework/issues/579 -// Issue 579 test case is in checker/tests/nullness/java8/Issue579.java -// A similar test case appears in -// checker-framework/framework/tests/all-systems/InferTypeArgsConditionalExpression.java - -import org.checkerframework.checker.nullness.qual.NonNull; - -class InferTypeArgsConditionalExpression { - - public void foo(Generic real, Generic other, boolean flag) { - // :: error: (type.argument.type.incompatible) - bar(flag ? real : other); - } - - <@NonNull Q extends @NonNull Object> void bar(Generic parm) {} - - interface Generic {} -} diff --git a/checker/tests/nullness/InitSuppressWarnings.java b/checker/tests/nullness/InitSuppressWarnings.java index 010a0c9b61e6..5c28f43b7704 100644 --- a/checker/tests/nullness/InitSuppressWarnings.java +++ b/checker/tests/nullness/InitSuppressWarnings.java @@ -4,7 +4,7 @@ public class InitSuppressWarnings { private void init_vars(@UnderInitialization(Object.class) InitSuppressWarnings this) { - @SuppressWarnings({"nullness"}) + @SuppressWarnings({"initialization"}) @Initialized InitSuppressWarnings initializedThis = this; } } diff --git a/checker/tests/nullness/InitializedField.java b/checker/tests/nullness/InitializedField.java index 658f4bbc6395..86976a6274ba 100644 --- a/checker/tests/nullness/InitializedField.java +++ b/checker/tests/nullness/InitializedField.java @@ -1,8 +1,9 @@ -import java.util.Stack; import org.checkerframework.checker.initialization.qual.*; import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.dataflow.qual.*; +import java.util.Stack; + public final class InitializedField { private Stack stack; diff --git a/checker/tests/nullness/InnerCrash.java b/checker/tests/nullness/InnerCrash.java new file mode 100644 index 000000000000..3aafe26b9d0e --- /dev/null +++ b/checker/tests/nullness/InnerCrash.java @@ -0,0 +1,14 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +class InnerCrash { + class Inner {} + + static @Nullable InnerCrash getInnerCrash() { + return null; + } + + static void foo() { + // :: error: (dereference.of.nullable) + Object o = getInnerCrash().new Inner(); + } +} diff --git a/checker/tests/nullness/IsEmptyPoll.java b/checker/tests/nullness/IsEmptyPoll.java index 67ed4b6ca41c..30f618aca32e 100644 --- a/checker/tests/nullness/IsEmptyPoll.java +++ b/checker/tests/nullness/IsEmptyPoll.java @@ -3,11 +3,12 @@ // @skip-test until the issue is fixed -import java.util.ArrayList; -import java.util.Queue; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.ArrayList; +import java.util.Queue; + public final class IsEmptyPoll extends ArrayList { void mNonNull(Queue q) { diff --git a/checker/tests/nullness/Issue1027.java b/checker/tests/nullness/Issue1027.java index 61f33482fd8c..b593b288a4ed 100644 --- a/checker/tests/nullness/Issue1027.java +++ b/checker/tests/nullness/Issue1027.java @@ -1,17 +1,18 @@ // Test case for Issue 1027: // https://github.com/typetools/checker-framework/issues/1027 -// Use -J-XX:MaxJavaStackTraceDepth=1000000 as parameter +// Use -J-XX:MaxJavaStackTraceDepth=1000000 as parameter // to javac to see a longer stacktrace. +import org.checkerframework.checker.nullness.qual.KeyFor; + import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.checkerframework.checker.nullness.qual.KeyFor; -class Issue1027 { +public class Issue1027 { // Stand-alone reproduction @@ -19,7 +20,7 @@ class Repr { void bar(Function p) {} } - @SuppressWarnings("nullness") + @SuppressWarnings({"nullness", "keyfor"}) Repr<@KeyFor("this") String> foo() { return null; } diff --git a/checker/tests/nullness/Issue1059.java b/checker/tests/nullness/Issue1059.java index d1fd2dd50527..df652f8720ec 100644 --- a/checker/tests/nullness/Issue1059.java +++ b/checker/tests/nullness/Issue1059.java @@ -4,7 +4,7 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.Nullable; -class Issue1059 { +public class Issue1059 { @Nullable Object f; @EnsuresNonNull({"f"}) diff --git a/checker/tests/nullness/Issue1102.java b/checker/tests/nullness/Issue1102.java index 4d871d0452a5..c3fba7d9d062 100644 --- a/checker/tests/nullness/Issue1102.java +++ b/checker/tests/nullness/Issue1102.java @@ -15,15 +15,15 @@ static Issue1102Decl } } -// Don't bother initializing the fields properly -@SuppressWarnings("initialization.fields.uninitialized") class Issue1102Use { + @SuppressWarnings("initialization.field.uninitialized") U f; - @Nullable U g; + + @Nullable U g = null; void bar() { Issue1102Decl d = Issue1102Decl.newInstance(f); - // :: error: (argument.type.incompatible) + // :: error: (type.argument.type.incompatible) d = Issue1102Decl.newInstance(g); } } diff --git a/checker/tests/nullness/Issue1147.java b/checker/tests/nullness/Issue1147.java index c6ec60067177..65ae001664aa 100644 --- a/checker/tests/nullness/Issue1147.java +++ b/checker/tests/nullness/Issue1147.java @@ -2,7 +2,7 @@ import java.util.StringJoiner; -class Issue1147 { +public class Issue1147 { public static void main(String[] args) { diff --git a/checker/tests/nullness/Issue1307.java b/checker/tests/nullness/Issue1307.java index c52fe0c04fc6..a5978de12d8e 100644 --- a/checker/tests/nullness/Issue1307.java +++ b/checker/tests/nullness/Issue1307.java @@ -6,7 +6,7 @@ @DefaultQualifier(value = Nullable.class, locations = TypeUseLocation.FIELD) @DefaultQualifier(value = Nullable.class, locations = TypeUseLocation.PARAMETER) -class Issue1307 { +public class Issue1307 { Object nullableField = null; void perl(Integer a) { diff --git a/checker/tests/nullness/Issue1406.java b/checker/tests/nullness/Issue1406.java index c01e016c4a49..1f33f7bd2fc2 100644 --- a/checker/tests/nullness/Issue1406.java +++ b/checker/tests/nullness/Issue1406.java @@ -1,11 +1,12 @@ // Test case for Issue 1406 // https://github.com/typetools/checker-framework/issues/1406 -import java.util.ArrayList; -import java.util.List; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.dataflow.qual.Pure; +import java.util.ArrayList; +import java.util.List; + @SuppressWarnings({"purity", "contracts.postcondition.not.satisfied"}) // Only test parsing public class Issue1406 { diff --git a/checker/tests/nullness/Issue1522.java b/checker/tests/nullness/Issue1522.java index cf51eeb99d50..032e1dee1939 100644 --- a/checker/tests/nullness/Issue1522.java +++ b/checker/tests/nullness/Issue1522.java @@ -1,10 +1,11 @@ // Test case for Issue 1522 // https://github.com/typetools/checker-framework/issues/1522 -import java.util.Vector; import org.checkerframework.checker.nullness.qual.Nullable; -class Issue1522 { +import java.util.Vector; + +public class Issue1522 { void copyInto(String p) {} void bar() { diff --git a/checker/tests/nullness/Issue1555.java b/checker/tests/nullness/Issue1555.java index 38352c2a2a88..451989b72abd 100644 --- a/checker/tests/nullness/Issue1555.java +++ b/checker/tests/nullness/Issue1555.java @@ -1,10 +1,10 @@ // Test case for Issue 1555 // https://github.com/typetools/checker-framework/issues/1555 -import org.checkerframework.checker.nullness.NullnessUtil; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.util.NullnessUtil; -class Issue1555 { +public class Issue1555 { private @MonotonicNonNull String x; diff --git a/checker/tests/nullness/Issue1628.java b/checker/tests/nullness/Issue1628.java index 1904ceade03e..363571381cc7 100644 --- a/checker/tests/nullness/Issue1628.java +++ b/checker/tests/nullness/Issue1628.java @@ -4,7 +4,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.qual.Pure; -class Issue1628> implements Issue1628R { +public class Issue1628> implements Issue1628R { public boolean isEmpty() { return false; diff --git a/checker/tests/nullness/Issue1797.java b/checker/tests/nullness/Issue1797.java index f611eb9fb845..f1bcf42dfe8d 100644 --- a/checker/tests/nullness/Issue1797.java +++ b/checker/tests/nullness/Issue1797.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; -class Issue1797 { +public class Issue1797 { void fooReturn(@Nullable Object o) { try { return; diff --git a/checker/tests/nullness/Issue1847.java b/checker/tests/nullness/Issue1847.java index 48aa880979d5..f4ff9f855205 100644 --- a/checker/tests/nullness/Issue1847.java +++ b/checker/tests/nullness/Issue1847.java @@ -1,8 +1,9 @@ +import org.checkerframework.checker.nullness.qual.KeyFor; + import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.function.Function; -import org.checkerframework.checker.nullness.qual.KeyFor; public class Issue1847 { final Map map = new HashMap<>(); diff --git a/checker/tests/nullness/Issue1847B.java b/checker/tests/nullness/Issue1847B.java index 20522e469e11..5366b3e72772 100644 --- a/checker/tests/nullness/Issue1847B.java +++ b/checker/tests/nullness/Issue1847B.java @@ -1,6 +1,7 @@ -import java.util.function.Function; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.function.Function; + public class Issue1847B { public void test1() { // :: error: (dereference.of.nullable) diff --git a/checker/tests/nullness/Issue1922.java b/checker/tests/nullness/Issue1922.java index 861aa8357cbd..097d187b0746 100644 --- a/checker/tests/nullness/Issue1922.java +++ b/checker/tests/nullness/Issue1922.java @@ -1,10 +1,11 @@ -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + public class Issue1922 { // A method to find a K in the collection and return it, or return null. public static @Nullable K findKey(Collection<@NonNull K> keys, Object target) { diff --git a/checker/tests/nullness/Issue1949.java b/checker/tests/nullness/Issue1949.java index ab63801c9d37..6de4e8618d9f 100644 --- a/checker/tests/nullness/Issue1949.java +++ b/checker/tests/nullness/Issue1949.java @@ -1,6 +1,7 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.nullness.qual.*; public class Issue1949 { public interface Base {} diff --git a/checker/tests/nullness/Issue1981.java b/checker/tests/nullness/Issue1981.java index 2f4d98fd6ad5..98dd8b92426a 100644 --- a/checker/tests/nullness/Issue1981.java +++ b/checker/tests/nullness/Issue1981.java @@ -3,7 +3,7 @@ import java.util.List; -class Issue1981 { +public class Issue1981 { void test(List ids) { for (List l : func2(func1(ids))) {} diff --git a/checker/tests/nullness/Issue1983.java b/checker/tests/nullness/Issue1983.java index 20d963b4776f..80ea4fcad59e 100644 --- a/checker/tests/nullness/Issue1983.java +++ b/checker/tests/nullness/Issue1983.java @@ -1,13 +1,14 @@ // Test case for Issue 1983: // https://github.com/typetools/checker-framework/issues/1983 +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.List; import java.util.function.Function; -import org.checkerframework.checker.nullness.qual.Nullable; -@SuppressWarnings("initialization.fields.uninitialized") -class Issue1983 { +public class Issue1983 { + @SuppressWarnings("initialization.field.uninitialized") Converter converter; void test(List params) { diff --git a/checker/tests/nullness/Issue2031.java b/checker/tests/nullness/Issue2031.java index 491825474fa9..81c68709e690 100644 --- a/checker/tests/nullness/Issue2031.java +++ b/checker/tests/nullness/Issue2031.java @@ -1,6 +1,7 @@ -import java.util.Map; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Map; + public class Issue2031 { public interface InterfaceA {} @@ -9,7 +10,6 @@ public interface InterfaceB {} abstract static class OperatorSection & InterfaceB> { C makeExpression(Map expressions) { @Nullable C e = expressions.get(""); - // :: warning: (known.nonnull) if (e != null) { return e; } else { @@ -32,7 +32,6 @@ static class BinaryOperatorSection & B expressions) { @Nullable EXPRESSION e = expressions.get(""); - // :: warning: (known.nonnull) if (e != null) { return e; } else { diff --git a/checker/tests/nullness/Issue2048.java b/checker/tests/nullness/Issue2048.java index f7f09a4fed36..e99e61623e3b 100644 --- a/checker/tests/nullness/Issue2048.java +++ b/checker/tests/nullness/Issue2048.java @@ -4,19 +4,27 @@ // There are two versions: // framework/tests/all-systems // checker/tests/nullness +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; -class Issue2048 { +public class Issue2048 { interface Foo {} - interface Fooer {} + static class Fooer {} class UseNbl { - // T by default is @Nullable and therefore doesn't - // fulfill the bound of R. - // :: error: (type.argument.type.incompatible) void foo(Fooer fooer) {} } + // :: error: (type.argument.type.incompatible) + Fooer<@Nullable Foo> nblFooer = new Fooer<>(); + Fooer<@NonNull Foo> nnFooer = new Fooer<>(); + + void use(UseNbl<@Nullable Foo> useNbl) { + useNbl.foo(nblFooer); + useNbl.foo(nnFooer); + } + class UseNN { void foo(Fooer fooer) {} } diff --git a/checker/tests/nullness/Issue2171.java b/checker/tests/nullness/Issue2171.java index 6f6a4a70e598..bf93a890dabc 100644 --- a/checker/tests/nullness/Issue2171.java +++ b/checker/tests/nullness/Issue2171.java @@ -1,6 +1,7 @@ -import java.util.List; import org.checkerframework.checker.nullness.qual.*; +import java.util.List; + public class Issue2171 { static void varArgsMethod(@PolyNull Object... args) {} diff --git a/checker/tests/nullness/Issue2247.java b/checker/tests/nullness/Issue2247.java index 0244340067cc..4bc729457cc9 100644 --- a/checker/tests/nullness/Issue2247.java +++ b/checker/tests/nullness/Issue2247.java @@ -1,14 +1,15 @@ // This is a test case for issue 2247: // https://github.com/typetools/checker-framework/issues/2247 -import java.util.List; -import java.util.Map; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.List; +import java.util.Map; + public class Issue2247 { - static @NonNull class DeclaredClass {} + @NonNull static class DeclaredClass {} class ValidUseType { diff --git a/checker/tests/nullness/Issue2407.java b/checker/tests/nullness/Issue2407.java index b0948f38cbd9..1510ccbd2f1a 100644 --- a/checker/tests/nullness/Issue2407.java +++ b/checker/tests/nullness/Issue2407.java @@ -2,7 +2,7 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.RequiresNonNull; -class Issue2407 { +public class Issue2407 { @RequiresNonNull("#1") void setMessage(String message) {} diff --git a/checker/tests/nullness/Issue2432.java b/checker/tests/nullness/Issue2432.java index 35dca6e1c475..e35a633e07e4 100644 --- a/checker/tests/nullness/Issue2432.java +++ b/checker/tests/nullness/Issue2432.java @@ -1,12 +1,13 @@ // Test case for issue 2432: // https://github.com/typetools/checker-framework/issues/2432 -import java.util.List; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.PolyNull; -class Issue2432 { +import java.util.List; + +public class Issue2432 { void jdkAnnotation(List<@PolyNull Object> nl, @Nullable Object no, @PolyNull Object po) { // :: error: (argument.type.incompatible) @@ -88,8 +89,7 @@ void lubWithReceiver( pc.tripleAdd(no, nno, nno); } - // ensure poly annotations from declaration is handled separately from poly from other - // context + // ensure poly annotations from declaration is handled separately from poly from other context void declarationPolyInParameter( TypeArgClass<@PolyNull Object> pc, @Nullable Object no, @NonNull Object nno) { // No error diff --git a/checker/tests/nullness/Issue2432b.java b/checker/tests/nullness/Issue2432b.java index 49d132b0de9b..804b32eeba08 100644 --- a/checker/tests/nullness/Issue2432b.java +++ b/checker/tests/nullness/Issue2432b.java @@ -1,11 +1,12 @@ // Ensure correct handling of type parameters and arrays. // https://github.com/typetools/checker-framework/issues/2432 +import org.checkerframework.checker.nullness.qual.PolyNull; + import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.nullness.qual.PolyNull; -class Issue2432b { +public class Issue2432b { void objectAsTypeArg() { List objs = new ArrayList<>(); // no error diff --git a/checker/tests/nullness/Issue2470.java b/checker/tests/nullness/Issue2470.java index 39272dc01b7e..d5017e6d4cab 100644 --- a/checker/tests/nullness/Issue2470.java +++ b/checker/tests/nullness/Issue2470.java @@ -14,6 +14,14 @@ public Example setS(String s1) { return this; } + // TODO: Support "return" in Java Expression syntax. + // @EnsuresNonNull("return.s") + @EnsuresNonNull("this.s") + public Example setS2(String s1) { + this.s = s1; + return this; + } + @RequiresNonNull("this.s") public void print() { System.out.println(this.s.toString()); @@ -27,6 +35,28 @@ static void buggy() { } static void ok() { - new Example().setS("test").print(); + Example e = new Example(); + e.setS("test"); + e.print(); + } + + static void buggy2() { + new Example() + .setS("test") + // :: error:(contracts.precondition.not.satisfied) + .print(); + } + + // TODO: These should be legal, once "return" is supported in Java Expression syntax. + // of a method. + /* + static void ok3() { + Example e = new Example().setS2("test"); + e.print(); + } + + static void ok2() { + new Example().setS2("test").print(); } + */ } diff --git a/checker/tests/nullness/Issue2564.java b/checker/tests/nullness/Issue2564.java index 558d5134422e..769f7442e0aa 100644 --- a/checker/tests/nullness/Issue2564.java +++ b/checker/tests/nullness/Issue2564.java @@ -1,12 +1,13 @@ +import org.checkerframework.checker.nullness.qual.KeyFor; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.nullness.qual.KeyFor; public abstract class Issue2564 { public enum EnumType { - // :: error: (assignment.type.incompatible) + // :: error: (enum.declaration.type.incompatible) @KeyFor("myMap") MY_KEY, - // :: error: (assignment.type.incompatible) + // :: error: (enum.declaration.type.incompatible) @KeyFor("enumMap") ENUM_KEY; private static final Map enumMap = new HashMap<>(); diff --git a/checker/tests/nullness/Issue2587.java b/checker/tests/nullness/Issue2587.java index 08f80aa8b1e4..bc1db838c019 100644 --- a/checker/tests/nullness/Issue2587.java +++ b/checker/tests/nullness/Issue2587.java @@ -1,8 +1,12 @@ +import org.checkerframework.checker.nullness.qual.KeyFor; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.nullness.qual.KeyFor; -@SuppressWarnings("assignment.type.incompatible") // These warnings are not relevant +@SuppressWarnings({ + "enum.declaration.type.incompatible", + "assignment.type.incompatible" +}) // These warnings are not relevant public abstract class Issue2587 { public enum EnumType { // :: error: (expression.unparsable.type.invalid) diff --git a/checker/tests/nullness/Issue2619.java b/checker/tests/nullness/Issue2619.java index 01c030051e1c..f72e1b177a8d 100644 --- a/checker/tests/nullness/Issue2619.java +++ b/checker/tests/nullness/Issue2619.java @@ -1,12 +1,19 @@ -import java.util.HashMap; -import java.util.Map; import org.checkerframework.checker.nullness.qual.EnsuresKeyForIf; import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.dataflow.qual.Pure; +import java.util.HashMap; +import java.util.Map; + public class Issue2619 { public Map map = new HashMap<>(); + void m00(Aux aux1) { + if (aux1.hasValue(Aux.MINIMUM_VALUE)) { + @KeyFor({"aux1.map"}) String s1 = Aux.MINIMUM_VALUE; + } + } + void m01(Aux aux1, Aux aux2) { if (aux1.hasValue(Aux.MINIMUM_VALUE) && aux2.hasValue(Aux.MINIMUM_VALUE)) { @KeyFor({"aux1.map", "aux2.map"}) String s1 = Aux.MINIMUM_VALUE; diff --git a/checker/tests/nullness/Issue2619b.java b/checker/tests/nullness/Issue2619b.java index ae0777e2b390..8eda3e0e3e77 100644 --- a/checker/tests/nullness/Issue2619b.java +++ b/checker/tests/nullness/Issue2619b.java @@ -1,14 +1,15 @@ -import java.util.HashMap; -import java.util.Map; import org.checkerframework.checker.nullness.qual.EnsuresKeyForIf; import org.checkerframework.checker.nullness.qual.KeyFor; +import java.util.HashMap; +import java.util.Map; + public class Issue2619b { public Map map = new HashMap<>(); void m01(Aux aux1, Aux aux2) { if (aux1.hasValue(Aux.MINIMUM_VALUE) && aux2.hasValue(Aux.MINIMUM_VALUE)) { - // hasValue is not sideeffect free, so the @KeyFor("aux1.map") is cleared rather than + // hasValue is not side-effect-free, so the @KeyFor("aux1.map") is cleared rather than // glb'ed. // :: error: (assignment.type.incompatible) @KeyFor({"aux1.map", "aux2.map"}) String s1 = Aux.MINIMUM_VALUE; @@ -23,7 +24,7 @@ void m02(Aux aux1, Aux aux2) { void m03(Aux aux1, Aux aux2) { if (aux1.hasValue(Aux.MINIMUM_VALUE) && map.containsKey(Aux.MINIMUM_VALUE)) { - // ok because map.containsKey is sideeffect free. + // ok because map.containsKey is side-effect-free. @KeyFor({"aux1.map", "map"}) String s1 = Aux.MINIMUM_VALUE; @KeyFor("map") String s2 = Aux.MINIMUM_VALUE; } diff --git a/checker/tests/nullness/Issue266.java b/checker/tests/nullness/Issue266.java index 5ab25c4b7296..0b1df73bc96a 100644 --- a/checker/tests/nullness/Issue266.java +++ b/checker/tests/nullness/Issue266.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.nullness.qual.*; -class Issue266 { +public class Issue266 { abstract static class Inner { abstract String getThing(); diff --git a/checker/tests/nullness/Issue266a.java b/checker/tests/nullness/Issue266a.java index 43488399f6b8..9919404293c2 100644 --- a/checker/tests/nullness/Issue266a.java +++ b/checker/tests/nullness/Issue266a.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.nullness.qual.*; -class Issue266a { +public class Issue266a { private final Object mBar; public Issue266a() { diff --git a/checker/tests/nullness/Issue2721.java b/checker/tests/nullness/Issue2721.java index 5ec429b2785d..089a98281278 100644 --- a/checker/tests/nullness/Issue2721.java +++ b/checker/tests/nullness/Issue2721.java @@ -3,7 +3,7 @@ import java.util.List; @SuppressWarnings("all") // Just check for crashes. -class Issue2721 { +public class Issue2721 { void foo() { passThrough(asList(asList(1))).get(0).get(0).intValue(); } diff --git a/checker/tests/nullness/Issue273.java b/checker/tests/nullness/Issue273.java index c0210c00e3f8..bfb30c619abc 100644 --- a/checker/tests/nullness/Issue273.java +++ b/checker/tests/nullness/Issue273.java @@ -1,11 +1,12 @@ // Test case for issue #273: // https://github.com/typetools/checker-framework/issues/273 +import org.checkerframework.checker.nullness.qual.*; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.nullness.qual.*; -class Issue273 { +public class Issue273 { public static void main(String... p) { Map m0 = new HashMap<>(); Map m1 = new HashMap<>(); diff --git a/checker/tests/nullness/Issue2888.java b/checker/tests/nullness/Issue2888.java index 2795a145984b..6713ae743292 100644 --- a/checker/tests/nullness/Issue2888.java +++ b/checker/tests/nullness/Issue2888.java @@ -1,6 +1,6 @@ import com.sun.istack.internal.Nullable; -class Issue2888 { +public class Issue2888 { @Nullable Object[] noa; void foo() { @@ -24,7 +24,7 @@ public void bar1(@Nullable String... args) { private void bar2(@Nullable String... args) { if (args != null && args.length > 0) { @Nullable final String arg0 = args[0]; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (arg0 != null) { System.out.println("arg0: " + arg0); } diff --git a/checker/tests/nullness/Issue289.java b/checker/tests/nullness/Issue289.java index 3834cf99c0bf..4caf0afb135b 100644 --- a/checker/tests/nullness/Issue289.java +++ b/checker/tests/nullness/Issue289.java @@ -1,11 +1,12 @@ // Test for Issue 289: // https://github.com/typetools/checker-framework/issues/289 +import org.checkerframework.checker.nullness.qual.*; + import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.nullness.qual.*; -class Issue289 { +public class Issue289 { void simple() { List lo = new ArrayList<>(); List ls = new ArrayList<>(); diff --git a/checker/tests/nullness/Issue293.java b/checker/tests/nullness/Issue293.java index aafb3ccc8546..c17ab75fe766 100644 --- a/checker/tests/nullness/Issue293.java +++ b/checker/tests/nullness/Issue293.java @@ -1,7 +1,7 @@ // Test for Issue 293: // https://github.com/typetools/checker-framework/issues/293 -class Issue293 { +public class Issue293 { void test1() { String s; try { diff --git a/checker/tests/nullness/Issue296.java b/checker/tests/nullness/Issue296.java index 507179c3c77d..889164d0fad3 100644 --- a/checker/tests/nullness/Issue296.java +++ b/checker/tests/nullness/Issue296.java @@ -1,12 +1,13 @@ // Test case for Issue 296: // https://github.com/typetools/checker-framework/issues/296 -import java.util.Arrays; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Arrays; + // Note that with -AinvariantArrays we would get additional errors. -class Issue296 { +public class Issue296 { public static void f1(T[] a) { @Nullable T[] r1 = Arrays.copyOf(a, a.length + 1); // :: error: (argument.type.incompatible) diff --git a/checker/tests/nullness/Issue3013.java b/checker/tests/nullness/Issue3013.java new file mode 100644 index 000000000000..94040e59dfc0 --- /dev/null +++ b/checker/tests/nullness/Issue3013.java @@ -0,0 +1,11 @@ +import org.checkerframework.checker.nullness.qual.NonNull; + +abstract class Issue3013 { + static class Nested { + Nested(Issue3013 list) { + for (Object o : list.asIterable()) {} + } + } + + abstract Iterable asIterable(); +} diff --git a/checker/tests/nullness/Issue3015.java b/checker/tests/nullness/Issue3015.java new file mode 100644 index 000000000000..d4a503d66c2b --- /dev/null +++ b/checker/tests/nullness/Issue3015.java @@ -0,0 +1,20 @@ +// Test case for issue #3015: +// https://github.com/typetools/checker-framework/issues/3015 + +class Issue3015 { + void acquire() { + String s = ""; + + try { + return; + } finally { + try { + signal(); + } finally { + s.toString(); + } + } + } + + void signal() {} +} diff --git a/checker/tests/nullness/Issue3033.java b/checker/tests/nullness/Issue3033.java new file mode 100644 index 000000000000..c636dbd9b80c --- /dev/null +++ b/checker/tests/nullness/Issue3033.java @@ -0,0 +1,27 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class Issue3033 { + + class Test { + + void main() { + Test obj1 = new Test(); + + // No error as no explicit @Nullable or @NonNull annotation is given. + if (obj1 instanceof Test) { + Test obj2 = new Test(); + + // :: error: (instanceof.nullable) + if (obj1 instanceof @Nullable Test) { + obj1 = null; + } + + // :: warning: (instanceof.nonnull.redundant) + if (obj2 instanceof @NonNull Test) { + obj2 = obj1; + } + } + } + } +} diff --git a/checker/tests/nullness/Issue306.java b/checker/tests/nullness/Issue306.java index 67bcf19704ce..5c81748c455e 100644 --- a/checker/tests/nullness/Issue306.java +++ b/checker/tests/nullness/Issue306.java @@ -6,7 +6,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; -class Issue306 { +public class Issue306 { @MonotonicNonNull Object x; static T check(T var) { @@ -15,19 +15,16 @@ static T check(T var) { void fakeMethod() { // @MonotonicNonNull is not reflexive. - // However, it is the most specific type argument - // inferred for check. Therefore, an error is + // However, it is the most specific type argument inferred for check. Therefore, an error is // raised. - // We need a mechanism to not consider a - // qualifier in type inference. + // We need a mechanism to not consider a qualifier in type inference. check(x); // Ugly way around the problem: Issue306.<@Nullable Object>check(x); - // The following error has to be raised: from - // the signature it is not guaranteed that - // the parameter is returned. + // The following error has to be raised: from the signature it is not guaranteed that the + // parameter is returned. // :: error: (monotonic.type.incompatible) x = check(x); } @@ -39,8 +36,7 @@ void realError(@MonotonicNonNull Object p) { x = y; // :: error: (monotonic.type.incompatible) x = p; - // It would be nice not to raise the following - // error. + // It would be nice not to raise the following error. // :: error: (monotonic.type.incompatible) x = x; } diff --git a/checker/tests/nullness/Issue308.java b/checker/tests/nullness/Issue308.java index dc709375af66..5b00672f0728 100644 --- a/checker/tests/nullness/Issue308.java +++ b/checker/tests/nullness/Issue308.java @@ -1,7 +1,11 @@ -import javax.validation.constraints.NotNull; import org.checkerframework.checker.nullness.qual.*; -class Issue308 { +import javax.validation.constraints.NotNull; + +// @skip-test The clean-room implementation of javax.validation.constraints.NotNull is not in this +// repository because Oracle claims a license over its specification and is lawsuit-happy. + +public class Issue308 { @NonNull Object nonnull = new Object(); @Nullable Object nullable; diff --git a/checker/tests/nullness/Issue3150.java b/checker/tests/nullness/Issue3150.java index 7b7f4ce98491..24d2507cc997 100644 --- a/checker/tests/nullness/Issue3150.java +++ b/checker/tests/nullness/Issue3150.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -class Issue3150 { +public class Issue3150 { void foo(@Nullable Object nble, @NonNull Object nn) { // :: error: (type.argument.type.incompatible) requireNonNull1(null); diff --git a/checker/tests/nullness/Issue328.java b/checker/tests/nullness/Issue328.java index 11bb3d312827..81c902e17263 100644 --- a/checker/tests/nullness/Issue328.java +++ b/checker/tests/nullness/Issue328.java @@ -1,7 +1,8 @@ -import java.util.Map; import org.checkerframework.checker.nullness.qual.*; -class Issue328 { +import java.util.Map; + +public class Issue328 { public static void m(Map a, Map b, Object ka, Object kb) { if (a.containsKey(ka)) { @NonNull Object i = a.get(ka); // OK diff --git a/checker/tests/nullness/Issue3349.java b/checker/tests/nullness/Issue3349.java new file mode 100644 index 000000000000..7d38d5929b3f --- /dev/null +++ b/checker/tests/nullness/Issue3349.java @@ -0,0 +1,11 @@ +import org.checkerframework.checker.nullness.qual.*; + +import java.io.Serializable; + +// :: warning: (explicit.annotation.ignored) +public class Issue3349 { + void foo(T p1) { + @NonNull Serializable s = p1; + @NonNull Object o = p1; + } +} diff --git a/checker/tests/nullness/Issue338.java b/checker/tests/nullness/Issue338.java index 8e5934d8bed6..f2f1e66099d3 100644 --- a/checker/tests/nullness/Issue338.java +++ b/checker/tests/nullness/Issue338.java @@ -2,7 +2,7 @@ interface Foo338 { Class get(); } -class Issue338 { +public class Issue338 { static void m2(Foo338 foo) { Class clazz = foo.get(); } diff --git a/checker/tests/nullness/Issue3443.java b/checker/tests/nullness/Issue3443.java new file mode 100644 index 000000000000..34057774d1d4 --- /dev/null +++ b/checker/tests/nullness/Issue3443.java @@ -0,0 +1,19 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +public 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/Issue355.java b/checker/tests/nullness/Issue355.java index 83e40cd0d940..5a6849917e9b 100644 --- a/checker/tests/nullness/Issue355.java +++ b/checker/tests/nullness/Issue355.java @@ -1,11 +1,12 @@ // Test case for Issue 355: // https://github.com/typetools/checker-framework/issues/355 -import java.util.List; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -class Issue355 { +import java.util.List; + +public class Issue355 { static @NonNull T checkNotNull(@Nullable T sample) { throw new RuntimeException(); } diff --git a/checker/tests/nullness/Issue3614.java b/checker/tests/nullness/Issue3614.java new file mode 100644 index 000000000000..17e278775fbb --- /dev/null +++ b/checker/tests/nullness/Issue3614.java @@ -0,0 +1,117 @@ +// Test case for https://tinyurl.com/cfissue/3614 + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.PolyNull; + +public class Issue3614 { + + public static @Nullable Boolean m1(@PolyNull Boolean b) { + return (b == null) ? b : b; + } + + public static @NonNull Boolean m2(@PolyNull Boolean b) { + return (b == null) ? Boolean.TRUE : !b; + } + + public static @PolyNull Boolean m3(@PolyNull Boolean b) { + return (b == null) ? null : Boolean.TRUE; + } + + public static @PolyNull Boolean m4(@PolyNull Boolean b) { + return (b == null) ? null : b; + } + + public static @PolyNull Boolean m5(@PolyNull Boolean b) { + return (b == null) ? b : Boolean.TRUE; + } + + public static @PolyNull Boolean not1(@PolyNull Boolean b) { + return (b == null) ? null : !b; + } + + public static @PolyNull Boolean not2(@PolyNull Boolean b) { + // :: error: (unboxing.of.nullable) + return (b == null) ? b : !b; + } + + public static @PolyNull Boolean not3(@PolyNull Boolean b) { + if (b == null) { + return null; + } else { + return !b; + } + } + + public static <@Nullable T> T of1(T a) { + return a == null ? null : a; + } + + public static <@Nullable T> T of2(T a) { + if (a == null) { + return null; + } else { + return a; + } + } + + public static @PolyNull Integer plus1(@PolyNull Integer b0, @PolyNull Integer b1) { + return (b0 == null || b1 == null) ? null : (b0 + b1); + } + + public static @PolyNull Integer plus2(@PolyNull Integer b0, @PolyNull Integer b1) { + if (b0 == null || b1 == null) { + return null; + } else { + return b0 + b1; + } + } + + public static @PolyNull Integer plus3(@PolyNull Integer a, @PolyNull Integer b) { + if (a == null) { + return null; + } + if (b == null) { + return null; + } + return a + b; + } + + public static @PolyNull Integer plus1Err(@PolyNull Integer b0, @PolyNull Integer b1) { + // :: error: (return.type.incompatible) :: error: (unboxing.of.nullable) + return (b0 == null) ? null : (b0 + b1); + } + + public static @PolyNull Integer plus2Err(@PolyNull Integer b0, @PolyNull Integer b1) { + if (b0 == null) { + return null; + } else { + // :: error: (unboxing.of.nullable) + return b0 + b1; + } + } + + public static @PolyNull Integer plus3Err(@PolyNull Integer a, @PolyNull Integer b) { + if (a == null) { + return null; + } + // :: error: (unboxing.of.nullable) + return a + b; + } + + public static @PolyNull /*("elt")*/ String @PolyNull /*("container")*/ [] typeArray( + @PolyNull /*("elt")*/ Object @PolyNull /*("container")*/ [] seq) { + if (seq == null) { + return null; + } + @PolyNull /*("elt")*/ String[] retval = new @PolyNull /*("elt")*/ String[seq.length]; + for (int i = 0; i < seq.length; i++) { + if (seq[i] == null) { + retval[i] = null; + } else { + retval[i] = seq[i].getClass().toString(); + } + } + return retval; + } +} diff --git a/checker/tests/nullness/Issue3622.java b/checker/tests/nullness/Issue3622.java new file mode 100644 index 000000000000..23b57dc5bd44 --- /dev/null +++ b/checker/tests/nullness/Issue3622.java @@ -0,0 +1,118 @@ +// Test case for https://tinyurl.com/cfissue/3622 + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.List; + +public class Issue3622 { + + public class ImmutableIntList1 { + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof ImmutableIntList1) { + return true; + } else { + return obj instanceof List; + } + } + } + + public class ImmutableIntList2 { + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof ImmutableIntList2; + } + } + + public class ImmutableIntList3 { + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof ImmutableIntList3) { + return true; + } else { + return false; + } + } + } + + public class ImmutableIntList4 { + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof ImmutableIntList4 ? true : obj instanceof List; + } + } + + public class ImmutableIntList5 { + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof ImmutableIntList5 + ? obj instanceof ImmutableIntList5 + : obj instanceof ImmutableIntList5; + } + } + + public class ImmutableIntList6 { + + @Override + public boolean equals(@Nullable Object obj) { + return true ? obj instanceof ImmutableIntList6 : obj instanceof ImmutableIntList6; + } + } + + public class ImmutableIntList7 { + @Override + public boolean equals(@Nullable Object obj) { + // :: error: (contracts.conditional.postcondition.not.satisfied) + return (obj instanceof ImmutableIntList7) ? true : !(obj instanceof List); + } + } + + public class ImmutableIntList8 { + + @Override + // The ternary expression has the condition of literal `true`, so the false-expression is + // unreachable. However the store in the unreachable false-branch (where `obj` is @Nullable) + // is propagated to the merge point, which causes the false positive. + // TODO: prune the dead branch like https://github.com/typetools/checker-framework/pull/3389 + @SuppressWarnings("contracts.conditional.postcondition.not.satisfied") + public boolean equals(@Nullable Object obj) { + return true ? obj instanceof ImmutableIntList8 : false; + } + } + + public class ImmutableIntList9 { + + @Override + // The false expression of the tenary expression is literal `false`. In this case only the + // else-store after `false` should be propagated to the else-store of the merge point. + // TODO: adapt the way of store propagation for boolean variables. i.e. for `true`, only + // then-store is propagated; and for `false`, only else-store is propagated. + @SuppressWarnings("contracts.conditional.postcondition.not.satisfied") + public boolean equals(@Nullable Object obj) { + return obj instanceof ImmutableIntList9 ? true : false; + } + } + + public class ImmutableIntList10 { + + @Override + // The false positive is because in the Nullness analysis the values of boolean variables + // are not stored, therefore the relation between boolean variable `b` and `obj` is not + // known + @SuppressWarnings("contracts.conditional.postcondition.not.satisfied") + public boolean equals(@Nullable Object obj) { + boolean b; + if (obj instanceof ImmutableIntList10) { + b = true; + } else { + b = false; + } + return b; + } + } +} diff --git a/checker/tests/nullness/Issue3631.java b/checker/tests/nullness/Issue3631.java new file mode 100644 index 000000000000..4704ec06e9e4 --- /dev/null +++ b/checker/tests/nullness/Issue3631.java @@ -0,0 +1,18 @@ +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +public class Issue3631 { + + void f(Object otherArg) { + // Casts aren't a supported JavaExpression. + // :: error: (contracts.precondition.not.satisfied) + ((Issue3631Helper) otherArg).m(); + } +} + +class Issue3631Helper { + + String type = "foo"; + + @RequiresNonNull("type") + void m() {} +} diff --git a/checker/tests/nullness/Issue3681.java b/checker/tests/nullness/Issue3681.java new file mode 100644 index 000000000000..6637dedb0497 --- /dev/null +++ b/checker/tests/nullness/Issue3681.java @@ -0,0 +1,49 @@ +// Test case for isse #3681: https://tinyurl.com/cfissue/3681 + +// @below-java11-jdk-skip-test +package org.jro.tests.checkerfwk.utils; + +public class Issue3681 { + interface PartialFunction { + R apply(T t); + + boolean isDefinedAt(T value); + } + + interface Either { + R get(); + + boolean isRight(); + } + + public static PartialFunction, R> createKeepRight() { + return new PartialFunction<>() { + + @Override + public R apply(final Either either) { + return either.get(); + } + + @Override + public boolean isDefinedAt(final Either value) { + return value.isRight(); + } + }; + } + + public static + PartialFunction, ? extends R> createRCovariantKeepRight() { + return new PartialFunction<>() { + + @Override + public R apply(final Either either) { + return either.get(); + } + + @Override + public boolean isDefinedAt(final Either value) { + return value.isRight(); + } + }; + } +} diff --git a/checker/tests/nullness/Issue370.java b/checker/tests/nullness/Issue370.java index 63d46365d816..293556fb5fb2 100644 --- a/checker/tests/nullness/Issue370.java +++ b/checker/tests/nullness/Issue370.java @@ -3,7 +3,7 @@ import java.util.Collections; -class Issue370 { +public class Issue370 { Iterable foo() { return Collections.emptyList(); diff --git a/checker/tests/nullness/Issue372.java b/checker/tests/nullness/Issue372.java index 43baac60a436..8fcdb417c665 100644 --- a/checker/tests/nullness/Issue372.java +++ b/checker/tests/nullness/Issue372.java @@ -1,9 +1,10 @@ // Test case for Issue 372: // https://github.com/typetools/checker-framework/issues/372 +import org.checkerframework.checker.nullness.qual.EnsuresKeyFor; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.nullness.qual.EnsuresKeyFor; public class Issue372 { private final Map labels = new HashMap<>(); diff --git a/checker/tests/nullness/Issue3754.java b/checker/tests/nullness/Issue3754.java new file mode 100644 index 000000000000..4e952d798aac --- /dev/null +++ b/checker/tests/nullness/Issue3754.java @@ -0,0 +1,21 @@ +// @above-java17-jdk-skip-test TODO: reinstate, false positives may be due to issue #979 + +import org.checkerframework.checker.nullness.qual.Nullable; + +class Issue3754 { + interface Supplier { + U get(); + } + + Object x(Supplier bar) { + return bar.get(); + } + + interface Supplier2 { + U get(); + } + + Object x(Supplier2 bar) { + return bar.get(); + } +} diff --git a/checker/tests/nullness/Issue376.java b/checker/tests/nullness/Issue376.java index b8b725d82251..34219f1d53a6 100644 --- a/checker/tests/nullness/Issue376.java +++ b/checker/tests/nullness/Issue376.java @@ -1,7 +1,7 @@ // Test case for Issue 376: // https://github.com/typetools/checker-framework/issues/376 -class Issue376Test { +public class Issue376 { interface I {} diff --git a/checker/tests/nullness/Issue3792.java b/checker/tests/nullness/Issue3792.java new file mode 100644 index 000000000000..d63f2b1a56e7 --- /dev/null +++ b/checker/tests/nullness/Issue3792.java @@ -0,0 +1,16 @@ +import java.util.Collection; +import java.util.NavigableMap; + +public abstract class Issue3792 { + static class Instant {} + + void method( + NavigableMap> contents, + Instant minTimestamp, + Instant limitTimestamp) { + contents.subMap(minTimestamp, true, limitTimestamp, false).entrySet().stream() + .flatMap(e -> e.getValue().stream().map(v -> of(v, e.getKey()))); + } + + abstract Object of(T v, Instant key); +} diff --git a/checker/tests/nullness/Issue3845.java b/checker/tests/nullness/Issue3845.java new file mode 100644 index 000000000000..8decaf633fa7 --- /dev/null +++ b/checker/tests/nullness/Issue3845.java @@ -0,0 +1,30 @@ +package wildcards; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.checkerframework.framework.qual.TypeUseLocation; + +public class Issue3845 { + static class Holder { + final T value; + + Holder(T value) { + this.value = value; + } + } + + interface HolderSupplier> { + H get(); + } + + @DefaultQualifier(value = Nullable.class, locations = TypeUseLocation.ALL) + static class DefaultClash { + + Object go(HolderSupplier s) { + if (s != null) { + return s.get(); + } + return ""; + } + } +} diff --git a/checker/tests/nullness/Issue3850.java b/checker/tests/nullness/Issue3850.java new file mode 100644 index 000000000000..056dbb9eb21d --- /dev/null +++ b/checker/tests/nullness/Issue3850.java @@ -0,0 +1,15 @@ +import org.checkerframework.checker.nullness.qual.PolyNull; + +public class Issue3850 { + + private static Iterable<@PolyNull String> toPos(Iterable nodes) { + // :: error: (return.type.incompatible) + return transform(nodes, node -> node == null ? null : node.toString()); + } + + public static Iterable transform( + Iterable iterable, + java.util.function.Function function) { + throw new Error("implementation is irrelevant"); + } +} diff --git a/checker/tests/nullness/Issue388.java b/checker/tests/nullness/Issue388.java index d6f00c32995c..702eeb190168 100644 --- a/checker/tests/nullness/Issue388.java +++ b/checker/tests/nullness/Issue388.java @@ -3,7 +3,7 @@ import java.util.Map; -class MapKeyConstant { +public class Issue388 { static class Holder { static final String KEY = "key"; } diff --git a/checker/tests/nullness/Issue3884.java b/checker/tests/nullness/Issue3884.java new file mode 100644 index 000000000000..68609d9b1c98 --- /dev/null +++ b/checker/tests/nullness/Issue3884.java @@ -0,0 +1,18 @@ +interface Issue3884 { + String go(Kind kind); + + Issue3884 FOO = + kind -> { + switch (kind) { + case A: + break; + } + return ""; + }; + + enum Kind { + A, + B, + C; + } +} diff --git a/checker/tests/nullness/Issue3888.java b/checker/tests/nullness/Issue3888.java new file mode 100644 index 000000000000..585d3438e559 --- /dev/null +++ b/checker/tests/nullness/Issue3888.java @@ -0,0 +1,18 @@ +import org.checkerframework.checker.nullness.qual.PolyNull; + +abstract class Issue3888 { + + interface L {} + + interface E {} + + public interface F { + Y a(V v); + } + + abstract void f(F f); + + void c(F o) { + f((E vm) -> o.a(vm) == null); + } +} diff --git a/checker/tests/nullness/Issue3929.java b/checker/tests/nullness/Issue3929.java new file mode 100644 index 000000000000..ac6d4b377192 --- /dev/null +++ b/checker/tests/nullness/Issue3929.java @@ -0,0 +1,35 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class Issue3929 { + + public void endElement(MyClass3929 arg) { + for (Object o : arg.getKeys()) { + o.toString(); + } + } + + public void endElement(NullableMyClass3929 arg) { + for (Object o : arg.getKeys()) { + // TODO: add a conservative option to get a warning here + o.toString(); + } + } +} + +class MyClass3929> { + public List getKeys() { + return new ArrayList<>(); + } +} + +// TODO: This is a false positive. +// See https://github.com/typetools/checker-framework/issues/2174 +// :: error: (type.argument.type.incompatible) +class NullableMyClass3929> { + public List getKeys() { + return new ArrayList<>(); + } +} diff --git a/checker/tests/nullness/Issue3935.java b/checker/tests/nullness/Issue3935.java new file mode 100644 index 000000000000..a180ae339bf9 --- /dev/null +++ b/checker/tests/nullness/Issue3935.java @@ -0,0 +1,10 @@ +import android.annotation.Nullable; + +public class Issue3935 { + // Note: Nullable is a declaration annotation and applies to the array, not the array element. + private @Nullable byte[] data; + + // Declaration annotations on primitives are ignored, but this should issue + // a nullness.on.primitive error. + @Nullable byte b; +} diff --git a/checker/tests/nullness/Issue3970.java b/checker/tests/nullness/Issue3970.java new file mode 100644 index 000000000000..5c7711fd70aa --- /dev/null +++ b/checker/tests/nullness/Issue3970.java @@ -0,0 +1,19 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +public class Issue3970 { + + public interface InterfaceA> extends InterfaceB {} + + public interface InterfaceB> { + int f(); + + @Nullable T g(); + } + + void t(InterfaceA a) { + if (a.f() == 1) { + // :: error: (assignment.type.incompatible) + InterfaceA a2 = a.g(); + } + } +} diff --git a/checker/tests/nullness/Issue4007.java b/checker/tests/nullness/Issue4007.java new file mode 100644 index 000000000000..c85c937a29cc --- /dev/null +++ b/checker/tests/nullness/Issue4007.java @@ -0,0 +1,35 @@ +// Test case for issue #4007: https://tinyurl.com/cfissue/4007 + +// @skip-test until the issue is fixed + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.List; +import java.util.Optional; + +final class Issue4007 { + Optional m1(List list) { + return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0)); + } + + Optional> m2(List list) { + return Optional.of(list.isEmpty() ? Optional.empty() : Optional.of(list.get(0))); + } + + Optional> m3(List list) { + return Optional.of( + list.isEmpty() ? Optional.<@NonNull String>empty() : Optional.of(list.get(0))); + } + + Optional> m4(List list) { + return Optional.of( + list.isEmpty() ? Optional.empty() : Optional.<@NonNull String>of(list.get(0))); + } + + Optional> m5(List list) { + return Optional.of( + list.isEmpty() + ? Optional.<@NonNull String>empty() + : Optional.<@NonNull String>of(list.get(0))); + } +} diff --git a/checker/tests/nullness/Issue411.java b/checker/tests/nullness/Issue411.java index 00af90fe7b2b..6274162a797f 100644 --- a/checker/tests/nullness/Issue411.java +++ b/checker/tests/nullness/Issue411.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.nullness.qual.*; -class Test { +public class Issue411 { @MonotonicNonNull Object field1 = null; final @Nullable Object field2 = null; diff --git a/checker/tests/nullness/Issue414.java b/checker/tests/nullness/Issue414.java index a1cc11e525d6..17e2b1782926 100644 --- a/checker/tests/nullness/Issue414.java +++ b/checker/tests/nullness/Issue414.java @@ -1,13 +1,14 @@ // Test case for Issue 414. // https://github.com/typetools/checker-framework/issues/414 +import org.checkerframework.checker.nullness.qual.KeyFor; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.checkerframework.checker.nullness.qual.KeyFor; -class Issue414 { +public class Issue414 { void simple(String s) { Map mymap = new HashMap<>(); diff --git a/checker/tests/nullness/Issue415.java b/checker/tests/nullness/Issue415.java index 62d00d443074..22d74fcb6544 100644 --- a/checker/tests/nullness/Issue415.java +++ b/checker/tests/nullness/Issue415.java @@ -1,16 +1,18 @@ // Test case for Issue 415 // https://github.com/typetools/checker-framework/issues/415 +import org.checkerframework.checker.nullness.qual.*; +import org.checkerframework.dataflow.qual.*; + import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.Set; -import org.checkerframework.checker.nullness.qual.*; -import org.checkerframework.dataflow.qual.*; public final class Issue415 { Map mymap = new HashMap<>(); + // :: error: (expression.unparsable.type.invalid) public static void usesField(Set<@KeyFor("this.mymap") String> keySet) { // :: error: (expression.unparsable.type.invalid) diff --git a/checker/tests/nullness/Issue419.java b/checker/tests/nullness/Issue419.java index c9b7103daf65..46e787ee69b0 100644 --- a/checker/tests/nullness/Issue419.java +++ b/checker/tests/nullness/Issue419.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.nullness.qual.*; -class Issue419 { +public class Issue419 { @SuppressWarnings("nullness") @NonNull T verifyNotNull(@Nullable T o) { return o; diff --git a/checker/tests/nullness/Issue425.java b/checker/tests/nullness/Issue425.java index 7622a03d3c22..4346cebabac6 100644 --- a/checker/tests/nullness/Issue425.java +++ b/checker/tests/nullness/Issue425.java @@ -3,9 +3,10 @@ // @skip-test until the issue is fixed +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.HashSet; import java.util.Set; -import org.checkerframework.checker.nullness.qual.Nullable; public class Issue425 { private @Nullable Set field = null; diff --git a/checker/tests/nullness/Issue427.java b/checker/tests/nullness/Issue427.java index 9b298e074c62..09c54640635a 100644 --- a/checker/tests/nullness/Issue427.java +++ b/checker/tests/nullness/Issue427.java @@ -3,9 +3,10 @@ // We need to add a warning when an @AssumeAssertion is missing its @ symbol (as below). -import java.util.Map; import org.checkerframework.checker.nullness.qual.*; +import java.util.Map; + public class Issue427 { public static void assumeAssertionKeyFor1(String var, Map m) { diff --git a/checker/tests/nullness/Issue4372.java b/checker/tests/nullness/Issue4372.java new file mode 100644 index 000000000000..aeffd463f172 --- /dev/null +++ b/checker/tests/nullness/Issue4372.java @@ -0,0 +1,20 @@ +import java.util.Map; +import java.util.Optional; + +public class Issue4372 { + private Optional> o; + + public Issue4372() { + this.o = Optional.>empty(); + } + + void f(String k, Optional x) { + if (!o.isPresent() || !o.get().containsKey(k)) { + return; + } + Integer y = o.get().get(k); + if (!x.isPresent()) { + return; + } + } +} diff --git a/checker/tests/nullness/Issue4381.java b/checker/tests/nullness/Issue4381.java new file mode 100644 index 000000000000..2ed7c4911202 --- /dev/null +++ b/checker/tests/nullness/Issue4381.java @@ -0,0 +1,19 @@ +import java.io.IOException; +import java.security.GeneralSecurityException; + +abstract class Issue4381 { + + public void t() { + int m = 0; + try { + f(); + } catch (IllegalArgumentException | IOException e) { + } catch (GeneralSecurityException e) { + g(m); + } + } + + abstract void g(int x); + + abstract void f() throws IllegalArgumentException, IOException, GeneralSecurityException; +} diff --git a/checker/tests/nullness/Issue4412.java b/checker/tests/nullness/Issue4412.java new file mode 100644 index 000000000000..1410e155eae3 --- /dev/null +++ b/checker/tests/nullness/Issue4412.java @@ -0,0 +1,227 @@ +// @skip-test This test passes, but is slow. So skip it until performance improves. + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Objects; + +public interface Issue4412< + LeftLeftType extends Issue4412.LeftLeft, + LeftType extends Issue4412.Left, + RightType extends Issue4412.Right, + RightRightType extends + Issue4412.RightRight> { + + T reduce( + @NonNull Function1 leftLeftReducer, + @NonNull Function1 leftReducer, + @NonNull Function1 rightReducer, + @NonNull Function1 rightRightReducer); + + void act( + @NonNull VoidFunction1 leftLeftAction, + @NonNull VoidFunction1 leftAction, + @NonNull VoidFunction1 rightAction, + @NonNull VoidFunction1 rightRightAction); + + interface LeftLeft< + LeftLeftType extends + LeftLeft, + LeftType extends Left, + RightType extends Right, + RightRightType extends + RightRight> + extends Issue4412 {} + + // Not final to allow reification + abstract class LeftLeftImpl< + LeftLeftType extends + LeftLeft, + LeftType extends Left, + RightType extends Right, + RightRightType extends + RightRight> + implements LeftLeft { + + private final Class selfClass; + + protected LeftLeftImpl(Class selfClass) { + this.selfClass = selfClass; + } + + @Override + public final T reduce( + @NonNull Function1 leftLeftReducer, + @NonNull Function1 leftReducer, + @NonNull Function1 rightReducer, + @NonNull Function1 rightRightReducer) { + return leftLeftReducer.apply(getSelf()); + } + + @Override + public final void act( + @NonNull VoidFunction1 leftLeftAction, + @NonNull VoidFunction1 leftAction, + @NonNull VoidFunction1 rightAction, + @NonNull VoidFunction1 rightRightAction) { + leftLeftAction.apply(getSelf()); + } + + private LeftLeftType getSelf() { + return Objects.requireNonNull(selfClass.cast(this)); + } + } + + interface Left< + LeftLeftType extends + LeftLeft, + LeftType extends Left, + RightType extends Right, + RightRightType extends + RightRight> + extends Issue4412 {} + + // Not final to allow reification + abstract class LeftImpl< + LeftLeftType extends + LeftLeft, + LeftType extends Left, + RightType extends Right, + RightRightType extends + RightRight> + implements Left { + + private final Class selfClass; + + protected LeftImpl(@NonNull Class selfClass) { + this.selfClass = selfClass; + } + + @Override + public final T reduce( + @NonNull Function1 leftLeftReducer, + @NonNull Function1 leftReducer, + @NonNull Function1 rightReducer, + @NonNull Function1 rightRightReducer) { + return leftReducer.apply(getSelf()); + } + + @Override + public final void act( + @NonNull VoidFunction1 leftLeftAction, + @NonNull VoidFunction1 leftAction, + @NonNull VoidFunction1 rightAction, + @NonNull VoidFunction1 rightRightAction) { + leftAction.apply(getSelf()); + } + + private LeftType getSelf() { + return Objects.requireNonNull(selfClass.cast(this)); + } + } + + interface Right< + LeftLeftType extends + LeftLeft, + LeftType extends Left, + RightType extends Right, + RightRightType extends + RightRight> + extends Issue4412 {} + + // Not final to allow reification + abstract class RightImpl< + LeftLeftType extends + LeftLeft, + LeftType extends Left, + RightType extends Right, + RightRightType extends + RightRight> + implements Right { + + private final Class selfClass; + + protected RightImpl(@NonNull Class selfClass) { + this.selfClass = selfClass; + } + + @Override + public final T reduce( + @NonNull Function1 leftLeftReducer, + @NonNull Function1 leftReducer, + @NonNull Function1 rightReducer, + @NonNull Function1 rightRightReducer) { + return rightReducer.apply(getSelf()); + } + + @Override + public final void act( + @NonNull VoidFunction1 leftLeftAction, + @NonNull VoidFunction1 leftAction, + @NonNull VoidFunction1 rightAction, + @NonNull VoidFunction1 rightRightAction) { + rightAction.apply(getSelf()); + } + + private RightType getSelf() { + return Objects.requireNonNull(selfClass.cast(this)); + } + } + + interface RightRight< + LeftLeftType extends + LeftLeft, + LeftType extends Left, + RightType extends Right, + RightRightType extends + RightRight> + extends Issue4412 {} + + // Not final to allow reification + abstract class RightRightImpl< + LeftLeftType extends + LeftLeft, + LeftType extends Left, + RightType extends Right, + RightRightType extends + RightRight> + implements RightRight { + + private final Class selfClass; + + protected RightRightImpl(@NonNull Class selfClass) { + this.selfClass = selfClass; + } + + @Override + public final T reduce( + @NonNull Function1 leftLeftReducer, + @NonNull Function1 leftReducer, + @NonNull Function1 rightReducer, + @NonNull Function1 rightRightReducer) { + return rightRightReducer.apply(getSelf()); + } + + @Override + public final void act( + @NonNull VoidFunction1 leftLeftAction, + @NonNull VoidFunction1 leftAction, + @NonNull VoidFunction1 rightAction, + @NonNull VoidFunction1 rightRightAction) { + rightRightAction.apply(getSelf()); + } + + private RightRightType getSelf() { + return Objects.requireNonNull(selfClass.cast(this)); + } + } + + interface VoidFunction1 { + + void apply(@NonNull T t); + } + + interface Function1 { + + @NonNull R apply(@NonNull T t); + } +} diff --git a/checker/tests/nullness/Issue4523.java b/checker/tests/nullness/Issue4523.java new file mode 100644 index 000000000000..e1795e9c343b --- /dev/null +++ b/checker/tests/nullness/Issue4523.java @@ -0,0 +1,14 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +public class Issue4523 { + + interface InterfaceA> extends InterfaceB {} + + interface InterfaceB> { + @Nullable T g(); + } + + void f(InterfaceA x) { + InterfaceA y = x.g() != null ? x.g() : x; + } +} diff --git a/checker/tests/nullness/Issue4579.java b/checker/tests/nullness/Issue4579.java new file mode 100644 index 000000000000..5c0e490340b1 --- /dev/null +++ b/checker/tests/nullness/Issue4579.java @@ -0,0 +1,41 @@ +// Test case for issue #4579 + +// @skip-test until the issue is fixed + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; + +public class Issue4579 { + + public Issue4579() { + final JButton button = new JButton(); + + // this reports a warning about under initialization + button.addActionListener(l -> doAction()); + + // this reports no warnings + button.addActionListener(new ListenerClass()); + + // this reports a warning about under initialization + button.addActionListener( + new ActionListener() { + public void actionPerformed(final ActionEvent e) { + doAction(); + } + }); + } + + private void doAction() { + System.out.println("Action"); + } + + private class ListenerClass implements ActionListener { + + @Override + public void actionPerformed(final ActionEvent e) { + doAction(); + } + } +} diff --git a/checker/tests/nullness/Issue4593.java b/checker/tests/nullness/Issue4593.java new file mode 100644 index 000000000000..c4552e6f02fc --- /dev/null +++ b/checker/tests/nullness/Issue4593.java @@ -0,0 +1,21 @@ +// Test case for https://github.com/typetools/checker-framework/issues/4593 . + +// @skip-test until the bug is fixed + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class Issue4593 { + + void getContext(@Nullable String nble) { + Map map = new HashMap<>(); + map.put("configDir", nble); + } + + void getContextWithVar(@Nullable String nble) { + var map = new HashMap(); + map.put("configDir", nble); + } +} diff --git a/checker/tests/nullness/Issue4614.java b/checker/tests/nullness/Issue4614.java new file mode 100644 index 000000000000..e913ff1fce0f --- /dev/null +++ b/checker/tests/nullness/Issue4614.java @@ -0,0 +1,26 @@ +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public final class Issue4614 { + + public static Map getAllVersionInformation() { + return new HashMap<>(); + } + + public void method1() { + final String versionInfo = + Issue4614.getAllVersionInformation().entrySet().stream() // + .map(e -> String.format("%s:%s", e.getKey(), e.getValue())) // + .collect(Collectors.joining("\n")); + } + + Map allVersionInformation = new HashMap<>(); + + public void method2() { + final String versionInfo = + allVersionInformation.entrySet().stream() // + .map(e -> String.format("%s:%s", e.getKey(), e.getValue())) // + .collect(Collectors.joining("\n")); + } +} diff --git a/checker/tests/nullness/Issue471.java b/checker/tests/nullness/Issue471.java index e15195662a3c..7026790f3cc8 100644 --- a/checker/tests/nullness/Issue471.java +++ b/checker/tests/nullness/Issue471.java @@ -4,7 +4,7 @@ import javax.annotation.Nullable; -class Issue471 { +public class Issue471 { @Nullable T t; Issue471(@Nullable T t) { diff --git a/checker/tests/nullness/Issue4853Nullness.java b/checker/tests/nullness/Issue4853Nullness.java new file mode 100644 index 000000000000..2d49a3000d2b --- /dev/null +++ b/checker/tests/nullness/Issue4853Nullness.java @@ -0,0 +1,18 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +public class Issue4853Nullness { + interface Interface {} + + static class MyClass { + class InnerMyClass implements Interface {} + } + + abstract static class SubMyClass extends MyClass<@Nullable String> { + protected void f() { + // :: error: (argument.type.incompatible) + method(new InnerMyClass()); + } + + abstract void method(Interface callback); + } +} diff --git a/checker/tests/nullness/Issue4889.java b/checker/tests/nullness/Issue4889.java new file mode 100644 index 000000000000..e9a8e8f6e3b9 --- /dev/null +++ b/checker/tests/nullness/Issue4889.java @@ -0,0 +1,15 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Objects; + +class Issue4889 { + void f(@Nullable String s) { + Objects.toString(s, "").toString(); + } + + void g(@Nullable String s) { + @NonNull String x = Objects.toString(s, ""); + @Nullable String y = Objects.toString(s, null); + } +} diff --git a/checker/tests/nullness/Issue4923.java b/checker/tests/nullness/Issue4923.java new file mode 100644 index 000000000000..829c8f61d083 --- /dev/null +++ b/checker/tests/nullness/Issue4923.java @@ -0,0 +1,19 @@ +class Issue4923 { + interface Go { + void go(); + } + + final Go go = + new Go() { + @Override + public void go() { + synchronized (x) { + } + } + }; + final Object x = new Object(); + + // Make sure that initializer type is compatible with declared type + // :: error: (assignment.type.incompatible) + final Object y = null; +} diff --git a/checker/tests/nullness/Issue4924.java b/checker/tests/nullness/Issue4924.java new file mode 100644 index 000000000000..bd982f5c2a3c --- /dev/null +++ b/checker/tests/nullness/Issue4924.java @@ -0,0 +1,25 @@ +// Test case for issue #4924: https://tinyurl.com/cfissue/4924 + +class Issue4924 {} + +interface Callback4924 {} + +class Template4924 { + interface Putter4924 { + void put(T result); + } + + class Adapter4924 implements Callback4924 { + Adapter4924(Putter4924 putter) {} + } +} + +class Super4924 extends Template4924 {} + +class Issue extends Super4924 { + void go(Callback4924 callback) {} + + void foo() { + go(new Adapter4924(result -> {})); + } +} diff --git a/checker/tests/nullness/Issue500.java b/checker/tests/nullness/Issue500.java index b7b5e7661149..b905c84a03db 100644 --- a/checker/tests/nullness/Issue500.java +++ b/checker/tests/nullness/Issue500.java @@ -1,16 +1,18 @@ // Test case for Issue 500: // https://github.com/typetools/checker-framework/issues/500 +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.AbstractList; import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.nullness.qual.Nullable; public class Issue500 { // Tests GLB public Issue500(@Nullable List list) { if (list instanceof ArrayList) {} } + // Tests GLB public Issue500(@Nullable AbstractList list) { if (list instanceof ArrayList) {} diff --git a/checker/tests/nullness/Issue5042.java b/checker/tests/nullness/Issue5042.java new file mode 100644 index 000000000000..5f3f01a2d61f --- /dev/null +++ b/checker/tests/nullness/Issue5042.java @@ -0,0 +1,68 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.function.Function; + +public class Issue5042 { + interface PromptViewModel { + boolean isPending(); + + @Nullable PromptButtonViewModel getConfirmationButton(); + } + + interface PromptButtonViewModel { + @Nullable ConfirmationPopupViewModel getConfirmationPopup(); + } + + interface ConfirmationPopupViewModel { + boolean isShowingConfirmation(); + } + + boolean f(PromptViewModel viewModel) { + PromptButtonViewModel prompt = viewModel.getConfirmationButton(); + ConfirmationPopupViewModel popup = prompt != null ? prompt.getConfirmationPopup() : null; + return viewModel.isPending() || (popup != null && popup.isShowingConfirmation()); + } + + static final Function IS_PENDING_OR_SHOWING_CONFIRMATION = + (viewModel) -> { + @Nullable PromptButtonViewModel promptLambda = viewModel.getConfirmationButton(); + @Nullable ConfirmationPopupViewModel popup = + promptLambda != null ? promptLambda.getConfirmationPopup() : null; + return viewModel.isPending() || (popup != null && popup.isShowingConfirmation()); + }; + + final Function IS_PENDING_OR_SHOWING_CONFIRMATION2 = + (viewModel) -> { + @Nullable PromptButtonViewModel prompt = viewModel.getConfirmationButton(); + @Nullable ConfirmationPopupViewModel popup = + prompt == null ? null : prompt.getConfirmationPopup(); + return viewModel.isPending() || (popup != null && popup.isShowingConfirmation()); + }; + + @Nullable PromptButtonViewModel promptfield; + Producer o = + () -> { + @Nullable ConfirmationPopupViewModel popup = + promptfield == null ? null : promptfield.getConfirmationPopup(); + return (popup != null && popup.isShowingConfirmation()); + }; + + static @Nullable PromptButtonViewModel promptfield2; + + static Producer o2 = + () -> { + @Nullable ConfirmationPopupViewModel popup = + promptfield2 == null ? null : promptfield2.getConfirmationPopup(); + return (popup != null && popup.isShowingConfirmation()); + }; + + interface Producer { + Object apply(); + } + + Issue5042(int i, int i2) {} + + Issue5042(int i) {} + + Issue5042() {} +} diff --git a/checker/tests/nullness/Issue5075NPE.java b/checker/tests/nullness/Issue5075NPE.java new file mode 100644 index 000000000000..7cf68db88ec1 --- /dev/null +++ b/checker/tests/nullness/Issue5075NPE.java @@ -0,0 +1,45 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class Issue5075NPE { + + static class C { + void useBoxer(Boxer b) { + // :: error: (assignment.type.incompatible) + Box o = b.getBox(); + o.get().toString(); + } + + void useC(C c) {} + + class Boxer { + V v; + + Boxer(V in) { + this.v = in; + } + + Box getBox() { + return new Box(v); + } + } + } + + // Doesn't matter whether T's bound is explicit + static class Box { + T f; + + Box(T p) { + this.f = p; + } + + T get() { + return f; + } + } + + public static void main(String[] args) { + C<@Nullable Issue5075NPE> c = new C<>(); + c.useBoxer(c.new Boxer(null)); + } +} diff --git a/checker/tests/nullness/Issue5075a.java b/checker/tests/nullness/Issue5075a.java new file mode 100644 index 000000000000..42d4dd137d31 --- /dev/null +++ b/checker/tests/nullness/Issue5075a.java @@ -0,0 +1,31 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +class Issue5075a { + class AExpl { + I i1() { + // Type arguments aren't inferred correctly, test with #979 + // :: error: (return.type.incompatible) :: error: (argument.type.incompatible) + return new BExpl<>(this); + } + + I i2() { + return new BExpl(this); + } + } + + class BExpl implements I { + BExpl(AExpl a) {} + } + + class AImpl { + I i() { + return new BImpl<>(this); + } + } + + class BImpl implements I { + BImpl(AImpl a) {} + } + + interface I {} +} diff --git a/checker/tests/nullness/Issue5075b.java b/checker/tests/nullness/Issue5075b.java new file mode 100644 index 000000000000..021128efaa02 --- /dev/null +++ b/checker/tests/nullness/Issue5075b.java @@ -0,0 +1,33 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +class Issue5075b { + static class CExpl { + I c(N n) { + return h(n.i()); + } + + abstract class N { + abstract I i(); + } + + static I h(I i) { + return i; + } + } + + static class CImpl { + I c(N n) { + return h(n.i()); + } + + abstract class N { + abstract I i(); + } + + static I h(I i) { + return i; + } + } + + interface I {} +} diff --git a/checker/tests/nullness/Issue5189a.java b/checker/tests/nullness/Issue5189a.java new file mode 100644 index 000000000000..5e37c9523f14 --- /dev/null +++ b/checker/tests/nullness/Issue5189a.java @@ -0,0 +1,5 @@ +enum Issue5189a { + EMPTY() {}; + + Issue5189a(String... args) {} +} diff --git a/checker/tests/nullness/Issue5189b.java b/checker/tests/nullness/Issue5189b.java new file mode 100644 index 000000000000..e82058012c83 --- /dev/null +++ b/checker/tests/nullness/Issue5189b.java @@ -0,0 +1,9 @@ +class Issue5189b { + class Inner { + Inner(String... args) {} + } + + void foo() { + Object o = new Inner() {}; + } +} diff --git a/checker/tests/nullness/Issue520.java b/checker/tests/nullness/Issue520.java index 19bbe4626021..9d3da80a2149 100644 --- a/checker/tests/nullness/Issue520.java +++ b/checker/tests/nullness/Issue520.java @@ -2,12 +2,13 @@ // Test case for issue #520: https://github.com/typetools/checker-framework/issues/520 -// compile with: $CHECKERFRAMEWORK/checker/bin-devel/javac -g Issue520.java -processor nullness +// compile with: $CHECKERFRAMEWORK/checker/bin/javac -g Issue520.java -processor nullness // -AprintAllQualifiers +import org.checkerframework.checker.nullness.qual.*; + import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.nullness.qual.*; public class Issue520 {} diff --git a/checker/tests/nullness/Issue5245.java b/checker/tests/nullness/Issue5245.java new file mode 100644 index 000000000000..c0d469ea1f4a --- /dev/null +++ b/checker/tests/nullness/Issue5245.java @@ -0,0 +1,9 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5245 +// @below-java9-jdk-skip-test +import java.util.List; + +class Issue5245 { + final Issue5245> repro = new Issue5245<>(List.of()); + + Issue5245(V unknownObj) {} +} diff --git a/checker/tests/nullness/Issue563.java b/checker/tests/nullness/Issue563.java index 21811d6d1e10..ab48ea41f83e 100644 --- a/checker/tests/nullness/Issue563.java +++ b/checker/tests/nullness/Issue563.java @@ -1,6 +1,6 @@ // Test case for Issue 563: // https://github.com/typetools/checker-framework/issues/563 -class Issue563 { +public class Issue563 { void bar() { Object x = null; if (Object.class.isInstance(x)) { diff --git a/checker/tests/nullness/Issue578.java b/checker/tests/nullness/Issue578.java index b4308bdf8d15..a80d4af39727 100644 --- a/checker/tests/nullness/Issue578.java +++ b/checker/tests/nullness/Issue578.java @@ -1,5 +1,5 @@ // Test case for issue #578: https://github.com/typetools/checker-framework/issues/578 -class Issue578 { +public class Issue578 { void eval(Helper helper, Interface anInterface) { Object o = new SomeGenericClass<>(helper.helperMethod(anInterface)); } diff --git a/checker/tests/nullness/Issue579Error.java b/checker/tests/nullness/Issue579Error.java index 93ce4f61ff80..55f6e35b4272 100644 --- a/checker/tests/nullness/Issue579Error.java +++ b/checker/tests/nullness/Issue579Error.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; -class Issue579Error { +public class Issue579Error { public void foo(Generic real, Generic other, boolean flag) { // :: error: (type.argument.type.incompatible) diff --git a/checker/tests/nullness/Issue602.java b/checker/tests/nullness/Issue602.java index 26f62cd3db77..1e9fcffb3593 100644 --- a/checker/tests/nullness/Issue602.java +++ b/checker/tests/nullness/Issue602.java @@ -4,7 +4,7 @@ // Test case for Issue 602 // https://github.com/typetools/checker-framework/issues/602 // @skip-test -class Issue602 { +public class Issue602 { @PolyNull String id(@PolyNull String o) { return o; } diff --git a/checker/tests/nullness/Issue6260.java b/checker/tests/nullness/Issue6260.java new file mode 100644 index 000000000000..a175ab9831c2 --- /dev/null +++ b/checker/tests/nullness/Issue6260.java @@ -0,0 +1,21 @@ +// A similar test is in +// framework/tests/all-systems/EnumSwitch.java +public class Issue6260 { + enum MyE { + FOO; + + MyE getIt() { + return FOO; + } + + String go() { + MyE e = getIt(); + switch (e) { + case FOO: + return "foo"; + } + // This is not dead code! + throw new AssertionError(e); + } + } +} diff --git a/checker/tests/nullness/Issue653.java b/checker/tests/nullness/Issue653.java index c68ff7d76b45..84ed2694ad0b 100644 --- a/checker/tests/nullness/Issue653.java +++ b/checker/tests/nullness/Issue653.java @@ -3,7 +3,7 @@ // @skip-test Commented out until the bug is fixed -class Issue653 { +public class Issue653 { public static @PolyNull String[] concat( @PolyNull String @Nullable [] a, @PolyNull String @Nullable [] b) { diff --git a/checker/tests/nullness/Issue67.java b/checker/tests/nullness/Issue67.java index 14f7fcb4a257..e2f6db08199d 100644 --- a/checker/tests/nullness/Issue67.java +++ b/checker/tests/nullness/Issue67.java @@ -4,7 +4,7 @@ import java.util.HashMap; import java.util.Map; -class Issue67 { +public class Issue67 { private static final String KEY = "key"; private static final String KEY2 = "key2"; @@ -13,7 +13,7 @@ void test() { if (map.containsKey(KEY)) { map.get(KEY).toString(); // no problem } - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (map.containsKey(KEY2) && map.get(KEY2).toString() != null) { // error // do nothing } diff --git a/checker/tests/nullness/Issue752.java b/checker/tests/nullness/Issue752.java index e43ffaf5d6aa..c78cdb06ca25 100644 --- a/checker/tests/nullness/Issue752.java +++ b/checker/tests/nullness/Issue752.java @@ -15,7 +15,7 @@ static Issue752 staticMethod() { return staticField; } - // A package name without a class name is not a valid flow expression string. + // A package name without a class name is not a valid JavaExpression string. @RequiresNonNull("java.lang") // :: error: (flowexpr.parse.error) void method1() {} @@ -23,7 +23,7 @@ void method1() {} @RequiresNonNull("java.lang.String.class") void method2() {} - // A package name without a class name is not a valid flow expression string. + // A package name without a class name is not a valid JavaExpression string. @RequiresNonNull("a.b.c") // :: error: (flowexpr.parse.error) void method3() {} diff --git a/checker/tests/nullness/Issue765.java b/checker/tests/nullness/Issue765.java index a4f119e4b44f..ae47273e0ca2 100644 --- a/checker/tests/nullness/Issue765.java +++ b/checker/tests/nullness/Issue765.java @@ -1,6 +1,6 @@ // Test case for Issue 765 // https://github.com/typetools/checker-framework/issues/765 -class Issue765 { +public class Issue765 { Thread thread = new Thread() {}; void execute() { diff --git a/checker/tests/nullness/Issue868.java b/checker/tests/nullness/Issue868.java index 34715a201ad6..0a2f99600825 100644 --- a/checker/tests/nullness/Issue868.java +++ b/checker/tests/nullness/Issue868.java @@ -1,17 +1,59 @@ // Test case for Issue 868 // https://github.com/typetools/checker-framework/issues/868 -// @skip-test -import java.util.List; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; public class Issue868 { - void test2(E e) { + interface MyList {} + + void test1(E e) { + // :: error: (dereference.of.nullable) + e.toString(); + } + + void test2(E e) { + // :: error: (dereference.of.nullable) + e.toString(); + } + + void test3(E e) { + // :: error: (dereference.of.nullable) + e.toString(); + } + + void test4(E e) { + e.toString(); + } + + // :: warning: (explicit.annotation.ignored) + void test5(E e) { + e.toString(); + } + + // :: warning: (explicit.annotation.ignored) + void test6(E e) { // :: error: (dereference.of.nullable) e.toString(); } void use() { - test2(null); + this.<@Nullable MyList>test1(null); + this.<@Nullable MyList>test2(null); + this.<@Nullable MyList>test3(null); + // :: error: (type.argument.type.incompatible) + this.<@Nullable MyList>test4(null); + // :: error: (type.argument.type.incompatible) + this.<@Nullable MyList>test5(null); + this.<@Nullable MyList>test6(null); + } + + void use2(T t, @NonNull T nonNullT) { + this.test1(t); + // :: error: (argument.type.incompatible) + this.<@NonNull T>test3(t); + this.<@NonNull T>test3(nonNullT); + // :: error: (type.argument.type.incompatible) + this.test5(t); } } diff --git a/checker/tests/nullness/Issue906.java b/checker/tests/nullness/Issue906.java index cd88f28069f3..71a22872d0b2 100644 --- a/checker/tests/nullness/Issue906.java +++ b/checker/tests/nullness/Issue906.java @@ -1,6 +1,8 @@ // Test case for Issue 906 // https://github.com/typetools/checker-framework/issues/906 -/** @author Michael Grafl */ +/** + * @author Michael Grafl + */ public class Issue906 { @SuppressWarnings("unchecked") public void start(A a, Class cb) { diff --git a/checker/tests/nullness/Issue961.java b/checker/tests/nullness/Issue961.java index 0d3b3d5ff0e6..e9f5e621e92f 100644 --- a/checker/tests/nullness/Issue961.java +++ b/checker/tests/nullness/Issue961.java @@ -1,6 +1,7 @@ +import org.checkerframework.checker.nullness.qual.NonNull; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.nullness.qual.NonNull; // Test case for Issue 961 // https://github.com/typetools/checker-framework/issues/961 diff --git a/checker/tests/nullness/Issue989.java b/checker/tests/nullness/Issue989.java index 4bb501c72306..5f96a498a547 100644 --- a/checker/tests/nullness/Issue989.java +++ b/checker/tests/nullness/Issue989.java @@ -1,10 +1,11 @@ // Test case for Issue 989: // https://github.com/typetools/checker-framework/issues/989 +import org.checkerframework.checker.nullness.qual.NonNull; + import java.io.Serializable; import java.util.Collection; import java.util.List; -import org.checkerframework.checker.nullness.qual.NonNull; interface ListWrapper989a extends List<@NonNull E> {} diff --git a/checker/tests/nullness/Iterate.java b/checker/tests/nullness/Iterate.java new file mode 100644 index 000000000000..d607d2a2018a --- /dev/null +++ b/checker/tests/nullness/Iterate.java @@ -0,0 +1,11 @@ +// @above-java17-jdk-skip-test TODO: reinstate, false positives may be due to issue #979 + +package wildcards; + +public class Iterate { + void method(Iterable files) { + for (Object file : files) { + file.getClass(); + } + } +} diff --git a/checker/tests/nullness/IteratorEarlyExit.java b/checker/tests/nullness/IteratorEarlyExit.java index 77e7c04c1054..41f134027b91 100644 --- a/checker/tests/nullness/IteratorEarlyExit.java +++ b/checker/tests/nullness/IteratorEarlyExit.java @@ -1,7 +1,8 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.io.*; import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.nullness.qual.*; public class IteratorEarlyExit { public static void m1() { diff --git a/checker/tests/nullness/JPanelTest.java b/checker/tests/nullness/JPanelTest.java index b2af926203c7..1ffe2dbdde64 100644 --- a/checker/tests/nullness/JPanelTest.java +++ b/checker/tests/nullness/JPanelTest.java @@ -1,3 +1,3 @@ // Minimal test case for Issue #244 // https://github.com/typetools/checker-framework/issues/244 -class JPanelTest extends javax.swing.JPanel {} +public class JPanelTest extends javax.swing.JPanel {} diff --git a/checker/tests/nullness/JUnitNull.java b/checker/tests/nullness/JUnitNull.java new file mode 100644 index 000000000000..37b3e32d5561 --- /dev/null +++ b/checker/tests/nullness/JUnitNull.java @@ -0,0 +1,10 @@ +// Simple test that the `@StubFiles({"junit-assertions.astub"})` in the Nullness Checker works +// correctly. + +import org.junit.jupiter.api.Assertions; + +class JUnitNull { + { + Assertions.assertEquals(null, "dummy"); + } +} diff --git a/checker/tests/nullness/JavaCopExplosion.java b/checker/tests/nullness/JavaCopExplosion.java index 293b6d7306c4..fde4396cc7c1 100644 --- a/checker/tests/nullness/JavaCopExplosion.java +++ b/checker/tests/nullness/JavaCopExplosion.java @@ -1,14 +1,15 @@ -import java.util.List; import org.checkerframework.checker.nullness.qual.*; +import java.util.List; + @org.checkerframework.framework.qual.DefaultQualifier(Nullable.class) -class Explosion { +public class JavaCopExplosion { public static class ExplosiveException extends Exception {} @NonNull Integer m_nni = 1; final String m_astring; - Explosion() { + JavaCopExplosion() { // m_nni = 1;\ m_astring = "hi"; try { @@ -23,7 +24,7 @@ static void main(String @NonNull [] args) { @NonNull String s = "Dan"; String s2; s2 = null; - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (s2 != null || s != null) { // :: error: (assignment.type.incompatible) s = s2; @@ -33,14 +34,14 @@ static void main(String @NonNull [] args) { s2 = args[0]; // :: error: (dereference.of.nullable) System.out.println("Possibly cause null pointer with this: " + s2.length()); - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (s2 == null) { // do nothing } else { System.out.println("Can't cause null pointer here: " + s2.length()); s = s2; } - // :: warning: (known.nonnull) + // :: warning: (nulltest.redundant) if (s == null ? s2 != null : s2 != null) { s = s2; } diff --git a/checker/tests/nullness/JavaCopFlow.java b/checker/tests/nullness/JavaCopFlow.java index 8a8f3c06f6b2..959848862e01 100644 --- a/checker/tests/nullness/JavaCopFlow.java +++ b/checker/tests/nullness/JavaCopFlow.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.nullness.qual.*; @org.checkerframework.framework.qual.DefaultQualifier(Nullable.class) -class JavaCopFlow { +public class JavaCopFlow { public void testIf(String str) { diff --git a/checker/tests/nullness/JavaCopRandomTests.java b/checker/tests/nullness/JavaCopRandomTests.java index bb3ba313e2b1..8ecde2bdf518 100644 --- a/checker/tests/nullness/JavaCopRandomTests.java +++ b/checker/tests/nullness/JavaCopRandomTests.java @@ -1,16 +1,16 @@ import org.checkerframework.checker.nullness.qual.*; -class RandomTests { +public class JavaCopRandomTests { final int a; final int b = 1; final int c; - RandomTests() { + JavaCopRandomTests() { String s = null; a = 2; } - RandomTests(String s) throws Exception { + JavaCopRandomTests(String s) throws Exception { // this(); a = 2; if (a > 1) { diff --git a/checker/tests/nullness/JavaExprContext.java b/checker/tests/nullness/JavaExprContext.java index 46a9393d8e6e..28e5ceeafd62 100644 --- a/checker/tests/nullness/JavaExprContext.java +++ b/checker/tests/nullness/JavaExprContext.java @@ -1,6 +1,7 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.nullness.qual.*; // See issue 241: https://github.com/typetools/checker-framework/issues/241 @@ -80,7 +81,7 @@ public void buildGraph1(@KeyFor("graphField1.adjList") String hero) { graphField1.addEdge2( hero, graphField1); // Calling a static method from an instance object. Ensuring this - // doesn't confuse the FlowExpression parsing. + // doesn't confuse the JavaExpression parsing. graphField1.addEdge3(hero); } @@ -95,7 +96,7 @@ public void buildGraph3(staticGraphClass myGraph, @KeyFor("#1.adjList") String h myGraph.addEdge2( hero, myGraph); // Calling a static method from an instance object. Ensuring this - // doesn't confuse the FlowExpression parsing. + // doesn't confuse the JavaExpression parsing. myGraph.addEdge3(hero); } @@ -116,7 +117,7 @@ public void buildGraph1(@KeyFor("graphField1.adjList") String hero) { graphField1.addEdge2( hero, graphField1); // Calling a static method from an instance object. Ensuring this - // doesn't confuse the FlowExpression parsing. + // doesn't confuse the JavaExpression parsing. graphField1.addEdge3(hero); } @@ -126,7 +127,7 @@ public void buildGraph3(@KeyFor("graphField2.adjList") String hero) { graphField2.addEdge2( hero, graphField2); // Calling a static method from an instance object. Ensuring this - // doesn't confuse the FlowExpression parsing. + // doesn't confuse the JavaExpression parsing. graphField2.addEdge3(hero); } @@ -136,7 +137,7 @@ public void buildGraph5(staticGraphClass myGraph, @KeyFor("#1.adjList") String h myGraph.addEdge2( hero, myGraph); // Calling a static method from an instance object. Ensuring this - // doesn't confuse the FlowExpression parsing. + // doesn't confuse the JavaExpression parsing. myGraph.addEdge3(hero); } @@ -151,7 +152,7 @@ public static void buildGraph7(@KeyFor("graphField2.adjList") String hero) { graphField2.addEdge2( hero, graphField2); // Calling a static method from an instance object. Ensuring this - // doesn't confuse the FlowExpression parsing. + // doesn't confuse the JavaExpression parsing. graphField2.addEdge3(hero); } @@ -162,7 +163,7 @@ public static void buildGraph9( myGraph.addEdge2( hero, myGraph); // Calling a static method from an instance object. Ensuring this - // doesn't confuse the FlowExpression parsing. + // doesn't confuse the JavaExpression parsing. myGraph.addEdge3(hero); } diff --git a/checker/tests/nullness/KeyForChecked.java b/checker/tests/nullness/KeyForChecked.java index affa74b31256..ebfe724c7512 100644 --- a/checker/tests/nullness/KeyForChecked.java +++ b/checker/tests/nullness/KeyForChecked.java @@ -1,9 +1,3 @@ -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.KeyForBottom; import org.checkerframework.checker.nullness.qual.NonNull; @@ -13,6 +7,13 @@ import org.checkerframework.framework.qual.DefaultQualifier; import org.checkerframework.framework.qual.TypeUseLocation; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + @DefaultQualifier(value = NonNull.class, locations = TypeUseLocation.IMPLICIT_UPPER_BOUND) public class KeyForChecked { diff --git a/checker/tests/nullness/KeyForDiamond.java b/checker/tests/nullness/KeyForDiamond.java index 453a88f44638..15a3a6de8c04 100644 --- a/checker/tests/nullness/KeyForDiamond.java +++ b/checker/tests/nullness/KeyForDiamond.java @@ -1,6 +1,7 @@ -import java.util.*; import org.checkerframework.checker.nullness.qual.KeyFor; +import java.util.*; + public class KeyForDiamond { private final Map map = new HashMap<>(); diff --git a/checker/tests/nullness/KeyForFlow.java b/checker/tests/nullness/KeyForFlow.java index 225cff9f26b2..6e3117dece20 100644 --- a/checker/tests/nullness/KeyForFlow.java +++ b/checker/tests/nullness/KeyForFlow.java @@ -1,6 +1,7 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.util.HashMap; import java.util.Vector; -import org.checkerframework.checker.nullness.qual.*; public class KeyForFlow extends HashMap { diff --git a/checker/tests/nullness/KeyForIssue328.java b/checker/tests/nullness/KeyForIssue328.java index 0bbcfe1b9dbb..fe92c91e85cf 100644 --- a/checker/tests/nullness/KeyForIssue328.java +++ b/checker/tests/nullness/KeyForIssue328.java @@ -1,10 +1,11 @@ // Test case for Issue 328: // https://github.com/typetools/checker-framework/issues/328 -import java.util.Map; import org.checkerframework.checker.nullness.qual.*; -class KeyForIssue328 { +import java.util.Map; + +public class KeyForIssue328 { public static void m(Map a, Map b, Object ka, Object kb) { if (a.containsKey(ka)) { @NonNull Object i = a.get(ka); // OK diff --git a/checker/tests/nullness/KeyForLocalSideEffect.java b/checker/tests/nullness/KeyForLocalSideEffect.java new file mode 100644 index 000000000000..5c1bca529e1b --- /dev/null +++ b/checker/tests/nullness/KeyForLocalSideEffect.java @@ -0,0 +1,24 @@ +import org.checkerframework.checker.nullness.qual.*; + +import java.util.HashMap; + +public class KeyForLocalSideEffect { + + String k = "key"; + HashMap m = new HashMap<>(); + + void testContainsKeyForFieldKeyAndLocalMap() { + HashMap m_local = m; + + if (m_local.containsKey(k)) { + @KeyFor("m_local") String s = k; + havoc(); + // TODO: This should be an error, because s is no longer a key for m_local. + @NonNull Integer val = m_local.get(s); + } + } + + void havoc() { + m = new HashMap<>(); + } +} diff --git a/checker/tests/nullness/KeyForLocalVariable.java b/checker/tests/nullness/KeyForLocalVariable.java index 038324fc814e..baa1283bcb3a 100644 --- a/checker/tests/nullness/KeyForLocalVariable.java +++ b/checker/tests/nullness/KeyForLocalVariable.java @@ -1,11 +1,12 @@ // Test for Checker Framework issue 795 // https://github.com/typetools/checker-framework/issues/795 +import org.checkerframework.checker.nullness.qual.*; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.nullness.qual.*; -class KeyForLocalVariable { +public class KeyForLocalVariable { public static void localVariableShadowing() { // :: error: (expression.unparsable.type.invalid) diff --git a/checker/tests/nullness/KeyForLub.java b/checker/tests/nullness/KeyForLub.java index 47af01e26008..94b2b62fca74 100644 --- a/checker/tests/nullness/KeyForLub.java +++ b/checker/tests/nullness/KeyForLub.java @@ -1,12 +1,13 @@ package keyfor; -import java.util.HashMap; -import java.util.Map; import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.KeyForBottom; import org.checkerframework.checker.nullness.qual.PolyKeyFor; import org.checkerframework.checker.nullness.qual.UnknownKeyFor; +import java.util.HashMap; +import java.util.Map; + public class KeyForLub { public static boolean flag; Map map1 = new HashMap<>(); @@ -31,11 +32,12 @@ void method( return flag ? key1 : poly; } + // :: error: (type.invalid.annotations.on.location) void poly2(@PolyKeyFor String poly, @UnknownKeyFor String unknown, @KeyForBottom String bot) { // :: error: (assignment.type.incompatible) @PolyKeyFor String s1 = flag ? poly : unknown; @PolyKeyFor String s2 = flag ? poly : bot; - // :: error: (assignment.type.incompatible) + // :: error: (assignment.type.incompatible) :: error: (type.invalid.annotations.on.location) @KeyForBottom String s3 = flag ? poly : bot; } } diff --git a/checker/tests/nullness/KeyForMultiple.java b/checker/tests/nullness/KeyForMultiple.java index 7c2e1c911110..c22be7d16cd3 100644 --- a/checker/tests/nullness/KeyForMultiple.java +++ b/checker/tests/nullness/KeyForMultiple.java @@ -2,10 +2,11 @@ // @skip-test until the bug is fixed. +import org.checkerframework.checker.nullness.qual.KeyFor; + import java.util.HashMap; import java.util.Map; import java.util.Set; -import org.checkerframework.checker.nullness.qual.KeyFor; public class KeyForMultiple { diff --git a/checker/tests/nullness/KeyForPolymorphism.java b/checker/tests/nullness/KeyForPolymorphism.java index 7a6bd52dcde8..462727d834e9 100644 --- a/checker/tests/nullness/KeyForPolymorphism.java +++ b/checker/tests/nullness/KeyForPolymorphism.java @@ -1,9 +1,10 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.nullness.qual.*; // test related to issue 429: https://github.com/typetools/checker-framework/issues/429 -class KeyForPolymorphism { +public class KeyForPolymorphism { Map m1 = new HashMap<>(); Map m2 = new HashMap<>(); diff --git a/checker/tests/nullness/KeyForPostcondition.java b/checker/tests/nullness/KeyForPostcondition.java index 3e39b9f09116..efed1c0d8b22 100644 --- a/checker/tests/nullness/KeyForPostcondition.java +++ b/checker/tests/nullness/KeyForPostcondition.java @@ -1,10 +1,11 @@ -import java.util.HashMap; -import java.util.Map; import org.checkerframework.checker.nullness.qual.EnsuresKeyFor; import org.checkerframework.checker.nullness.qual.EnsuresKeyForIf; import org.checkerframework.checker.nullness.qual.KeyFor; -class KeyForPostcondition { +import java.util.HashMap; +import java.util.Map; + +public class KeyForPostcondition { public static Map m = new HashMap<>(); diff --git a/checker/tests/nullness/KeyForPropagation.java b/checker/tests/nullness/KeyForPropagation.java index 0c97ab5cc8a1..9e5e7e8890f0 100644 --- a/checker/tests/nullness/KeyForPropagation.java +++ b/checker/tests/nullness/KeyForPropagation.java @@ -1,8 +1,9 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; -import org.checkerframework.checker.nullness.qual.*; // interface Dest { // } @@ -13,7 +14,7 @@ // // class Source extends HashMap implements Inter2 {} -class KeyForPropagation { +public class KeyForPropagation { { List<@KeyFor("a") String> a = new ArrayList(); diff --git a/checker/tests/nullness/KeyForShadowing.java b/checker/tests/nullness/KeyForShadowing.java index 4850970f4283..46d5aca3b7ef 100644 --- a/checker/tests/nullness/KeyForShadowing.java +++ b/checker/tests/nullness/KeyForShadowing.java @@ -1,35 +1,34 @@ // Test for Checker Framework issue 273: // https://github.com/typetools/checker-framework/issues/273 +import org.checkerframework.checker.nullness.qual.*; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.nullness.qual.*; -class KeyForShadowing { +public class KeyForShadowing { public static void main(String... p) { Map m0 = new HashMap<>(); Map m1 = new HashMap<>(); String k = "key"; m0.put(k, 1); // k is @KeyFor("m0") after this line - // We expect an error for the next one since we are not - // respecting the method contract. It expects the - // key to be for the second parameter, not the first. + // We expect an error for the next one since we are not respecting the method contract. It + // expects the key to be for the second parameter, not the first. // :: error: (argument.type.incompatible) getMap3(m0, m1, k).toString(); - // We expect an error for the next one since although - // we are respecting the method contract, since the - // key is for the first parameter, the Nullness Checker - // is misinterpreting "m1" to be the local m1 to this - // method, and not the first parameter to the method. + // We expect an error for the next one since although we are respecting the method contract, + // since the key is for the first parameter, the Nullness Checker is misinterpreting "m1" to + // be the local m1 to this method, and not the first parameter to the method. // :: error: (argument.type.incompatible) getMap2(m0, m1, k).toString(); // :: error: (argument.type.incompatible) getMap1(m0, m1, k).toString(); + getMap4(m0, m1, k).toString(); } diff --git a/checker/tests/nullness/KeyForStaticField.java b/checker/tests/nullness/KeyForStaticField.java index e72fddb0707a..f87920ad6ed2 100644 --- a/checker/tests/nullness/KeyForStaticField.java +++ b/checker/tests/nullness/KeyForStaticField.java @@ -2,11 +2,12 @@ // https://github.com/typetools/checker-framework/issues/877 // @skip-test until the issue is fixed. +import org.checkerframework.checker.nullness.qual.*; + import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.nullness.qual.*; -class KeyForStaticField { +public class KeyForStaticField { @SuppressWarnings("keyfor") public static final @KeyFor("this.map") String STATIC_KEY = "some text"; @@ -14,7 +15,7 @@ class KeyForStaticField { public KeyForStaticField() { map = new HashMap<>(); - put(STATIC_KEY, 0); + map.put(STATIC_KEY, 0); } /** Returns the value for the given key, which must be present in the map. */ diff --git a/checker/tests/nullness/KeyForSubst.java b/checker/tests/nullness/KeyForSubst.java index 5a7a5245497e..a5c38ef908e9 100644 --- a/checker/tests/nullness/KeyForSubst.java +++ b/checker/tests/nullness/KeyForSubst.java @@ -1,6 +1,7 @@ -import java.util.List; import org.checkerframework.checker.nullness.qual.*; +import java.util.List; + public class KeyForSubst { /* static class MyClass { diff --git a/checker/tests/nullness/KeyForSubtyping.java b/checker/tests/nullness/KeyForSubtyping.java index a5cfd67201dd..7ba7654f9a0e 100644 --- a/checker/tests/nullness/KeyForSubtyping.java +++ b/checker/tests/nullness/KeyForSubtyping.java @@ -1,6 +1,7 @@ -import java.util.HashMap; import org.checkerframework.checker.nullness.qual.*; +import java.util.HashMap; + public class KeyForSubtyping { HashMap mapA = new HashMap<>(); HashMap mapB = new HashMap<>(); @@ -11,10 +12,9 @@ public void testSubtypeAssignments( @KeyFor("this.mapA") String a, @KeyFor("this.mapB") String b, @KeyFor({"this.mapA", "this.mapB"}) String ab) { - // Try the error cases first, otherwise dataflow will change the inferred - // annotations on the variables such that a line of code can have an - // effect on a subsequent line of code. We want each of these tests to - // be independent. + // Try the error cases first, otherwise dataflow will change the inferred annotations on the + // variables such that a line of code can have an effect on a subsequent line of code. We + // want each of these tests to be independent. // :: error: (assignment.type.incompatible) ab = a; diff --git a/checker/tests/nullness/KeyForTypeVar.java b/checker/tests/nullness/KeyForTypeVar.java new file mode 100644 index 000000000000..f14f9e14e23b --- /dev/null +++ b/checker/tests/nullness/KeyForTypeVar.java @@ -0,0 +1,12 @@ +import org.checkerframework.checker.nullness.qual.KeyFor; +import org.checkerframework.checker.nullness.qual.KeyForBottom; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Map; + +public class KeyForTypeVar { + <@KeyForBottom E extends @KeyFor("#1") T> T method(Map m, E key) { + @NonNull String s = m.get(key); + throw new RuntimeException(); + } +} diff --git a/checker/tests/nullness/KeyFor_DirectionsFinder.java b/checker/tests/nullness/KeyFor_DirectionsFinder.java index 64beaa83803e..9762b45cd20e 100644 --- a/checker/tests/nullness/KeyFor_DirectionsFinder.java +++ b/checker/tests/nullness/KeyFor_DirectionsFinder.java @@ -1,11 +1,12 @@ // @skip-test +import org.checkerframework.checker.nullness.qual.*; + import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import org.checkerframework.checker.nullness.qual.*; public class KeyFor_DirectionsFinder { diff --git a/checker/tests/nullness/KeyFors.java b/checker/tests/nullness/KeyFors.java index a20840544fdd..d90caf6c5acb 100644 --- a/checker/tests/nullness/KeyFors.java +++ b/checker/tests/nullness/KeyFors.java @@ -1,3 +1,5 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -6,7 +8,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import org.checkerframework.checker.nullness.qual.*; public class KeyFors { diff --git a/checker/tests/nullness/LogicOperations.java b/checker/tests/nullness/LogicOperations.java index 782255e637fb..3333958daf66 100644 --- a/checker/tests/nullness/LogicOperations.java +++ b/checker/tests/nullness/LogicOperations.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.checker.nullness.qual.NonNull; -class LogicOperations { +public class LogicOperations { void andTrueClause(@Nullable Object a) { if (a != null && helper()) { a.toString(); diff --git a/checker/tests/nullness/LubTest.java b/checker/tests/nullness/LubTest.java index 9fc06cb4ac38..b5e1ad2df9e5 100644 --- a/checker/tests/nullness/LubTest.java +++ b/checker/tests/nullness/LubTest.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class LubTest { +public class LubTest { @Nullable String str; diff --git a/checker/tests/nullness/MapGetNullable.java b/checker/tests/nullness/MapGetNullable.java index 26f8ad6e8026..306098ce4704 100644 --- a/checker/tests/nullness/MapGetNullable.java +++ b/checker/tests/nullness/MapGetNullable.java @@ -1,9 +1,10 @@ -import java.util.HashMap; -import java.util.Map; import org.checkerframework.checker.nullness.qual.KeyFor; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.HashMap; +import java.util.Map; + public class MapGetNullable { void foo0(Map m, @KeyFor("#1") String key) { diff --git a/checker/tests/nullness/MapMerge.java b/checker/tests/nullness/MapMerge.java index b23d9bc1aaf7..24922889f36f 100644 --- a/checker/tests/nullness/MapMerge.java +++ b/checker/tests/nullness/MapMerge.java @@ -2,7 +2,7 @@ import java.util.Map; import java.util.function.BiFunction; -class MapMerge { +public class MapMerge { public static void main(String[] args) { Map map = new HashMap<>(); map.put("k", "v"); diff --git a/checker/tests/nullness/Marino.java b/checker/tests/nullness/Marino.java index 38107a8bdc92..028cef7bc61b 100644 --- a/checker/tests/nullness/Marino.java +++ b/checker/tests/nullness/Marino.java @@ -32,13 +32,11 @@ void testWhile() throws Exception { a = null; } // Checker doesn't catch that m_str not initialized. - // This is Caveat 2 in the manual, but note that it - // is not limited to contructors. + // This is Caveat 2 in the manual, but note that it is not limited to contructors. System.out.println("Member string has length: " + m_str.length()); // Dereference of any static field is allowed. - // I suppose this is a design decision - // for practicality in interacting with libraries...? + // I suppose this is a design decision for practicality in interacting with libraries...? // :: error: (dereference.of.nullable) System.out.println("Member string has length: " + ms_str.length()); System.out.println( diff --git a/checker/tests/nullness/MethodOverloadingContractsKeyFor.java b/checker/tests/nullness/MethodOverloadingContractsKeyFor.java new file mode 100644 index 000000000000..8ad6f6ad1935 --- /dev/null +++ b/checker/tests/nullness/MethodOverloadingContractsKeyFor.java @@ -0,0 +1,42 @@ +import org.checkerframework.checker.nullness.qual.EnsuresKeyFor; +import org.checkerframework.dataflow.qual.Pure; + +import java.util.HashMap; +import java.util.Map; + +public class MethodOverloadingContractsKeyFor { + + static class ClassA {} + + static class ClassB extends ClassA {} + + @Pure + String name(ClassA classA) { + return "asClassA"; + } + + @Pure + Object name(ClassB classB) { + return "asClassB"; + } + + Map map = new HashMap<>(); + + @EnsuresKeyFor(value = "name(#1)", map = "map") + void put(ClassA classA) { + map.put(name(classA), ""); + } + + void test(ClassA classA, ClassB classB) { + put(classA); + map.get(name(classA)).toString(); + + put(classB); + // :: error: (dereference.of.nullable) + map.get(name(classB)).toString(); + } + + public static void main(String[] args) { + new MethodOverloadingContractsKeyFor().test(new ClassA(), new ClassB()); + } +} diff --git a/checker/tests/nullness/MethodTypeVars4.java b/checker/tests/nullness/MethodTypeVars4.java index 1278cf280972..efa4846b4d3b 100644 --- a/checker/tests/nullness/MethodTypeVars4.java +++ b/checker/tests/nullness/MethodTypeVars4.java @@ -1,9 +1,10 @@ -import java.util.List; import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.framework.qual.DefaultQualifier; import org.checkerframework.framework.qual.TypeUseLocation; -class MethodTypeVars4 { +import java.util.List; + +public class MethodTypeVars4 { @DefaultQualifier(value = NonNull.class, locations = TypeUseLocation.IMPLICIT_UPPER_BOUND) interface I { T doit(); diff --git a/checker/tests/nullness/MissingBoundAnnotations.java b/checker/tests/nullness/MissingBoundAnnotations.java index c698d7817612..096ca97ea12f 100644 --- a/checker/tests/nullness/MissingBoundAnnotations.java +++ b/checker/tests/nullness/MissingBoundAnnotations.java @@ -1,8 +1,9 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Map; -import org.checkerframework.checker.nullness.qual.*; public final class MissingBoundAnnotations { diff --git a/checker/tests/nullness/MisuseProperties.java b/checker/tests/nullness/MisuseProperties.java index 0fbcc358dec2..999dabce4122 100644 --- a/checker/tests/nullness/MisuseProperties.java +++ b/checker/tests/nullness/MisuseProperties.java @@ -1,3 +1,5 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.util.Collection; import java.util.Collections; import java.util.Dictionary; @@ -5,7 +7,6 @@ import java.util.Map; import java.util.Properties; import java.util.Set; -import org.checkerframework.checker.nullness.qual.*; public class MisuseProperties { @@ -16,11 +17,10 @@ void propertiesToHashtable(Properties p) { p.put("line.separator", null); Hashtable h = p; // Error, because HashTable value has NonNull bound. - // TODO: false negative. See #365. - //// :: error: (argument.type.incompatible) :: warning: [unchecked] unchecked call to - //// put(K,V) as a member of the raw type java.util.Hashtable + // put(K,V) as a member of the raw type java.util.Hashtable // :: warning: [unchecked] unchecked call to put(K,V) as a member of the raw type // java.util.Hashtable + // :: error: (argument.type.incompatible) h.put("line.separator", null); // :: error: (argument.type.incompatible) System.setProperty("line.separator", null); @@ -40,8 +40,7 @@ void propertiesToHashtable(Properties p) { System.clearProperty("foo.bar"); // OK - // Each of the following should cause an error, because it leaves - // line.separator null. + // Each of the following should cause an error, because it leaves line.separator null. // These first few need to be special-cased, I think: @@ -62,7 +61,7 @@ void propertiesToHashtable(Properties p) { // treatment as a supertype) for such properties. Set<@KeyFor("p") Object> keys = p.keySet(); - // now remove "line.separator" from the set + // now remove "line.separator" from the set keys.remove("line.separator"); keys.removeAll(keys); keys.clear(); diff --git a/checker/tests/nullness/MonotonicNonNullFieldTest.java b/checker/tests/nullness/MonotonicNonNullFieldTest.java new file mode 100644 index 000000000000..cb69a4f13355 --- /dev/null +++ b/checker/tests/nullness/MonotonicNonNullFieldTest.java @@ -0,0 +1,23 @@ +// Testcase for Issue553 +// https://github.com/typetools/checker-framework/issues/553 +import org.checkerframework.checker.nullness.qual.*; + +public class MonotonicNonNullFieldTest { + class Data { + @MonotonicNonNull Object field; + } + + void method(Object object) {} + + @RequiresNonNull("#1.field") + void test(final Data data) { + method(data.field); // checks OK + + Runnable callback = + new Runnable() { + public void run() { + method(data.field); // used to issue error + } + }; + } +} diff --git a/checker/tests/nullness/NNOEMoreTests.java b/checker/tests/nullness/NNOEMoreTests.java index 0a7e69062c55..0e8cb3f7f81b 100644 --- a/checker/tests/nullness/NNOEMoreTests.java +++ b/checker/tests/nullness/NNOEMoreTests.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.checker.nullness.qual.RequiresNonNull; -class NNOEMoreTests { +public class NNOEMoreTests { class NNOEMain { protected @Nullable String nullable = null; @Nullable String otherNullable = null; diff --git a/checker/tests/nullness/NNOEStaticFields.java b/checker/tests/nullness/NNOEStaticFields.java index f6625d5d30bd..a3f61adfc079 100644 --- a/checker/tests/nullness/NNOEStaticFields.java +++ b/checker/tests/nullness/NNOEStaticFields.java @@ -1,10 +1,11 @@ -import java.util.Collections; -import java.util.Set; import org.checkerframework.checker.initialization.qual.*; import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.checker.nullness.qual.RequiresNonNull; -class NNOEStaticFields { +import java.util.Collections; +import java.util.Set; + +public class NNOEStaticFields { static @Nullable String nullable = null; static @Nullable String otherNullable = null; diff --git a/checker/tests/nullness/NegatingConditionalNullness.java b/checker/tests/nullness/NegatingConditionalNullness.java index a9450ec184fc..a193d51308f8 100644 --- a/checker/tests/nullness/NegatingConditionalNullness.java +++ b/checker/tests/nullness/NegatingConditionalNullness.java @@ -1,9 +1,9 @@ -import java.util.List; import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; -class PptTopLevel { - /** List of all of the splitters for this ppt. */ +import java.util.List; + +public class NegatingConditionalNullness { public @MonotonicNonNull List splitters = null; @EnsuresNonNullIf(result = true, expression = "splitters") @@ -11,37 +11,37 @@ public boolean has_splitters() { return (splitters != null); } - static void testPptTopLevel(PptTopLevel ppt) { + static void test(NegatingConditionalNullness ppt) { if (!ppt.has_splitters()) { return; } @NonNull Object s2 = ppt.splitters; } - static void testPptTopLevelAssert(PptTopLevel ppt) { + static void testAssert(NegatingConditionalNullness ppt) { assert ppt.has_splitters() : "@AssumeAssertion(nullness)"; @NonNull Object s2 = ppt.splitters; } - static void testSimple(PptTopLevel ppt) { + static void testSimple(NegatingConditionalNullness ppt) { if (ppt.has_splitters()) { @NonNull Object s2 = ppt.splitters; } } // False tests - static void testFalse(PptTopLevel ppt) { + static void testFalse(NegatingConditionalNullness ppt) { // :: error: (dereference.of.nullable) ppt.splitters.toString(); // error } - static void testFalseNoAssertion(PptTopLevel ppt) { + static void testFalseNoAssertion(NegatingConditionalNullness ppt) { ppt.has_splitters(); // :: error: (dereference.of.nullable) ppt.splitters.toString(); // error } - static void testFalseIf(PptTopLevel ppt) { + static void testFalseIf(NegatingConditionalNullness ppt) { if (ppt.has_splitters()) { return; } @@ -49,7 +49,7 @@ static void testFalseIf(PptTopLevel ppt) { ppt.splitters.toString(); // error } - // static void testFalseIfBody(PptTopLevel ppt) { + // static void testFalseIfBody(NegatingConditionalNullness ppt) { // if (!ppt.has_splitters()) { // // :: error: (dereference.of.nullable) // ppt.splitters.toString(); // error diff --git a/checker/tests/nullness/NewNullable.java b/checker/tests/nullness/NewNullable.java index 9c8fc0f35ee1..860ed8ad8ad4 100644 --- a/checker/tests/nullness/NewNullable.java +++ b/checker/tests/nullness/NewNullable.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class NewNullable { +public class NewNullable { Object o = new Object(); Object nn = new @NonNull Object(); // :: warning: (new.class.type.invalid) diff --git a/checker/tests/nullness/NewObjectNonNull.java b/checker/tests/nullness/NewObjectNonNull.java index d31c7c062d4b..5f865bf81e57 100644 --- a/checker/tests/nullness/NewObjectNonNull.java +++ b/checker/tests/nullness/NewObjectNonNull.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.framework.qual.DefaultQualifier; -class NewObjectNonNull { +public class NewObjectNonNull { @DefaultQualifier(Nullable.class) class A { A() {} diff --git a/checker/tests/nullness/NonEmptyCollection.java b/checker/tests/nullness/NonEmptyCollection.java index eb5fdb89298d..80edd7fb4506 100644 --- a/checker/tests/nullness/NonEmptyCollection.java +++ b/checker/tests/nullness/NonEmptyCollection.java @@ -1,8 +1,9 @@ -import java.io.*; -import java.util.SortedMap; import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import java.io.*; +import java.util.SortedMap; + public class NonEmptyCollection { public static @NonNull String returnRemove(@NonNull PriorityQueue1<@NonNull String> pq) { diff --git a/checker/tests/nullness/NonNullInitialization.java b/checker/tests/nullness/NonNullInitialization.java index 8929dfa698c0..9e39a99636ca 100644 --- a/checker/tests/nullness/NonNullInitialization.java +++ b/checker/tests/nullness/NonNullInitialization.java @@ -1,8 +1,7 @@ import org.checkerframework.checker.nullness.qual.*; -// :: error: (initialization.fields.uninitialized) public class NonNullInitialization { - private String test; + private String test = "test"; public static void main(String[] args) { NonNullInitialization n = new NonNullInitialization(); diff --git a/checker/tests/nullness/NonNullIteratorNext.java b/checker/tests/nullness/NonNullIteratorNext.java index bdec6ec91505..7930447d4d2f 100644 --- a/checker/tests/nullness/NonNullIteratorNext.java +++ b/checker/tests/nullness/NonNullIteratorNext.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; -class NonNullIteratorNext { +public class NonNullIteratorNext { interface MyIterator extends java.util.Iterator { @NonNull E next(); } diff --git a/checker/tests/nullness/NullableArrays.java b/checker/tests/nullness/NullableArrays.java index 4eea1c0e1098..5a9cc220d2c5 100644 --- a/checker/tests/nullness/NullableArrays.java +++ b/checker/tests/nullness/NullableArrays.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class NullableArrays { +public class NullableArrays { private byte @Nullable [] padding; public NullableArrays(byte @Nullable [] padding) { diff --git a/checker/tests/nullness/NullableConstructor.java b/checker/tests/nullness/NullableConstructor.java new file mode 100644 index 000000000000..deeea46ea723 --- /dev/null +++ b/checker/tests/nullness/NullableConstructor.java @@ -0,0 +1,7 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +public class NullableConstructor { + + // :: error: (nullness.on.constructor) + @Nullable NullableConstructor() {} +} diff --git a/checker/tests/nullness/NullnessFieldInvar.java b/checker/tests/nullness/NullnessFieldInvar.java index ff8163c83f49..44c4365092f9 100644 --- a/checker/tests/nullness/NullnessFieldInvar.java +++ b/checker/tests/nullness/NullnessFieldInvar.java @@ -82,6 +82,7 @@ voi class SuperWithNonFinal { @Nullable Object nonfinal = null; } + // nonfinal isn't final // :: error: (field.invariant.not.final) @FieldInvariant(field = "nonfinal", qualifier = NonNull.class) @@ -143,6 +144,7 @@ class Super2 {} field = {}, qualifier = NonNull.class) class Invalid1 extends Super2 {} + // :: error: (field.invariant.not.wellformed) @FieldInvariant( field = {"a", "b"}, diff --git a/checker/tests/nullness/NullnessIssue4996.java b/checker/tests/nullness/NullnessIssue4996.java new file mode 100644 index 000000000000..05688b3a00b7 --- /dev/null +++ b/checker/tests/nullness/NullnessIssue4996.java @@ -0,0 +1,21 @@ +class NullnessIssue4996 { + abstract class CaptureOuter { + abstract T get(); + + abstract class Inner { + abstract T get(); + } + } + + class Client { + Object getFrom(CaptureOuter o) { + // :: error: (return.type.incompatible) + return o.get(); + } + + Object getFrom(CaptureOuter.Inner o) { + // :: error: (return.type.incompatible) + return o.get(); + } + } +} diff --git a/checker/tests/nullness/ObjectsRequireNonNull.java b/checker/tests/nullness/ObjectsRequireNonNull.java index 3d84d4b04d3f..a4274f05b49c 100644 --- a/checker/tests/nullness/ObjectsRequireNonNull.java +++ b/checker/tests/nullness/ObjectsRequireNonNull.java @@ -1,10 +1,11 @@ // Test case for https://tinyurl.com/cfissue/3149 . -import java.util.Objects; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -class ObjectsRequireNonNull { +import java.util.Objects; + +public class ObjectsRequireNonNull { void foo(@Nullable Object nble, @NonNull Object nn) { // :: error: (argument.type.incompatible) Objects.requireNonNull(null); diff --git a/checker/tests/nullness/ObjectsRequireNonNullElse.java b/checker/tests/nullness/ObjectsRequireNonNullElse.java index cc3e652783fa..90d1c24c1823 100644 --- a/checker/tests/nullness/ObjectsRequireNonNullElse.java +++ b/checker/tests/nullness/ObjectsRequireNonNullElse.java @@ -6,7 +6,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; -class ObjectsRequireNonNullElse { +public class ObjectsRequireNonNullElse { public static void main(String[] args) { @NonNull String value = requireNonNullElse(null, "Something"); System.err.println(requireNonNullElse(null, "Something")); diff --git a/checker/tests/nullness/OptTest.java b/checker/tests/nullness/OptTest.java new file mode 100644 index 000000000000..be6f20fabfe0 --- /dev/null +++ b/checker/tests/nullness/OptTest.java @@ -0,0 +1,16 @@ +// Tests of the `Opt` utility class. + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.util.Opt; + +public class OptTest { + + @SuppressWarnings("dereference.of.nullable") // requires refinement like for Optional.ifPresent. + void m1(@Nullable String o) { + Opt.ifPresent(o, s -> o.toString()); + } + + void m2(@Nullable String o) { + Opt.ifPresent(o, s -> s.toString()); + } +} diff --git a/checker/tests/nullness/OverrideANNA.java b/checker/tests/nullness/OverrideANNA.java index 1b40406d2a8c..1cc7cba6e542 100644 --- a/checker/tests/nullness/OverrideANNA.java +++ b/checker/tests/nullness/OverrideANNA.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.initialization.qual.*; import org.checkerframework.checker.nullness.qual.*; -class OverrideANNA { +public class OverrideANNA { static class Super { Object f; diff --git a/checker/tests/nullness/OverrideANNA2.java b/checker/tests/nullness/OverrideANNA2.java index ecdff3d503dc..90f3782358bf 100644 --- a/checker/tests/nullness/OverrideANNA2.java +++ b/checker/tests/nullness/OverrideANNA2.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.initialization.qual.*; import org.checkerframework.checker.nullness.qual.*; -class OverrideANNA2 { +public class OverrideANNA2 { static class Super { Object f; diff --git a/checker/tests/nullness/OverrideANNA3.java b/checker/tests/nullness/OverrideANNA3.java index 8b972c70e83d..98f38ca0ea16 100644 --- a/checker/tests/nullness/OverrideANNA3.java +++ b/checker/tests/nullness/OverrideANNA3.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; -class OverrideANNA3 { +public class OverrideANNA3 { static class Super { Object f; Object g; diff --git a/checker/tests/nullness/OverrideGenerics.java b/checker/tests/nullness/OverrideGenerics.java index c9ca242b307e..faed45db09a6 100644 --- a/checker/tests/nullness/OverrideGenerics.java +++ b/checker/tests/nullness/OverrideGenerics.java @@ -1,13 +1,13 @@ import org.checkerframework.checker.nullness.qual.*; -class Super { +class OGSuper { public void m(S p) {} } -class Impl1 extends Super { +class OGImpl1 extends OGSuper { public void m(T p) {} } -class Impl2 extends Super { +class OGImpl2 extends OGSuper { public void m(T p) {} } diff --git a/checker/tests/nullness/OverrideNNOE.java b/checker/tests/nullness/OverrideNNOE.java index 82b878101c2c..dd96eea0cdec 100644 --- a/checker/tests/nullness/OverrideNNOE.java +++ b/checker/tests/nullness/OverrideNNOE.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.checker.nullness.qual.RequiresNonNull; -class OverrideNNOE { +public class OverrideNNOE { static class Super { @Nullable Object f; diff --git a/checker/tests/nullness/OverrideNNOE2.java b/checker/tests/nullness/OverrideNNOE2.java index 6fdf67f6a9a6..7f98f0039677 100644 --- a/checker/tests/nullness/OverrideNNOE2.java +++ b/checker/tests/nullness/OverrideNNOE2.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.checker.nullness.qual.RequiresNonNull; -class OverrideNNOE2 { +public class OverrideNNOE2 { static class Super { @Nullable Object f; diff --git a/checker/tests/nullness/PackageDecl.java b/checker/tests/nullness/PackageDecl.java index 532b01050045..1e299befe545 100644 --- a/checker/tests/nullness/PackageDecl.java +++ b/checker/tests/nullness/PackageDecl.java @@ -2,4 +2,4 @@ import org.checkerframework.checker.nullness.qual.*; -class PackageDecl {} +public class PackageDecl {} diff --git a/checker/tests/nullness/ParameterExpression.java b/checker/tests/nullness/ParameterExpression.java index 97d2304dc407..24dae25094e6 100644 --- a/checker/tests/nullness/ParameterExpression.java +++ b/checker/tests/nullness/ParameterExpression.java @@ -1,6 +1,7 @@ -import java.util.Map; import org.checkerframework.checker.nullness.qual.*; +import java.util.Map; + public class ParameterExpression { public void m1( @Nullable Object o, @Nullable Object o1, @Nullable Object o2, @Nullable Object o3) { @@ -32,10 +33,9 @@ public void m3(final @Nullable Object o) {} @EnsuresNonNull("#3") public void m4(@Nullable Object x1, @Nullable Object x2, final @Nullable Object x3) {} - // Formal parameter names should not be used on pre/postcondition, conditional postcondition - // and formal parameter annotations in the same method declaration as the formal parameter - // being referred. In this case, "#paramNum" should be used. This is because - // the parameter names are not saved in bytecode. + // Formal parameter names should not be used in signatures (pre/postcondition, conditional + // postcondition, and formal parameter annotations). Use "#paramNum", because the parameter + // names are not saved in bytecode. @Nullable Object field = null; @@ -47,37 +47,76 @@ public void m5() { @EnsuresNonNull("param") // :: error: (flowexpr.parse.error) - // :: warning: (contracts.postcondition.expression.parameter.name) - public void m6(Object param) { + public void m6a(Object param) { param = new Object(); } - // Warning issued. 'field' is a field, but in this case what matters is that it is the name of a - // formal parameter. + @EnsuresNonNull("param") + // :: error: (flowexpr.parse.error) + public void m6b(Object param) { + // :: error: (assignment.type.incompatible) + param = null; + } + + @EnsuresNonNull("param") + // :: error: (flowexpr.parse.error) + public void m6c(@Nullable Object param) { + param = new Object(); + } + + @EnsuresNonNull("param") + // :: error: (flowexpr.parse.error) + public void m6d(@Nullable Object param) { + param = null; + } + + @EnsuresNonNull("param.toString()") + // :: error: (flowexpr.parse.error) + public void m6e(@Nullable Object param) { + param = null; + } + + @EnsuresNonNull("field") + // :: error: (contracts.postcondition.not.satisfied) + // :: warning: (expression.parameter.name.shadows.field) + public void m7a(Object field) { + field = new Object(); + } + + @EnsuresNonNull("field") + // :: error: (contracts.postcondition.not.satisfied) + // :: warning: (expression.parameter.name.shadows.field) + public void m7b(Object field) { + // :: error: (assignment.type.incompatible) + field = null; + } + @EnsuresNonNull("field") - // The user can write "#1" if they meant the formal parameter, and "this.field" if they meant - // the field. // :: error: (contracts.postcondition.not.satisfied) - // :: warning: (contracts.postcondition.expression.parameter.name) - public void m7(Object field) { + // :: warning: (expression.parameter.name.shadows.field) + public void m7c(@Nullable Object field) { field = new Object(); } + @EnsuresNonNull("field") + // :: error: (contracts.postcondition.not.satisfied) + // :: warning: (expression.parameter.name.shadows.field) + public void m7d(@Nullable Object field) { + field = null; + } + // Preconditions @RequiresNonNull("field") // OK public void m8() {} @RequiresNonNull("param") // :: error: (flowexpr.parse.error) - // :: warning: (contracts.precondition.expression.parameter.name) public void m9(Object param) {} // Warning issued. 'field' is a field, but in this case what matters is that it is the name of a // formal parameter. @RequiresNonNull("field") - // The user can write "#1" if they meant the formal parameter, and "this.field" if they meant - // the field. - // :: warning: (contracts.precondition.expression.parameter.name) + // :: warning: (expression.parameter.name.shadows.field) public void m10(Object field) {} // Conditional postconditions @@ -89,7 +128,6 @@ public boolean m11() { @EnsuresNonNullIf(result = true, expression = "param") // :: error: (flowexpr.parse.error) - // :: warning: (contracts.conditional.postcondition.expression.parameter.name) public boolean m12(Object param) { param = new Object(); return true; @@ -98,15 +136,35 @@ public boolean m12(Object param) { // Warning issued. 'field' is a field, but in this case what matters is that it is the name of a // formal parameter. @EnsuresNonNullIf(result = true, expression = "field") - // The user can write "#1" if they meant the formal parameter, and "this.field" if they meant - // the field. - // :: warning: (contracts.conditional.postcondition.expression.parameter.name) - public boolean m13(Object field) { + // :: warning: (expression.parameter.name.shadows.field) + public boolean m13a(@Nullable Object field) { field = new Object(); // :: error: (contracts.conditional.postcondition.not.satisfied) return true; } + @EnsuresNonNullIf(result = true, expression = "field") + // :: warning: (expression.parameter.name.shadows.field) + public boolean m13b(@Nullable Object field) { + field = new Object(); + return false; + } + + @EnsuresNonNullIf(result = true, expression = "field") + // :: warning: (expression.parameter.name.shadows.field) + public boolean m13c(@Nullable Object field) { + field = null; + // :: error: (contracts.conditional.postcondition.not.satisfied) + return true; + } + + @EnsuresNonNullIf(result = true, expression = "field") + // :: warning: (expression.parameter.name.shadows.field) + public boolean m13d(@Nullable Object field) { + field = null; + return false; + } + // Annotations on formal parameters referring to a formal parameter of the same method. // :: error: (expression.unparsable.type.invalid) public void m14(@KeyFor("param2") Object param1, Map param2) {} diff --git a/checker/tests/nullness/PolyTest.java b/checker/tests/nullness/PolyTest.java new file mode 100644 index 000000000000..05bdee71b0fa --- /dev/null +++ b/checker/tests/nullness/PolyTest.java @@ -0,0 +1,15 @@ +import org.checkerframework.checker.nullness.qual.PolyNull; + +class PolyTest { + void foo1(@PolyNull Object nbl) { + if (nbl == null) { + // :: error: (dereference.of.nullable) + nbl.toString(); + } + } + + void foo2(@PolyNull Object nbl) { + // :: error: (dereference.of.nullable) + nbl.toString(); + } +} diff --git a/checker/tests/nullness/PreconditionFieldNotInStore.java b/checker/tests/nullness/PreconditionFieldNotInStore.java new file mode 100644 index 000000000000..aab732f1cc6b --- /dev/null +++ b/checker/tests/nullness/PreconditionFieldNotInStore.java @@ -0,0 +1,25 @@ +// This test checks that a precondition can use the declared type +// of a field when it isn't in the store. Based on a false positive +// encountered in BCELUtil while testing WPI. + +import java.io.PrintStream; + +class PreconditionFieldNotInStore { + + private @org.checkerframework.checker.nullness.qual.MonotonicNonNull String filename; + + @org.checkerframework.framework.qual.RequiresQualifier( + expression = {"this.filename"}, + qualifier = org.checkerframework.checker.nullness.qual.Nullable.class) + @org.checkerframework.checker.nullness.qual.NonNull String getIndentString() { + return "indentString"; + } + + public void logStackTrace(PrintStream logfile, int[] ste_arr) { + for (int ii = 2; ii < ste_arr.length; ii++) { + int ste = ste_arr[ii]; + logfile.printf("%s %s%n", getIndentString(), ste); + } + logfile.flush(); + } +} diff --git a/checker/tests/nullness/PreventClearProperty.java b/checker/tests/nullness/PreventClearProperty.java index 54b6e95b1eb6..64fa8700d2c8 100644 --- a/checker/tests/nullness/PreventClearProperty.java +++ b/checker/tests/nullness/PreventClearProperty.java @@ -1,9 +1,10 @@ // Same code (but different expected errors) as test PermitClearProperty.java . -import java.util.Properties; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.common.value.qual.StringVal; +import java.util.Properties; + public class PreventClearProperty { static final @StringVal("line.separator") String LINE_SEPARATOR = "line.separator"; diff --git a/checker/tests/nullness/PureTest.java b/checker/tests/nullness/PureTest.java index e9dead4f3442..bcad3f3d378d 100644 --- a/checker/tests/nullness/PureTest.java +++ b/checker/tests/nullness/PureTest.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.dataflow.qual.*; -class PureTest { +public class PureTest { @org.checkerframework.dataflow.qual.Pure @Nullable Object puremethod(@Nullable Object a) { return a; diff --git a/checker/tests/nullness/README b/checker/tests/nullness/README index 4e2fdb0bcd52..fa0e61914279 100644 --- a/checker/tests/nullness/README +++ b/checker/tests/nullness/README @@ -3,4 +3,4 @@ For more details, see ../README To run the tests, do - (cd $CHECKERFRAMEWORK && ./gradlew NullnessFBCTest) + (cd $CHECKERFRAMEWORK && ./gradlew NullnessTest) diff --git a/checker/tests/nullness/RawAndPrimitive.java b/checker/tests/nullness/RawAndPrimitive.java index 5377fd8e670f..2d50290f8b62 100644 --- a/checker/tests/nullness/RawAndPrimitive.java +++ b/checker/tests/nullness/RawAndPrimitive.java @@ -1,4 +1,4 @@ -class RawAndPrimitive { +public class RawAndPrimitive { public T foo(T startValue) { return startValue; } diff --git a/checker/tests/nullness/RawMethodInvocation.java b/checker/tests/nullness/RawMethodInvocation.java deleted file mode 100644 index cb0e5e7b32b9..000000000000 --- a/checker/tests/nullness/RawMethodInvocation.java +++ /dev/null @@ -1,39 +0,0 @@ -import org.checkerframework.checker.initialization.qual.UnknownInitialization; -import org.checkerframework.checker.nullness.qual.*; -import org.checkerframework.checker.nullness.qual.EnsuresNonNull; - -@org.checkerframework.framework.qual.DefaultQualifier(Nullable.class) -class RawMethodInvocation { - Object a; - Object b; - - RawMethodInvocation(boolean constructor_inits_a) { - a = 1; - init_b(); - } - - @EnsuresNonNull("b") - void init_b(@UnknownInitialization RawMethodInvocation this) { - b = 2; - } - - RawMethodInvocation(int constructor_inits_none) { - init_ab(); - } - - @EnsuresNonNull({"a", "b"}) - void init_ab(@UnknownInitialization RawMethodInvocation this) { - a = 1; - b = 2; - } - - RawMethodInvocation(long constructor_escapes_raw) { - a = 1; - // this call is not valid, this is still raw - // :: error: (method.invocation.invalid) - nonRawMethod(); - b = 2; - } - - void nonRawMethod() {} -} diff --git a/checker/tests/nullness/RawSuper.java b/checker/tests/nullness/RawSuper.java index 0c2bc3fac052..74b9e92a675a 100644 --- a/checker/tests/nullness/RawSuper.java +++ b/checker/tests/nullness/RawSuper.java @@ -1,9 +1,8 @@ import org.checkerframework.checker.nullness.qual.*; // @skip-test -// This test is broken as it uses multiple classes. Javac halts -// when seeing the first error -class RawSuper { +// This test is broken as it uses multiple classes. Javac halts when seeing the first error +public class RawSuper { class A { @NonNull Object afield; @@ -49,6 +48,7 @@ void raw(B this) { super.nonRaw(); } } + // This test may be extraneous class C extends B { @NonNull Object cfield; diff --git a/checker/tests/nullness/RawTypesAssignment.java b/checker/tests/nullness/RawTypesAssignment.java index cdc95044ab26..481fbcf10692 100644 --- a/checker/tests/nullness/RawTypesAssignment.java +++ b/checker/tests/nullness/RawTypesAssignment.java @@ -1,7 +1,8 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.HashMap; import java.util.List; import java.util.Map; -import org.checkerframework.checker.nullness.qual.Nullable; public class RawTypesAssignment { Map rawMap = new HashMap(); diff --git a/checker/tests/nullness/RawTypesNullness.java b/checker/tests/nullness/RawTypesNullness.java index b69104e92157..8d58fcd4d233 100644 --- a/checker/tests/nullness/RawTypesNullness.java +++ b/checker/tests/nullness/RawTypesNullness.java @@ -1,3 +1,5 @@ +// @above-java17-jdk-skip-test TODO: reinstate, false negatives may be due to issue #979 + import org.checkerframework.checker.nullness.qual.Nullable; class Generic {} @@ -35,13 +37,11 @@ void useWildCard() { BoundedGeneric generic3 = rawLocal; } + @SuppressWarnings("unchecked") // only needed on JDK 17 and lower void useBoundedWildCard() { BoundedGeneric rawLocal = new BoundedGeneric(); - // :: warning: [unchecked] unchecked conversion BoundedGeneric generic1 = rawReturn(); - // :: warning: [unchecked] unchecked conversion BoundedGeneric generic2 = rawField; - // :: warning: [unchecked] unchecked conversion BoundedGeneric generic3 = rawLocal; } diff --git a/checker/tests/nullness/RawTypesUses.java b/checker/tests/nullness/RawTypesUses.java index a8d1985f7fcb..45a974dbcc84 100644 --- a/checker/tests/nullness/RawTypesUses.java +++ b/checker/tests/nullness/RawTypesUses.java @@ -14,8 +14,7 @@ void foo() { @NonNull Object o1 = notRawNullable.foo(); Generic rawNullable = new Generic<@Nullable String>(); - // TODO: false negative. See #635. - //// :: error: (assignment.type.incompatible) + // :: error: (assignment.type.incompatible) @NonNull Object o2 = rawNullable.foo(); Generic<@NonNull String> notRawNonNull = new Generic<>(); @@ -23,11 +22,9 @@ void foo() { Generic rawNonNull = new Generic<@NonNull String>(); Generic rawNonNullAlais = rawNonNull; - // TODO: false negative. See #635. - //// :: error: (assignment.type.incompatible) + // :: error: (assignment.type.incompatible) @NonNull Object o4 = rawNonNull.foo(); - // TODO: false negative. See #635. - //// :: error: (assignment.type.incompatible) + // :: error: (assignment.type.incompatible) @NonNull Object o5 = rawNonNullAlais.foo(); } @@ -40,18 +37,15 @@ void bar() { @NonNull Object o1 = notRawNullable.foo(); Generic rawNullable = rawReturn(); - // TODO: false negative. See #635. - //// :: error: (assignment.type.incompatible) + // :: error: (assignment.type.incompatible) @NonNull Object o2 = rawNullable.foo(); - // TODO: false negative. See #635. - //// :: error: (assignment.type.incompatible) + // :: error: (assignment.type.incompatible) @NonNull Object o3 = rawReturn().foo(); Generic local = rawReturn(); Generic localAlias = local; - // TODO: false negative. See #635. - //// :: error: (assignment.type.incompatible) + // :: error: (assignment.type.incompatible) @NonNull Object o4 = local.foo(); } } diff --git a/checker/tests/nullness/ReadyReadLine.java b/checker/tests/nullness/ReadyReadLine.java index cbb2f5e24474..77a7c5eac646 100644 --- a/checker/tests/nullness/ReadyReadLine.java +++ b/checker/tests/nullness/ReadyReadLine.java @@ -2,7 +2,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.qual.Pure; -class ReadyReadLine { +public class ReadyReadLine { void m(MyBufferedReader buf) throws Exception { if (buf.ready()) { @@ -17,7 +17,7 @@ void m(MyBufferedReader buf) throws Exception { } } -// this is a replication of the JDK BufferedReader (with only the relevant methods) +// This is a replication of the JDK BufferedReader, with only the relevant methods. class MyBufferedReader { public @Nullable String readLine() throws Exception { return null; diff --git a/checker/tests/nullness/ReceiverAnnotation.java b/checker/tests/nullness/ReceiverAnnotation.java new file mode 100644 index 000000000000..9f2fc729fdf5 --- /dev/null +++ b/checker/tests/nullness/ReceiverAnnotation.java @@ -0,0 +1,13 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class ReceiverAnnotation { + + void receiver1(ReceiverAnnotation this) {} + + // :: error: (nullness.on.receiver) + void receiver2(@NonNull ReceiverAnnotation this) {} + + // :: error: (nullness.on.receiver) + void receiver3(@Nullable ReceiverAnnotation this) {} +} diff --git a/checker/tests/nullness/RefineArray.java b/checker/tests/nullness/RefineArray.java index 9d302b2525a6..a9e34be7b1f3 100644 --- a/checker/tests/nullness/RefineArray.java +++ b/checker/tests/nullness/RefineArray.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.nullness.qual.*; // TODO: Add as test -class RefineArray { +public class RefineArray { public static T[] concat(T @Nullable [] a, T @Nullable [] b) { if (a == null) { if (b != null) { diff --git a/checker/tests/nullness/RepeatEnsuresKeyFor.java b/checker/tests/nullness/RepeatEnsuresKeyFor.java index d06b1e0b192c..7bc85529c780 100644 --- a/checker/tests/nullness/RepeatEnsuresKeyFor.java +++ b/checker/tests/nullness/RepeatEnsuresKeyFor.java @@ -1,8 +1,9 @@ -import java.util.HashMap; -import java.util.Map; import org.checkerframework.checker.nullness.qual.EnsuresKeyFor; import org.checkerframework.checker.nullness.qual.EnsuresKeyForIf; +import java.util.HashMap; +import java.util.Map; + public class RepeatEnsuresKeyFor { Map map = new HashMap<>(); diff --git a/checker/tests/nullness/RepeatEnsuresKeyForWithError.java b/checker/tests/nullness/RepeatEnsuresKeyForWithError.java index 6af1bdceae0e..8a2e77b8ed0c 100644 --- a/checker/tests/nullness/RepeatEnsuresKeyForWithError.java +++ b/checker/tests/nullness/RepeatEnsuresKeyForWithError.java @@ -1,8 +1,9 @@ -import java.util.HashMap; -import java.util.Map; import org.checkerframework.checker.nullness.qual.EnsuresKeyFor; import org.checkerframework.checker.nullness.qual.EnsuresKeyForIf; +import java.util.HashMap; +import java.util.Map; + public class RepeatEnsuresKeyForWithError { Map map = new HashMap<>(); diff --git a/checker/tests/nullness/RepeatedRequiresNonNull.java b/checker/tests/nullness/RepeatedRequiresNonNull.java new file mode 100644 index 000000000000..5809f8afb09a --- /dev/null +++ b/checker/tests/nullness/RepeatedRequiresNonNull.java @@ -0,0 +1,78 @@ +// A test that multiple @RequiresNonNull annotations can be written on the same +// method and work correctly. + +import org.checkerframework.checker.nullness.qual.*; +import org.checkerframework.framework.qual.RequiresQualifier; + +class RepeatedRequiresNonNull { + @Nullable Object f1; + @Nullable Object f2; + + @RequiresNonNull("this.f1") + @RequiresNonNull("this.f2") + void test() { + f1.toString(); + f2.toString(); + } + + void use1() { + // :: error: (contracts.precondition.not.satisfied) + test(); + } + + void use2() { + if (this.f1 != null) { + // :: error: (contracts.precondition.not.satisfied) + test(); + } + } + + void use3() { + if (this.f2 != null) { + // :: error: (contracts.precondition.not.satisfied) + test(); + } + } + + void use4() { + if (this.f1 != null && this.f2 != null) { + test(); + } + } + + // This part of the test is to ensure that @RequiresNonNull and @RequiresQualifier behave + // the same way. It is identical, but uses @RequiresQualifier on the test2() method instead + // of the @RequiresNonNull on the test() method. + + @RequiresQualifier(expression = "this.f1", qualifier = NonNull.class) + @RequiresQualifier(expression = "this.f2", qualifier = NonNull.class) + void test2() { + f1.toString(); + f2.toString(); + } + + void use21() { + // :: error: (contracts.precondition.not.satisfied) + test2(); + } + + void use22() { + if (this.f1 != null) { + // :: error: (contracts.precondition.not.satisfied) + test2(); + } + } + + void use23() { + if (this.f2 != null) { + // :: error: (contracts.precondition.not.satisfied) + test2(); + } + } + + void use24() { + if (this.f1 != null && this.f2 != null) { + test2(); + } + } +} diff --git a/checker/tests/nullness/RequiresNonNullTest.java b/checker/tests/nullness/RequiresNonNullTest.java index bc49bda530bb..2966042e1a51 100644 --- a/checker/tests/nullness/RequiresNonNullTest.java +++ b/checker/tests/nullness/RequiresNonNullTest.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.dataflow.qual.Pure; -class RequiresNonNullTest { +public class RequiresNonNullTest { @Nullable Object field1; @Nullable Object field2; @@ -115,13 +115,14 @@ class NNOEHidingTest extends RequiresNonNullTest { public void hidingClient1(NNOEHidingTest arg5) { arg5.field = "ha!"; - /* We should be testing that the Object "field" from the superclass - * is non-null. We currently only match on the field name and do not - * handle hiding correctly. Instead, we output an error, if we - * detect that hiding happened. - * TODO: correctly resolve hidden fields. - */ + + // TODO: The error message should say something about the hidden field. + // :: error: (contracts.precondition.not.satisfied) arg5.requiresNonNullField(); + + // TODO: Add test like: + // arg5.ensuresNonNullField(); + // arg5.requiresNonNullField(); } public void hidingClient2(NNOEHidingTest arg6) { diff --git a/checker/tests/nullness/ScopingConstruct.java b/checker/tests/nullness/ScopingConstruct.java new file mode 100644 index 000000000000..22099cee3d69 --- /dev/null +++ b/checker/tests/nullness/ScopingConstruct.java @@ -0,0 +1,510 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +@SuppressWarnings("initialization.field.uninitialized") +public class ScopingConstruct { + + // TODO: add nested classes within these two? + static class StaticNested implements AutoCloseable { + public void close() {} + + static class NestedNested implements AutoCloseable { + public void close() {} + } + + class NestedInner implements AutoCloseable { + public void close() {} + } + } + + class Inner implements AutoCloseable { + public void close() {} + + // This is a Java error. + // static class InnerNested {} + + class InnerInner implements AutoCloseable { + public void close() {} + } + } + + StaticNested sn; + + @Nullable StaticNested nsn; + + Inner i; + + @Nullable Inner ni; + + ScopingConstruct.StaticNested scsn; + + // This is a Java error. + // @Nullable ScopingConstruct.StaticNested nscsn; + + ScopingConstruct.@Nullable StaticNested scnsn; + + // This is a Java error. + // ScopingConstruct.@Nullable StaticNested.NestedNested scnsnnn; + + // This is a Java error. + // ScopingConstruct.@Nullable StaticNested.@Nullable NestedNested scnsnnnn; + + // :: error: (nullness.on.outer) + ScopingConstruct.@Nullable StaticNested.NestedInner scnsnni; + + // :: error: (nullness.on.outer) + ScopingConstruct.@Nullable StaticNested.@Nullable NestedInner scnsnnni; + + ScopingConstruct.Inner sci; + + ScopingConstruct.Inner.InnerInner sciii; + + ScopingConstruct.Inner.@Nullable InnerInner scinii; + + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner nsci; + + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner.InnerInner nsciii; + + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner.@Nullable InnerInner nscinii; + + ScopingConstruct.@Nullable Inner scni; + + // :: error: (nullness.on.outer) + ScopingConstruct.@Nullable Inner.InnerInner scniii; + + // :: error: (nullness.on.outer) + ScopingConstruct.@Nullable Inner.@Nullable InnerInner scninii; + + ScopingConstruct.StaticNested.NestedInner scsnni; + + ScopingConstruct.StaticNested.@Nullable NestedInner scsnnni; + + // This is a Java error. + // @Nullable ScopingConstruct.StaticNested.NestedInner nscsnni; + + // This is a Java error. + // @Nullable ScopingConstruct.StaticNested.@Nullable NestedInner nscsnnni; + + // This is a Java error. + // @Nullable ScopingConstruct.@Nullable StaticNested.NestedInner nscnsnni; + + // This is a Java error. + // @Nullable ScopingConstruct.@Nullable StaticNested.@Nullable NestedInner nscnsnnni; + + ScopingConstruct.Inner @Nullable [] scina; + + ScopingConstruct.Inner.InnerInner @Nullable [] sciiina; + + ScopingConstruct.Inner.@Nullable InnerInner @Nullable [] sciniina; + + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner @Nullable [] nscina; + + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner.InnerInner @Nullable [] nsciiina; + + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner.@Nullable InnerInner @Nullable [] nsciniina; + + ScopingConstruct.@Nullable Inner @Nullable [] scnina; + + // :: error: (nullness.on.outer) + ScopingConstruct.@Nullable Inner.InnerInner @Nullable [] scniina; + + // :: error: (nullness.on.outer) + ScopingConstruct.@Nullable Inner.@Nullable InnerInner @Nullable [] scniniina; + + ScopingConstruct.Inner sci() { + throw new Error("not implemented"); + } + + ScopingConstruct.Inner.InnerInner sciii() { + throw new Error("not implemented"); + } + + ScopingConstruct.Inner.@Nullable InnerInner scinii() { + throw new Error("not implemented"); + } + + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner nsci() { + throw new Error("not implemented"); + } + + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner.InnerInner nsciii() { + throw new Error("not implemented"); + } + + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner.@Nullable InnerInner nscinii() { + throw new Error("not implemented"); + } + + ScopingConstruct.@Nullable Inner scni() { + throw new Error("not implemented"); + } + + // :: error: (nullness.on.outer) + ScopingConstruct.@Nullable Inner.InnerInner scniii() { + throw new Error("not implemented"); + } + + // :: error: (nullness.on.outer) + ScopingConstruct.@Nullable Inner.@Nullable InnerInner scninii() { + throw new Error("not implemented"); + } + + ScopingConstruct.Inner @Nullable [] scin() { + throw new Error("not implemented"); + } + + ScopingConstruct.Inner.InnerInner @Nullable [] sciiin() { + throw new Error("not implemented"); + } + + ScopingConstruct.Inner.@Nullable InnerInner @Nullable [] sciniin() { + throw new Error("not implemented"); + } + + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner @Nullable [] nscin() { + throw new Error("not implemented"); + } + + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner.InnerInner @Nullable [] nsciiin() { + throw new Error("not implemented"); + } + + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner.@Nullable InnerInner @Nullable [] nsciniin() { + throw new Error("not implemented"); + } + + ScopingConstruct.@Nullable Inner @Nullable [] scnin() { + throw new Error("not implemented"); + } + + // :: error: (nullness.on.outer) + ScopingConstruct.@Nullable Inner.InnerInner @Nullable [] scniiin() { + throw new Error("not implemented"); + } + + // :: error: (nullness.on.outer) + ScopingConstruct.@Nullable Inner.@Nullable InnerInner @Nullable [] scniniin() { + throw new Error("not implemented"); + } + + /// + /// Formal parameters + /// + + void fsn(StaticNested sn) {} + + void fnsn(@Nullable StaticNested nsn) {} + + void fi(Inner i) {} + + void fni(@Nullable Inner ni) {} + + void fscsn(ScopingConstruct.StaticNested scsn) {} + + void fscnsn(ScopingConstruct.@Nullable StaticNested scnsn) {} + + // :: error: (nullness.on.outer) + void fscnsnni(ScopingConstruct.@Nullable StaticNested.NestedInner scnsnni) {} + + // :: error: (nullness.on.outer) + void fscnsnnni(ScopingConstruct.@Nullable StaticNested.@Nullable NestedInner scnsnnni) {} + + void fsci(ScopingConstruct.Inner sci) {} + + void fsciii(ScopingConstruct.Inner.InnerInner sciii) {} + + void fscinii(ScopingConstruct.Inner.@Nullable InnerInner scinii) {} + + // :: error: (nullness.on.outer) + void fnsci(@Nullable ScopingConstruct.Inner nsci) {} + + // :: error: (nullness.on.outer) + void fnsciii(@Nullable ScopingConstruct.Inner.InnerInner nsciii) {} + + // :: error: (nullness.on.outer) + void fnscinii(@Nullable ScopingConstruct.Inner.@Nullable InnerInner nscinii) {} + + void fscni(ScopingConstruct.@Nullable Inner scni) {} + + // :: error: (nullness.on.outer) + void fscniii(ScopingConstruct.@Nullable Inner.InnerInner scniii) {} + + // :: error: (nullness.on.outer) + void fscninii(ScopingConstruct.@Nullable Inner.@Nullable InnerInner scninii) {} + + void fscsnni(ScopingConstruct.StaticNested.NestedInner scsnni) {} + + void fscsnnni(ScopingConstruct.StaticNested.@Nullable NestedInner scsnnni) {} + + /// + /// Local variables + /// + + void lvsn() { + StaticNested sn; + } + + void lvnsn() { + @Nullable StaticNested nsn; + } + + void lvi() { + Inner i; + } + + void lvni() { + @Nullable Inner ni; + } + + void lvscsn() { + ScopingConstruct.StaticNested scsn; + } + + void lvscnsn() { + ScopingConstruct.@Nullable StaticNested scnsn; + } + + void lvscnsnni() { + // :: error: (nullness.on.outer) + ScopingConstruct.@Nullable StaticNested.NestedInner scnsnni; + } + + void lvscnsnnni() { + // :: error: (nullness.on.outer) + ScopingConstruct.@Nullable StaticNested.@Nullable NestedInner scnsnnni; + } + + void lvsci() { + ScopingConstruct.Inner sci; + } + + void lvsciii() { + ScopingConstruct.Inner.InnerInner sciii; + } + + void lvscinii() { + ScopingConstruct.Inner.@Nullable InnerInner scinii; + } + + void lvnsci() { + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner nsci; + } + + void lvnsciii() { + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner.InnerInner nsciii; + } + + void lvnscinii() { + // :: error: (nullness.on.outer) + @Nullable ScopingConstruct.Inner.@Nullable InnerInner nscinii; + } + + void lvscni() { + ScopingConstruct.@Nullable Inner scni; + } + + void lvscniii() { + // :: error: (nullness.on.outer) + ScopingConstruct.@Nullable Inner.InnerInner scniii; + } + + void lvscninii() { + // :: error: (nullness.on.outer) + ScopingConstruct.@Nullable Inner.@Nullable InnerInner scninii; + } + + void lvscsnni() { + ScopingConstruct.StaticNested.NestedInner scsnni; + } + + void lvscsnnni() { + ScopingConstruct.StaticNested.@Nullable NestedInner scsnnni; + } + + /// + /// Resource variables + /// + + void rvsn() { + try (StaticNested sn = null) {} + } + + void rvnsn() { + try (@Nullable StaticNested nsn = null) {} + } + + void rvi() { + try (Inner i = null) {} + } + + void rvni() { + try (@Nullable Inner ni = null) {} + } + + void rvscsn() { + try (ScopingConstruct.StaticNested scsn = null) {} + } + + void rvscnsn() { + try (ScopingConstruct.@Nullable StaticNested scnsn = null) {} + } + + void rvscnsnni() { + // :: error: (nullness.on.outer) + try (ScopingConstruct.@Nullable StaticNested.NestedInner scnsnni = null) {} + } + + void rvscnsnnni() { + // :: error: (nullness.on.outer) + try (ScopingConstruct.@Nullable StaticNested.@Nullable NestedInner scnsnnni = null) {} + } + + void rvsci() { + try (ScopingConstruct.Inner sci = null) {} + } + + void rvsciii() { + try (ScopingConstruct.Inner.InnerInner sciii = null) {} + } + + void rvscinii() { + try (ScopingConstruct.Inner.@Nullable InnerInner scinii = null) {} + } + + void rvnsci() { + // :: error: (nullness.on.outer) + try (@Nullable ScopingConstruct.Inner nsci = null) {} + } + + void rvnsciii() { + // :: error: (nullness.on.outer) + try (@Nullable ScopingConstruct.Inner.InnerInner nsciii = null) {} + } + + void rvnscinii() { + // :: error: (nullness.on.outer) + try (@Nullable ScopingConstruct.Inner.@Nullable InnerInner nscinii = null) {} + } + + void rvscni() { + try (ScopingConstruct.@Nullable Inner scni = null) {} + } + + void rvscniii() { + // :: error: (nullness.on.outer) + try (ScopingConstruct.@Nullable Inner.InnerInner scniii = null) {} + } + + void rvscninii() { + // :: error: (nullness.on.outer) + try (ScopingConstruct.@Nullable Inner.@Nullable InnerInner scninii = null) {} + } + + void rvscsnni() { + try (ScopingConstruct.StaticNested.NestedInner scsnni = null) {} + } + + void rvscsnnni() { + try (ScopingConstruct.StaticNested.@Nullable NestedInner scsnnni = null) {} + } + + /// + /// For variables + /// + + void fvsn() { + for (StaticNested sn = null; ; ) {} + } + + void fvnsn() { + for (@Nullable StaticNested nsn = null; ; ) {} + } + + void fvi() { + for (Inner i = null; ; ) {} + } + + void fvni() { + for (@Nullable Inner ni = null; ; ) {} + } + + void fvscsn() { + for (ScopingConstruct.StaticNested scsn = null; ; ) {} + } + + void fvscnsn() { + for (ScopingConstruct.@Nullable StaticNested scnsn = null; ; ) {} + } + + void fvscnsnni() { + // :: error: (nullness.on.outer) + for (ScopingConstruct.@Nullable StaticNested.NestedInner scnsnni = null; ; ) {} + } + + void fvscnsnnni() { + // :: error: (nullness.on.outer) + for (ScopingConstruct.@Nullable StaticNested.@Nullable NestedInner scnsnnni = null; ; ) {} + } + + void fvsci() { + for (ScopingConstruct.Inner sci = null; ; ) {} + } + + void fvsciii() { + for (ScopingConstruct.Inner.InnerInner sciii = null; ; ) {} + } + + void fvscinii() { + for (ScopingConstruct.Inner.@Nullable InnerInner scinii = null; ; ) {} + } + + void fvnsci() { + // :: error: (nullness.on.outer) + for (@Nullable ScopingConstruct.Inner nsci = null; ; ) {} + } + + void fvnsciii() { + // :: error: (nullness.on.outer) + for (@Nullable ScopingConstruct.Inner.InnerInner nsciii = null; ; ) {} + } + + void fvnscinii() { + // :: error: (nullness.on.outer) + for (@Nullable ScopingConstruct.Inner.@Nullable InnerInner nscinii = null; ; ) {} + } + + void fvscni() { + for (ScopingConstruct.@Nullable Inner scni = null; ; ) {} + } + + void fvscniii() { + // :: error: (nullness.on.outer) + for (ScopingConstruct.@Nullable Inner.InnerInner scniii = null; ; ) {} + } + + void fvscninii() { + // :: error: (nullness.on.outer) + for (ScopingConstruct.@Nullable Inner.@Nullable InnerInner scninii = null; ; ) {} + } + + void fvscsnni() { + for (ScopingConstruct.StaticNested.NestedInner scsnni = null; ; ) {} + } + + void fvscsnnni() { + for (ScopingConstruct.StaticNested.@Nullable NestedInner scsnnni = null; ; ) {} + } +} diff --git a/checker/tests/nullness/SelfAssignment.java b/checker/tests/nullness/SelfAssignment.java index 434115bb7391..3fb4a63cee9e 100644 --- a/checker/tests/nullness/SelfAssignment.java +++ b/checker/tests/nullness/SelfAssignment.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.Nullable; -class SelfAssignment { +public class SelfAssignment { void test(@Nullable String s) { assertNonNull(s); diff --git a/checker/tests/nullness/SelfDependentType.java b/checker/tests/nullness/SelfDependentType.java new file mode 100644 index 000000000000..155f82727194 --- /dev/null +++ b/checker/tests/nullness/SelfDependentType.java @@ -0,0 +1,153 @@ +// Test case for issue #4142: https://tinyurl.com/cfissue/4142 + +// @skip-test until the issue is fixed + +import org.checkerframework.checker.nullness.qual.*; + +import java.util.HashMap; +import java.util.List; + +public class SelfDependentType { + + public void copy1( + HashMap> a, + HashMap> b) { + a = b; + } + + public void copy2() { + HashMap> a = null; + HashMap> b = null; + a = b; + } + + class SdtGraph1 { + + HashMap> childMap; + + // :: error: (expression.parameter.name) + public SdtGraph1(HashMap> childMap) { + this.childMap = childMap; + } + } + + class SdtGraph2 { + + HashMap> childMap; + + // :: error: (expression.parameter.name) + public SdtGraph2(HashMap> childMap) { + this.childMap = childMap; + } + } + + class SdtGraph3 { + + HashMap> childMap; + + public SdtGraph3(HashMap> childMap) { + this.childMap = childMap; + } + } + + class SdtGraph4 { + + HashMap> childMap; + + public SdtGraph4(HashMap> childMap) { + this.childMap = childMap; + } + } + + class SdtGraph5 { + + HashMap> childMap; + + public SdtGraph5(HashMap> childMap) { + this.childMap = childMap; + } + } + + class SdtGraph6 { + + HashMap> childMap; + + public SdtGraph6(HashMap> childMap) { + this.childMap = childMap; + } + } + + class SdtGraph11 { + + HashMap> childMapField; + + // :: error: (expression.parameter.name) + public SdtGraph11(HashMap> childMap) { + this.childMapField = childMap; + } + } + + class SdtGraph12 { + + HashMap> childMapField; + + // :: error: (expression.parameter.name) + public SdtGraph12(HashMap> childMap) { + this.childMapField = childMap; + } + } + + class SdtGraph13 { + + HashMap> childMapField; + + public SdtGraph13(HashMap> childMap) { + this.childMapField = childMap; + } + } + + class SdtGraph14 { + + HashMap> childMapField; + + public SdtGraph14(HashMap> childMap) { + this.childMapField = childMap; + } + } + + class SdtGraph15 { + + HashMap> childMapField; + + public SdtGraph15(HashMap> childMap) { + this.childMapField = childMap; + } + } + + class SdtGraph16 { + + HashMap> childMapField; + + public SdtGraph16(HashMap> childMap) { + this.childMapField = childMap; + } + } + + class SdtGraph17 { + + HashMap> childMapField; + + public SdtGraph17(HashMap> childMap) { + this.childMapField = childMap; + } + } + + class SdtGraph18 { + + HashMap> childMapField; + + public SdtGraph18(HashMap> childMap) { + this.childMapField = childMap; + } + } +} diff --git a/checker/tests/nullness/StaticInitializer.java b/checker/tests/nullness/StaticInitializer.java deleted file mode 100644 index f5b65f95558b..000000000000 --- a/checker/tests/nullness/StaticInitializer.java +++ /dev/null @@ -1,59 +0,0 @@ -import org.checkerframework.checker.initialization.qual.*; -import org.checkerframework.checker.nullness.qual.*; - -// :: error: (initialization.static.fields.uninitialized) -class StaticInitializer { - - public static String a; - public static String b; - - static { - a = ""; - } - - public StaticInitializer() {} -} - -// :: error: (initialization.static.fields.uninitialized) -class StaticInitializer2 { - public static String a; - public static String b; -} - -class StaticInitializer3 { - public static String a = ""; -} - -class StaticInitializer4 { - public static String a = ""; - public static String b; - - static { - b = ""; - } -} - -class StaticInitializer5 { - public static String a = ""; - - static { - a.toString(); - } - - public static String b = ""; -} - -class StaticInitializer6 { - public static String a = ""; - - public static String b; - - static { - // TODO error expected. See #556. - b.toString(); - } - - static { - b = ""; - } -} diff --git a/checker/tests/nullness/StaticInitializer2.java b/checker/tests/nullness/StaticInitializer2.java index e123aa95a35e..dfcf577ba6c2 100644 --- a/checker/tests/nullness/StaticInitializer2.java +++ b/checker/tests/nullness/StaticInitializer2.java @@ -8,7 +8,7 @@ import org.checkerframework.checker.initialization.qual.*; import org.checkerframework.checker.nullness.qual.*; -class StaticInitializer2 { +public class StaticInitializer2 { static String a; diff --git a/checker/tests/nullness/Stats.java b/checker/tests/nullness/Stats.java index 12146312e474..63cf1782e71a 100644 --- a/checker/tests/nullness/Stats.java +++ b/checker/tests/nullness/Stats.java @@ -1,8 +1,9 @@ // @skip-tests Failing, but commented out to avoid breaking the build -import java.util.Map; import org.checkerframework.checker.nullness.qual.*; +import java.util.Map; + public class Stats { @Nullable Map inv_map = null; diff --git a/checker/tests/nullness/SuppressDeprecation.java b/checker/tests/nullness/SuppressDeprecation.java index 6768981f167f..97b88c300b08 100644 --- a/checker/tests/nullness/SuppressDeprecation.java +++ b/checker/tests/nullness/SuppressDeprecation.java @@ -5,7 +5,7 @@ class SuppressDeprecationOther { void old() {} } -class SuppressDeprecation { +public class SuppressDeprecation { @MonotonicNonNull String tz1; diff --git a/checker/tests/nullness/SuppressWarningsTest.java b/checker/tests/nullness/SuppressWarningsTest.java index bde8b1449412..8a64666a1330 100644 --- a/checker/tests/nullness/SuppressWarningsTest.java +++ b/checker/tests/nullness/SuppressWarningsTest.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class SuppressWarningsTest { +public class SuppressWarningsTest { @SuppressWarnings("all") void test() { diff --git a/checker/tests/nullness/TernaryNested.java b/checker/tests/nullness/TernaryNested.java index 330a8601e93d..84926f7226aa 100644 --- a/checker/tests/nullness/TernaryNested.java +++ b/checker/tests/nullness/TernaryNested.java @@ -1,11 +1,12 @@ // Test case for Issue 331: // https://github.com/typetools/checker-framework/issues/331 -import java.util.List; import org.checkerframework.checker.initialization.qual.*; import org.checkerframework.checker.nullness.qual.*; -class TernaryNested { +import java.util.List; + +public class TernaryNested { Object foo(boolean b) { Object o = b ? "" : (b ? "" : ""); return o; diff --git a/checker/tests/nullness/TestFromPullRequest880.java b/checker/tests/nullness/TestFromPullRequest880.java index 454b81c185fc..3f8624f00efc 100644 --- a/checker/tests/nullness/TestFromPullRequest880.java +++ b/checker/tests/nullness/TestFromPullRequest880.java @@ -5,9 +5,10 @@ // Also note a test that uses multiple compilation units at: // checker/jtreg/nullness/annotationsOnExtends/ +import org.checkerframework.checker.nullness.qual.NonNull; + import java.io.Serializable; import java.util.List; -import org.checkerframework.checker.nullness.qual.NonNull; class TFPR880Test implements Serializable {} diff --git a/checker/tests/nullness/TestInfer.java b/checker/tests/nullness/TestInfer.java index edbc9fb214b0..a16b8fed2d51 100644 --- a/checker/tests/nullness/TestInfer.java +++ b/checker/tests/nullness/TestInfer.java @@ -1,10 +1,11 @@ // Test case for issue #238: https://github.com/typetools/checker-framework/issues/238 +import org.checkerframework.checker.nullness.qual.UnknownKeyFor; + import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.nullness.qual.UnknownKeyFor; -class TestInfer { +public class TestInfer { T getValue(List l) { return l.get(0); } @@ -15,8 +16,7 @@ void foo() { List<@UnknownKeyFor ? extends Object> ls = new ArrayList<>(); bar(getValue(ls)); // this fails, but just getValue(ls) is OK // casting is also OK, ie bar((Object)getValue(ls)) - // the constraint should be T<:Object, which should typecheck - // since ls:List unifies with List where - // T<:Object. + // The constraint should be T<:Object, which should typecheck since ls:List unifies with List where T<:Object. } } diff --git a/checker/tests/nullness/TestPolyNull.java b/checker/tests/nullness/TestPolyNull.java index 58624de1b558..452205b02635 100644 --- a/checker/tests/nullness/TestPolyNull.java +++ b/checker/tests/nullness/TestPolyNull.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class TestPolyNull { +public class TestPolyNull { @PolyNull String identity(@PolyNull String str) { return str; } @@ -38,9 +38,6 @@ void test2() { } public static @PolyNull String identity2(@PolyNull String a) { - // TODO: it would be nice, if this code type-checks (just like identity and identity3), - // but currently a technical limitation in the flow analysis prevents this - // :: error: (return.type.incompatible) return (a == null) ? null : a; } diff --git a/checker/tests/nullness/TestValOf.java b/checker/tests/nullness/TestValOf.java index a0acb4b54c71..31952742d686 100644 --- a/checker/tests/nullness/TestValOf.java +++ b/checker/tests/nullness/TestValOf.java @@ -1,6 +1,6 @@ // Test case for issue #243: https://github.com/typetools/checker-framework/issues/243 -class TestValOf> { +public class TestValOf> { private final Class enumClass; diff --git a/checker/tests/nullness/ThisIsNN.java b/checker/tests/nullness/ThisIsNN.java index 41e8147625da..139e94ad5902 100644 --- a/checker/tests/nullness/ThisIsNN.java +++ b/checker/tests/nullness/ThisIsNN.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class ThisIsNN { +public class ThisIsNN { Object out = new Object(); class Inner { diff --git a/checker/tests/nullness/ThisLiteral.java b/checker/tests/nullness/ThisLiteral.java deleted file mode 100644 index ab533b44d372..000000000000 --- a/checker/tests/nullness/ThisLiteral.java +++ /dev/null @@ -1,20 +0,0 @@ -import org.checkerframework.checker.initialization.qual.*; -import org.checkerframework.checker.nullness.qual.*; - -public class ThisLiteral { - public ThisLiteral() { - new Object() { - void test() { - @UnderInitialization ThisLiteral l1 = ThisLiteral.this; - // :: error: (assignment.type.incompatible) - @Initialized ThisLiteral l2 = ThisLiteral.this; - - ThisLiteral.this.foo(); - // :: error: (method.invocation.invalid) - foo(); - } - }; - } - - void foo() {} -} diff --git a/checker/tests/nullness/ThisLiteralQualified.java b/checker/tests/nullness/ThisLiteralQualified.java deleted file mode 100644 index 1ba9f17fea8f..000000000000 --- a/checker/tests/nullness/ThisLiteralQualified.java +++ /dev/null @@ -1,14 +0,0 @@ -// Test case for Issue #2208: -// https://github.com/typetools/checker-framework/issues/2208 - -// @skip-test until the issue is fixed - -import org.checkerframework.checker.initialization.qual.UnderInitialization; - -public class ThisLiteralQualified { - public ThisLiteralQualified() { - super(); - @UnderInitialization ThisLiteralQualified a = this; - @UnderInitialization ThisLiteralQualified b = ThisLiteralQualified.this; - } -} diff --git a/checker/tests/nullness/ThisQualified.java b/checker/tests/nullness/ThisQualified.java new file mode 100644 index 000000000000..87c8f07efd6d --- /dev/null +++ b/checker/tests/nullness/ThisQualified.java @@ -0,0 +1,12 @@ +// Test case for Issue #2208: +// https://github.com/typetools/checker-framework/issues/2208 + +import org.checkerframework.checker.initialization.qual.UnderInitialization; + +public class ThisQualified { + public ThisQualified() { + super(); + @UnderInitialization ThisQualified a = this; + @UnderInitialization ThisQualified b = ThisQualified.this; + } +} diff --git a/checker/tests/nullness/ThisTest.java b/checker/tests/nullness/ThisTest.java index db178070835f..3c33ff026780 100644 --- a/checker/tests/nullness/ThisTest.java +++ b/checker/tests/nullness/ThisTest.java @@ -2,7 +2,7 @@ @org.checkerframework.framework.qual.DefaultQualifier( org.checkerframework.checker.nullness.qual.NonNull.class) -class ThisTest { +public class ThisTest { public String field; diff --git a/checker/tests/nullness/ThreadLocalTest2.java b/checker/tests/nullness/ThreadLocalTest2.java index dd630efc5f4b..53f205b369ba 100644 --- a/checker/tests/nullness/ThreadLocalTest2.java +++ b/checker/tests/nullness/ThreadLocalTest2.java @@ -28,7 +28,7 @@ protected Integer initialValue() { class MyThreadLocalNN extends ThreadLocal<@NonNull Integer> { @Override protected Integer initialValue() { - return new Integer(0); + return Integer.valueOf(0); } } @@ -53,7 +53,7 @@ class MyThreadLocalNble extends ThreadLocal<@Nullable Integer> { class MyThreadLocalNbleStrongerOverride extends ThreadLocal<@Nullable Integer> { @Override protected @NonNull Integer initialValue() { - return new Integer(0); + return Integer.valueOf(0); } } diff --git a/checker/tests/nullness/ToArrayDiagnostics.java b/checker/tests/nullness/ToArrayDiagnostics.java index c9d8de57587c..174e51ee0e86 100644 --- a/checker/tests/nullness/ToArrayDiagnostics.java +++ b/checker/tests/nullness/ToArrayDiagnostics.java @@ -17,14 +17,14 @@ String[] ok4(ArrayList list) { String[] warn1(ArrayList list) { // :: error: (new.array.type.invalid) String[] resultArray = new String[list.size()]; - // :: error: (return.type.incompatible) :: warning: (toArray.nullable.elements.not.newarray) + // :: error: (return.type.incompatible) :: warning: (toarray.nullable.elements.not.newarray) return list.toArray(resultArray); } String[] warn2(ArrayList list) { int size = list.size(); // :: error: (new.array.type.invalid) :: error: (return.type.incompatible) :: warning: - // (toArray.nullable.elements.mismatched.size) + // (toarray.nullable.elements.mismatched.size) return list.toArray(new String[size]); } } diff --git a/checker/tests/nullness/ToArrayNullness.java b/checker/tests/nullness/ToArrayNullness.java index 5f17d107df6f..c42943bf2342 100644 --- a/checker/tests/nullness/ToArrayNullness.java +++ b/checker/tests/nullness/ToArrayNullness.java @@ -1,107 +1,108 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.util.ArrayList; import java.util.Collection; import java.util.List; -import org.checkerframework.checker.nullness.qual.*; public class ToArrayNullness { private List<@Nullable String> nullableList = new ArrayList<>(); private List<@NonNull String> nonnullList = new ArrayList<>(); void listToArrayObject() { - for (@Nullable Object o : nullableList.toArray()) ; + for (@Nullable Object o : nullableList.toArray()) {} // :: error: (enhancedfor.type.incompatible) - for (@NonNull Object o : nullableList.toArray()) ; // error + for (@NonNull Object o : nullableList.toArray()) {} - for (@Nullable Object o : nonnullList.toArray()) ; - for (@NonNull Object o : nonnullList.toArray()) ; + for (@Nullable Object o : nonnullList.toArray()) {} + for (@NonNull Object o : nonnullList.toArray()) {} } void listToArrayE() { - for (@Nullable String o : nullableList.toArray(new @Nullable String[0])) ; + for (@Nullable String o : nullableList.toArray(new @Nullable String[0])) {} // :: error: (enhancedfor.type.incompatible) - for (@NonNull String o : nullableList.toArray(new @Nullable String[0])) ; // error + for (@NonNull String o : nullableList.toArray(new @Nullable String[0])) {} // TODOINVARR:: error: (argument.type.incompatible) - for (@Nullable String o : nullableList.toArray(new @NonNull String[0])) ; + for (@Nullable String o : nullableList.toArray(new @NonNull String[0])) {} // TODOINVARR:: error: (argument.type.incompatible) // :: error: (enhancedfor.type.incompatible) - for (@NonNull String o : nullableList.toArray(new @NonNull String[0])) ; + for (@NonNull String o : nullableList.toArray(new @NonNull String[0])) {} - for (@Nullable String o : nonnullList.toArray(new String[0])) ; + for (@Nullable String o : nonnullList.toArray(new String[0])) {} // No error expected here. Note that the heuristics determine that the given array // is not used and that a new one will be created. - for (@NonNull String o : nonnullList.toArray(new @Nullable String[0])) ; - for (@Nullable String o : nonnullList.toArray(new @NonNull String[0])) ; - for (@NonNull String o : nonnullList.toArray(new @NonNull String[0])) ; + for (@NonNull String o : nonnullList.toArray(new @Nullable String[0])) {} + for (@Nullable String o : nonnullList.toArray(new @NonNull String[0])) {} + for (@NonNull String o : nonnullList.toArray(new @NonNull String[0])) {} } private Collection<@Nullable String> nullableCol = new ArrayList<@Nullable String>(); private Collection<@NonNull String> nonnullCol = new ArrayList<@NonNull String>(); void colToArrayObject() { - for (@Nullable Object o : nullableCol.toArray()) ; + for (@Nullable Object o : nullableCol.toArray()) {} // :: error: (enhancedfor.type.incompatible) - for (@NonNull Object o : nullableCol.toArray()) ; // error + for (@NonNull Object o : nullableCol.toArray()) {} - for (@Nullable Object o : nonnullCol.toArray()) ; - for (@NonNull Object o : nonnullCol.toArray()) ; + for (@Nullable Object o : nonnullCol.toArray()) {} + for (@NonNull Object o : nonnullCol.toArray()) {} } void colToArrayE() { - for (@Nullable String o : nullableCol.toArray(new @Nullable String[0])) ; + for (@Nullable String o : nullableCol.toArray(new @Nullable String[0])) {} // :: error: (enhancedfor.type.incompatible) - for (@NonNull String o : nullableCol.toArray(new @Nullable String[0])) ; // error + for (@NonNull String o : nullableCol.toArray(new @Nullable String[0])) {} // TODOINVARR:: error: (argument.type.incompatible) - for (@Nullable String o : nullableCol.toArray(new @NonNull String[0])) ; + for (@Nullable String o : nullableCol.toArray(new @NonNull String[0])) {} // TODOINVARR:: error: (argument.type.incompatible) // :: error: (enhancedfor.type.incompatible) - for (@NonNull String o : nullableCol.toArray(new @NonNull String[0])) ; // error + for (@NonNull String o : nullableCol.toArray(new @NonNull String[0])) {} - for (@Nullable String o : nonnullCol.toArray(new String[0])) ; + for (@Nullable String o : nonnullCol.toArray(new String[0])) {} // No error expected here. Note that the heuristics determine that the given array // is not used and that a new one will be created. - for (@NonNull String o : nonnullCol.toArray(new @Nullable String[0])) ; - for (@Nullable String o : nonnullCol.toArray(new @NonNull String[0])) ; - for (@NonNull String o : nonnullCol.toArray(new @NonNull String[0])) ; + for (@NonNull String o : nonnullCol.toArray(new @Nullable String[0])) {} + for (@Nullable String o : nonnullCol.toArray(new @NonNull String[0])) {} + for (@NonNull String o : nonnullCol.toArray(new @NonNull String[0])) {} } void testHearusitics() { - for (@Nullable String o : nonnullCol.toArray(new String[] {})) ; - for (@NonNull String o : nonnullCol.toArray(new String[] {})) ; - for (@Nullable String o : nonnullCol.toArray(new String[0])) ; - for (@NonNull String o : nonnullCol.toArray(new String[0])) ; - for (@Nullable String o : nonnullCol.toArray(new String[nonnullCol.size()])) ; - for (@NonNull String o : nonnullCol.toArray(new String[nonnullCol.size()])) ; + for (@Nullable String o : nonnullCol.toArray(new String[] {})) {} + for (@NonNull String o : nonnullCol.toArray(new String[] {})) {} + for (@Nullable String o : nonnullCol.toArray(new String[0])) {} + for (@NonNull String o : nonnullCol.toArray(new String[0])) {} + for (@Nullable String o : nonnullCol.toArray(new String[nonnullCol.size()])) {} + for (@NonNull String o : nonnullCol.toArray(new String[nonnullCol.size()])) {} - // :: warning: (toArray.nullable.elements.mismatched.size) - for (@Nullable String o : nonnullCol.toArray(new @Nullable String[] {null})) ; + // :: warning: (toarray.nullable.elements.mismatched.size) + for (@Nullable String o : nonnullCol.toArray(new @Nullable String[] {null})) {} // :: error: (enhancedfor.type.incompatible) :: warning: - // (toArray.nullable.elements.mismatched.size) - for (@NonNull String o : nonnullCol.toArray(new @Nullable String[] {null})) ; // error + // (toarray.nullable.elements.mismatched.size) + for (@NonNull String o : nonnullCol.toArray(new @Nullable String[] {null})) {} // Size 1 is too big for an empty array. Complain. TODO: Could allow as result is Nullable. // :: error: (new.array.type.invalid) :: warning: - // (toArray.nullable.elements.mismatched.size) - for (@Nullable String o : nonnullCol.toArray(new String[1])) ; + // (toarray.nullable.elements.mismatched.size) + for (@Nullable String o : nonnullCol.toArray(new String[1])) {} // :: error: (enhancedfor.type.incompatible) :: error: (new.array.type.invalid) :: warning: - // (toArray.nullable.elements.mismatched.size) - for (@NonNull String o : nonnullCol.toArray(new String[1])) ; // error + // (toarray.nullable.elements.mismatched.size) + for (@NonNull String o : nonnullCol.toArray(new String[1])) {} // Array too big -> complain. TODO: Could allow as result is Nullable. // :: error: (new.array.type.invalid) :: warning: - // (toArray.nullable.elements.mismatched.size) - for (@Nullable String o : nonnullCol.toArray(new String[nonnullCol.size() + 1])) ; + // (toarray.nullable.elements.mismatched.size) + for (@Nullable String o : nonnullCol.toArray(new String[nonnullCol.size() + 1])) {} // Array too big -> complain. // :: error: (enhancedfor.type.incompatible) :: error: (new.array.type.invalid) :: warning: - // (toArray.nullable.elements.mismatched.size) - for (@NonNull String o : nonnullCol.toArray(new String[nonnullCol.size() + 1])) ; // error + // (toarray.nullable.elements.mismatched.size) + for (@NonNull String o : nonnullCol.toArray(new String[nonnullCol.size() + 1])) {} // cannot handle the following cases for now // new array not size 0 or .size -> complain about cration. TODO: Could allow as result is // Nullable. // :: error: (new.array.type.invalid) :: warning: - // (toArray.nullable.elements.mismatched.size) - for (@Nullable String o : nonnullCol.toArray(new String[nonnullCol.size() - 1])) ; + // (toarray.nullable.elements.mismatched.size) + for (@Nullable String o : nonnullCol.toArray(new String[nonnullCol.size() - 1])) {} // New array not size 0 or .size -> complain about creation. // :: error: (enhancedfor.type.incompatible) :: error: (new.array.type.invalid) :: warning: - // (toArray.nullable.elements.mismatched.size) - for (@NonNull String o : nonnullCol.toArray(new String[nonnullCol.size() - 1])) ; // error + // (toarray.nullable.elements.mismatched.size) + for (@NonNull String o : nonnullCol.toArray(new String[nonnullCol.size() - 1])) {} } } diff --git a/checker/tests/nullness/ToArrayTest.java b/checker/tests/nullness/ToArrayTest.java new file mode 100644 index 000000000000..513578bcaa5b --- /dev/null +++ b/checker/tests/nullness/ToArrayTest.java @@ -0,0 +1,21 @@ +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Collection; + +public final class ToArrayTest { + + public static void isReverse1(@NonNull Collection seq1) { + Object[] seq1_array_TMP2 = seq1.toArray(); + Object[] seq1_array = seq1.toArray(new Object[] {}); + } + + public static void isReverse2(@NonNull Collection seq1) { + @NonNull Object @NonNull [] seq1_array_TMP = new Object[] {}; + Object[] seq1_array = seq1.toArray(new Object[] {}); + } + + public static void isReverse3(@NonNull Collection seq1) { + @NonNull Object @NonNull [] seq1_array_TMP = new Object[] {}; + Object[] seq1_array = seq1.toArray(new Object[] {}); + } +} diff --git a/checker/tests/nullness/TryCatch.java b/checker/tests/nullness/TryCatch.java deleted file mode 100644 index d76765033e4a..000000000000 --- a/checker/tests/nullness/TryCatch.java +++ /dev/null @@ -1,29 +0,0 @@ -import java.io.*; -import java.util.ArrayList; -import java.util.List; -import org.checkerframework.checker.nullness.qual.*; - -class EntryReader { - public EntryReader() throws IOException {} -} - -class TryCatch { - void constructorException() throws IOException { - List file_errors = new ArrayList<>(); - try { - new EntryReader(); - } catch (FileNotFoundException e) { - file_errors.add(e); - } - } - - void unreachableCatch(String[] xs) { - String t = ""; - t.toString(); - try { - } catch (Throwable e) { - // :: error: (dereference.of.nullable) - t.toString(); - } - } -} diff --git a/checker/tests/nullness/TryWithResources.java b/checker/tests/nullness/TryWithResources.java index 7b755c1d5d4d..cbe611cecd42 100644 --- a/checker/tests/nullness/TryWithResources.java +++ b/checker/tests/nullness/TryWithResources.java @@ -1,6 +1,9 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + import java.io.*; +import java.util.zip.ZipFile; -class TryWithResources { +public class TryWithResources { void m1(InputStream stream) { try (BufferedReader in = new BufferedReader(new InputStreamReader(stream))) { in.toString(); @@ -15,4 +18,29 @@ void m2() { } catch (Exception e) { } } + + // Check that catch blocks and code after try-catch are part of CFG (and flow-sensitive + // type-refinements work there). + boolean m3(@Nullable Object x) { + try (ZipFile f = openZipFile()) { + return true; + } catch (IOException e) { + if (x != null) { + // OK + x.toString(); + } + } + + if (x != null) { + // OK + return x.equals(x); + } + + return false; + } + + // Helper + private static ZipFile openZipFile() throws IOException { + throw new IOException("No zip-file for you!"); + } } diff --git a/checker/tests/nullness/TypeVarPrimitivesNullness.java b/checker/tests/nullness/TypeVarPrimitivesNullness.java index afec3238d53b..939129406eba 100644 --- a/checker/tests/nullness/TypeVarPrimitivesNullness.java +++ b/checker/tests/nullness/TypeVarPrimitivesNullness.java @@ -9,7 +9,7 @@ public class TypeVarPrimitivesNullness { } void methodIntersection(T tLong) { - // :: error: (assignment.type.incompatible) + // :: error: (unboxing.of.nullable) long l = tLong; } @@ -27,7 +27,7 @@ public class TypeVarPrimitivesNullness { } void methodIntersection3(@Nullable T tLong) { - // :: error: (assignment.type.incompatible) + // :: error: (unboxing.of.nullable) long l = tLong; } } diff --git a/checker/tests/nullness/UnannoPrimitives.java b/checker/tests/nullness/UnannoPrimitives.java index 32f001477b23..4e70f9ae072c 100644 --- a/checker/tests/nullness/UnannoPrimitives.java +++ b/checker/tests/nullness/UnannoPrimitives.java @@ -1,10 +1,10 @@ import org.checkerframework.checker.nullness.qual.*; -class UnannoPrimitives { - // :: error: (type.invalid.annotations.on.use) +public class UnannoPrimitives { + // :: error: (nullness.on.primitive) @Nullable int f; - // TODO:: error: (type.invalid) + // :: error: (nullness.on.primitive) @NonNull int g; void local() { @@ -15,10 +15,10 @@ void local() { int i = Integer.valueOf(99) + 1900; int j = 7 + 1900; - // :: error: (type.invalid.annotations.on.use) + // :: error: (nullness.on.primitive) @Nullable int f; - // TODO:: error: (type.invalid) + // :: error: (nullness.on.primitive) @NonNull int g; } @@ -28,22 +28,31 @@ static void testDate() { String strDate = "/" + year; } - // :: error: (type.invalid.annotations.on.use) + // :: error: (nullness.on.primitive) @Nullable byte[] d1 = {4}; byte @Nullable [] d1b = {4}; + // :: error: (nullness.on.primitive) + @Nullable byte[][] twoD = {{4}}; + + // :: error: (nullness.on.primitive) + @Nullable byte[][][] threeD = {{{4}}}; + + // :: error: (nullness.on.primitive) + @Nullable byte[][][][] fourD = {{{{4}}}}; + @SuppressWarnings("ha!") byte[] d2 = {4}; - // :: error: (type.invalid.annotations.on.use) + // :: error: (nullness.on.primitive) Object ar = new @Nullable byte[] {4}; - // TODO:: error: (type.invalid) + // :: error: (nullness.on.primitive) Object ar2 = new @NonNull byte[] {42}; void testCasts(Integer i1) { Object i2 = (int) i1; - // :: error: (type.invalid.annotations.on.use) + // :: error: (nullness.on.primitive) Object i3 = (@Nullable int) i1; } } diff --git a/checker/tests/nullness/UnboxConditions.java b/checker/tests/nullness/UnboxConditions.java index f9619f49fb83..856e9e0c85ae 100644 --- a/checker/tests/nullness/UnboxConditions.java +++ b/checker/tests/nullness/UnboxConditions.java @@ -6,19 +6,16 @@ public static void main(String[] args) { Boolean b3 = null; Boolean b4 = null; // :: error: (condition.nullable) - if (b) {; - } + if (b) {} // :: error: (condition.nullable) b = b1 ? b : b; // :: error: (condition.nullable) - while (b2) {; - } - do {; + while (b2) {} + do { // :: error: (condition.nullable) } while (b3); // :: error: (condition.nullable) - for (; b4; ) {; - } + for (; b4; ) {} // legal! for (; ; ) { break; diff --git a/checker/tests/nullness/Unboxing.java b/checker/tests/nullness/Unboxing.java index 8d4424dc2c2a..3a8c762a04e0 100644 --- a/checker/tests/nullness/Unboxing.java +++ b/checker/tests/nullness/Unboxing.java @@ -1,12 +1,12 @@ import org.checkerframework.checker.nullness.qual.*; -class Unboxing { +public class Unboxing { @Nullable Integer f; public void t1() { // :: error: (unboxing.of.nullable) - @NonNull int l = f + 1; + int l = f + 1; // no error, since f has been unboxed f.toString(); } @@ -14,11 +14,11 @@ public void t1() { public void t2() { try { // :: error: (unboxing.of.nullable) - @NonNull int l = f + 1; + int l = f + 1; } catch (NullPointerException npe) { // f is known to be null on the exception edge // :: error: (unboxing.of.nullable) - @NonNull int m = f + 1; + int m = f + 1; } // after the merge, f cannot be null f.toString(); diff --git a/checker/tests/nullness/UnexpectedRaw.java b/checker/tests/nullness/UnexpectedRaw.java index 342a80032606..dfaf44027d65 100644 --- a/checker/tests/nullness/UnexpectedRaw.java +++ b/checker/tests/nullness/UnexpectedRaw.java @@ -28,9 +28,8 @@ class Utils { // The primary annotations on nullConsumer and the formal parameter consumer are // identical, so it comes down to the annotations on the type arguments. - // Let X stand in for the type argument of nullConsumer. For it to be a valid - // parameter, X must be contained by the type argument of the formal parameter, - // ? super C. + // Let X stand in for the type argument of nullConsumer. For it to be a valid parameter, X + // must be contained by the type argument of the formal parameter, ? super C. // // In other words, the following constraints must hold: // diff --git a/checker/tests/nullness/UnusedNullness.java b/checker/tests/nullness/UnusedNullness.java index 105b41ab7de5..e35f8e5d00d9 100644 --- a/checker/tests/nullness/UnusedNullness.java +++ b/checker/tests/nullness/UnusedNullness.java @@ -1,15 +1,14 @@ -import java.lang.annotation.ElementType; -import java.lang.annotation.Target; import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.Unused; -// TODO: feature request: the Nullness Checker should be aware of -// the @Unused annotation. -// This is difficult to implement: one needs to determine the correct -// AnnotatedTypeFactory for the "when" type system and use it -// to determine the right annotated type. We currently don't have -// a mechanism to do this. +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +// TODO: feature request: the Nullness Checker should be aware of the @Unused annotation. +// This is difficult to implement: one needs to determine the correct AnnotatedTypeFactory for the +// "when" type system and use it to determine the right annotated type. We currently don't have a +// mechanism to do this. // // @skip-test public class UnusedNullness { diff --git a/checker/tests/nullness/UnusedOnClass.java b/checker/tests/nullness/UnusedOnClass.java index becd658ea3e4..26b554089df8 100644 --- a/checker/tests/nullness/UnusedOnClass.java +++ b/checker/tests/nullness/UnusedOnClass.java @@ -1,9 +1,10 @@ -import java.lang.annotation.ElementType; -import java.lang.annotation.Target; import org.checkerframework.framework.qual.DefaultQualifierInHierarchy; import org.checkerframework.framework.qual.SubtypeOf; import org.checkerframework.framework.qual.Unused; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + public final class UnusedOnClass { public static void read_serialized_pptmap2(@MyNonPrototype MyInvariant2 inv) { inv.ppt.toString(); diff --git a/checker/tests/nullness/UtilArrays.java b/checker/tests/nullness/UtilArrays.java new file mode 100644 index 000000000000..5c9f3e91ad6c --- /dev/null +++ b/checker/tests/nullness/UtilArrays.java @@ -0,0 +1,52 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Arrays; +import java.util.List; + +// Illustrate various mis-uses of java.util.Arrays. +public class UtilArrays { + public static void main(String[] args) { + @Nullable Object[] arrayWithNull = {"", null, ""}; + Object[] arrayWithoutNull = {""}; + + try { + // :: error: (argument.type.incompatible) + Arrays.binarySearch(arrayWithNull, ""); + } catch (NullPointerException e) { + System.out.println("got NPE for array containing null"); + } + + try { + // :: error: (argument.type.incompatible) + Arrays.binarySearch(arrayWithoutNull, null); + } catch (NullPointerException e) { + System.out.println("got NPE for null key"); + } + + try { + // :: error: (argument.type.incompatible) + Arrays.sort(arrayWithNull, null); + } catch (NullPointerException e) { + System.out.println("got NPE for sort"); + } + + try { + // TODO: false negative: covariant arrays and polymorphism cause that this call is + // allowed. + Arrays.fill(arrayWithoutNull, null); + arrayWithoutNull[0].toString(); + } catch (NullPointerException e) { + System.out.println("got NPE for fill"); + } + + try { + // TODO: false negative: covariant arrays and captured argument cause that this call is + // allowed. + List<@Nullable Object> ls = Arrays.asList(arrayWithoutNull); + ls.set(0, null); + arrayWithoutNull[0].toString(); + } catch (NullPointerException e) { + System.out.println("got NPE for asList"); + } + } +} diff --git a/checker/tests/nullness/VarargsNullness2.java b/checker/tests/nullness/VarargsNullness2.java new file mode 100644 index 000000000000..2e9ec0280528 --- /dev/null +++ b/checker/tests/nullness/VarargsNullness2.java @@ -0,0 +1,18 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +public class VarargsNullness2 { + + public static void method1(Object... args) {} + + public static void method2(Object @Nullable ... args) {} + + public static void main(String[] args) { + // :: error: (argument.type.incompatible) + // :: warning: non-varargs call of varargs method with inexact argument type for last + // parameter; + method1(null); + // :: warning: non-varargs call of varargs method with inexact argument type for last + // parameter; + method2(null); + } +} diff --git a/checker/tests/nullness/VoidUse.java b/checker/tests/nullness/VoidUse.java index d7387a665851..ca636bc26b3f 100644 --- a/checker/tests/nullness/VoidUse.java +++ b/checker/tests/nullness/VoidUse.java @@ -11,8 +11,7 @@ public Void voidReturn(Void p) { return null; } - // Void is treated as Nullable. Is there a value on having it be - // NonNull? + // Void is treated as Nullable. Is there a value on having it be NonNull? public abstract static class VoidTestNode {} public static class VoidTestInvNode extends VoidTestNode<@NonNull Void> {} diff --git a/checker/tests/nullness/WeakHasherMapNonNull.java b/checker/tests/nullness/WeakHasherMapNonNull.java index 6c2daa0643e8..05e1325c065b 100644 --- a/checker/tests/nullness/WeakHasherMapNonNull.java +++ b/checker/tests/nullness/WeakHasherMapNonNull.java @@ -1,12 +1,13 @@ -import java.util.AbstractMap; -import java.util.Map; import org.checkerframework.checker.initialization.qual.*; import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.checker.regex.qual.*; -// :: error: (initialization.fields.uninitialized) +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Map; + public abstract class WeakHasherMapNonNull extends AbstractMap implements Map { - private Map hash; + private Map hash = new HashMap<>(); @org.checkerframework.dataflow.qual.Pure public boolean containsKey(@NonNull Object key) { diff --git a/checker/tests/nullness/WeakHasherMapNullable.java b/checker/tests/nullness/WeakHasherMapNullable.java index 0d8a6c67c12d..4949ec8021ff 100644 --- a/checker/tests/nullness/WeakHasherMapNullable.java +++ b/checker/tests/nullness/WeakHasherMapNullable.java @@ -1,11 +1,12 @@ -import java.util.AbstractMap; -import java.util.Map; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.qual.Pure; -// :: error: (initialization.fields.uninitialized) +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Map; + public abstract class WeakHasherMapNullable extends AbstractMap implements Map { - private Map hash; + private Map hash = new HashMap<>(); @Pure public boolean containsKey(@Nullable Object key) { diff --git a/checker/tests/nullness/WeakIdentityPair.java b/checker/tests/nullness/WeakIdentityPair.java index c6b14d11ba68..d2b608e0c49f 100644 --- a/checker/tests/nullness/WeakIdentityPair.java +++ b/checker/tests/nullness/WeakIdentityPair.java @@ -1,6 +1,7 @@ -import java.lang.ref.WeakReference; import org.checkerframework.checker.nullness.qual.*; +import java.lang.ref.WeakReference; + public class WeakIdentityPair { private final WeakReference a; diff --git a/checker/tests/nullness/WeakRef.java b/checker/tests/nullness/WeakRef.java index 67bf000317c9..fc846f494c7f 100644 --- a/checker/tests/nullness/WeakRef.java +++ b/checker/tests/nullness/WeakRef.java @@ -1,7 +1,8 @@ -import java.lang.ref.WeakReference; import org.checkerframework.checker.nullness.qual.*; -class WeakRef { +import java.lang.ref.WeakReference; + +public class WeakRef { @PolyNull Object @Nullable [] foo(WeakReference<@PolyNull Object[]> lookup) { return lookup.get(); } diff --git a/checker/tests/nullness/WhileTest.java b/checker/tests/nullness/WhileTest.java index 906e3170566a..2b21b114e253 100644 --- a/checker/tests/nullness/WhileTest.java +++ b/checker/tests/nullness/WhileTest.java @@ -24,8 +24,7 @@ public void testwhile1() { public void testwhile2() { z = null; - while (z == null) {; - } + while (z == null) {} nnz = z; } @@ -40,8 +39,7 @@ public void testdo1() { public void testdo2() { z = null; - do {; - } while (z == null); + do {} while (z == null); nnz = z; } @@ -56,8 +54,7 @@ public void testfor1() { public void testfor2() { z = null; - for (; z == null; ) {; - } + for (; z == null; ) {} nnz = z; } } diff --git a/checker/tests/nullness/Widening.java b/checker/tests/nullness/Widening.java index d7b4878723c1..4da7df66ecf5 100644 --- a/checker/tests/nullness/Widening.java +++ b/checker/tests/nullness/Widening.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; -class Widening { +public class Widening { @Nullable Integer i; void inc(long amt) {} diff --git a/checker/tests/nullness/WildcardGLB.java b/checker/tests/nullness/WildcardGLB.java new file mode 100644 index 000000000000..0d7634a29da0 --- /dev/null +++ b/checker/tests/nullness/WildcardGLB.java @@ -0,0 +1,67 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class WildcardGLB { + + static class MyClass> { + E getE() { + throw new RuntimeException(); + } + } + + // The captured type variable for + // ? extends @List<@NonNull String> + // is + // capture#865 extends @NonNull List<@Nullable String> + // . The upper bound of the captured type variable is not a subtype of the extends bound of the + // wildcard because the glb of the type parameter bound and the wildcard extends bound does not + // exist. I don't think this leads to unsoundness, but it makes it so that this method can't be + // called without an error. The method testUse below demos this. + // :: error: (type.argument.type.incompatible) + void use(MyClass> s) { + // :: error: (assignment.type.incompatible) + List f = s.getE(); + List<@Nullable String> f2 = s.getE(); + } + + void testUse( + // :: error: (type.argument.type.incompatible) + MyClass> p1, + // A comment to force a line break. + MyClass> p2) { + use(p1); + // :: error: (argument.type.incompatible) + use(p2); + } + + // capture#196 extends @NonNull ArrayList<@NonNull String> + // :: error: (type.argument.type.incompatible) + void use2(MyClass> s) { // error: type.argument + List f = s.getE(); + // :: error: (assignment.type.incompatible) + List<@Nullable String> f2 = s.getE(); // error: assignment + } + + static class MyClass2> { + E getE() { + throw new RuntimeException(); + } + } + + // capture#952 extends @NonNull ArrayList<@Nullable String> + // :: error: (type.argument.type.incompatible) + void use3(MyClass2> s) { + // :: error: (assignment.type.incompatible) + List f = s.getE(); + List<@Nullable String> f2 = s.getE(); + } + + // :: error: (type.argument.type.incompatible) + void use4(MyClass2> s) { + // :: error: (assignment.type.incompatible) + List f = s.getE(); + List<@Nullable String> f2 = s.getE(); // ok + } +} diff --git a/checker/tests/nullness/WildcardSubtype.java b/checker/tests/nullness/WildcardSubtype.java index 29536a429825..66fea84d5c82 100644 --- a/checker/tests/nullness/WildcardSubtype.java +++ b/checker/tests/nullness/WildcardSubtype.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -class WildcardSubtype { +public class WildcardSubtype { class MyClass {} class Visitor { diff --git a/checker/tests/nullness/Wildcards.java b/checker/tests/nullness/Wildcards.java index d33ad2896bf9..54dd0cef5d96 100644 --- a/checker/tests/nullness/Wildcards.java +++ b/checker/tests/nullness/Wildcards.java @@ -1,8 +1,9 @@ // Test case for issue #234. +import org.checkerframework.checker.nullness.qual.*; + import java.util.Iterator; import java.util.List; -import org.checkerframework.checker.nullness.qual.*; public class Wildcards { diff --git a/checker/tests/nullness/ZeroVarargs.java b/checker/tests/nullness/ZeroVarargs.java new file mode 100644 index 000000000000..42d8ea7d3598 --- /dev/null +++ b/checker/tests/nullness/ZeroVarargs.java @@ -0,0 +1,7 @@ +// Test case for https://tinyurl.com/cfissue/5189 + +enum CFRepro { + EMPTY() {}; + + CFRepro(String... args) {} +} diff --git a/checker/tests/nullness/flow/EisopIssue300.java b/checker/tests/nullness/flow/EisopIssue300.java new file mode 100644 index 000000000000..097f5c870e12 --- /dev/null +++ b/checker/tests/nullness/flow/EisopIssue300.java @@ -0,0 +1,27 @@ +// https://github.com/eisop/checker-framework/issues/300 +// The receiver could be nullable after an invocation. + +import org.checkerframework.checker.nullness.qual.Nullable; + +public class EisopIssue300 { + class Bug { + void setFieldNull(Bug b) { + EisopIssue300.this.currentNode = null; + } + } + + @Nullable Bug currentNode = new Bug(); + + void test() { + if (currentNode == null) { + return; + } + currentNode.setFieldNull(currentNode); + // :: error: (dereference.of.nullable) + currentNode.toString(); + } + + public static void main(String[] args) { + new EisopIssue300().test(); + } +} diff --git a/checker/tests/nullness/flow/EisopIssue300B.java b/checker/tests/nullness/flow/EisopIssue300B.java new file mode 100644 index 000000000000..704f8303437c --- /dev/null +++ b/checker/tests/nullness/flow/EisopIssue300B.java @@ -0,0 +1,23 @@ +// https://github.com/eisop/checker-framework/issues/300 +// The argument could be nullable after an invocation. + +import org.checkerframework.checker.nullness.qual.Nullable; + +public class EisopIssue300B { + @Nullable Object f = ""; + + void m(Object o) { + f = null; + } + + public static void main(String[] args) { + EisopIssue300B r = new EisopIssue300B(); + if (r.f == null) { + return; + } + + r.m(r.f); + // :: error: (dereference.of.nullable) + r.f.toString(); + } +} diff --git a/checker/tests/nullness/flow/EisopIssue300C.java b/checker/tests/nullness/flow/EisopIssue300C.java new file mode 100644 index 000000000000..9c64deeabdea --- /dev/null +++ b/checker/tests/nullness/flow/EisopIssue300C.java @@ -0,0 +1,33 @@ +// https://github.com/eisop/checker-framework/issues/300 +// Impure method can make the method return value nullable + +import org.checkerframework.checker.initialization.qual.NotOnlyInitialized; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.*; + +public final class EisopIssue300C { + @NotOnlyInitialized @Nullable EisopIssue300C f; + + EisopIssue300C() { + this.f = this; + } + + void m2() { + f = null; + } + + @Pure + @Nullable EisopIssue300C getF() { + return f; + } + + public static void main(String[] args) { + EisopIssue300C r = new EisopIssue300C(); + + if (r.getF() != null) { + r.getF().m2(); + // :: error: (dereference.of.nullable) + r.getF().toString(); + } + } +} diff --git a/checker/tests/nullness/flow/EisopIssue553.java b/checker/tests/nullness/flow/EisopIssue553.java new file mode 100644 index 000000000000..acdf33bd7165 --- /dev/null +++ b/checker/tests/nullness/flow/EisopIssue553.java @@ -0,0 +1,25 @@ +// Test case for EISOP Issue 553: +// https://github.com/eisop/checker-framework/issues/553 +import org.checkerframework.checker.nullness.qual.Nullable; + +public class EisopIssue553 { + static @Nullable Object sfield = ""; + Object field = ""; + + static void n(Object o) { + sfield = null; + } + + public static void main(String[] args) { + EisopIssue553 x = null; + Object o = x.sfield; + // :: error: (dereference.of.nullable) + o = x.field; + if (x.sfield == null) { + return; + } + x.n(x.sfield); + // :: error: (dereference.of.nullable) + x.sfield.toString(); + } +} diff --git a/checker/tests/nullness/flow/Issue1345.java b/checker/tests/nullness/flow/Issue1345.java index 0ba39878e00e..859f31e13bc1 100644 --- a/checker/tests/nullness/flow/Issue1345.java +++ b/checker/tests/nullness/flow/Issue1345.java @@ -3,10 +3,11 @@ // @skip-test until the issue is resolved +import org.checkerframework.checker.nullness.qual.*; +import org.checkerframework.checker.nullness.util.Opt; + import java.math.BigDecimal; import java.util.stream.Stream; -import org.checkerframework.checker.nullness.Opt; -import org.checkerframework.checker.nullness.qual.*; public class Issue1345 { diff --git a/checker/tests/nullness/flow/Issue3267.java b/checker/tests/nullness/flow/Issue3267.java index 179d2a2688c1..57b392e9b8b5 100644 --- a/checker/tests/nullness/flow/Issue3267.java +++ b/checker/tests/nullness/flow/Issue3267.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; -class Issue3267 { +public class Issue3267 { void m1(@Nullable Object obj) { if (true) { // :: error: (dereference.of.nullable) diff --git a/checker/tests/nullness/flow/Issue3275.java b/checker/tests/nullness/flow/Issue3275.java new file mode 100644 index 000000000000..05b615b457f4 --- /dev/null +++ b/checker/tests/nullness/flow/Issue3275.java @@ -0,0 +1,206 @@ +// Test case for issue #3275: +// https://github.com/typetools/checker-framework/issues/3275 + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class Issue3275 { + public @NonNull Object f = new Object(); + public boolean b = false; + + void return_n(@Nullable Object obj) { + if (obj != null) { + obj.toString(); + } + } + + void return_np(@Nullable Object obj) { + if ((obj != null)) { + obj.toString(); + } + } + + void return_en(@Nullable Object obj) { + if (!(obj == null)) { + obj.toString(); + } + } + + void return_eet(@Nullable Object obj) { + if ((obj == null) == true) { + // :: error: (dereference.of.nullable) + obj.toString(); + } + } + + void return_eef(@Nullable Object obj) { + if ((obj == null) == false) { + obj.toString(); + } + } + + void return_eeb(@Nullable Object obj) { + if ((obj == null) == b) { + // :: error: (dereference.of.nullable) + obj.toString(); + } + } + + void return_ent(@Nullable Object obj) { + if ((obj == null) != true) { + obj.toString(); + } + } + + void return_enf(@Nullable Object obj) { + if ((obj == null) != false) { + // :: error: (dereference.of.nullable) + obj.toString(); + } + } + + void return_enb(@Nullable Object obj) { + if ((obj == null) != b) { + // :: error: (dereference.of.nullable) + obj.toString(); + } + } + + void return_net(@Nullable Object obj) { + if ((obj != null) == true) { + obj.toString(); + } + } + + void return_nef(@Nullable Object obj) { + if ((obj != null) == false) { + // :: error: (dereference.of.nullable) + obj.toString(); + } + } + + void return_neb(@Nullable Object obj) { + if ((obj != null) == b) { + // :: error: (dereference.of.nullable) + obj.toString(); + } + } + + void return_nnt(@Nullable Object obj) { + if ((obj != null) != true) { + // :: error: (dereference.of.nullable) + obj.toString(); + } + } + + void return_nnf(@Nullable Object obj) { + if ((obj != null) != false) { + obj.toString(); + } + } + + void return_nnb(@Nullable Object obj) { + if ((obj != null) != b) { + // :: error: (dereference.of.nullable) + obj.toString(); + } + } + + void assign_n(@Nullable Object obj) { + if (obj != null) { + f = obj; + } + } + + void assign_np(@Nullable Object obj) { + if ((obj != null)) { + f = obj; + } + } + + void assign_en(@Nullable Object obj) { + if (!(obj == null)) { + f = obj; + } + } + + void assign_eet(@Nullable Object obj) { + if ((obj == null) == true) { + // :: error: (assignment.type.incompatible) + f = obj; + } + } + + void assign_eef(@Nullable Object obj) { + if ((obj == null) == false) { + f = obj; + } + } + + void assign_eeb(@Nullable Object obj) { + if ((obj == null) == b) { + // :: error: (assignment.type.incompatible) + f = obj; + } + } + + void assign_ent(@Nullable Object obj) { + if ((obj == null) != true) { + f = obj; + } + } + + void assign_enf(@Nullable Object obj) { + if ((obj == null) != false) { + // :: error: (assignment.type.incompatible) + f = obj; + } + } + + void assign_enb(@Nullable Object obj) { + if ((obj == null) != b) { + // :: error: (assignment.type.incompatible) + f = obj; + } + } + + void assign_net(@Nullable Object obj) { + if ((obj != null) == true) { + f = obj; + } + } + + void assign_nef(@Nullable Object obj) { + if ((obj != null) == false) { + // :: error: (assignment.type.incompatible) + f = obj; + } + } + + void assign_neb(@Nullable Object obj) { + if ((obj != null) == b) { + // :: error: (assignment.type.incompatible) + f = obj; + } + } + + void assign_nnt(@Nullable Object obj) { + if ((obj != null) != true) { + // :: error: (assignment.type.incompatible) + f = obj; + } + } + + void assign_nnf(@Nullable Object obj) { + if ((obj != null) != false) { + f = obj; + } + } + + void assign_nnb(@Nullable Object obj) { + if ((obj != null) != b) { + // :: error: (assignment.type.incompatible) + f = obj; + } + } +} diff --git a/checker/tests/nullness/flow/Issue341.java b/checker/tests/nullness/flow/Issue341.java index c757f49184ae..a22df59fe933 100644 --- a/checker/tests/nullness/flow/Issue341.java +++ b/checker/tests/nullness/flow/Issue341.java @@ -1,7 +1,7 @@ // Test case for issue #341: // https://github.com/typetools/checker-framework/issues/341 -class Test { +public class Issue341 { static class Provider { public final Object get = new Object(); diff --git a/checker/tests/nullness/flow/Issue818.java b/checker/tests/nullness/flow/Issue818.java index a4e24918f542..7005fa925ced 100644 --- a/checker/tests/nullness/flow/Issue818.java +++ b/checker/tests/nullness/flow/Issue818.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.nullness.qual.*; -class Issue818 { +public class Issue818 { public static @Nullable Object o = null; void method() { diff --git a/checker/tests/nullness/flow/MapGet.java b/checker/tests/nullness/flow/MapGet.java index 6c691eda98f5..7d74f678ea3b 100644 --- a/checker/tests/nullness/flow/MapGet.java +++ b/checker/tests/nullness/flow/MapGet.java @@ -3,12 +3,13 @@ // @skip-test until the issue is fixed -import java.util.HashMap; -import java.util.Map; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.NonNull; -class MapGet { +import java.util.HashMap; +import java.util.Map; + +public class MapGet { private final Map labels = new HashMap<>(); void foo1(String v) { diff --git a/checker/tests/nullness/flow/PureAndFlow.java b/checker/tests/nullness/flow/PureAndFlow.java index 02f759d65bf0..88d2d99a414f 100644 --- a/checker/tests/nullness/flow/PureAndFlow.java +++ b/checker/tests/nullness/flow/PureAndFlow.java @@ -51,12 +51,10 @@ interface IFace { class Cons { @org.checkerframework.dataflow.qual.Pure // :: warning: (purity.deterministic.constructor) - // :: error: (purity.not.deterministic.not.sideeffectfree.call.method) Cons(String s) {} @org.checkerframework.dataflow.qual.Deterministic // :: warning: (purity.deterministic.constructor) - // :: error: (purity.not.deterministic.call.method) Cons(int i) {} } } diff --git a/checker/tests/nullness/flow/PurityError.java b/checker/tests/nullness/flow/PurityError.java index 188c106ebdea..746dfbd6316b 100644 --- a/checker/tests/nullness/flow/PurityError.java +++ b/checker/tests/nullness/flow/PurityError.java @@ -7,7 +7,7 @@ void method() {} @Pure Object method2() { - // :: error: (purity.not.deterministic.call.method) + // :: error: (purity.not.deterministic.call) method(); return ""; } diff --git a/checker/tests/nullness/flow/TestNullnessUtil.java b/checker/tests/nullness/flow/TestNullnessUtil.java index 5cb3b589c666..aec51c8426dc 100644 --- a/checker/tests/nullness/flow/TestNullnessUtil.java +++ b/checker/tests/nullness/flow/TestNullnessUtil.java @@ -1,8 +1,8 @@ -import org.checkerframework.checker.nullness.NullnessUtil; import org.checkerframework.checker.nullness.qual.*; +import org.checkerframework.checker.nullness.util.NullnessUtil; -/** Test class org.checkerframework.checker.nullness.NullnessUtil. */ -class TestNullnessUtil { +/** Test class org.checkerframework.checker.nullness.util.NullnessUtil. */ +public class TestNullnessUtil { void testRef1(@Nullable Object o) { // one way to use as a cast: @NonNull Object l1 = NullnessUtil.castNonNull(o); diff --git a/checker/tests/nullness/flow/TestOpt.java b/checker/tests/nullness/flow/TestOpt.java index a48c9bff242e..f2407e1dad16 100644 --- a/checker/tests/nullness/flow/TestOpt.java +++ b/checker/tests/nullness/flow/TestOpt.java @@ -1,8 +1,8 @@ -import org.checkerframework.checker.nullness.Opt; import org.checkerframework.checker.nullness.qual.*; +import org.checkerframework.checker.nullness.util.Opt; -/** Test class org.checkerframework.checker.nullness.Opt. */ -class TestOpt { +/** Test class org.checkerframework.checker.nullness.util.Opt. */ +public class TestOpt { void foo1(@Nullable Object p) { if (Opt.isPresent(p)) { p.toString(); // Flow refinement diff --git a/checker/tests/nullness/generics/AnnonymousClass.java b/checker/tests/nullness/generics/AnnonymousClass.java deleted file mode 100644 index 9bde5f290d22..000000000000 --- a/checker/tests/nullness/generics/AnnonymousClass.java +++ /dev/null @@ -1,17 +0,0 @@ -import org.checkerframework.checker.nullness.qual.*; - -class AnonymousClass { - - class Bound {} - - void test() { - // :: error: (type.argument.type.incompatible) - new Bound<@Nullable String>() {}; - } - - // The dummy parameter tests ParamApplier - void test(Object dummy) { - // :: error: (type.argument.type.incompatible) - new Bound<@Nullable String>() {}; - } -} diff --git a/checker/tests/nullness/generics/AnnotatedGenerics3.java b/checker/tests/nullness/generics/AnnotatedGenerics3.java index 9f58aefa08d7..31490a8d7f94 100644 --- a/checker/tests/nullness/generics/AnnotatedGenerics3.java +++ b/checker/tests/nullness/generics/AnnotatedGenerics3.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class AnnotatedGenerics3 { +public class AnnotatedGenerics3 { class Cell { T f; diff --git a/checker/tests/nullness/generics/AnnotatedTypeParams2.java b/checker/tests/nullness/generics/AnnotatedTypeParams2.java index d20598f236b2..99c89df3ba00 100644 --- a/checker/tests/nullness/generics/AnnotatedTypeParams2.java +++ b/checker/tests/nullness/generics/AnnotatedTypeParams2.java @@ -6,7 +6,7 @@ T get() { } } -class AnnotatedTypeParams { +public class AnnotatedTypeParams2 { void testPositive() { SomeClass<@Nullable String> l = new SomeClass<>(); diff --git a/checker/tests/nullness/generics/AnnotatedTypeParams4.java b/checker/tests/nullness/generics/AnnotatedTypeParams4.java index a93faba91f6b..ee3c93ce00b6 100644 --- a/checker/tests/nullness/generics/AnnotatedTypeParams4.java +++ b/checker/tests/nullness/generics/AnnotatedTypeParams4.java @@ -1,9 +1,10 @@ import org.checkerframework.checker.nullness.qual.*; -class Test { +public class AnnotatedTypeParams4 { class Test1 { CONTENT a; + // To prevent the warning about un-initialized fields. Test1(CONTENT p1) { a = p1; @@ -21,6 +22,7 @@ public CONTENT get2() { class Test2 { @NonNull CONTENT a; + // To prevent the warning about un-initialized fields. Test2(@NonNull CONTENT p1) { a = p1; diff --git a/checker/tests/nullness/generics/AnonymousClass.java b/checker/tests/nullness/generics/AnonymousClass.java new file mode 100644 index 000000000000..9fd3545eda8c --- /dev/null +++ b/checker/tests/nullness/generics/AnonymousClass.java @@ -0,0 +1,17 @@ +import org.checkerframework.checker.nullness.qual.*; + +public class AnonymousClass { + + class Bound {} + + void test() { + // :: error: (type.argument.type.incompatible) + new Bound<@Nullable String>() {}; + } + + // The dummy parameter tests ParamApplier + void test(Object dummy) { + // :: error: (type.argument.type.incompatible) + new Bound<@Nullable String>() {}; + } +} diff --git a/checker/tests/nullness/generics/BoundedWildcardTest.java b/checker/tests/nullness/generics/BoundedWildcardTest.java index 51eee182bb3a..71876cede1e3 100644 --- a/checker/tests/nullness/generics/BoundedWildcardTest.java +++ b/checker/tests/nullness/generics/BoundedWildcardTest.java @@ -1,12 +1,13 @@ // Test case from // http://stackoverflow.com/questions/38339332/in-a-bounded-wildcard-where-does-the-annotation-belong -import java.util.List; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.List; + class Styleable {} -class BoundedWildcardTest { +public class BoundedWildcardTest { private void locChildren(Styleable c) { // ... diff --git a/checker/tests/nullness/generics/BoxingGenerics.java b/checker/tests/nullness/generics/BoxingGenerics.java index 218ff3aeb87e..13fb50f2bb91 100644 --- a/checker/tests/nullness/generics/BoxingGenerics.java +++ b/checker/tests/nullness/generics/BoxingGenerics.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class BoxingGenerics { +public class BoxingGenerics { static class X { public static X foo(T x) { return new X<>(); diff --git a/checker/tests/nullness/generics/CapturedWildcards.java b/checker/tests/nullness/generics/CapturedWildcards.java new file mode 100644 index 000000000000..96984bc06f25 --- /dev/null +++ b/checker/tests/nullness/generics/CapturedWildcards.java @@ -0,0 +1,18 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.List; + +public class CapturedWildcards { + abstract static class MyClass { + abstract boolean contains(MyClass other); + } + + public boolean pass(List list, MyClass other) { + return list.stream().anyMatch(je -> je != null && je.contains(other)); + } + + public boolean fail(List list, MyClass other) { + // :: error: (dereference.of.nullable) + return list.stream().anyMatch(je -> je.contains(other)); + } +} diff --git a/checker/tests/nullness/generics/CollectionsAnnotationsMin.java b/checker/tests/nullness/generics/CollectionsAnnotationsMin.java index 34310d89bb12..67120e3985bf 100644 --- a/checker/tests/nullness/generics/CollectionsAnnotationsMin.java +++ b/checker/tests/nullness/generics/CollectionsAnnotationsMin.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class CollectionsAnnotationsMin { +public class CollectionsAnnotationsMin { static class Collection1 { public void add(E elt) { // :: error: (dereference.of.nullable) diff --git a/checker/tests/nullness/generics/GenericArgs.java b/checker/tests/nullness/generics/GenericArgs.java index 8cb484371737..eb2e54281515 100644 --- a/checker/tests/nullness/generics/GenericArgs.java +++ b/checker/tests/nullness/generics/GenericArgs.java @@ -1,9 +1,10 @@ +import org.checkerframework.checker.nullness.qual.*; +import org.checkerframework.dataflow.qual.*; + import java.io.*; import java.util.Comparator; import java.util.HashSet; import java.util.Set; -import org.checkerframework.checker.nullness.qual.*; -import org.checkerframework.dataflow.qual.*; @org.checkerframework.framework.qual.DefaultQualifier(Nullable.class) public class GenericArgs { diff --git a/checker/tests/nullness/generics/GenericArgs2.java b/checker/tests/nullness/generics/GenericArgs2.java index 3b7a6de8488f..29d7de40d90a 100644 --- a/checker/tests/nullness/generics/GenericArgs2.java +++ b/checker/tests/nullness/generics/GenericArgs2.java @@ -1,13 +1,14 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.io.*; import java.util.HashMap; import java.util.Map; -import org.checkerframework.checker.nullness.qual.*; class Cell { void add(T arg) {} } -class GenericArgs2 { +public class GenericArgs2 { static void test1(Cell collection) { // :: error: (argument.type.incompatible) collection.add(null); // should fail @@ -21,6 +22,7 @@ static void test1(Cell collection) { static void test3(Cell<@Nullable Object> collection) { collection.add(null); // valid } + // No "" version of the above, as that is illegal in Java. static class InvariantFilter {} diff --git a/checker/tests/nullness/generics/GenericArgs3.java b/checker/tests/nullness/generics/GenericArgs3.java index 7a25d02b5d38..8d4f008df9cd 100644 --- a/checker/tests/nullness/generics/GenericArgs3.java +++ b/checker/tests/nullness/generics/GenericArgs3.java @@ -1,9 +1,10 @@ +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.Pure; + import java.util.Collection; import java.util.Enumeration; import java.util.Iterator; import java.util.Map; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.qual.Pure; class Other { public static final class StaticIterator implements Iterator { diff --git a/checker/tests/nullness/generics/GenericTest11.java b/checker/tests/nullness/generics/GenericTest11.java index 12bf4c0668ba..560303c00b34 100644 --- a/checker/tests/nullness/generics/GenericTest11.java +++ b/checker/tests/nullness/generics/GenericTest11.java @@ -2,7 +2,7 @@ import org.checkerframework.checker.nullness.qual.*; -class GenericTest11 { +public class GenericTest11 { public void m(BeanManager beanManager) { Bean bean = beanManager.getBeans(GenericTest11.class).iterator().next(); CreationalContext context = beanManager.createCreationalContext(bean); diff --git a/checker/tests/nullness/generics/GenericsBounds4.java b/checker/tests/nullness/generics/GenericsBounds4.java index 5258cea3a124..6c87882a971e 100644 --- a/checker/tests/nullness/generics/GenericsBounds4.java +++ b/checker/tests/nullness/generics/GenericsBounds4.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class GenericsBounds4 { +public class GenericsBounds4 { class Collection1 { public void add(E elt) { // :: error: (dereference.of.nullable) diff --git a/checker/tests/nullness/generics/GenericsBounds5.java b/checker/tests/nullness/generics/GenericsBounds5.java index 157ce94d5c3a..8a5a170af928 100644 --- a/checker/tests/nullness/generics/GenericsBounds5.java +++ b/checker/tests/nullness/generics/GenericsBounds5.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class GenericsBounds5 { +public class GenericsBounds5 { class Collection1 { public void add(E elt) { // This call is forbidden, because elt might be null. diff --git a/checker/tests/nullness/generics/GenericsExample.java b/checker/tests/nullness/generics/GenericsExample.java index 6ae072c39d2a..4b4ad2dd653c 100644 --- a/checker/tests/nullness/generics/GenericsExample.java +++ b/checker/tests/nullness/generics/GenericsExample.java @@ -3,7 +3,7 @@ // This is the example from manual section: // "Generics (parametric polymorphism or type polymorphism)" // whose source code is ../../../docs/manual/advanced-features.tex -class GenericsExample { +public class GenericsExample { class MyList1<@Nullable T> { T t; diff --git a/checker/tests/nullness/generics/GenericsExampleMin.java b/checker/tests/nullness/generics/GenericsExampleMin.java index 22b31e04cee9..4dfa292039dd 100644 --- a/checker/tests/nullness/generics/GenericsExampleMin.java +++ b/checker/tests/nullness/generics/GenericsExampleMin.java @@ -3,7 +3,7 @@ // This is the example from manual section: // "Generics (parametric polymorphism or type polymorphism)" // whose source code is ../../../docs/manual/advanced-features.tex -class GenericsExampleMin { +public class GenericsExampleMin { class MyList1<@Nullable T> { T t; @@ -23,8 +23,7 @@ T get(int i) { // This method works. // Note that it fails to work if it is moved after m2() in the syntax tree. - // TODO: the above comment seems out-of-date, as method - // m3 below works. + // TODO: the above comment seems out-of-date, as method m3 below works. void m1() { t = this.get(0); nble = this.get(0); diff --git a/checker/tests/nullness/generics/InferedPrimitive.java b/checker/tests/nullness/generics/InferedPrimitive.java deleted file mode 100644 index 223fe031aac7..000000000000 --- a/checker/tests/nullness/generics/InferedPrimitive.java +++ /dev/null @@ -1,6 +0,0 @@ -/** Test case for Issue 143: https://github.com/typetools/checker-framework/issues/143 */ -class InferredPrimitive { - public static void main(String[] args) { - java.util.Set s = java.util.Collections.singleton(123L); - } -} diff --git a/checker/tests/nullness/generics/InferredPrimitive.java b/checker/tests/nullness/generics/InferredPrimitive.java new file mode 100644 index 000000000000..68288facda15 --- /dev/null +++ b/checker/tests/nullness/generics/InferredPrimitive.java @@ -0,0 +1,6 @@ +/** Test case for Issue 143: https://github.com/typetools/checker-framework/issues/143 */ +public class InferredPrimitive { + public static void main(String[] args) { + java.util.Set s = java.util.Collections.singleton(123L); + } +} diff --git a/checker/tests/nullness/generics/Issue1838.java b/checker/tests/nullness/generics/Issue1838.java index ef762e836873..269f1995d0a3 100644 --- a/checker/tests/nullness/generics/Issue1838.java +++ b/checker/tests/nullness/generics/Issue1838.java @@ -1,11 +1,12 @@ // Test case for Issue 1838: // https://github.com/typetools/checker-framework/issues/1838 +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.nullness.qual.Nullable; -class Issue1838 { +public class Issue1838 { public static void main(String[] args) { f(); } diff --git a/checker/tests/nullness/generics/Issue1838Min.java b/checker/tests/nullness/generics/Issue1838Min.java index 8111486d3f27..96ae2c2ecca0 100644 --- a/checker/tests/nullness/generics/Issue1838Min.java +++ b/checker/tests/nullness/generics/Issue1838Min.java @@ -1,11 +1,12 @@ // Test case for Issue 1838: // https://github.com/typetools/checker-framework/issues/1838 +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.nullness.qual.Nullable; -class Issue1838Min { +public class Issue1838Min { List> llno = new ArrayList<>(); // :: error: (assignment.type.incompatible) List> lweo = llno; diff --git a/checker/tests/nullness/generics/Issue269.java b/checker/tests/nullness/generics/Issue269.java index 39500228c2ee..4705dc6aba71 100644 --- a/checker/tests/nullness/generics/Issue269.java +++ b/checker/tests/nullness/generics/Issue269.java @@ -1,6 +1,6 @@ // Test case for Issue 269 // https://github.com/typetools/checker-framework/issues/269 -class Repro { +class Issue269 { // Implicitly G has bound @Nullable Object interface Callback { public boolean handler(G arg); @@ -9,7 +9,7 @@ interface Callback { void method1(Callback callback) { // Allow this call. // :: warning: [unchecked] unchecked call to handler(G) as a member of the raw type - // Repro.Callback + // Issue269.Callback callback.handler(this); } @@ -20,10 +20,9 @@ interface CallbackNN { void method2(CallbackNN callback) { // Forbid this call, because the bound is not respected. - // TODO: false negative. See #635. - //// :: error: (argument.type.incompatible) + // :: error: (argument.type.incompatible) // :: warning: [unchecked] unchecked call to handler(H) as a member of the raw type - // Repro.CallbackNN + // Issue269.CallbackNN callback.handler(null); } } diff --git a/checker/tests/nullness/generics/Issue270.java b/checker/tests/nullness/generics/Issue270.java index 6ce9a0890423..500fcdc5d78c 100644 --- a/checker/tests/nullness/generics/Issue270.java +++ b/checker/tests/nullness/generics/Issue270.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.nullness.qual.*; // ::error: (bound.type.incompatible) -class Issue270<@Nullable TypeParam extends @NonNull Object> { +public class Issue270<@Nullable TypeParam extends @NonNull Object> { public static void main() { // ::error: (type.argument.type.incompatible) diff --git a/checker/tests/nullness/generics/Issue2722.java b/checker/tests/nullness/generics/Issue2722.java new file mode 100644 index 000000000000..d364e68ef6a8 --- /dev/null +++ b/checker/tests/nullness/generics/Issue2722.java @@ -0,0 +1,19 @@ +// Test case for issue #2722: +// https://github.com/typetools/checker-framework/issues/2722 + +import java.util.Arrays; +import java.util.List; + +class Issue2722 { + void foo() { + passThrough(Arrays.asList("x")).get(0).length(); + } + + String bar() { + return passThrough(Arrays.asList("x")).get(0); + } + + List passThrough(List object) { + return object; + } +} diff --git a/checker/tests/nullness/generics/Issue282.java b/checker/tests/nullness/generics/Issue282.java index 0a37fd93910f..268d7b60fbfc 100644 --- a/checker/tests/nullness/generics/Issue282.java +++ b/checker/tests/nullness/generics/Issue282.java @@ -1,10 +1,11 @@ // Test case for Issue 282 // https://github.com/typetools/checker-framework/issues/282 +import org.checkerframework.checker.nullness.qual.*; + import java.util.Collection; import java.util.Comparator; import java.util.Set; -import org.checkerframework.checker.nullness.qual.*; @SuppressWarnings("nullness") abstract class ImmutableSortedSet implements Set { diff --git a/checker/tests/nullness/generics/Issue282Min.java b/checker/tests/nullness/generics/Issue282Min.java index c82b790d727d..887fa641f716 100644 --- a/checker/tests/nullness/generics/Issue282Min.java +++ b/checker/tests/nullness/generics/Issue282Min.java @@ -1,12 +1,13 @@ // Test case for Issue 282 (minimized) // https://github.com/typetools/checker-framework/issues/282 +import org.checkerframework.checker.nullness.qual.*; + import java.util.Collection; import java.util.Comparator; import java.util.Set; -import org.checkerframework.checker.nullness.qual.*; -class Issue282Min { +public class Issue282Min { static Set copyOf(Comparator comparator, Collection elements) { // :: error: (return.type.incompatible) return null; diff --git a/checker/tests/nullness/generics/Issue2995.java b/checker/tests/nullness/generics/Issue2995.java new file mode 100644 index 000000000000..0a0baa1a2d22 --- /dev/null +++ b/checker/tests/nullness/generics/Issue2995.java @@ -0,0 +1,14 @@ +// Test case for issue #2995: +// https://github.com/typetools/checker-framework/issues/2995 + +import org.checkerframework.checker.nullness.qual.Nullable; + +class Issue2995 { + interface Set {} + + class Map { + Set keySet = new KeySet(); + + class KeySet implements Set {} + } +} diff --git a/checker/tests/nullness/generics/Issue3025.java b/checker/tests/nullness/generics/Issue3025.java new file mode 100644 index 000000000000..8e7bab4f2c27 --- /dev/null +++ b/checker/tests/nullness/generics/Issue3025.java @@ -0,0 +1,18 @@ +// Test case for issue #3025: +// https://github.com/typetools/checker-framework/issues/3025 + +import org.checkerframework.checker.nullness.qual.Nullable; + +// Classes need to be separate top-level classes to reproduce the issue +class Issue3025Caller { + void foo(Issue3025Sub arg) { + bar(arg); + hashCode(); + } + + void bar(Issue3025Sub arg) {} +} + +interface Issue3025Super {} + +interface Issue3025Sub extends Issue3025Super<@Nullable T> {} diff --git a/checker/tests/nullness/generics/Issue3027.java b/checker/tests/nullness/generics/Issue3027.java new file mode 100644 index 000000000000..c58933839b13 --- /dev/null +++ b/checker/tests/nullness/generics/Issue3027.java @@ -0,0 +1,18 @@ +// Test case for issue #3027: +// https://github.com/typetools/checker-framework/issues/3027 + +import org.checkerframework.checker.nullness.qual.Nullable; + +class Issue3027 { + class Caller { + void foo(Multiset multiset) { + Entry entry = multiset.someEntry(); + } + } + + interface Multiset { + Entry someEntry(); + } + + interface Entry {} +} diff --git a/checker/tests/nullness/generics/Issue312.java b/checker/tests/nullness/generics/Issue312.java index b04ba1b286bb..45b2b1b064d1 100644 --- a/checker/tests/nullness/generics/Issue312.java +++ b/checker/tests/nullness/generics/Issue312.java @@ -20,7 +20,7 @@ public List sortedCopy(Iterable elements) { } } -class Issue312 { +public class Issue312 { void test(List list) { Ordering312.natural().reverse().sortedCopy(list); } @@ -32,7 +32,7 @@ void test(List list) { import com.google.common.collect.Ordering312; import java.util.List; -class Issue312 { +public class Issue312 { void test() { List list = Lists.newArrayList(); Ordering.natural().reverse().sortedCopy(list); diff --git a/checker/tests/nullness/generics/Issue313.java b/checker/tests/nullness/generics/Issue313.java index 19c7a26f1eae..fcaa819ffda5 100644 --- a/checker/tests/nullness/generics/Issue313.java +++ b/checker/tests/nullness/generics/Issue313.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class Issue313 { +public class Issue313 { class A<@NonNull T extends @Nullable Object> {} <@NonNull X extends @Nullable Object> void m() { diff --git a/checker/tests/nullness/generics/Issue319.java b/checker/tests/nullness/generics/Issue319.java index b437fc8963fe..fadf581879ab 100644 --- a/checker/tests/nullness/generics/Issue319.java +++ b/checker/tests/nullness/generics/Issue319.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.nullness.qual.*; -class Issue319 { +public class Issue319 { class Foo { Foo(@Nullable T t) {} } diff --git a/checker/tests/nullness/generics/Issue326.java b/checker/tests/nullness/generics/Issue326.java index 97e0a802c2d1..2a3c7048379d 100644 --- a/checker/tests/nullness/generics/Issue326.java +++ b/checker/tests/nullness/generics/Issue326.java @@ -1,8 +1,9 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.HashSet; import java.util.Set; -import org.checkerframework.checker.nullness.qual.Nullable; -class Issue326 { +public class Issue326 { { Set<@Nullable String> local = new HashSet<>(); } diff --git a/checker/tests/nullness/generics/Issue335.java b/checker/tests/nullness/generics/Issue335.java index d2411c9b7a1a..78037ab5d7c7 100644 --- a/checker/tests/nullness/generics/Issue335.java +++ b/checker/tests/nullness/generics/Issue335.java @@ -15,7 +15,7 @@ static Optional of(T reference) { } } -class Issue335 { +public class Issue335 { Optional> m(String one, String two) { return Optional.of(Pair.of(one, two)); } diff --git a/checker/tests/nullness/generics/Issue339.java b/checker/tests/nullness/generics/Issue339.java index 0d5abb3d0c27..34c67a9544db 100644 --- a/checker/tests/nullness/generics/Issue339.java +++ b/checker/tests/nullness/generics/Issue339.java @@ -3,7 +3,7 @@ import org.checkerframework.checker.nullness.qual.*; -class Issue339 { +public class Issue339 { static @NonNull T checkNotNull(T p) { throw new RuntimeException(); } diff --git a/checker/tests/nullness/generics/Issue421.java b/checker/tests/nullness/generics/Issue421.java index da8f6d976e59..02f80b90b8a8 100644 --- a/checker/tests/nullness/generics/Issue421.java +++ b/checker/tests/nullness/generics/Issue421.java @@ -1,4 +1,4 @@ -class Issue421 { +public class Issue421 { abstract static class C { abstract X getX(); } diff --git a/checker/tests/nullness/generics/Issue428.java b/checker/tests/nullness/generics/Issue428.java index 1de5d8144ceb..c5bca87fe007 100644 --- a/checker/tests/nullness/generics/Issue428.java +++ b/checker/tests/nullness/generics/Issue428.java @@ -3,7 +3,7 @@ import java.util.List; -interface Issue428 {} +public interface Issue428 {} class Test428 { void m(List> is) { diff --git a/checker/tests/nullness/generics/Issue5006.java b/checker/tests/nullness/generics/Issue5006.java new file mode 100644 index 000000000000..30c88dc53932 --- /dev/null +++ b/checker/tests/nullness/generics/Issue5006.java @@ -0,0 +1,18 @@ +public class Issue5006 { + + static class C { + T get() { + throw new RuntimeException(""); + } + } + + interface X { + C get(); + } + + interface Y extends X { + @Override + // :: error: (type.invalid.super.wildcard) + C get(); + } +} diff --git a/checker/tests/nullness/generics/Issue6374.java b/checker/tests/nullness/generics/Issue6374.java new file mode 100644 index 000000000000..199a46d9ffc3 --- /dev/null +++ b/checker/tests/nullness/generics/Issue6374.java @@ -0,0 +1,37 @@ +// Test case for typetools issue #6374: +// https://github.com/typetools/checker-framework/issues/6374 +// Also see checker/jtreg/nullness/issue6374/ + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jmlspecs.annotation.NonNull; + +public class Issue6374 { + + @SuppressWarnings("unchecked") // ignore heap pollution + static class Lib { + // element type inferred, array non-null + static void none(T... o) {} + + // element type inferred, array non-null + static void decl(@NonNull T... o) {} + + // element type nullable, array non-null + static void type(@Nullable T... o) {} + + // element type nullable, array nullable + static void typenn(@Nullable T @Nullable ... o) {} + } + + class User { + void go() { + Lib.decl("", null); + // :: error: (argument.type.incompatible) + Lib.decl((Object[]) null); + Lib.type("", null); + // :: error: (argument.type.incompatible) + Lib.type((Object[]) null); + Lib.typenn("", null); + Lib.typenn((Object[]) null); + } + } +} diff --git a/checker/tests/nullness/generics/Issue783c.java b/checker/tests/nullness/generics/Issue783c.java deleted file mode 100644 index d285300b9d08..000000000000 --- a/checker/tests/nullness/generics/Issue783c.java +++ /dev/null @@ -1,12 +0,0 @@ -// :: error: (initialization.fields.uninitialized) -public class Issue783c { - private T val; - - public void set(T val) { - this.val = val; - } - - public T get() { - return val; - } -} diff --git a/checker/tests/nullness/generics/Issue849.java b/checker/tests/nullness/generics/Issue849.java index caf8b196076a..fc51a7ee45a3 100644 --- a/checker/tests/nullness/generics/Issue849.java +++ b/checker/tests/nullness/generics/Issue849.java @@ -4,7 +4,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -class Issue849 { +public class Issue849 { class Gen {} void nullness(Gen> genGenNonNull) { diff --git a/checker/tests/nullness/generics/KeyForPolyKeyFor.java b/checker/tests/nullness/generics/KeyForPolyKeyFor.java index b347d990b6ac..4ed32c406e3f 100644 --- a/checker/tests/nullness/generics/KeyForPolyKeyFor.java +++ b/checker/tests/nullness/generics/KeyForPolyKeyFor.java @@ -1,12 +1,13 @@ package nullness.generics; +import org.checkerframework.checker.nullness.qual.*; + import java.util.HashMap; import java.util.Map; import java.util.Set; -import org.checkerframework.checker.nullness.qual.*; // test related to issue 429: https://github.com/typetools/checker-framework/issues/429 -class KeyForPolyKeyFor { +public class KeyForPolyKeyFor { // TODO: Figure out why diamond operator does not work: // Map<@KeyFor("dict") String, String> dict = new HashMap<>(); Map<@KeyFor("dict") String, String> dict = new HashMap<@KeyFor("dict") String, String>(); @@ -17,10 +18,9 @@ void m() { for (@KeyFor("dict") String noun : nounSubset(dict.keySet())) {} } - // This method's declaration uses no @KeyFor annotations - // because in addition to being used by the dictionary feature, - // it is also used by a spell checker that only stores sets of words - // and does not use the notions of dictionaries, maps or keys. + // This method's declaration uses no @KeyFor annotations because in addition to being used by + // the dictionary feature, it is also used by a spell checker that only stores sets of words and + // does not use the notions of dictionaries, maps or keys. Set<@PolyKeyFor String> nounSubset(Set<@PolyKeyFor String> words) { return words; } diff --git a/checker/tests/nullness/generics/MapLoop.java b/checker/tests/nullness/generics/MapLoop.java index 2fa775a8d7b0..0017b9463b53 100644 --- a/checker/tests/nullness/generics/MapLoop.java +++ b/checker/tests/nullness/generics/MapLoop.java @@ -1,7 +1,8 @@ -import java.util.Map; import org.checkerframework.checker.nullness.qual.*; -class MapLoop { +import java.util.Map; + +public class MapLoop { void test1(Map map) { for (Map.Entry<@KeyFor("map") String, String> entry : map.entrySet()) {} } diff --git a/checker/tests/nullness/generics/MethodTypeVars.java b/checker/tests/nullness/generics/MethodTypeVars.java index ba124208f050..38e0423b63ae 100644 --- a/checker/tests/nullness/generics/MethodTypeVars.java +++ b/checker/tests/nullness/generics/MethodTypeVars.java @@ -27,12 +27,12 @@ class A { class B { public void indexOf1(T[] a, @Nullable Object elt) {} + // This is not valid Java syntax. // public void indexOf2(?[] a, @Nullable Object elt) {} void call() { Integer[] arg = new Integer[] {1, 2, 3, 4}; indexOf1(arg, Integer.valueOf(5)); - // indexOf2(arg, new Integer(5)); } } diff --git a/checker/tests/nullness/generics/MethodTypeVars3.java b/checker/tests/nullness/generics/MethodTypeVars3.java index fc801d4caab9..79e2cbf10dbe 100644 --- a/checker/tests/nullness/generics/MethodTypeVars3.java +++ b/checker/tests/nullness/generics/MethodTypeVars3.java @@ -1,12 +1,13 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import org.checkerframework.checker.nullness.qual.*; -class MethodTypeVars3 { +public class MethodTypeVars3 { public static <@KeyFor("#1") T extends @KeyFor("#1") Object> Map> dominators( Map> preds) { List nodes = new ArrayList<>(preds.keySet()); diff --git a/checker/tests/nullness/generics/MethodTypeVars5.java b/checker/tests/nullness/generics/MethodTypeVars5.java index 0bbe31f78a45..13bbb31d1586 100644 --- a/checker/tests/nullness/generics/MethodTypeVars5.java +++ b/checker/tests/nullness/generics/MethodTypeVars5.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class MethodTypeVars5 { +public class MethodTypeVars5 { class B { S t; diff --git a/checker/tests/nullness/generics/MixTypeAndDeclAnno.java b/checker/tests/nullness/generics/MixTypeAndDeclAnno.java new file mode 100644 index 000000000000..3e326cce6819 --- /dev/null +++ b/checker/tests/nullness/generics/MixTypeAndDeclAnno.java @@ -0,0 +1,38 @@ +// Test case for eisop issue #204: +// https://github.com/eisop/checker-framework/issues/204 +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +class MixTypeAndDeclAnno { + @NonNull T t; + @android.annotation.NonNull T tdecl; + // :: error: (type.invalid.conflicting.annos) + @android.annotation.NonNull @Nullable T tdecl2; + // :: error: (type.invalid.conflicting.annos) + @android.annotation.NonNull @android.annotation.Nullable Object f1; + // :: error: (type.invalid.conflicting.annos) + @android.annotation.NonNull @Nullable Object f2; + // Nullable applies to the array itself, while NonNull apply to components of the array. + @android.annotation.Nullable @NonNull Object[] g; + // :: error: (type.invalid.conflicting.annos) + @android.annotation.Nullable Object @NonNull [] k; + + MixTypeAndDeclAnno( + @NonNull T t, + @android.annotation.NonNull T tdecl, + // :: error: (type.invalid.conflicting.annos) + @android.annotation.NonNull @android.annotation.Nullable Object f1, + // :: error: (type.invalid.conflicting.annos) + @android.annotation.NonNull @Nullable Object f2, + // :: error: (type.invalid.conflicting.annos) + @android.annotation.Nullable Object @NonNull [] k) { + this.t = t; + this.tdecl = tdecl; + // :: error: (type.invalid.conflicting.annos) + this.f1 = f1; + // :: error: (type.invalid.conflicting.annos) + this.f2 = f2; + // :: error: (type.invalid.conflicting.annos) + this.k = k; + } +} diff --git a/checker/tests/nullness/generics/MyMap.java b/checker/tests/nullness/generics/MyMap.java index 27f246da957f..f81f38ba34ad 100644 --- a/checker/tests/nullness/generics/MyMap.java +++ b/checker/tests/nullness/generics/MyMap.java @@ -1,6 +1,7 @@ -import java.util.Map; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Map; + // Test case for Issue 173 // https://github.com/typetools/checker-framework/issues/173 public abstract class MyMap implements Map { diff --git a/checker/tests/nullness/generics/NullnessBound.java b/checker/tests/nullness/generics/NullnessBound.java index ced84afa3a37..70911d72edf5 100644 --- a/checker/tests/nullness/generics/NullnessBound.java +++ b/checker/tests/nullness/generics/NullnessBound.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class NullnessBound { +public class NullnessBound { public void test() { Gen1<@Nullable String> t1 = new Gen1<>(); diff --git a/checker/tests/nullness/generics/RawTypesGenerics.java b/checker/tests/nullness/generics/RawTypesGenerics.java index c9aeb28363b1..634f23fa1f6b 100644 --- a/checker/tests/nullness/generics/RawTypesGenerics.java +++ b/checker/tests/nullness/generics/RawTypesGenerics.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class RawTypesGenerics { +public class RawTypesGenerics { void m() throws ClassNotFoundException { Class c1 = Class.forName("bla"); Class c2 = Class.forName("bla"); diff --git a/checker/tests/nullness/generics/SuperRawness.java b/checker/tests/nullness/generics/SuperRawness.java index 9efc3960c678..d6f7e9f6e909 100644 --- a/checker/tests/nullness/generics/SuperRawness.java +++ b/checker/tests/nullness/generics/SuperRawness.java @@ -1,7 +1,7 @@ import java.util.Arrays; import java.util.Set; -class SuperRawness { +public class SuperRawness { // :: warning: [unchecked] Possible heap pollution from parameterized vararg type // java.util.Set static void test(Set... args) { diff --git a/checker/tests/nullness/generics/TernaryGenerics.java b/checker/tests/nullness/generics/TernaryGenerics.java index b384457205a1..dadbdc49b09d 100644 --- a/checker/tests/nullness/generics/TernaryGenerics.java +++ b/checker/tests/nullness/generics/TernaryGenerics.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class TernaryGenerics { +public class TernaryGenerics { class Generic1 { void cond(boolean b, T p) { // :: error: (assignment.type.incompatible) diff --git a/checker/tests/nullness/generics/VarArgs.java b/checker/tests/nullness/generics/VarArgs.java deleted file mode 100644 index 7e7dd10549ad..000000000000 --- a/checker/tests/nullness/generics/VarArgs.java +++ /dev/null @@ -1,12 +0,0 @@ -import java.util.Arrays; -import java.util.Set; - -class AaTest { - // :: warning: [unchecked] Possible heap pollution from parameterized vararg type - // java.util.Set - void test(Set... args) { - Arrays.asList(args); - } - // static void test(Set... args) { test2(Arrays.asList(args)); } - // static void test2(Iterable> args) {} -} diff --git a/checker/tests/nullness/generics/VarArgsTest.java b/checker/tests/nullness/generics/VarArgsTest.java new file mode 100644 index 000000000000..3c5633085367 --- /dev/null +++ b/checker/tests/nullness/generics/VarArgsTest.java @@ -0,0 +1,12 @@ +import java.util.Arrays; +import java.util.Set; + +public class VarArgsTest { + // :: warning: [unchecked] Possible heap pollution from parameterized vararg type + // java.util.Set + void test(Set... args) { + Arrays.asList(args); + } + // static void test(Set... args) { test2(Arrays.asList(args)); } + // static void test2(Iterable> args) {} +} diff --git a/checker/tests/nullness/generics/WildcardAnnos.java b/checker/tests/nullness/generics/WildcardAnnos.java index 81a42ba29210..efb5a21ca584 100644 --- a/checker/tests/nullness/generics/WildcardAnnos.java +++ b/checker/tests/nullness/generics/WildcardAnnos.java @@ -1,20 +1,26 @@ -import java.util.List; import org.checkerframework.checker.nullness.qual.*; -class WildcardAnnos { +import java.util.List; + +public class WildcardAnnos { // :: error: (bound.type.incompatible) @Nullable List<@Nullable ? extends @NonNull Object> l1 = null; @Nullable List<@NonNull ? extends @Nullable Object> l2 = null; // The implicit upper bound is Nullable, because the annotation // on the wildcard is propagated. Therefore this type is: - // @Nullable List l3 = null; - @Nullable List<@Nullable ? super @NonNull Object> l3 = null; + // @Nullable List l3 = null; + @Nullable List<@Nullable ? super @NonNull String> l3 = null; + + // The bounds need to have the same annotations because capture conversion + // converts the type argument to just Object. + // :: error: (type.invalid.super.wildcard) + @Nullable List<@Nullable ? super @NonNull Object> l3b = null; // :: error: (bound.type.incompatible) - @Nullable List<@NonNull ? super @Nullable Object> l4 = null; + @Nullable List<@NonNull ? super @Nullable String> l4 = null; - @Nullable List l5 = null; + @Nullable List l5 = null; @Nullable List inReturn() { return null; @@ -25,9 +31,9 @@ void asParam(List p) {} // :: error: (type.invalid.conflicting.annos) @Nullable List<@Nullable @NonNull ? extends @Nullable Object> l6 = null; // :: error: (type.invalid.conflicting.annos) - @Nullable List<@Nullable @NonNull ? super @NonNull Object> l7 = null; + @Nullable List<@Nullable @NonNull ? super @NonNull String> l7 = null; // :: error: (type.invalid.conflicting.annos) @Nullable List l8 = null; // :: error: (type.invalid.conflicting.annos) - @Nullable List l9 = null; + @Nullable List l9 = null; } diff --git a/checker/tests/nullness/generics/WildcardBoundDefault.java b/checker/tests/nullness/generics/WildcardBoundDefault.java index 2cb4ca53e716..b922e8df12d2 100644 --- a/checker/tests/nullness/generics/WildcardBoundDefault.java +++ b/checker/tests/nullness/generics/WildcardBoundDefault.java @@ -5,7 +5,7 @@ class MyGenClass {} @DefaultQualifier(value = Nullable.class, locations = TypeUseLocation.UPPER_BOUND) -class Varargs { +public class WildcardBoundDefault { void test() { ignore(newInstance()); } diff --git a/checker/tests/nullness/generics/WildcardOverride.java b/checker/tests/nullness/generics/WildcardOverride.java index a21cf4cd8f79..c57fe4dc71f1 100644 --- a/checker/tests/nullness/generics/WildcardOverride.java +++ b/checker/tests/nullness/generics/WildcardOverride.java @@ -2,34 +2,27 @@ // see also framework/tests/all-systems/WildcardSuper2 -import java.util.List; import org.checkerframework.checker.nullness.qual.NonNull; +import java.util.List; + interface ToOverride { - // For nullness this should default to type @NonNull List public abstract int transform(List function); } -class WildcardOverride implements ToOverride { - // invalid because the overriden method takes @Nullable args and this one doesn't +public class WildcardOverride implements ToOverride { @Override - // :: error: (override.param.invalid) public int transform(List function) { return 0; } } interface ToOverride2 { - // For nullness this should be typed as - // @NonNull List // :: error: (bound.type.incompatible) public abstract int transform(List<@NonNull ? super T> function); } class WildcardOverride2 implements ToOverride2 { - // valid because the overriden method takes ONLY @NonNull args and this one takes @NonNull args - // as well @Override public int transform(List function) { return 0; diff --git a/checker/tests/nullness/generics/WildcardSubtyping.java b/checker/tests/nullness/generics/WildcardSubtyping.java index ef4eeb6a90b3..80d8300870f6 100644 --- a/checker/tests/nullness/generics/WildcardSubtyping.java +++ b/checker/tests/nullness/generics/WildcardSubtyping.java @@ -1,10 +1,11 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.lang.annotation.Annotation; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import org.checkerframework.checker.nullness.qual.*; class Utils { @@ -56,7 +57,6 @@ class UseMyGeneric { class MyGenericExactBounds<@NonNull T extends @NonNull Number> {} class UseMyGenericExactBounds { - // :: error: (type.argument.type.incompatible) MyGenericExactBounds wildcardOutsideUBError = new MyGenericExactBounds<>(); MyGenericExactBounds wildcardOutside = new MyGenericExactBounds<>(); diff --git a/checker/tests/nullness/generics/WildcardSubtyping2.java b/checker/tests/nullness/generics/WildcardSubtyping2.java index 1198ee59449f..8b01eeb591c6 100644 --- a/checker/tests/nullness/generics/WildcardSubtyping2.java +++ b/checker/tests/nullness/generics/WildcardSubtyping2.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.*; -class WildcardSubtyping2 { +public class WildcardSubtyping2 { class MyClass {} class MyCloneClass extends MyClass implements Cloneable {} diff --git a/checker/tests/nullness/generics/WildcardSubtypingTypeArray.java b/checker/tests/nullness/generics/WildcardSubtypingTypeArray.java index 19832c4b80c3..1735657e68f9 100644 --- a/checker/tests/nullness/generics/WildcardSubtypingTypeArray.java +++ b/checker/tests/nullness/generics/WildcardSubtypingTypeArray.java @@ -1,10 +1,11 @@ -import java.util.List; import org.checkerframework.checker.nullness.qual.*; -class WildcardSubtypingTypeArray { +import java.util.List; + +public class WildcardSubtypingTypeArray { void test(List list) { test2(list.get(0)); } - void test2(A x) {}; + void test2(A x) {} } diff --git a/checker/tests/nullness/generics/WildcardSuper.java b/checker/tests/nullness/generics/WildcardSuper.java index 5fa6420c69ed..ae7a9f22463c 100644 --- a/checker/tests/nullness/generics/WildcardSuper.java +++ b/checker/tests/nullness/generics/WildcardSuper.java @@ -1,22 +1,14 @@ import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.framework.qual.DefaultQualifier; -class WildcardSuper { +public class WildcardSuper { void testWithSuper(Cell cell) { - // TODO: Address comments. Since ? is explicitly lower bounded, I have made a judgment that - // it should be implicitly upper bounded. - // This is valid because the default upper bound is NonNull // :: error: (dereference.of.nullable) cell.get().toString(); } - // TODO: THIS SHOULD JUST ISSUE A WARNING, WHY WOULD PEOPLE WANT TO WRITE CONTRADICTING BOUNDS? void testWithContradiction(Cell cell) { - // This is actually valid, because it's a contradiction, b/c - // the implicit upper bound is NonNull. - // We are free to do anything, as the method is not callable. - // TODO: test whether all calls of method fail. // :: error: (dereference.of.nullable) cell.get().toString(); } diff --git a/checker/tests/nullness/init/RawMethodInvocation.java b/checker/tests/nullness/init/RawMethodInvocation.java deleted file mode 100644 index 093bf24c1f9e..000000000000 --- a/checker/tests/nullness/init/RawMethodInvocation.java +++ /dev/null @@ -1,49 +0,0 @@ -import org.checkerframework.checker.initialization.qual.UnknownInitialization; -import org.checkerframework.checker.nullness.qual.*; -import org.checkerframework.checker.nullness.qual.EnsuresNonNull; - -@org.checkerframework.framework.qual.DefaultQualifier(Nullable.class) -class RawMethodInvocation { - @NonNull String a; - @NonNull String b; - - RawMethodInvocation(boolean constructor_inits_a) { - a = ""; - init_b(); - } - - @EnsuresNonNull("b") - void init_b(@UnknownInitialization RawMethodInvocation this) { - b = ""; - } - - // :: error: (initialization.fields.uninitialized) - RawMethodInvocation(Byte constructor_inits_b) { - init_b(); - } - - // :: error: (initialization.fields.uninitialized) - RawMethodInvocation(byte constructor_inits_b) { - b = ""; - init_b(); - } - - RawMethodInvocation(int constructor_inits_none) { - init_ab(); - } - - @EnsuresNonNull({"a", "b"}) - void init_ab(@UnknownInitialization RawMethodInvocation this) { - a = ""; - b = ""; - } - - RawMethodInvocation(long constructor_escapes_raw) { - a = ""; - // :: error: (method.invocation.invalid) - nonRawMethod(); - b = ""; - } - - void nonRawMethod() {} -} diff --git a/checker/tests/nullness/init/Uninit.java b/checker/tests/nullness/init/Uninit.java deleted file mode 100644 index b9d72ccf82b0..000000000000 --- a/checker/tests/nullness/init/Uninit.java +++ /dev/null @@ -1,4 +0,0 @@ -// :: error: (initialization.fields.uninitialized) -public class Uninit { - Object a; -} diff --git a/checker/tests/nullness/init/Uninit5.java b/checker/tests/nullness/init/Uninit5.java deleted file mode 100644 index 4648a6ba8b7c..000000000000 --- a/checker/tests/nullness/init/Uninit5.java +++ /dev/null @@ -1,4 +0,0 @@ -// :: error: (initialization.fields.uninitialized) -public class Uninit5 { - String x; -} diff --git a/checker/tests/nullness/java-unsound/Figure1.java b/checker/tests/nullness/java-unsound/Figure1.java index e260f7646cad..77ae99a175cd 100644 --- a/checker/tests/nullness/java-unsound/Figure1.java +++ b/checker/tests/nullness/java-unsound/Figure1.java @@ -1,5 +1,5 @@ // Unsound only in Java 8, Java 9+ already gives an error -// @below-java9-jdk-skip-test +// @skip-test no need to test for the javac error. public class Figure1 { static class Constrain {} diff --git a/checker/tests/nullness/java-unsound/Figure3.java b/checker/tests/nullness/java-unsound/Figure3.java index d4a2bbe8d5e7..37dc8925901b 100644 --- a/checker/tests/nullness/java-unsound/Figure3.java +++ b/checker/tests/nullness/java-unsound/Figure3.java @@ -8,6 +8,10 @@ Constraint bad() { } A coerce(B b) { + // type of expression: capture#703[ extends @Initialized @Nullable Object super B[ + // extends @Initialized @Nullable Object super @Initialized @NonNull Void]] + // method return type: A[ extends @Initialized @Nullable Object super @Initialized + // @NonNull Void] return pair(this.bad(), b).value; } } diff --git a/checker/tests/nullness/java-unsound/Figure4.java b/checker/tests/nullness/java-unsound/Figure4.java index 48025e918fda..b6df5c030f42 100644 --- a/checker/tests/nullness/java-unsound/Figure4.java +++ b/checker/tests/nullness/java-unsound/Figure4.java @@ -1,5 +1,5 @@ // Unsound only in Java 8, Java 9+ already gives an error -// @below-java9-jdk-skip-test +// @skip-test no need to test for the javac error. public class Figure4 { static class Constrain {} diff --git a/checker/tests/nullness/java-unsound/Figure7.java b/checker/tests/nullness/java-unsound/Figure7.java index c4b29baf41d8..9abf06ef2703 100644 --- a/checker/tests/nullness/java-unsound/Figure7.java +++ b/checker/tests/nullness/java-unsound/Figure7.java @@ -1,5 +1,5 @@ // Unsound only in Java 8, Java 9+ already gives an error -// @below-java9-jdk-skip-test +// @skip-test no need to test for the javac error. public class Figure7 { class Constrain {} diff --git a/checker/tests/nullness/java17/Greeting.java b/checker/tests/nullness/java17/Greeting.java new file mode 100644 index 000000000000..3507794645e6 --- /dev/null +++ b/checker/tests/nullness/java17/Greeting.java @@ -0,0 +1,30 @@ +// @below-java16-jdk-skip-test +// Test case for https://github.com/typetools/checker-framework/issues/5039 +package com.example.hello_world; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Objects; + +public final class Greeting { + public final @Nullable String name; + + public Greeting(@Nullable String name) { + this.name = name; + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public boolean equals(@Nullable Object o) { + return o == this || o instanceof Greeting that && Objects.equals(name, that.name); + } + + @Override + public String toString() { + return name == null ? "World" : name; + } +} diff --git a/checker/tests/nullness/java17/InstanceOfPatternVariable.java b/checker/tests/nullness/java17/InstanceOfPatternVariable.java new file mode 100644 index 000000000000..6159498aecb4 --- /dev/null +++ b/checker/tests/nullness/java17/InstanceOfPatternVariable.java @@ -0,0 +1,17 @@ +// @below-java17-jdk-skip-test +// Test case for https://github.com/typetools/checker-framework/issues/5240 + +import org.checkerframework.checker.nullness.qual.KeyFor; + +import java.util.Map; + +public class InstanceOfPatternVariable { + + public void doSomething(final Object x) { + if (x instanceof Map m) { + // final var ct = (ClassOrInterfaceType) type; + + @KeyFor("m") Object y = m.keySet().iterator().next(); + } + } +} diff --git a/checker/tests/nullness/java17/Issue5047.java b/checker/tests/nullness/java17/Issue5047.java new file mode 100644 index 000000000000..595f68e5d644 --- /dev/null +++ b/checker/tests/nullness/java17/Issue5047.java @@ -0,0 +1,25 @@ +// @below-java16-jdk-skip-test +// Test case for issue #5047: https://tinyurl.com/cfissue/5047 + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Objects; + +public class Issue5047 {} + +class NumberParameterBuilder { + + @Nullable Object minimum; + @Nullable Object maximum; + + public boolean equals(final @Nullable Object o) { + + if (o instanceof NumberParameterBuilder b) { + return super.equals(o) + && Objects.equals(this.minimum, b.minimum) + && Objects.equals(this.maximum, b.maximum); + } else { + return false; + } + } +} diff --git a/checker/tests/nullness/java17/Issue5967.java b/checker/tests/nullness/java17/Issue5967.java new file mode 100644 index 000000000000..4531b034d8e5 --- /dev/null +++ b/checker/tests/nullness/java17/Issue5967.java @@ -0,0 +1,25 @@ +// @below-java17-jdk-skip-test + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.function.Supplier; + +public final class Issue5967 { + + enum TestEnum { + FIRST, + SECOND; + } + + public static void main(String[] args) { + TestEnum testEnum = TestEnum.FIRST; + Supplier supplier = + switch (testEnum) { + case FIRST: + yield () -> 1; + case SECOND: + yield () -> 2; + }; + @NonNull Supplier supplier1 = supplier; + } +} diff --git a/checker/tests/nullness/java17/NullnessSwitchArrows.java b/checker/tests/nullness/java17/NullnessSwitchArrows.java new file mode 100644 index 000000000000..4ee842d21cbd --- /dev/null +++ b/checker/tests/nullness/java17/NullnessSwitchArrows.java @@ -0,0 +1,78 @@ +// @below-java14-jdk-skip-test +public class NullnessSwitchArrows { + public enum Day { + SUNDAY, + MONDAY, + TUESDAY, + WEDNESDAY, + THURSDAY, + FRIDAY, + SATURDAY; + } + + void method1() { + Object o; + Day day = Day.WEDNESDAY; + switch (day) { + case MONDAY, FRIDAY, SUNDAY -> o = "hello"; + case TUESDAY -> o = null; + case THURSDAY, SATURDAY -> o = "hello"; + case WEDNESDAY -> o = "hello"; + default -> throw new IllegalStateException("Invalid day: " + day); + } + + // :: error: (dereference.of.nullable) + o.toString(); + } + + void method2() { + Object o; + Day day = Day.WEDNESDAY; + switch (day) { + case MONDAY, FRIDAY, SUNDAY -> o = "hello"; + case TUESDAY -> o = "hello"; + case THURSDAY, SATURDAY -> o = "hello"; + case WEDNESDAY -> o = "hello"; + default -> throw new IllegalStateException("Invalid day: " + day); + } + + o.toString(); + } + + void method2b() { + Object o; + Day day = Day.WEDNESDAY; + switch (day) { + case MONDAY, FRIDAY, SUNDAY: + o = "hello"; + break; + case TUESDAY: + o = "hello"; + break; + case THURSDAY, SATURDAY: + o = "hello"; + break; + case WEDNESDAY: + o = "hello"; + break; + default: + throw new IllegalStateException("Invalid day: " + day); + } + + o.toString(); + } + + void method3() { + Object o; + Day day = Day.WEDNESDAY; + switch (day) { + case MONDAY, FRIDAY, SUNDAY -> o = "hello"; + case TUESDAY -> o = "hello"; + case THURSDAY, SATURDAY -> o = "hello"; + case WEDNESDAY -> o = "hello"; + default -> o = "hello"; + } + + o.toString(); + } +} diff --git a/checker/tests/nullness/java17/NullnessSwitchExpressionLambda.java b/checker/tests/nullness/java17/NullnessSwitchExpressionLambda.java new file mode 100644 index 000000000000..a42799c0d0db --- /dev/null +++ b/checker/tests/nullness/java17/NullnessSwitchExpressionLambda.java @@ -0,0 +1,20 @@ +// @below-java14-jdk-skip-test +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.function.Function; + +public class NullnessSwitchExpressionLambda { + int anInt; + + void switchExprLambda() { + Function f = + (n) -> + switch (n.anInt) { + case 3, 4, 5 -> new Object(); + default -> null; + }; + Object o = f.apply(new NullnessSwitchExpressionLambda()); + // :: error: (dereference.of.nullable) + o.toString(); + } +} diff --git a/checker/tests/nullness/java17/NullnessSwitchExpressions.java b/checker/tests/nullness/java17/NullnessSwitchExpressions.java new file mode 100644 index 000000000000..bc70a587b792 --- /dev/null +++ b/checker/tests/nullness/java17/NullnessSwitchExpressions.java @@ -0,0 +1,63 @@ +// @below-java14-jdk-skip-test +public class NullnessSwitchExpressions { + public enum Day { + SUNDAY, + MONDAY, + TUESDAY, + WEDNESDAY, + THURSDAY, + FRIDAY, + SATURDAY; + } + + void method1() { + Day day = Day.WEDNESDAY; + Object o = + switch (day) { + case MONDAY, FRIDAY, SUNDAY -> "hello"; + case TUESDAY -> null; + case THURSDAY, SATURDAY -> "hello"; + case WEDNESDAY -> "hello"; + default -> throw new IllegalStateException("Invalid day: " + day); + }; + + // :: error: (dereference.of.nullable) + o.toString(); + } + + void method2() { + Day day = Day.WEDNESDAY; + Object o = + switch (day) { + case MONDAY, FRIDAY, SUNDAY -> "hello"; + case TUESDAY -> "hello"; + case THURSDAY, SATURDAY -> "hello"; + case WEDNESDAY -> "hello"; + default -> throw new IllegalStateException("Invalid day: " + day); + }; + + o.toString(); + } + + void method3() { + Day day = Day.WEDNESDAY; + Object o = + switch (day) { + case MONDAY, FRIDAY, SUNDAY -> "hello"; + case TUESDAY -> "hello"; + case THURSDAY, SATURDAY -> { + String s = null; + if (day == Day.THURSDAY) { + s = "hello"; + s.toString(); + } + yield s; + } + case WEDNESDAY -> "hello"; + default -> throw new IllegalStateException("Invalid day: " + day); + }; + + // :: error: (dereference.of.nullable) + o.toString(); + } +} diff --git a/checker/tests/nullness/java17/NullnessSwitchStatementRules.java b/checker/tests/nullness/java17/NullnessSwitchStatementRules.java new file mode 100644 index 000000000000..bf460af93f12 --- /dev/null +++ b/checker/tests/nullness/java17/NullnessSwitchStatementRules.java @@ -0,0 +1,43 @@ +// @below-java14-jdk-skip-test +import org.checkerframework.checker.nullness.qual.Nullable; + +public class NullnessSwitchStatementRules { + @Nullable Object field = null; + + void method(int selector) { + field = new Object(); + switch (selector) { + case 1 -> field = null; + case 2 -> field.toString(); + } + + field = new Object(); + switch (selector) { + case 1 -> { + field = null; + } + case 2 -> { + field.toString(); + } + } + + field = new Object(); + switch (selector) { + case 1 -> { + field = null; + } + case 2 -> { + field.toString(); + } + } + + field = new Object(); + switch (selector) { + case 1: + field = null; + case 2: + // :: error: (dereference.of.nullable) + field.toString(); + } + } +} diff --git a/checker/tests/nullness/java17/SwitchExpressionInvariant.java b/checker/tests/nullness/java17/SwitchExpressionInvariant.java new file mode 100644 index 000000000000..2e38564b5f25 --- /dev/null +++ b/checker/tests/nullness/java17/SwitchExpressionInvariant.java @@ -0,0 +1,30 @@ +// @below-java14-jdk-skip-test +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.List; + +public class SwitchExpressionInvariant { + public static boolean flag = false; + + void method( + List<@NonNull String> nonnullStrings, + List<@Nullable String> nullableStrings, + int fenum) { + + List<@NonNull String> list = + // :: error: (assignment.type.incompatible) + switch (fenum) { + // :: error: (switch.expression.type.incompatible) + case 1 -> nonnullStrings; + default -> nullableStrings; + }; + + List<@Nullable String> list2 = + switch (fenum) { + // :: error: (switch.expression.type.incompatible) + case 1 -> nonnullStrings; + default -> nullableStrings; + }; + } +} diff --git a/checker/tests/nullness/java17/SwitchTestIssue5412.java b/checker/tests/nullness/java17/SwitchTestIssue5412.java new file mode 100644 index 000000000000..75a52539eefc --- /dev/null +++ b/checker/tests/nullness/java17/SwitchTestIssue5412.java @@ -0,0 +1,129 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5412 + +// @below-java17-jdk-skip-test + +import org.checkerframework.checker.nullness.qual.NonNull; + +enum MyEnum { + VAL1, + VAL2, + VAL3 +} + +class SwitchTestExhaustive { + public String foo1(MyEnum b) { + final var s = + switch (b) { + case VAL1 -> "1"; + case VAL2 -> "2"; + case VAL3 -> "3"; + }; + return s; + } + + public String foo1a(MyEnum b) { + final var s = + switch (b) { + case VAL1 -> "1"; + case VAL2 -> "2"; + case VAL3 -> "3"; + // The default case is dead code, so it would be possible for type-checking + // to skip it and not issue this warning. But giving the warning is also + // good. + default -> null; + }; + // :: error: (return.type.incompatible) + return s; + } + + public String foo2(MyEnum b) { + final var s = + switch (b) { + case VAL1 -> "1"; + case VAL2 -> "2"; + case VAL3 -> "3"; + default -> throw new RuntimeException(); + }; + return s; + } + + public String foo3(MyEnum b) { + return switch (b) { + case VAL1 -> "1"; + case VAL2 -> "2"; + case VAL3 -> "3"; + }; + } + + public String foo4(MyEnum b) { + String aString = "foo"; + switch (b) { + case VAL1: + return "a"; + case VAL2: + return "b"; + case VAL3: + return "c"; + default: + System.out.println(aString.hashCode()); + throw new Error(); + } + } + + public String foo4a(MyEnum b) { + String aString = null; + switch (b) { + case VAL1: + aString = "a"; + break; + case VAL2: + aString = "b"; + break; + case VAL3: + aString = "c"; + break; + // The `default:` case is dead code, so it is acceptable for this method to compile + // without nullness errors. + default: + break; + } + // :: error: (return.type.incompatible) + return aString; + } + + public String foo4b(MyEnum b) { + String aString; + switch (b) { + case VAL1: + aString = "a"; + break; + case VAL2: + aString = "b"; + break; + case VAL3: + aString = "c"; + break; + // The `default:` case is dead code, so it is acceptable for this method to compile + // without nullness errors. + default: + aString = null; + break; + } + // :: error: (return.type.incompatible) + return aString; + } + + // TODO: test fallthrough to the default: case. + public @NonNull String foo5(MyEnum b) { + String aString = "foo"; + switch (b) { + case VAL1: + return aString; + case VAL2: + return aString; + case VAL3: + default: + return aString; + } + } +} diff --git a/checker/tests/nullness/java21/FlowSwitch.java b/checker/tests/nullness/java21/FlowSwitch.java new file mode 100644 index 000000000000..82933ec93c29 --- /dev/null +++ b/checker/tests/nullness/java21/FlowSwitch.java @@ -0,0 +1,108 @@ +// @below-java21-jdk-skip-test + +// None of the WPI formats support the new Java 21 languages features, so skip inference until they +// do. +// @infer-jaifs-skip-test +// @infer-ajava-skip-test +// @infer-stubs-skip-test + +public class FlowSwitch { + + void test0(Number n) { + String s = null; + switch (n) { + default: + { + // TODO: this should issue a dereference of nullable error. + n.toString(); + s = ""; + } + } + s.toString(); + } + + void test1(Integer i) { + String msg = null; + switch (i) { + case -1, 1: + msg = "-1 or 1"; + break; + case Integer j when j > 0: + msg = "pos"; + break; + case Integer j: + msg = "everything else"; + break; + } + msg.toString(); + } + + void test2(Integer i) { + String msg = null; + switch (i) { + case -1, 1: + msg = "-1 or 1"; + break; + default: + msg = "everythingything else"; + break; + case 2: + msg = "pos"; + break; + } + msg.toString(); + } + + class A {} + + class B extends A {} + + sealed interface I permits C, D {} + + final class C implements I {} + + final class D implements I {} + + record Pair(T x, T y) {} + + void testE(Pair p1) { + B e = + switch (p1) { + case Pair(A a, B b) -> b; + case Pair(B b, A a) -> b; + default -> null; + }; + B e2 = null; + switch (p1) { + case Pair(A a, B b) -> e2 = b; + case Pair(B b, A a) -> e2 = b; + default -> e2 = new B(); + } + e2.toString(); + } + + void test3(Pair p2) { + String s = null; + I e = null; + switch (p2) { + case Pair(I i, C c) -> { + e = c; + s = ""; + } + case Pair(I i, D d) -> { + e = d; + s = ""; + } + } + s.toString(); + e.toString(); + + I e2 = null; + switch (p2) { + case Pair(C c, I i) -> e2 = c; + case Pair(D d, C c) -> e2 = d; + case Pair(D d1, D d2) -> e2 = d2; + } + e2.toString(); + } +} diff --git a/checker/tests/nullness/java21/Issue6290.java b/checker/tests/nullness/java21/Issue6290.java new file mode 100644 index 000000000000..96f421d95870 --- /dev/null +++ b/checker/tests/nullness/java21/Issue6290.java @@ -0,0 +1,11 @@ +import java.util.Optional; + +// @below-java17-jdk-skip-test +public class Issue6290 { + + public Optional test(String param) { + var first = Optional.ofNullable(param); + var second = first.isPresent() ? first : Optional.ofNullable(param); + return second; + } +} diff --git a/checker/tests/nullness/java21/NullRedundant.java b/checker/tests/nullness/java21/NullRedundant.java new file mode 100644 index 000000000000..10151a8714e3 --- /dev/null +++ b/checker/tests/nullness/java21/NullRedundant.java @@ -0,0 +1,112 @@ +// Test case for EISOP issue 628: +// https://github.com/eisop/checker-framework/issues/628 + +// @below-java21-jdk-skip-test + +import org.checkerframework.checker.nullness.qual.Nullable; + +class NullRedundant { + void test1(Object o) { + // :: warning: (nulltest.redundant) + if (o == null) { + System.out.println("o is null"); + } + + switch (o) { + case Number n: + System.out.println("Number: " + n); + break; + // :: warning: (nulltest.redundant) + case null: + System.out.println("null"); + break; + default: + System.out.println("anything else"); + } + + switch (o) { + // :: warning: (nulltest.redundant) + case null, default: + System.out.println("null"); + break; + } + } + + Object test2(Object o) { + switch (o) { + case Number n -> System.out.println("Number: " + n); + // :: warning: (nulltest.redundant) + case null -> System.out.println("null"); + default -> System.out.println("anything else"); + } + ; + + switch (o) { + // :: warning: (nulltest.redundant) + case null, default -> System.out.println("null"); + } + + var output = + switch (o) { + case Number n -> "Number: " + n; + // :: warning: (nulltest.redundant) + case null -> "null"; + default -> "anything else"; + }; + + return switch (o) { + // :: warning: (nulltest.redundant) + case null -> "null"; + default -> "anything else"; + }; + } + + // Test with Nullable argument to make sure there is no false positive. + void test3(@Nullable Object o) { + if (o == null) { + System.out.println("o is null"); + } + + switch (o) { + case Number n: + System.out.println("Number: " + n); + break; + case null: + System.out.println("null"); + break; + default: + System.out.println("anything else"); + } + + switch (o) { + case null, default: + System.out.println("null"); + break; + } + } + + Object test4(@Nullable Object o) { + switch (o) { + case Number n -> System.out.println("Number: " + n); + case null -> System.out.println("null"); + default -> System.out.println("anything else"); + } + ; + + switch (o) { + case null, default -> System.out.println("null"); + } + + var output = + switch (o) { + case Number n -> "Number: " + n; + case null -> "null"; + default -> "anything else"; + }; + + return switch (o) { + case null -> "null"; + default -> "anything else"; + }; + } +} diff --git a/checker/tests/nullness/java21/NullableSwitchSelector.java b/checker/tests/nullness/java21/NullableSwitchSelector.java new file mode 100644 index 000000000000..49341e27958f --- /dev/null +++ b/checker/tests/nullness/java21/NullableSwitchSelector.java @@ -0,0 +1,42 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +// @below-java21-jdk-skip-test + +// None of the WPI formats support the new Java 21 languages features, so skip inference until they +// do. +// @infer-jaifs-skip-test +// @infer-ajava-skip-test +// @infer-stubs-skip-test +public class NullableSwitchSelector { + + static String formatterPatternSwitch1(@Nullable Object obj) { + return switch (obj) { + case Integer i -> obj.toString(); + case String s -> String.format("String %s", s); + // :: error: (dereference.of.nullable) + case null -> obj.toString(); + default -> obj.toString(); + }; + } + + static String formatterPatternSwitch2(@Nullable Object obj) { + // :: error: (switching.nullable) + return switch (obj) { + case Integer i -> obj.toString(); + case String s -> String.format("String %s", s); + // TODO: If obj is null, this case isn't reachable, because a null pointer exception + // happens at the selector expression. + // :: error: (dereference.of.nullable) + default -> obj.toString(); + }; + } + + static String formatterPatternSwitch3(@Nullable Object obj) { + return switch (obj) { + case Integer i -> obj.toString(); + case String s -> String.format("String %s", s); + // :: error: (dereference.of.nullable) + case null, default -> obj.toString(); + }; + } +} diff --git a/checker/tests/nullness/java21/SimpleCaseGuard.java b/checker/tests/nullness/java21/SimpleCaseGuard.java new file mode 100644 index 000000000000..9f3ea97d3a4c --- /dev/null +++ b/checker/tests/nullness/java21/SimpleCaseGuard.java @@ -0,0 +1,31 @@ +// @below-java21-jdk-skip-test + +// None of the WPI formats support the new Java 21 languages features, so skip inference until they +// do. +// @infer-jaifs-skip-test +// @infer-ajava-skip-test +// @infer-stubs-skip-test + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class SimpleCaseGuard { + + @Nullable String field; + + void test2(Object obj, boolean b) { + switch (obj) { + case String s when field != null -> { + @NonNull String z = field; + } + case String s -> { + // :: error: (assignment.type.incompatible) + @NonNull String z = field; + } + default -> { + // :: error: (assignment.type.incompatible) + @NonNull String z = field; + } + } + } +} diff --git a/checker/tests/nullness/java8/Issue1000.java b/checker/tests/nullness/java8/Issue1000.java index d1674c5bed1f..bdffbc42a62b 100644 --- a/checker/tests/nullness/java8/Issue1000.java +++ b/checker/tests/nullness/java8/Issue1000.java @@ -1,10 +1,11 @@ // Test case for issue #1000: // https://github.com/typetools/checker-framework/issues/1000 -import java.util.Optional; import org.checkerframework.checker.nullness.qual.Nullable; -class Issue1000 { +import java.util.Optional; + +public class Issue1000 { void illegalInstantiation(Optional<@Nullable String> arg) {} String orElseAppliedToNonNull(Optional opt) { diff --git a/checker/tests/nullness/java8/Issue1046Java8.java b/checker/tests/nullness/java8/Issue1046Java8.java index d81060ca54e4..81a20acc3f89 100644 --- a/checker/tests/nullness/java8/Issue1046Java8.java +++ b/checker/tests/nullness/java8/Issue1046Java8.java @@ -2,11 +2,12 @@ // https://github.com/typetools/checker-framework/issues/1046 // Additonal test case: checker/tests/nullness/Issue1046.java +import org.checkerframework.checker.nullness.qual.UnknownKeyFor; + import java.util.List; import java.util.function.Function; -import org.checkerframework.checker.nullness.qual.UnknownKeyFor; -class Issue1046Java8 { +public class Issue1046Java8 { interface EnumMarker {} enum MyEnum implements EnumMarker { diff --git a/checker/tests/nullness/java8/Issue1098.java b/checker/tests/nullness/java8/Issue1098.java index f37c81f7d71b..8067c7b47972 100644 --- a/checker/tests/nullness/java8/Issue1098.java +++ b/checker/tests/nullness/java8/Issue1098.java @@ -3,14 +3,15 @@ import java.util.Optional; -class Issue1098 { +public class Issue1098 { void opt(Optional p1, T p2) {} void cls(Class p1, T p2) {} + @SuppressWarnings("keyfor:type.argument") void use() { opt(Optional.empty(), null); - // TODO: false positive, because type 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 54265353cdbb..da7c3ee4f4d7 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 14bf178aa364..b3a04c576089 100644 --- a/checker/tests/nullness/java8/Issue1633.java +++ b/checker/tests/nullness/java8/Issue1633.java @@ -1,13 +1,14 @@ // Test case for Issue 1633: // https://github.com/typetools/checker-framework/issues/1633 -import java.util.function.Supplier; import org.checkerframework.checker.nullness.qual.*; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.framework.qual.Covariant; -class Issue1633 { +import java.util.function.Supplier; + +public class Issue1633 { // supplyNullable is a supplier that may return null. // supplyNonNull is a supplier that does not return null. @@ -53,7 +54,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); } @@ -77,7 +78,7 @@ void foo8nw(Optional1633 o, Supplier<@NonNull String> supplyNonNull) { // From the JDK @Covariant(0) -final @NonNull class Optional1633 { +@NonNull final class Optional1633 { /** If non-null, the value; if null, indicates no value is present. */ private final @Nullable T value = null; diff --git a/checker/tests/nullness/java8/Issue363.java b/checker/tests/nullness/java8/Issue363.java index 341138d94db0..e20d6dc25e81 100644 --- a/checker/tests/nullness/java8/Issue363.java +++ b/checker/tests/nullness/java8/Issue363.java @@ -1,7 +1,7 @@ // Test case for Issue 363: // https://github.com/typetools/checker-framework/issues/363 -class Issue363 { +public class Issue363 { void foo(java.util.OptionalInt value) { value.orElseThrow(() -> new Error()); } diff --git a/checker/tests/nullness/java8/Issue366.java b/checker/tests/nullness/java8/Issue366.java index 60eb7ec39e56..fa46b9ffb25b 100644 --- a/checker/tests/nullness/java8/Issue366.java +++ b/checker/tests/nullness/java8/Issue366.java @@ -3,11 +3,12 @@ // but amended for Issue 1098: // https://github.com/typetools/checker-framework/issues/1098 -import java.util.Optional; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -class Issue366 { +import java.util.Optional; + +public class Issue366 { static Optional<@NonNull String> getPossiblyEmptyString() { return Optional.ofNullable(null); } diff --git a/checker/tests/nullness/java8/Issue448Ext.java b/checker/tests/nullness/java8/Issue448Ext.java index d4f9371bd603..866b1c08dfa5 100644 --- a/checker/tests/nullness/java8/Issue448Ext.java +++ b/checker/tests/nullness/java8/Issue448Ext.java @@ -5,7 +5,7 @@ import java.util.function.IntPredicate; import java.util.stream.IntStream; -class Issue448Ext { +public class Issue448Ext { void getFor(int[] ia, int index) { Arrays.stream(ia).filter(x -> true); } diff --git a/checker/tests/nullness/java8/Issue496.java b/checker/tests/nullness/java8/Issue496.java index 66661d2870a4..00961e91ff7e 100644 --- a/checker/tests/nullness/java8/Issue496.java +++ b/checker/tests/nullness/java8/Issue496.java @@ -3,7 +3,7 @@ import java.util.Optional; -class Issue496 { +public class Issue496 { public static class Entity { public final T value; diff --git a/checker/tests/nullness/java8/Issue557.java b/checker/tests/nullness/java8/Issue557.java index 9f55e47e3870..706f2ddffb6d 100644 --- a/checker/tests/nullness/java8/Issue557.java +++ b/checker/tests/nullness/java8/Issue557.java @@ -1,9 +1,10 @@ // Test case for issue 557: // https://github.com/typetools/checker-framework/issues/557 -import java.util.Optional; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Optional; + @SuppressWarnings("nullness") class MyOpt { static MyOpt of(S p) { diff --git a/checker/tests/nullness/java8/Issue579.java b/checker/tests/nullness/java8/Issue579.java index 66482225f94a..b658af227f08 100644 --- a/checker/tests/nullness/java8/Issue579.java +++ b/checker/tests/nullness/java8/Issue579.java @@ -3,11 +3,11 @@ import java.util.Comparator; -class Issue570 implements Comparator { +public class Issue579 implements Comparator { private final Comparator real; @SuppressWarnings("unchecked") - Issue570(Comparator real) { + Issue579(Comparator real) { this.real = (Comparator) real; } @@ -18,7 +18,7 @@ public int compare(T a, T b) { @Override public Comparator thenComparing(Comparator other) { - // :: warning: (known.nonnull) - return new Issue570<>(real == null ? other : real.thenComparing(other)); + // :: warning: (nulltest.redundant) + return new Issue579<>(real == null ? other : real.thenComparing(other)); } } diff --git a/checker/tests/nullness/java8/Issue596.java b/checker/tests/nullness/java8/Issue596.java index 7b6062641d12..cc52b1cd7a85 100644 --- a/checker/tests/nullness/java8/Issue596.java +++ b/checker/tests/nullness/java8/Issue596.java @@ -1,12 +1,13 @@ // Test case for Issue 596: // https://github.com/typetools/checker-framework/issues/596 -import java.util.concurrent.atomic.AtomicReference; import org.checkerframework.checker.nullness.qual.*; -class Issue596 { +import java.util.concurrent.atomic.AtomicReference; + +public class Issue596 { - private static String getOrEmpty(AtomicReference ref) { + private static String getOrEmpty(AtomicReference<@Nullable String> ref) { return Optional596.fromNullable(ref.get()).or(""); } } diff --git a/checker/tests/nullness/java8/Issue720.java b/checker/tests/nullness/java8/Issue720.java index 36416e1b4f3e..d747160f515c 100644 --- a/checker/tests/nullness/java8/Issue720.java +++ b/checker/tests/nullness/java8/Issue720.java @@ -3,7 +3,7 @@ import java.util.function.IntConsumer; -class Issue720 { +public class Issue720 { static IntConsumer consumer = Issue720::method; static int method(int x) { diff --git a/checker/tests/nullness/java8/UnionTypeBug.java b/checker/tests/nullness/java8/UnionTypeBug.java index c3c56e01d8f2..4d441a037fca 100644 --- a/checker/tests/nullness/java8/UnionTypeBug.java +++ b/checker/tests/nullness/java8/UnionTypeBug.java @@ -14,8 +14,10 @@ void method() { badBoy(); + // :: warning: (nullness.on.exception.parameter) } catch (@NonNull InnerException1 | @NonNull InnerException2 e) { + // :: warning: (nullness.on.exception.parameter) } catch (@NonNull InnerException3 | @NonNull InnerException4 e) { } diff --git a/checker/tests/nullness/java8/lambda/Dataflow.java b/checker/tests/nullness/java8/lambda/Dataflow.java index c35f1edab175..0eae76bdbb40 100644 --- a/checker/tests/nullness/java8/lambda/Dataflow.java +++ b/checker/tests/nullness/java8/lambda/Dataflow.java @@ -2,7 +2,7 @@ import org.checkerframework.checker.nullness.qual.*; -class Dataflow { +public class Dataflow { void context() { FunctionDF<@Nullable Object, Object> o = a -> { diff --git a/checker/tests/nullness/java8/lambda/Initialization.java b/checker/tests/nullness/java8/lambda/Initialization.java deleted file mode 100644 index ac8ac9f166d8..000000000000 --- a/checker/tests/nullness/java8/lambda/Initialization.java +++ /dev/null @@ -1,205 +0,0 @@ -// Test field initialization -// fields, initializers, static initializers, constructors. - -import org.checkerframework.checker.nullness.qual.*; - -interface FunctionInit { - R apply(T t); -} - -interface Consumer { - void consume(T t); -} - -// For test purposes, f1 is never initialized -@SuppressWarnings({ - "initialization.fields.uninitialized", - "initialization.static.fields.uninitialized" -}) -class LambdaInit { - String f1; - String f2 = ""; - @Nullable String f3 = ""; - - String f1b; - FunctionInit ff0 = - s -> { - // :: error: (dereference.of.nullable) - f1.toString(); - // :: error: (dereference.of.nullable) - f1b.toString(); - f2.toString(); - // :: error: (dereference.of.nullable) - f3.toString(); - return ""; - }; - // Test field value refinement after initializer. f1b should still be @Nullable in the lambda. - Object o1 = f1b = ""; - - String f4; - - { - f3 = ""; - f4 = ""; - FunctionInit ff0 = - s -> { - // :: error: (dereference.of.nullable) - f1.toString(); - f2.toString(); - // :: error: (dereference.of.nullable) - f3.toString(); - f4.toString(); - return ""; - }; - } - - String f5; - - LambdaInit() { - f5 = ""; - FunctionInit ff0 = - s -> { - // :: error: (dereference.of.nullable) - f1.toString(); - f2.toString(); - // :: error: (dereference.of.nullable) - f3.toString(); - f5.toString(); - return ""; - }; - } - - // // This is a bug - // // Could probably be fixed with CommittmentTreeAnnotator::visitMethod - // // Or more likely, TypeFromTree::212 - // // AnnotatedTypeFactory::getImplicitReceiverType::1146(there is a todo...) - // Object o = new Object() { - // @Override - // public String toString() { - // f1.toString(); - // f2.toString(); - // return ""; - // } - // }; - // - - // Works! - void method() { - FunctionInit ff0 = - s -> { - f1.toString(); - f2.toString(); - // :: error: (dereference.of.nullable) - f3.toString(); - return ""; - }; - } - - // Test for nested - class Nested { - FunctionInit ff0 = - s -> { - f1.toString(); - f2.toString(); - // :: error: (dereference.of.nullable) - f3.toString(); - return ""; - }; - - String f4; - - { - f3 = ""; - f4 = ""; - FunctionInit ff0 = - s -> { - f1.toString(); - f2.toString(); - // :: error: (dereference.of.nullable) - f3.toString(); - f4.toString(); - return ""; - }; - } - - String f5; - - Nested() { - f5 = ""; - FunctionInit ff0 = - s -> { - f1.toString(); - f2.toString(); - // :: error: (dereference.of.nullable) - f3.toString(); - f5.toString(); - return ""; - }; - } - - void method() { - FunctionInit ff0 = - s -> { - f1.toString(); - f2.toString(); - // :: error: (dereference.of.nullable) - f3.toString(); - return ""; - }; - } - } - - // Test for nested in a lambda - Consumer func = - s -> { - Consumer ff0 = - s2 -> { - // :: error: (dereference.of.nullable) - f1.toString(); - f2.toString(); - // :: error: (dereference.of.nullable) - f3.toString(); - }; - }; - - // Tests for static initializers. - static String sf1; - static String sf2 = ""; - static @Nullable String sf3 = ""; - static String sf1b; - static FunctionInit sff0 = - s -> { - - // This is an issue with static initializers in general - // // :: error: (dereference.of.nullable) - sf1.toString(); - // This is an issue with static initializers in general - // // :: error: (dereference.of.nullable) - sf1b.toString(); - sf2.toString(); - // :: error: (dereference.of.nullable) - sf3.toString(); - return ""; - }; - // Test field value refinement after initializer. f1b should still be null. - static Object so1 = sf1b = ""; - - static String sf4; - - static { - sf3 = ""; - sf4 = ""; - FunctionInit sff0 = - s -> { - - // This is an issue with static initializers in general - // // :: error: (dereference.of.nullable) - sf1.toString(); - sf2.toString(); - // :: error: (dereference.of.nullable) - sf3.toString(); - sf4.toString(); - return ""; - }; - } -} diff --git a/checker/tests/nullness/java8/lambda/Issue1864b.java b/checker/tests/nullness/java8/lambda/Issue1864b.java index d4ec4bcdc566..074488ffd7b1 100644 --- a/checker/tests/nullness/java8/lambda/Issue1864b.java +++ b/checker/tests/nullness/java8/lambda/Issue1864b.java @@ -1,7 +1,7 @@ // Test case for Issue 1864: // https://github.com/typetools/checker-framework/issues/1864 -class Issue1864b { +public class Issue1864b { interface Supplier { Object get(); } diff --git a/checker/tests/nullness/java8/lambda/Issue1897.java b/checker/tests/nullness/java8/lambda/Issue1897.java index 3118d224dfba..aba8e531382d 100644 --- a/checker/tests/nullness/java8/lambda/Issue1897.java +++ b/checker/tests/nullness/java8/lambda/Issue1897.java @@ -3,7 +3,7 @@ import java.util.function.Function; -class Issue1897 { +public class Issue1897 { Issue1897() { final int length = 1; takesLambda(s -> length); diff --git a/checker/tests/nullness/java8/lambda/Issue3217.java b/checker/tests/nullness/java8/lambda/Issue3217.java index 1b92467642e9..b1540e06687c 100644 --- a/checker/tests/nullness/java8/lambda/Issue3217.java +++ b/checker/tests/nullness/java8/lambda/Issue3217.java @@ -1,6 +1,7 @@ -import java.util.function.Function; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.function.Function; + public class Issue3217 { private final Function, Function> proxyFunction; diff --git a/checker/tests/nullness/java8/lambda/Issue367.java b/checker/tests/nullness/java8/lambda/Issue367.java index d80b883330d7..816cc13d6a01 100644 --- a/checker/tests/nullness/java8/lambda/Issue367.java +++ b/checker/tests/nullness/java8/lambda/Issue367.java @@ -1,6 +1,6 @@ // Test case for Issue 367: // https://github.com/typetools/checker-framework/issues/367 -class Issue367 { +public class Issue367 { static void test(Iterable threads) { threads.forEach(thread -> System.out.println(thread)); } diff --git a/checker/tests/nullness/java8/lambda/Issue403.java b/checker/tests/nullness/java8/lambda/Issue403.java index b0251ed98721..6bb0a82f8a5b 100644 --- a/checker/tests/nullness/java8/lambda/Issue403.java +++ b/checker/tests/nullness/java8/lambda/Issue403.java @@ -3,7 +3,7 @@ import java.util.Comparator; -class Issue403 { +public class Issue403 { Comparator COMPARATOR = Comparator.comparing(w -> w.value); String value; diff --git a/checker/tests/nullness/java8/lambda/Issue436.java b/checker/tests/nullness/java8/lambda/Issue436.java index bcc6c6ee05e2..5710d916e0be 100644 --- a/checker/tests/nullness/java8/lambda/Issue436.java +++ b/checker/tests/nullness/java8/lambda/Issue436.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.function.Supplier; -class Issue436 { +public class Issue436 { public void makeALongFormConditionalLambdaReturningGenerics(boolean makeAll) { // TypeArgInferenceUtil.assignedTo used to try to use the method return rather than the // lambda return for those return statements below diff --git a/checker/tests/nullness/java8/lambda/Issue572.java b/checker/tests/nullness/java8/lambda/Issue572.java index 06c91520b014..c2a0bbd40490 100644 --- a/checker/tests/nullness/java8/lambda/Issue572.java +++ b/checker/tests/nullness/java8/lambda/Issue572.java @@ -1,10 +1,11 @@ // Test case for issue #572: https://github.com/typetools/checker-framework/issues/572 -import java.util.function.BiFunction; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -class ParenthesizedLambda { +import java.util.function.BiFunction; + +public class Issue572 { static C biApply(BiFunction f, A a, B b) { return f.apply(a, b); } diff --git a/checker/tests/nullness/java8/lambda/Issue953bLambda.java b/checker/tests/nullness/java8/lambda/Issue953bLambda.java index 1c331c62f66a..50b93240f189 100644 --- a/checker/tests/nullness/java8/lambda/Issue953bLambda.java +++ b/checker/tests/nullness/java8/lambda/Issue953bLambda.java @@ -1,13 +1,14 @@ // Test case for #953 // https://github.com/typetools/checker-framework/issues/953 +import org.checkerframework.checker.nullness.qual.NonNull; + import java.util.ArrayList; import java.util.List; import java.util.function.Function; -import org.checkerframework.checker.nullness.qual.NonNull; @SuppressWarnings("all") -class Issue953bLambda { +public class Issue953bLambda { private static List> strs = new ArrayList<>(); public static List<@NonNull R> mapList( diff --git a/checker/tests/nullness/java8/lambda/LambdaNullness.java b/checker/tests/nullness/java8/lambda/LambdaNullness.java index 96510a7f66dc..74377dbd7195 100644 --- a/checker/tests/nullness/java8/lambda/LambdaNullness.java +++ b/checker/tests/nullness/java8/lambda/LambdaNullness.java @@ -18,7 +18,7 @@ interface BiFunctionNull { R apply(T t, U u); } -class LambdaNullness { +public class LambdaNullness { // Annotations in lamba expressions, in static, instance of fields initializers are stored on // the last declared constructor. diff --git a/checker/tests/nullness/java8/lambda/Parameters.java b/checker/tests/nullness/java8/lambda/Parameters.java index 63d3ae5d9575..bdc6bfca4e76 100644 --- a/checker/tests/nullness/java8/lambda/Parameters.java +++ b/checker/tests/nullness/java8/lambda/Parameters.java @@ -16,38 +16,40 @@ class LambdaParam { // :: error: (lambda.param.type.incompatible) (@NonNull String i) -> {}; NullConsumer fn2 = (@Nullable String i) -> {}; + // :: error: (lambda.param.type.incompatible) NullConsumer fn3 = (String i) -> {}; NNConsumer fn4 = (String i) -> {}; NNConsumer fn5 = (@Nullable String i) -> {}; NNConsumer fn6 = (@NonNull String i) -> {}; - // Initializer blocks with annotations don't work yet because of javac compiler bug. - // https://bugs.openjdk.java.net/browse/JDK-8056970 - // { - // // :: error: (lambda.param.type.incompatible) - // NullConsumer fn1 = (@NonNull String i) -> {}; - // NullConsumer fn2 = (@Nullable String i) -> {}; - // NullConsumer fn3 = (String i) -> {}; - // NNConsumer fn4 = (String i) -> {}; - // NNConsumer fn5 = (@Nullable String i) -> {}; - // NNConsumer fn6 = (@NonNull String i) -> {}; - // } - // - // static { - // // :: error: (lambda.param.type.incompatible) - // NullConsumer fn1 = (@NonNull String i) -> {}; - // NullConsumer fn2 = (@Nullable String i) -> {}; - // NullConsumer fn3 = (String i) -> {}; - // NNConsumer fn4 = (String i) -> {}; - // NNConsumer fn5 = (@Nullable String i) -> {}; - // NNConsumer fn6 = (@NonNull String i) -> {}; - // } + { + // :: error: (lambda.param.type.incompatible) + NullConsumer fn1 = (@NonNull String i) -> {}; + NullConsumer fn2 = (@Nullable String i) -> {}; + // :: error: (lambda.param.type.incompatible) + NullConsumer fn3 = (String i) -> {}; + NNConsumer fn4 = (String i) -> {}; + NNConsumer fn5 = (@Nullable String i) -> {}; + NNConsumer fn6 = (@NonNull String i) -> {}; + } + + static { + // :: error: (lambda.param.type.incompatible) + NullConsumer fn1 = (@NonNull String i) -> {}; + NullConsumer fn2 = (@Nullable String i) -> {}; + // :: error: (lambda.param.type.incompatible) + NullConsumer fn3 = (String i) -> {}; + NNConsumer fn4 = (String i) -> {}; + NNConsumer fn5 = (@Nullable String i) -> {}; + NNConsumer fn6 = (@NonNull String i) -> {}; + } static void foo() { NullConsumer fn1 = // :: error: (lambda.param.type.incompatible) (@NonNull String i) -> {}; NullConsumer fn2 = (@Nullable String i) -> {}; + // :: error: (lambda.param.type.incompatible) NullConsumer fn3 = (String i) -> {}; NNConsumer fn4 = (String i) -> {}; NNConsumer fn5 = (@Nullable String i) -> {}; @@ -59,6 +61,7 @@ void bar() { // :: error: (lambda.param.type.incompatible) (@NonNull String i) -> {}; NullConsumer fn2 = (@Nullable String i) -> {}; + // :: error: (lambda.param.type.incompatible) NullConsumer fn3 = (String i) -> {}; NNConsumer fn4 = (String i) -> {}; NNConsumer fn5 = (@Nullable String i) -> {}; diff --git a/checker/tests/nullness/java8/lambda/ParametersInBody.java b/checker/tests/nullness/java8/lambda/ParametersInBody.java index 722a7c34d81b..234793a2a3a2 100644 --- a/checker/tests/nullness/java8/lambda/ParametersInBody.java +++ b/checker/tests/nullness/java8/lambda/ParametersInBody.java @@ -12,7 +12,7 @@ interface NNConsumerLPB { class LambdaParamBody { - // :: error: (dereference.of.nullable) + // :: error: (lambda.param.type.incompatible) ConsumerLPB fn0 = (String i) -> i.toString(); ConsumerLPB fn2 = (@Nullable String i) -> { @@ -20,8 +20,8 @@ class LambdaParamBody { i.toString(); }; ConsumerLPB fn3 = + // :: error: (lambda.param.type.incompatible) (String i) -> { - // :: error: (dereference.of.nullable) i.toString(); }; ConsumerLPB fn3b = diff --git a/checker/tests/nullness/java8/lambda/ParametersInBodyGenerics.java b/checker/tests/nullness/java8/lambda/ParametersInBodyGenerics.java index 6cd3b3b659b3..c7ae49095901 100644 --- a/checker/tests/nullness/java8/lambda/ParametersInBodyGenerics.java +++ b/checker/tests/nullness/java8/lambda/ParametersInBodyGenerics.java @@ -1,9 +1,10 @@ // Test that parameter annotations are correct in the body of a lambda -import java.util.List; import org.checkerframework.checker.nullness.qual.*; -class ParametersInBodyGenerics { +import java.util.List; + +public class ParametersInBodyGenerics { interface NullableConsumer { void method(List<@Nullable String> s); } diff --git a/checker/tests/nullness/java8/lambda/RefinedLocalInLambda.java b/checker/tests/nullness/java8/lambda/RefinedLocalInLambda.java index 4dec563592ca..f723d6dc61fd 100644 --- a/checker/tests/nullness/java8/lambda/RefinedLocalInLambda.java +++ b/checker/tests/nullness/java8/lambda/RefinedLocalInLambda.java @@ -1,10 +1,11 @@ // Test case for issue #1248: // https://github.com/typetools/checker-framework/issues/1248 -import java.util.function.Predicate; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.function.Predicate; + public class RefinedLocalInLambda { public static void main(String[] args) { diff --git a/checker/tests/nullness/java8/lambda/Returns.java b/checker/tests/nullness/java8/lambda/Returns.java index 39c32358c915..072a36ab0047 100644 --- a/checker/tests/nullness/java8/lambda/Returns.java +++ b/checker/tests/nullness/java8/lambda/Returns.java @@ -24,8 +24,8 @@ class MetaReturn { ConsumerSupplier t1 = () -> (s) -> s.toString(); ConsumerSupplier t2 = () -> { + // :: error: (lambda.param.type.incompatible) return (String s) -> { - // :: error: (dereference.of.nullable) s.toString(); }; }; diff --git a/checker/tests/nullness/java8/lambda/Shadowed.java b/checker/tests/nullness/java8/lambda/Shadowed.java index 4489d820320a..ff706b5ba436 100644 --- a/checker/tests/nullness/java8/lambda/Shadowed.java +++ b/checker/tests/nullness/java8/lambda/Shadowed.java @@ -10,7 +10,7 @@ interface NNConsumerS { void take(String s); } -class Shadowed { +public class Shadowed { ConsumerS c = s -> { diff --git a/checker/tests/nullness/java8/methodref/AssignmentContext.java b/checker/tests/nullness/java8/methodref/AssignmentContext.java deleted file mode 100644 index 250505a0ef37..000000000000 --- a/checker/tests/nullness/java8/methodref/AssignmentContext.java +++ /dev/null @@ -1,43 +0,0 @@ -import org.checkerframework.checker.nullness.qual.*; - -interface FunctionAC { - String apply(String s); -} - -interface FunctionAC2 { - String apply(@Nullable String s); -} - -class AssignmentContext { - // Test assign - FunctionAC f1 = String::toString; - // :: error: (methodref.receiver.invalid) - FunctionAC2 f2 = String::toString; - - // Test casts - Object o1 = (Object) (FunctionAC) String::toString; - // :: error: (methodref.receiver.invalid) - Object o2 = (Object) (FunctionAC2) String::toString; - - void take(FunctionAC f) { - // Test argument assingment - take(String::toString); - } - - void take2(FunctionAC2 f) { - // Test argument assingment - // :: error: (methodref.receiver.invalid) - take2(String::toString); - } - - FunctionAC supply() { - // Test return assingment - return String::toString; - } - - FunctionAC2 supply2() { - // Test return assingment - // :: error: (methodref.receiver.invalid) - return String::toString; - } -} diff --git a/checker/tests/nullness/java8/methodref/AssignmentContextTest.java b/checker/tests/nullness/java8/methodref/AssignmentContextTest.java new file mode 100644 index 000000000000..8c18c2339169 --- /dev/null +++ b/checker/tests/nullness/java8/methodref/AssignmentContextTest.java @@ -0,0 +1,43 @@ +import org.checkerframework.checker.nullness.qual.*; + +interface FunctionAC { + String apply(String s); +} + +interface FunctionAC2 { + String apply(@Nullable String s); +} + +public class AssignmentContextTest { + // Test assign + FunctionAC f1 = String::toString; + // :: error: (methodref.receiver.invalid) + FunctionAC2 f2 = String::toString; + + // Test casts + Object o1 = (Object) (FunctionAC) String::toString; + // :: error: (methodref.receiver.invalid) + Object o2 = (Object) (FunctionAC2) String::toString; + + void take(FunctionAC f) { + // Test argument assingment + take(String::toString); + } + + void take2(FunctionAC2 f) { + // Test argument assingment + // :: error: (methodref.receiver.invalid) + take2(String::toString); + } + + FunctionAC supply() { + // Test return assingment + return String::toString; + } + + FunctionAC2 supply2() { + // Test return assingment + // :: error: (methodref.receiver.invalid) + return String::toString; + } +} diff --git a/checker/tests/nullness/java8/methodref/FromByteCode.java b/checker/tests/nullness/java8/methodref/FromByteCode.java index 1c9a50c85a5a..89cb56114880 100644 --- a/checker/tests/nullness/java8/methodref/FromByteCode.java +++ b/checker/tests/nullness/java8/methodref/FromByteCode.java @@ -4,7 +4,7 @@ interface FunctionBC { R apply(T t); } -class FromByteCode { +public class FromByteCode { FunctionBC f1 = String::toString; diff --git a/checker/tests/nullness/java8/methodref/GroundTargetTypeLub.java b/checker/tests/nullness/java8/methodref/GroundTargetTypeLub.java index acbd4093b111..082321a3dcb8 100644 --- a/checker/tests/nullness/java8/methodref/GroundTargetTypeLub.java +++ b/checker/tests/nullness/java8/methodref/GroundTargetTypeLub.java @@ -14,7 +14,6 @@ class GroundTargetType { return null; } - // :: error: (type.argument.type.incompatible) Supplier fn = GroundTargetType::myMethod; // :: error: (methodref.return.invalid) Supplier fn2 = GroundTargetType::myMethod; diff --git a/checker/tests/nullness/java8/methodref/Postconditions.java b/checker/tests/nullness/java8/methodref/Postconditions.java index 44ffe9069ca2..5dd0d9f02f44 100644 --- a/checker/tests/nullness/java8/methodref/Postconditions.java +++ b/checker/tests/nullness/java8/methodref/Postconditions.java @@ -15,7 +15,7 @@ interface AssertFunc2 { boolean testParam(final @Nullable Object param); } -class AssertionTest { +public class AssertionTest { @EnsuresNonNullIf(result = true, expression = "#1") static boolean override(final @Nullable Object param) { return param != null; diff --git a/checker/tests/nullness/java8/methodref/ReceiversMethodref.java b/checker/tests/nullness/java8/methodref/ReceiversMethodref.java index f76d4f2bde04..153ca3686650 100644 --- a/checker/tests/nullness/java8/methodref/ReceiversMethodref.java +++ b/checker/tests/nullness/java8/methodref/ReceiversMethodref.java @@ -21,8 +21,10 @@ interface Bound { } class MyClass { + // :: error: (nullness.on.receiver) void take(@NonNull MyClass this) {} + // :: error: (nullness.on.receiver) void context1(@Nullable MyClass this, @NonNull MyClass my1, @Nullable MyClass my2) { Unbound1 u1 = MyClass::take; @@ -37,27 +39,32 @@ void context1(@Nullable MyClass this, @NonNull MyClass my1, @Nullable MyClass my Bound b11 = this::take; } + // :: error: (nullness.on.receiver) void context2(@NonNull MyClass this) { Bound b21 = this::take; } class MySubClass extends MyClass { + // :: error: (nullness.on.receiver) void context1(@Nullable MySubClass this) { // :: error: (methodref.receiver.bound.invalid) Bound b = super::take; } + // :: error: (nullness.on.receiver) void context2(@NonNull MySubClass this) { Bound b = super::take; } class Nested { + // :: error: (nullness.on.receiver) void context1(@Nullable Nested this) { // :: error: (methodref.receiver.bound.invalid) Bound b = MySubClass.super::take; } + // :: error: (nullness.on.receiver) void context2(@NonNull Nested this) { Bound b = MySubClass.super::take; } @@ -67,13 +74,16 @@ void context2(@NonNull Nested this) { class Outer { class Inner1 { + // :: error: (nullness.on.receiver) Inner1(@Nullable Outer Outer.this) {} } class Inner2 { + // :: error: (nullness.on.receiver) Inner2(@NonNull Outer Outer.this) {} } + // :: error: (nullness.on.receiver) void context(@Nullable Outer this) { // This one is unbound and needs an Outer as a param Supplier1 f1 = Inner1::new; diff --git a/checker/tests/nullness/java8inference/InLambda.java b/checker/tests/nullness/java8inference/InLambda.java index bc9256f12dc9..e7ff0430fdfc 100644 --- a/checker/tests/nullness/java8inference/InLambda.java +++ b/checker/tests/nullness/java8inference/InLambda.java @@ -1,4 +1,4 @@ -class InLambda { +public class InLambda { static class Mine { @SuppressWarnings("nullness") // just a utility static Mine some() { diff --git a/checker/tests/nullness/java8inference/InLambdaAnnotated.java b/checker/tests/nullness/java8inference/InLambdaAnnotated.java index 42250d6f6a5f..f15a3ca576fa 100644 --- a/checker/tests/nullness/java8inference/InLambdaAnnotated.java +++ b/checker/tests/nullness/java8inference/InLambdaAnnotated.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; -class InLambdaAnnotated { +public class InLambdaAnnotated { static class Mine { @SuppressWarnings("nullness") // just a utility static Mine some() { diff --git a/checker/tests/nullness/java8inference/Inference.java b/checker/tests/nullness/java8inference/Inference.java index eaf27ee77a9e..7152027ce857 100644 --- a/checker/tests/nullness/java8inference/Inference.java +++ b/checker/tests/nullness/java8inference/Inference.java @@ -10,7 +10,7 @@ R collect(MyCollector collector) { interface MyCollector {} -class Inference { +public class Inference { @SuppressWarnings("nullness") static MyCollector> toImmutableStream() { diff --git a/checker/tests/nullness/java8inference/InferenceSimpler.java b/checker/tests/nullness/java8inference/InferenceSimpler.java index f8538d1cdf1b..909e797c7ab1 100644 --- a/checker/tests/nullness/java8inference/InferenceSimpler.java +++ b/checker/tests/nullness/java8inference/InferenceSimpler.java @@ -14,7 +14,7 @@ static List empty() { } } -class InferenceSimpler { +public class InferenceSimpler { List> foo() { return ISOuter.wrap(ISOuter.empty()); } diff --git a/checker/tests/nullness/java8inference/Issue1032.java b/checker/tests/nullness/java8inference/Issue1032.java index 17573cfba8d3..b09a59413fd9 100644 --- a/checker/tests/nullness/java8inference/Issue1032.java +++ b/checker/tests/nullness/java8inference/Issue1032.java @@ -1,10 +1,11 @@ // Test case for issue #1032: // https://github.com/typetools/checker-framework/issues/1032 -import java.util.stream.Stream; import org.checkerframework.checker.nullness.qual.*; -class Issue1032 { +import java.util.stream.Stream; + +public class Issue1032 { @SuppressWarnings("nullness") static @NonNull String castStringToNonNull(@Nullable String arg) { diff --git a/checker/tests/nullness/java8inference/Issue1084.java b/checker/tests/nullness/java8inference/Issue1084.java index 5f8e7f65b223..62dfeedf9704 100644 --- a/checker/tests/nullness/java8inference/Issue1084.java +++ b/checker/tests/nullness/java8inference/Issue1084.java @@ -8,12 +8,13 @@ class MyOpt { throw new RuntimeException(); } + // :: error: (type.argument.type.incompatible) static MyOpt of(S p) { throw new RuntimeException(); } } -class Issue1084 { +public class Issue1084 { MyOpt get() { return this.hashCode() > 0 ? MyOpt.of(5L) : MyOpt.empty(); } diff --git a/checker/tests/nullness/java8inference/Issue1630.java b/checker/tests/nullness/java8inference/Issue1630.java index 838dc7e4c90f..33d5d3191500 100644 --- a/checker/tests/nullness/java8inference/Issue1630.java +++ b/checker/tests/nullness/java8inference/Issue1630.java @@ -1,10 +1,11 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; -import org.checkerframework.checker.nullness.qual.Nullable; -class Issue1630 { +public class Issue1630 { static @Nullable String toString(Object o) { return null; } diff --git a/checker/tests/nullness/java8inference/Issue1818.java b/checker/tests/nullness/java8inference/Issue1818.java index e1dfa5749912..79a6ec85b772 100644 --- a/checker/tests/nullness/java8inference/Issue1818.java +++ b/checker/tests/nullness/java8inference/Issue1818.java @@ -1,8 +1,9 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.List; import java.util.function.Consumer; -import org.checkerframework.checker.nullness.qual.Nullable; -class Issue1818 { +public class Issue1818 { void f() { Consumer> c = values -> values.forEach(value -> g(value)); } diff --git a/checker/tests/nullness/java8inference/Issue1954.java b/checker/tests/nullness/java8inference/Issue1954.java index edc7e8123b56..85129b1e55d3 100644 --- a/checker/tests/nullness/java8inference/Issue1954.java +++ b/checker/tests/nullness/java8inference/Issue1954.java @@ -1,6 +1,7 @@ +import org.checkerframework.checker.nullness.qual.*; + import java.util.function.*; import java.util.stream.*; -import org.checkerframework.checker.nullness.qual.*; // @skip-test public class Issue1954 { diff --git a/checker/tests/nullness/java8inference/Issue2719.java b/checker/tests/nullness/java8inference/Issue2719.java index 1f3555ddcf35..1562e63207d6 100644 --- a/checker/tests/nullness/java8inference/Issue2719.java +++ b/checker/tests/nullness/java8inference/Issue2719.java @@ -1,8 +1,9 @@ import static java.util.Arrays.asList; -import java.util.List; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.List; + public class Issue2719 { public static void main(String[] args) { List iList = asList(0); diff --git a/checker/tests/nullness/java8inference/Issue402.java b/checker/tests/nullness/java8inference/Issue402.java index ae30148d5b5f..63e0e0ef797c 100644 --- a/checker/tests/nullness/java8inference/Issue402.java +++ b/checker/tests/nullness/java8inference/Issue402.java @@ -2,6 +2,7 @@ // https://github.com/typetools/checker-framework/issues/979 import java.util.Comparator; + import javax.annotation.CheckForNull; import javax.annotation.Nullable; diff --git a/checker/tests/nullness/java8inference/Issue4048.java b/checker/tests/nullness/java8inference/Issue4048.java new file mode 100644 index 000000000000..a573de57734f --- /dev/null +++ b/checker/tests/nullness/java8inference/Issue4048.java @@ -0,0 +1,21 @@ +// Test case for issue #4048: https://tinyurl.com/cfissue/4048 + +// @skip-test until the issue is fixed + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.List; + +abstract class Issue4048 { + @Nullable Number m1(List numbers) { + return getOnlyElement1(numbers); + } + + abstract @Nullable T getOnlyElement1(Iterable values); + + @Nullable Number m2(List numbers) { + return getOnlyElement2(numbers); + } + + abstract @Nullable T getOnlyElement2(Iterable values); +} diff --git a/checker/tests/nullness/java8inference/Issue887.java b/checker/tests/nullness/java8inference/Issue887.java index 86783e8170b8..6bd83931ea6c 100644 --- a/checker/tests/nullness/java8inference/Issue887.java +++ b/checker/tests/nullness/java8inference/Issue887.java @@ -2,9 +2,10 @@ // https://github.com/typetools/checker-framework/issues/887 // Additional test case in framework/tests/all-systems/Issue887.java -import java.util.List; import org.checkerframework.checker.nullness.qual.*; +import java.util.List; + public abstract class Issue887 { void test() { // :: error: (argument.type.incompatible) :: error: (type.argument.type.incompatible) diff --git a/checker/tests/nullness/java8inference/Issue953bInference.java b/checker/tests/nullness/java8inference/Issue953bInference.java index f7a153917290..e9081a9b1407 100644 --- a/checker/tests/nullness/java8inference/Issue953bInference.java +++ b/checker/tests/nullness/java8inference/Issue953bInference.java @@ -1,9 +1,10 @@ // Test case that was submitted in Issue 953, but was combined with Issue 979 // https://github.com/typetools/checker-framework/issues/979 +import org.checkerframework.checker.nullness.qual.NonNull; + import java.util.*; import java.util.function.Function; -import org.checkerframework.checker.nullness.qual.NonNull; public class Issue953bInference { private static List> strs = new ArrayList<>(); diff --git a/checker/tests/nullness/java8inference/Issue980.java b/checker/tests/nullness/java8inference/Issue980.java index 498332f184c2..42b8874c2d1c 100644 --- a/checker/tests/nullness/java8inference/Issue980.java +++ b/checker/tests/nullness/java8inference/Issue980.java @@ -1,10 +1,13 @@ // Test case that was submitted in Issue 402, but was combined with Issue 979 // https://github.com/typetools/checker-framework/issues/979 +// @above-java17-jdk-skip-test TODO: reinstate, false positives may be due to issue #979 + +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.checkerframework.checker.nullness.qual.Nullable; public class Issue980 { diff --git a/checker/tests/nullness/java8inference/OneOf.java b/checker/tests/nullness/java8inference/OneOf.java index b6ca22099cdc..7a30da6972b7 100644 --- a/checker/tests/nullness/java8inference/OneOf.java +++ b/checker/tests/nullness/java8inference/OneOf.java @@ -3,8 +3,8 @@ import java.util.List; -@SuppressWarnings("nullness") // don't bother with implementations -class OneOf { +@SuppressWarnings({"initialization", "nullness"}) // don't bother with implementations +public class OneOf { static List alist; static V oneof(V v1, V v2) { diff --git a/checker/tests/nullness/java8inference/SimpleLambda.java b/checker/tests/nullness/java8inference/SimpleLambda.java index ebb05f0a9caf..7bae745c3b1f 100644 --- a/checker/tests/nullness/java8inference/SimpleLambda.java +++ b/checker/tests/nullness/java8inference/SimpleLambda.java @@ -1,9 +1,10 @@ // @skip-test until Issue 979 is fixed. -import java.util.function.Supplier; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.function.Supplier; + public class SimpleLambda { T perform(Supplier p) { return p.get(); diff --git a/checker/tests/nullness/jdkannotations/EisopIssue270.java b/checker/tests/nullness/jdkannotations/EisopIssue270.java new file mode 100644 index 000000000000..b77e1be6c126 --- /dev/null +++ b/checker/tests/nullness/jdkannotations/EisopIssue270.java @@ -0,0 +1,10 @@ +import java.util.Set; + +public class EisopIssue270 { + // In annotated jdk, the package-info of java.util defines KeyForBottom as the + // default qualifier for lower bound. + void foo(Set so, Set seo) { + // No errors if package-info is loaded correctly. + so.retainAll(seo); + } +} diff --git a/checker/tests/nullness/jdkannotations/HashtableTest.java b/checker/tests/nullness/jdkannotations/HashtableTest.java index 30416b563db6..20b58c04e6d0 100644 --- a/checker/tests/nullness/jdkannotations/HashtableTest.java +++ b/checker/tests/nullness/jdkannotations/HashtableTest.java @@ -1,7 +1,8 @@ -import java.util.Hashtable; import org.checkerframework.checker.nullness.qual.Nullable; -class HashtableTest { +import java.util.Hashtable; + +public class HashtableTest { public static void main(String[] args) { diff --git a/checker/tests/nullness/jdkannotations/Issue1142.java b/checker/tests/nullness/jdkannotations/Issue1142.java index fce9a401d142..8051eca322d9 100644 --- a/checker/tests/nullness/jdkannotations/Issue1142.java +++ b/checker/tests/nullness/jdkannotations/Issue1142.java @@ -1,9 +1,10 @@ // Issue 1142 https://github.com/typetools/checker-framework/issues/1142 -import java.util.concurrent.ConcurrentHashMap; import org.checkerframework.checker.nullness.qual.Nullable; -class Issue1142 { +import java.util.concurrent.ConcurrentHashMap; + +public class Issue1142 { void foo() { // :: error: (type.argument.type.incompatible) diff --git a/checker/tests/nullness/jdkannotations/TreeSetTest.java b/checker/tests/nullness/jdkannotations/TreeSetTest.java index 7134c829b381..dbe751224e9e 100644 --- a/checker/tests/nullness/jdkannotations/TreeSetTest.java +++ b/checker/tests/nullness/jdkannotations/TreeSetTest.java @@ -3,10 +3,11 @@ // @skip-test until we fix the issue -import java.util.TreeSet; import org.checkerframework.checker.nullness.qual.Nullable; -class TreeSetTest { +import java.util.TreeSet; + +public class TreeSetTest { public static void main(String[] args) { diff --git a/checker/tests/optional-pure-getters/PureGetterTest.java b/checker/tests/optional-pure-getters/PureGetterTest.java new file mode 100644 index 000000000000..2ca455458064 --- /dev/null +++ b/checker/tests/optional-pure-getters/PureGetterTest.java @@ -0,0 +1,78 @@ +import java.util.Optional; + +class PureGetterTest { + + @SuppressWarnings("optional.field") + Optional field; + + // This method will be treated as @Pure because of -AassumePureGetters. + Optional getOptional() { + return Optional.of("hello"); + } + + Optional otherOptional() { + return Optional.of("hello"); + } + + void sideEffect() {} + + void foo() { + if (field.isPresent()) { + field.get(); + } + if (field.isPresent()) { + sideEffect(); + // :: error: (method.invocation.invalid) + field.get(); + } + if (field.isPresent()) { + getOptional(); + field.get(); + } + if (field.isPresent()) { + otherOptional(); + // :: error: (method.invocation.invalid) + field.get(); + } + + if (getOptional().isPresent()) { + getOptional().get(); + } + if (getOptional().isPresent()) { + sideEffect(); + // :: error: (method.invocation.invalid) + getOptional().get(); + } + if (getOptional().isPresent()) { + getOptional(); + getOptional().get(); + } + if (getOptional().isPresent()) { + otherOptional(); + // :: error: (method.invocation.invalid) + getOptional().get(); + } + + if (otherOptional().isPresent()) { + // BUG: https://github.com/typetools/checker-framework/issues/6291 error: + // (method.invocation.invalid) + otherOptional().get(); + } + if (otherOptional().isPresent()) { + sideEffect(); + // :: error: (method.invocation.invalid) + otherOptional().get(); + } + if (otherOptional().isPresent()) { + getOptional(); + // BUG: https://github.com/typetools/checker-framework/issues/6291 error: + // (method.invocation.invalid) + otherOptional().get(); + } + if (otherOptional().isPresent()) { + otherOptional(); + // :: error: (method.invocation.invalid) + otherOptional().get(); + } + } +} diff --git a/checker/tests/optional/EnsuresPresentIfTest.java b/checker/tests/optional/EnsuresPresentIfTest.java new file mode 100644 index 000000000000..804fa1217921 --- /dev/null +++ b/checker/tests/optional/EnsuresPresentIfTest.java @@ -0,0 +1,86 @@ +import org.checkerframework.checker.optional.qual.EnsuresPresentIf; +import org.checkerframework.checker.optional.qual.Present; +import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.framework.qual.EnsuresQualifierIf; + +import java.util.Optional; + +public class EnsuresPresentIfTest { + + // :: warning: (optional.field) + private Optional optId = Optional.of("abc"); + + @Pure + public Optional getOptId() { + return Optional.of("abc"); + } + + @EnsuresPresentIf(result = true, expression = "getOptId()") + public boolean hasPresentId1() { + return getOptId().isPresent(); + } + + @EnsuresPresentIf(result = true, expression = "this.getOptId()") + public boolean hasPresentId2() { + return getOptId().isPresent(); + } + + @EnsuresQualifierIf(result = true, expression = "getOptId()", qualifier = Present.class) + public boolean hasPresentId3() { + return getOptId().isPresent(); + } + + @EnsuresQualifierIf(result = true, expression = "this.getOptId()", qualifier = Present.class) + public boolean hasPresentId4() { + return getOptId().isPresent(); + } + + @EnsuresPresentIf(result = true, expression = "optId") + public boolean hasPresentId5() { + return optId.isPresent(); + } + + @EnsuresPresentIf(result = true, expression = "this.optId") + public boolean hasPresentId6() { + return optId.isPresent(); + } + + @EnsuresQualifierIf(result = true, expression = "optId", qualifier = Present.class) + public boolean hasPresentId7() { + return optId.isPresent(); + } + + @EnsuresQualifierIf(result = true, expression = "this.optId", qualifier = Present.class) + public boolean hasPresentId8() { + return optId.isPresent(); + } + + void client() { + if (hasPresentId1()) { + getOptId().get(); + } + if (hasPresentId2()) { + getOptId().get(); + } + if (hasPresentId3()) { + getOptId().get(); + } + if (hasPresentId4()) { + getOptId().get(); + } + if (hasPresentId5()) { + optId.get(); + } + if (hasPresentId6()) { + optId.get(); + } + if (hasPresentId7()) { + optId.get(); + } + if (hasPresentId8()) { + optId.get(); + } + // :: error: (method.invocation.invalid) + optId.get(); + } +} diff --git a/checker/tests/optional/FilterIspresentMapGetTest.java b/checker/tests/optional/FilterIspresentMapGetTest.java new file mode 100644 index 000000000000..7dfaab9b1852 --- /dev/null +++ b/checker/tests/optional/FilterIspresentMapGetTest.java @@ -0,0 +1,9 @@ +import java.util.Optional; +import java.util.stream.Stream; + +class FilterIspresentMapGetTest { + + void m(Stream> ss) { + ss.filter(Optional::isPresent).map(Optional::get); + } +} diff --git a/checker/tests/optional/IfPresentRefinement.java b/checker/tests/optional/IfPresentRefinement.java new file mode 100644 index 000000000000..d0a62644219a --- /dev/null +++ b/checker/tests/optional/IfPresentRefinement.java @@ -0,0 +1,20 @@ +// @below-java9-jdk-skip-test + +import java.util.Optional; + +@SuppressWarnings("optional.parameter") +public class IfPresentRefinement { + + void m1(Optional o) { + o.ifPresent(s -> o.get()); + } + + void m2(Optional o) { + o.ifPresentOrElse(s -> o.get(), () -> {}); + } + + void m3(Optional o) { + // :: error: (method.invocation.invalid) + o.ifPresentOrElse(s -> o.get(), () -> o.get()); + } +} diff --git a/checker/tests/optional/JdkCheck.java b/checker/tests/optional/JdkCheck.java index aff34b11077a..14dcf8e3b72c 100644 --- a/checker/tests/optional/JdkCheck.java +++ b/checker/tests/optional/JdkCheck.java @@ -1,8 +1,9 @@ -import java.util.Optional; -import java.util.function.Supplier; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.optional.qual.Present; +import java.util.Optional; +import java.util.function.Supplier; + /** Test JDK annotations. */ @SuppressWarnings("optional.parameter") public class JdkCheck { @@ -21,10 +22,14 @@ String orElseThrowTest1( } String orElseThrowTest2(Optional mos, Supplier exceptionSupplier) { - // :: error: (method.invocation.invalid) return mos.orElseThrow(exceptionSupplier); } + String orElseThrowTestFlow(Optional mos, Supplier exceptionSupplier) { + mos.orElseThrow(exceptionSupplier); + return mos.get(); + } + String getTest1(@Present Optional pos) { return pos.get(); } diff --git a/checker/tests/optional/JdkCheck11.java b/checker/tests/optional/JdkCheck11.java new file mode 100644 index 000000000000..94fd70dd9db3 --- /dev/null +++ b/checker/tests/optional/JdkCheck11.java @@ -0,0 +1,32 @@ +// @below-java11-jdk-skip-test + +import org.checkerframework.checker.optional.qual.Present; + +import java.util.Optional; + +/** Test JDK annotations, for methods added after JDK 8. */ +@SuppressWarnings("optional.parameter") +public class JdkCheck11 { + + String isEmptyTest1(Optional pos, String fallback) { + if (pos.isEmpty()) { + return fallback; + } + return pos.get(); + } + + String orElseThrowTest1(@Present Optional pos) { + return pos.orElseThrow(); + } + + String orElseThrowTest2(Optional mos) { + // :: error: (method.invocation.invalid) + return mos.orElseThrow(); + } + + String orElseThrowTestFlow(Optional mos) { + // :: error: (method.invocation.invalid) + mos.orElseThrow(); + return mos.get(); + } +} diff --git a/checker/tests/optional/MapNoNewNull.java b/checker/tests/optional/MapNoNewNull.java new file mode 100644 index 000000000000..8552b38b6d42 --- /dev/null +++ b/checker/tests/optional/MapNoNewNull.java @@ -0,0 +1,17 @@ +import java.math.BigInteger; +import java.util.Optional; + +class MapNoNewNull { + + @SuppressWarnings("optional.parameter") + void m(Optional digitsAnnotation) { + if (digitsAnnotation.isPresent()) { + BigInteger maxValue = + digitsAnnotation.map(Digits::integer).map(BigInteger::valueOf).get(); + } + } +} + +@interface Digits { + public int integer(); +} diff --git a/checker/tests/optional/Marks1Partial.java b/checker/tests/optional/Marks1Partial.java new file mode 100644 index 000000000000..8b48b8d0282a --- /dev/null +++ b/checker/tests/optional/Marks1Partial.java @@ -0,0 +1,78 @@ +import java.util.Optional; + +/** + * Test case for rule #1: "Never, ever, use null for an Optional variable or return value." + * + *

    Warnings for assignment of null values to Optional types are handled by the Nullness Checker. + * This is a partial test suite for testing whether the Optional Checker detects comparisons of + * Optional values to null literals. + */ +public class Marks1Partial { + + @SuppressWarnings("optional.field") + Optional optField = Optional.ofNullable("f1"); + + @SuppressWarnings("optional.parameter") + void simpleEqualsCheck(Optional o1) { + // :: warning: (optional.null.comparison) + if (o1 != null) { + System.out.println("Don't compare optionals (lhs) to null literals."); + } + // :: warning: (optional.null.comparison) + if (null != o1) { + System.out.println("Don't compare optionals (rhs) to null literals."); + } + // :: warning: (optional.null.comparison) + if (o1 == null) { + System.out.println("Don't compare optionals (lhs) to null literals."); + } + // :: warning: (optional.null.comparison) + if (null == o1) { + System.out.println("Don't compare optionals (rhs) to null literals."); + } + } + + @SuppressWarnings("optional.parameter") + void moreComplexEqualsChecks(Optional o1) { + // :: warning: (optional.null.comparison) + if (o1 != null || 1 + 2 == 4) { + System.out.println("Don't compare optionals (lhs) to null literals."); + } + } + + @SuppressWarnings("optional.parameter") + void checkAgainstOptionalField() { + // :: warning: (optional.null.comparison) + if (this.getOptField() != null || 1 + 2 == 4) { + System.out.println("Don't compare optionals (lhs) to null literals."); + } + } + + public Optional getOptField() { + return optField; + } + + public void assignOptField() { + // :: warning: (optional.null.assignment) + optField = null; + } + + public void assignOptionalDeclaration() { + // :: warning: (optional.null.assignment) + Optional os1 = null; + Optional os2; + if (Math.random() > 0.5) { + os2 = Optional.of("hello"); + } else { + // :: warning: (optional.null.assignment) + os2 = null; + } + // :: warning: (optional.null.assignment) + Optional os3 = Math.random() > 0.5 ? Optional.of("hello") : null; + } + + public Optional returnNullOptional() { + // :: warning: (optional.null.assignment) + return (null); + } +} diff --git a/checker/tests/optional/Marks3a.java b/checker/tests/optional/Marks3a.java index e5a7c89b2999..293b3a8e5dfe 100644 --- a/checker/tests/optional/Marks3a.java +++ b/checker/tests/optional/Marks3a.java @@ -23,6 +23,13 @@ String customerNameByID_acceptable(List custList, int custID) { return opt.isPresent() ? opt.get().getName() : "UNKNOWN"; } + String customerNameByID_acceptable2(List custList, int custID) { + Optional opt = custList.stream().filter(c -> c.getID() == custID).findFirst(); + + // :: warning: (prefer.map.and.orelse) + return !opt.isPresent() ? "UNKNOWN" : opt.get().getName(); + } + String customerNameByID_better(List custList, int custID) { Optional opt = custList.stream().filter(c -> c.getID() == custID).findFirst(); diff --git a/checker/tests/optional/Marks3aJdk11.java b/checker/tests/optional/Marks3aJdk11.java new file mode 100644 index 000000000000..1114afdd945d --- /dev/null +++ b/checker/tests/optional/Marks3aJdk11.java @@ -0,0 +1,27 @@ +// @below-java11-jdk-skip-test + +import java.util.List; +import java.util.Optional; + +/** + * Test case for rule #3: "Prefer alternative APIs over Optional.isPresent() and Optional.get()." + */ +public class Marks3aJdk11 { + + class Customer { + int getID() { + return 42; + } + + String getName() { + return "Fozzy Bear"; + } + } + + String customerNameByID_acceptable3(List custList, int custID) { + Optional opt = custList.stream().filter(c -> c.getID() == custID).findFirst(); + + // :: warning: (prefer.map.and.orelse) + return opt.isEmpty() ? "UNKNOWN" : opt.get().getName(); + } +} diff --git a/checker/tests/optional/Marks3bJdk11.java b/checker/tests/optional/Marks3bJdk11.java new file mode 100644 index 000000000000..0e9209390eec --- /dev/null +++ b/checker/tests/optional/Marks3bJdk11.java @@ -0,0 +1,33 @@ +// @below-java11-jdk-skip-test + +import java.util.Optional; + +/** + * Test case for rule #3: "Prefer alternative APIs over Optional.isPresent() and Optional.get()." + */ +@SuppressWarnings("optional.parameter") +public class Marks3bJdk11 { + + class Task {} + + class Executor { + void runTask(Task t) {} + } + + Executor executor = new Executor(); + + void bad2(Optional oTask) { + // :: warning: (prefer.ifpresent) + if (!oTask.isEmpty()) { + executor.runTask(oTask.get()); + } + } + + void bad3(Optional oTask) { + // :: warning: (prefer.ifpresent) + if (oTask.isEmpty()) { + } else { + executor.runTask(oTask.get()); + } + } +} diff --git a/checker/tests/optional/Marks4.java b/checker/tests/optional/Marks4.java index 5d1567fe6467..9f93aa02ab9d 100644 --- a/checker/tests/optional/Marks4.java +++ b/checker/tests/optional/Marks4.java @@ -1,3 +1,4 @@ +import java.util.Objects; import java.util.Optional; /** @@ -15,7 +16,45 @@ String process_bad(String s) { return Optional.ofNullable(s).orElseGet(this::getDefault); } + String process_bad2(String s) { + // :: warning: (introduce.eliminate) + return Optional.empty().orElseGet(this::getDefault); + } + + String process_bad3(String s) { + // :: warning: (introduce.eliminate) + return Optional.of(s).orElseGet(this::getDefault); + } + String process_good(String s) { return (s != null) ? s : getDefault(); } + + String m1(String s) { + // :: warning: (introduce.eliminate) + return Optional.ofNullable(s).orElseGet(this::getDefault) + "hello"; + } + + boolean m2(String s) { + // :: warning: (introduce.eliminate) + return Objects.equals("hello", Optional.ofNullable(s).orElseGet(this::getDefault)); + } + + boolean m3(String s) { + // :: warning: (introduce.eliminate) + return "hello" == Optional.ofNullable(s).orElseGet(this::getDefault); + } + + String m4(String s) { + // :: warning: (introduce.eliminate) + return Optional.ofNullable(s).map(Object::toString).orElseGet(this::getDefault); + } + + String m5(String s) { + return Optional.ofNullable(s) + .map(Object::toString) + .map(Object::toString) + // :: warning: (introduce.eliminate) + .orElseGet(this::getDefault); + } } diff --git a/checker/tests/optional/Marks5.java b/checker/tests/optional/Marks5.java index a1b70902eaf8..54dbc0fb2caa 100644 --- a/checker/tests/optional/Marks5.java +++ b/checker/tests/optional/Marks5.java @@ -1,7 +1,8 @@ +import org.checkerframework.checker.optional.qual.Present; + import java.math.BigDecimal; import java.util.Optional; import java.util.stream.Stream; -import org.checkerframework.checker.optional.qual.Present; /** * Test case for rule #5: "If an Optional chain has a nested Optional chain, or has an intermediate @@ -33,6 +34,8 @@ Optional clever2(Optional first, Optional se return result; } + // The use of `map(Optional::of)` creates Optional, so a warning should be issued + // there. Optional moreClever(Optional first, Optional second) { Optional result = first.map(b -> second.map(b::add).orElse(b)).map(Optional::of).orElse(second); diff --git a/checker/tests/optional/Marks7.java b/checker/tests/optional/Marks7.java index 996648113bed..0161389f6eac 100644 --- a/checker/tests/optional/Marks7.java +++ b/checker/tests/optional/Marks7.java @@ -11,9 +11,9 @@ public class Marks7 { void illegalInstantiations() { - // :: error: (optional.collection) + // :: warning: (optional.collection) Optional> ols = Optional.of(new ArrayList()); - // :: error: (optional.collection) + // :: warning: (optional.collection) Optional> oss = Optional.of(new HashSet()); } } diff --git a/checker/tests/optional/NestedOptionalTest.java b/checker/tests/optional/NestedOptionalTest.java new file mode 100644 index 000000000000..4ce96e2994c6 --- /dev/null +++ b/checker/tests/optional/NestedOptionalTest.java @@ -0,0 +1,33 @@ +// @below-java10-jdk-skip-test + +import java.util.Collections; +import java.util.Optional; + +class NestedOptional { + + Object field; + + @SuppressWarnings("optional.parameter") + // :: warning: (optional.nesting) + Optional bar(Optional> optOptStr) { + if (optOptStr.isPresent()) { + return optOptStr.get(); + } + return Optional.empty(); + } + + void foo() { + // Explicitly providing a type annotation triggers the error + // :: warning: (optional.nesting) + var x = Optional.of(Optional.of("foo")); // I expect an error here. + + // :: warning: (optional.nesting) + bar(Optional.of(Optional.of("bar"))); + + // :: warning: (optional.nesting) + field = Optional.of(Optional.of("baz")); + + // :: warning: (optional.collection) + field = Optional.of(Collections.singleton("baz")); + } +} diff --git a/checker/tests/optional/OptionalBoxed.java b/checker/tests/optional/OptionalBoxed.java new file mode 100644 index 000000000000..c13430d402f0 --- /dev/null +++ b/checker/tests/optional/OptionalBoxed.java @@ -0,0 +1,24 @@ +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; + +class OptionalBoxed { + + // :: warning: (optional.field) + OptionalDouble aField; + + // :: warning: (optional.parameter) + void m(OptionalInt aParam) { + // :: warning: (introduce.eliminate) + int x = OptionalLong.of(1L).hashCode(); + // :: warning: (introduce.eliminate) + long y = OptionalLong.of(1L).orElse(2L); + // :: warning: (introduce.eliminate) + boolean b = Optional.empty().isPresent(); + // :: warning: (introduce.eliminate) + OptionalDouble.empty().ifPresent(d -> {}); + // :: warning: (introduce.eliminate) + boolean b4 = OptionalLong.empty().isPresent(); + } +} diff --git a/checker/tests/optional/OptionalMapMethodReference.java b/checker/tests/optional/OptionalMapMethodReference.java new file mode 100644 index 000000000000..f3f9d47026dc --- /dev/null +++ b/checker/tests/optional/OptionalMapMethodReference.java @@ -0,0 +1,35 @@ +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.PolyNull; +import org.checkerframework.checker.optional.qual.Present; + +import java.util.Optional; + +public class OptionalMapMethodReference { + Optional getString() { + return Optional.of(""); + } + + @Present Optional method() { + Optional o = getString(); + @Present Optional oInt; + if (o.isPresent()) { + // :: error: (assignment.type.incompatible) + oInt = o.map(this::convertNull); + oInt = o.map(this::convertPoly); + return o.map(this::convert); + } + return Optional.of(0); + } + + @Nullable Integer convertNull(String s) { + return null; + } + + @PolyNull Integer convertPoly(@PolyNull String s) { + return null; + } + + Integer convert(String s) { + return 0; + } +} diff --git a/checker/tests/optional/OptionalParameterTest.java b/checker/tests/optional/OptionalParameterTest.java new file mode 100644 index 000000000000..d1f200e1bc51 --- /dev/null +++ b/checker/tests/optional/OptionalParameterTest.java @@ -0,0 +1,13 @@ +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +class OptionalParameterTest { + public void findDatesByIds2(List ids) { + ids.stream() + .map(Optional::ofNullable) + .flatMap(optional -> optional.map(Stream::of).orElseGet(Stream::empty)) + .collect(Collectors.toList()); + } +} diff --git a/checker/tests/optional/RequiresPresentTest.java b/checker/tests/optional/RequiresPresentTest.java new file mode 100644 index 000000000000..966ae641c5bf --- /dev/null +++ b/checker/tests/optional/RequiresPresentTest.java @@ -0,0 +1,73 @@ +import org.checkerframework.checker.optional.qual.*; + +import java.util.Optional; + +public class RequiresPresentTest { + + // :: warning: (optional.field) + Optional field1 = Optional.of("abc"); + // :: warning: (optional.field) + Optional field2 = Optional.empty(); + + @RequiresPresent("field1") + void method1() { + field1.get().length(); // OK, field1 is known to be present (non-empty) + this.field1.get().length(); // OK, field1 is known to be present (non-empty) + // :: error: (method.invocation.invalid) + field2.get().length(); // error, might throw NoSuchElementException + } + + @RequiresPresent("field1") + void method2() { + // OK, an indirect call to method1. + method1(); + } + + void method3() { + field1 = Optional.of("abc"); + method1(); // OK, satisfied method precondition. + field1 = Optional.empty(); + // :: error: (contracts.precondition.not.satisfied) + method1(); // error, does not satisfy method precondition. + } + + // :: warning: (optional.field) + protected Optional field; + + @RequiresPresent("field") + public void requiresPresentField() {} + + public void clientFail(RequiresPresentTest arg1) { + // :: error: (contracts.precondition.not.satisfied) + arg1.requiresPresentField(); + } + + public void clientOK(RequiresPresentTest arg2) { + arg2.field = Optional.of("def"); + + // this is legal. + @Present Optional optField = arg2.field; + + // OK, field is known to be present. + arg2.requiresPresentField(); + } + + @RequiresPresent({"field1", "field2"}) + void method4() { + field1.get().length(); // OK, field1 is known to be present (non-empty) + this.field1.get().length(); // OK, field1 is known to be present (non-empty) + + field2.get().length(); // OK, field2 is known to be preent (non-empty) + this.field2.get().length(); // OK, field2 is known to be present (non-empty) + } + + void method5() { + field1 = Optional.of("abc"); + field2 = Optional.of("def"); + method4(); // OK, both preconditions now hold at this point. + + field1 = Optional.empty(); + // :: error: (contracts.precondition.not.satisfied) + method4(); // error, field1 is no longer present. + } +} diff --git a/checker/tests/optional/SubtypeCheck.java b/checker/tests/optional/SubtypeCheck.java index c3d43bf46e6f..1d4d926a2d2c 100644 --- a/checker/tests/optional/SubtypeCheck.java +++ b/checker/tests/optional/SubtypeCheck.java @@ -1,14 +1,28 @@ import org.checkerframework.checker.optional.qual.MaybePresent; +import org.checkerframework.checker.optional.qual.OptionalBottom; import org.checkerframework.checker.optional.qual.Present; +import java.util.Optional; + /** Basic test of subtyping. */ public class SubtypeCheck { - void foo(@MaybePresent int mp, @Present int p) { - @MaybePresent int mp2 = mp; - @MaybePresent int mp3 = p; + @SuppressWarnings("optional.parameter") + void foo( + @MaybePresent Optional mp, + @Present Optional p, + @OptionalBottom Optional ob) { + @MaybePresent Optional mp2 = mp; + @MaybePresent Optional mp3 = p; + @MaybePresent Optional mp4 = ob; + // :: error: assignment.type.incompatible + @Present Optional p2 = mp; + @Present Optional p3 = p; + @Present Optional p4 = ob; + // :: error: assignment.type.incompatible + @OptionalBottom Optional ob2 = mp; // :: error: assignment.type.incompatible - @Present int p2 = mp; - @Present int p3 = p; + @OptionalBottom Optional ob3 = p; + @OptionalBottom Optional ob4 = ob; } } diff --git a/checker/tests/optional/java17/OptionalSwitch.java b/checker/tests/optional/java17/OptionalSwitch.java new file mode 100644 index 000000000000..656c9a01fcc9 --- /dev/null +++ b/checker/tests/optional/java17/OptionalSwitch.java @@ -0,0 +1,11 @@ +// @below-java17-jdk-skip-test +public class OptionalSwitch { + public static boolean flag; + + public Object test(int c) { + return switch (c) { + case 3 -> flag ? "" : "obj.getBaseStat();"; + default -> null; + }; + } +} diff --git a/checker/tests/regex/AllowedTypes.java b/checker/tests/regex/AllowedTypes.java new file mode 100644 index 000000000000..28860cbc0622 --- /dev/null +++ b/checker/tests/regex/AllowedTypes.java @@ -0,0 +1,61 @@ +import org.checkerframework.checker.regex.qual.Regex; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.MatchResult; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.swing.text.Segment; + +public class AllowedTypes { + @Regex CharSequence cs; + @Regex String s11; + @Regex StringBuilder sb; + @Regex Segment s21; + @Regex char c; + @Regex Pattern p; + @Regex Matcher m; + @Regex Character c2; + @Regex Object o; + + abstract static class MyMatchResult implements MatchResult {} + + @Regex MyMatchResult mp; + + // :: error: (anno.on.irrelevant) + @Regex List l; + // :: error: (anno.on.irrelevant) + ArrayList<@Regex Double> al; + // :: error: (anno.on.irrelevant) + @Regex int i; + // :: error: (anno.on.irrelevant) + @Regex boolean b; + // :: error: (anno.on.irrelevant) + @Regex Integer i2; + + void testAllowedTypes() { + @Regex CharSequence cs; + @Regex String s11; + @Regex StringBuilder sb; + @Regex Segment s21; + @Regex char c; + @Regex Object o; + + // :: error: (anno.on.irrelevant) + @Regex List l; // error + // :: error: (anno.on.irrelevant) + ArrayList<@Regex Double> al; // error + // :: error: (anno.on.irrelevant) + @Regex int i; // error + // :: error: (anno.on.irrelevant) + @Regex boolean b; // error + + @Regex String regex = "a"; + // :: error: (compound.assignment.type.incompatible) + regex += "("; + + String nonRegex = "a"; + nonRegex += "("; + } +} diff --git a/checker/tests/regex/AnnotatedTypeParams3.java b/checker/tests/regex/AnnotatedTypeParams3.java index 1a81942ae934..cde1ef5ec9d2 100644 --- a/checker/tests/regex/AnnotatedTypeParams3.java +++ b/checker/tests/regex/AnnotatedTypeParams3.java @@ -1,8 +1,9 @@ +import org.checkerframework.checker.regex.qual.Regex; + import java.lang.annotation.Annotation; import java.lang.reflect.*; -import org.checkerframework.checker.regex.qual.Regex; -class AnnotatedTypeParams3 { +public class AnnotatedTypeParams3 { private T safeGetAnnotation(Field f, Class annotationClass) { T annotation; try { diff --git a/checker/tests/regex/Annotation.java b/checker/tests/regex/Annotation.java index 1f0aca553516..a0b60517db97 100644 --- a/checker/tests/regex/Annotation.java +++ b/checker/tests/regex/Annotation.java @@ -6,7 +6,7 @@ String[] value(); } -class Annotation { +public class Annotation { @A1({"a", "b"}) void m1() {} diff --git a/checker/tests/regex/Constructors.java b/checker/tests/regex/Constructors.java new file mode 100644 index 000000000000..2a7ff6c30d1b --- /dev/null +++ b/checker/tests/regex/Constructors.java @@ -0,0 +1,27 @@ +import org.checkerframework.checker.regex.qual.Regex; + +public class Constructors { + public Constructors(Constructors con) {} + + public Constructors(@Regex String s, int i) {} + + public class MyConstructors extends Constructors { + public MyConstructors(@Regex String s) { + super(s, 0); + } + } + + public void testAnonymousConstructor(String s) { + + Constructors m = new Constructors(null); + + // :: error: (argument.type.incompatible) + new MyConstructors(s); + // :: error: (argument.type.incompatible) + new MyConstructors(s) {}; + // :: error: (argument.type.incompatible) + m.new MyConstructors(s); + // :: error: (argument.type.incompatible) + m.new MyConstructors(s) {}; + } +} diff --git a/checker/tests/regex/Continue.java b/checker/tests/regex/Continue.java index db9201036a0c..ecca0b446143 100644 --- a/checker/tests/regex/Continue.java +++ b/checker/tests/regex/Continue.java @@ -1,7 +1,8 @@ +import org.checkerframework.checker.regex.util.RegexUtil; + import java.util.regex.Pattern; -import org.checkerframework.checker.regex.RegexUtil; -class Continue { +public class Continue { void test1(String[] a) { for (String s : a) { diff --git a/checker/tests/regex/ForEach.java b/checker/tests/regex/ForEach.java index cd023eab8276..e7c90c7873c2 100644 --- a/checker/tests/regex/ForEach.java +++ b/checker/tests/regex/ForEach.java @@ -1,4 +1,4 @@ -class ForEach { +public class ForEach { T iterate(T[] constants) { for (T constant : constants) { return constant; diff --git a/checker/tests/regex/GenericsBoundsRange.java b/checker/tests/regex/GenericsBoundsRange.java index af044be70fbf..88df34b1c88a 100644 --- a/checker/tests/regex/GenericsBoundsRange.java +++ b/checker/tests/regex/GenericsBoundsRange.java @@ -1,8 +1,9 @@ package regex; +import org.checkerframework.checker.regex.qual.Regex; + import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.checkerframework.checker.regex.qual.Regex; /** Designed to test whether or not a bounds range of generics actually works. */ public class GenericsBoundsRange<@Regex(3) T extends @Regex(1) String> { @@ -32,6 +33,6 @@ public GenericsBoundsRange(T t) { // Bounds used to not actually be bounds but instead exactly the lower bound // so line below would fail because the argument could only be Regex(0). So this - // tests BaseTypeValidator.checkTypeArguments range checking + // tests BaseTypeValidator.checkTypeArguments range checking. public void method(GenericsBoundsRange<@Regex(2) String> gbr) {} } diff --git a/checker/tests/regex/GroupCounts.java b/checker/tests/regex/GroupCounts.java index 2c62cf41a1d7..c610fdf74caf 100644 --- a/checker/tests/regex/GroupCounts.java +++ b/checker/tests/regex/GroupCounts.java @@ -1,7 +1,8 @@ +import org.checkerframework.checker.regex.qual.Regex; +import org.checkerframework.checker.regex.util.RegexUtil; + import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.checkerframework.checker.regex.RegexUtil; -import org.checkerframework.checker.regex.qual.Regex; public class GroupCounts { void testGroupCount() { @@ -28,18 +29,25 @@ void testGroupCount() { void testPatternCompileGroupCount(@Regex String r, @Regex(3) String r3, @Regex(5) String r5) { @Regex(5) Pattern p1 = Pattern.compile(r5); + @Regex(5) Pattern p1a = Pattern.compile(r5, 0); @Regex Pattern p2 = Pattern.compile(r5); @Regex Pattern p3 = Pattern.compile(r); // :: error: (assignment.type.incompatible) @Regex(6) Pattern p4 = Pattern.compile(r5); // error // :: error: (assignment.type.incompatible) + @Regex(6) Pattern p4a = Pattern.compile(r5, 0); // error + // :: error: (assignment.type.incompatible) @Regex(6) Pattern p5 = Pattern.compile(r3); // error + // :: error: (assignment.type.incompatible) + @Regex(6) Pattern p5a = Pattern.compile(r3, 0); // error // Make sure Pattern.compile still works when passed an @UnknownRegex String // that's actually a regex, with the warning suppressed. @SuppressWarnings("regex:argument.type.incompatible") Pattern p6 = Pattern.compile("(" + r + ")"); + @SuppressWarnings("regex:argument.type.incompatible") + Pattern p6a = Pattern.compile("(" + r + ")", 0); } void testConcatenationGroupCount(@Regex String r, @Regex(3) String r3, @Regex(5) String r5) { diff --git a/checker/tests/regex/IntCast.java b/checker/tests/regex/IntCast.java new file mode 100644 index 000000000000..0973b19a2f4e --- /dev/null +++ b/checker/tests/regex/IntCast.java @@ -0,0 +1,6 @@ +public class IntCast { + + int m() { + return (int) '\n'; + } +} diff --git a/checker/tests/regex/InvariantTypes.java b/checker/tests/regex/InvariantTypes.java index 87517eaf8441..97356307a97c 100644 --- a/checker/tests/regex/InvariantTypes.java +++ b/checker/tests/regex/InvariantTypes.java @@ -1,10 +1,9 @@ +import org.checkerframework.checker.regex.qual.Regex; + import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map; -import org.checkerframework.checker.regex.qual.*; -import org.checkerframework.framework.type.AnnotatedTypeMirror; public class InvariantTypes { String[] sa = {"a"}; @@ -105,14 +104,6 @@ String join(final String delimiter, final Collection objs) { String s1 = join(" ", Arrays.asList("1", "2", "3")); String s2 = "xxx" + join(" ", Arrays.asList("1", "2", "3")); - V mapGetHelper( - Map mappings) { - return null; - } - - Map mappings; - AnnotatedTypeMirror found = mapGetHelper(mappings); - class TV { List> emptylist = Collections.emptyList(); } diff --git a/checker/tests/regex/InvariantTypesAtm.java b/checker/tests/regex/InvariantTypesAtm.java new file mode 100644 index 000000000000..b5492eb40635 --- /dev/null +++ b/checker/tests/regex/InvariantTypesAtm.java @@ -0,0 +1,14 @@ +import org.checkerframework.framework.type.AnnotatedTypeMirror; + +import java.util.Map; + +public class InvariantTypesAtm { + + V mapGetHelper( + Map mappings) { + return null; + } + + Map mappings; + AnnotatedTypeMirror found = mapGetHelper(mappings); +} diff --git a/checker/tests/regex/Issue3267.java b/checker/tests/regex/Issue3267.java index cb754a5fec3a..91f866aa11f0 100644 --- a/checker/tests/regex/Issue3267.java +++ b/checker/tests/regex/Issue3267.java @@ -1,10 +1,11 @@ // Test case for issue #3267: // https://github.com/typetools/checker-framework/issues/3267 +import org.checkerframework.checker.regex.util.RegexUtil; + import java.util.regex.Pattern; -import org.checkerframework.checker.regex.RegexUtil; -class Issue3267 { +public class Issue3267 { void foo(String s) { if (RegexUtil.isRegex(s)) { } else { diff --git a/checker/tests/regex/Issue3281.java b/checker/tests/regex/Issue3281.java new file mode 100644 index 000000000000..95feaa90bfb6 --- /dev/null +++ b/checker/tests/regex/Issue3281.java @@ -0,0 +1,81 @@ +// Test case for Issue 3281: +// https://github.com/typetools/checker-framework/issues/3281 + +import org.checkerframework.checker.regex.qual.Regex; +import org.checkerframework.checker.regex.util.RegexUtil; + +import java.util.regex.Pattern; + +public class Issue3281 { + + @Regex String f = null; + + public boolean b = false; + + void m1(String s) { + if (true) { + // :: error: (argument.type.incompatible) + Pattern.compile(s); + } + } + + void m2(String s) { + RegexUtil.isRegex(s); + if (true) { + // :: error: (argument.type.incompatible) + Pattern.compile(s); + } + } + + void m2f(String s) { + RegexUtil.isRegex(s); + if (true) { + // :: error: (assignment.type.incompatible) + f = s; + } + } + + void m3(String s) { + if (RegexUtil.isRegex(s)) { + Pattern.compile(s); + } + } + + void m4(String s, String s2) { + RegexUtil.isRegex(s); + if (RegexUtil.isRegex(s2)) { + // :: error: (argument.type.incompatible) + Pattern.compile(s); + } + } + + void m4f(String s, String s2) { + RegexUtil.isRegex(s); + if (RegexUtil.isRegex(s2)) { + // :: error: (assignment.type.incompatible) + f = s; + } + } + + void m5f(String s, String s2) { + RegexUtil.isRegex(s); + if (b) { + // :: error: (assignment.type.incompatible) + f = s; + } + } + + void foo(String s1, String s2) { + bar( + RegexUtil.isRegex(s1), + // :: error: (argument.type.incompatible) + Pattern.compile(s1)); + boolean b; + bar( + b = RegexUtil.isRegex(s2), + // :: error: (argument.type.incompatible) + Pattern.compile(s2)); + } + + void bar(boolean b, Object o) {} +} diff --git a/checker/tests/regex/Issue809.java b/checker/tests/regex/Issue809.java index 7b2321ae1704..6ce471420fae 100644 --- a/checker/tests/regex/Issue809.java +++ b/checker/tests/regex/Issue809.java @@ -1,7 +1,7 @@ // Test case for issue #809: // https://github.com/typetools/checker-framework/issues/809 -class Issue809> { +public class Issue809> { K[] array; int index = 0; diff --git a/checker/tests/regex/MatcherGroupCount.java b/checker/tests/regex/MatcherGroupCount.java index c5ecb64e606d..11cabfe28db5 100644 --- a/checker/tests/regex/MatcherGroupCount.java +++ b/checker/tests/regex/MatcherGroupCount.java @@ -1,8 +1,9 @@ // Test case for Issue 291 // https://github.com/typetools/checker-framework/issues/291 +import org.checkerframework.checker.regex.util.RegexUtil; + import java.util.regex.*; -import org.checkerframework.checker.regex.RegexUtil; public class MatcherGroupCount { public static void main(String[] args) { diff --git a/checker/tests/regex/Nested.java b/checker/tests/regex/Nested.java new file mode 100644 index 000000000000..add8452bc3f6 --- /dev/null +++ b/checker/tests/regex/Nested.java @@ -0,0 +1,17 @@ +import org.checkerframework.checker.regex.qual.Regex; + +// TODO: @Regex is not allowed on arbitrary types. Find a better test case. +public class Nested { + + // :: error: (anno.on.irrelevant) + OuterI.@Regex InnerA fa = new OuterI.@Regex InnerA() {}; + + // :: error: (anno.on.irrelevant) + OuterI.@Regex InnerB fb = new OuterI.@Regex InnerB() {}; +} + +class OuterI { + static class InnerA {} + + static class InnerB {} +} diff --git a/checker/tests/regex/RawTypeTest.java b/checker/tests/regex/RawTypeTest.java index 8f628cb73636..37ed0cccc730 100644 --- a/checker/tests/regex/RawTypeTest.java +++ b/checker/tests/regex/RawTypeTest.java @@ -1,10 +1,11 @@ +import org.checkerframework.checker.regex.qual.*; + import java.lang.ref.WeakReference; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.List; -import org.checkerframework.checker.regex.qual.*; -class RawTypeTest { +public class RawTypeTest { public void m1(Class c) { Class x = c.asSubclass(I2.class); @@ -53,6 +54,7 @@ public void m3(Class c) { m2(c); } + @SuppressWarnings("removal") // AccessController is deprecated for removal in Java 17 public void m4() { AccessController.doPrivileged( new PrivilegedAction() { diff --git a/checker/tests/regex/RegexUtilClient.java b/checker/tests/regex/RegexUtilClient.java new file mode 100644 index 000000000000..9ed183bb1e4c --- /dev/null +++ b/checker/tests/regex/RegexUtilClient.java @@ -0,0 +1,97 @@ +import org.checkerframework.checker.regex.qual.Regex; +import org.checkerframework.framework.qual.EnsuresQualifierIf; + +public class RegexUtilClient { + void fullyQualifiedRegexUtil(String s) { + if (org.checkerframework.checker.regex.util.RegexUtil.isRegex(s, 2)) { + @Regex(2) String s2 = s; + } + @Regex(2) String s2 = org.checkerframework.checker.regex.util.RegexUtil.asRegex(s, 2); + } + + void unqualifiedRegexUtil(String s) { + if (RegexUtil.isRegex(s, 2)) { + @Regex(2) String s2 = s; + } + @Regex(2) String s2 = RegexUtil.asRegex(s, 2); + } + + void fullyQualifiedRegexUtilNoParamsArg(String s) { + if (org.checkerframework.checker.regex.util.RegexUtil.isRegex(s)) { + @Regex String s2 = s; + @Regex(0) String s3 = s; + } + @Regex String s2 = org.checkerframework.checker.regex.util.RegexUtil.asRegex(s); + @Regex(0) String s3 = org.checkerframework.checker.regex.util.RegexUtil.asRegex(s); + } + + void unqualifiedRegexUtilNoParamsArg(String s) { + if (RegexUtil.isRegex(s)) { + @Regex String s2 = s; + @Regex(0) String s3 = s; + } + @Regex String s2 = RegexUtil.asRegex(s, 2); + @Regex(0) String s3 = RegexUtil.asRegex(s, 2); + } + + void illegalName(String s) { + if (IllegalName.isRegex(s, 2)) { + // :: error: (assignment.type.incompatible) + @Regex(2) String s2 = s; + } + // :: error: (assignment.type.incompatible) + @Regex(2) String s2 = IllegalName.asRegex(s, 2); + } + + void illegalNameRegexUtil(String s) { + if (IllegalNameRegexUtil.isRegex(s, 2)) { + // :: error: (assignment.type.incompatible) + @Regex(2) String s2 = s; + } + // :: error: (assignment.type.incompatible) + @Regex(2) String s2 = IllegalNameRegexUtil.asRegex(s, 2); + } +} + +// A dummy RegexUtil class to make sure RegexUtil in no package works. +class RegexUtil { + @EnsuresQualifierIf(result = true, expression = "#1", qualifier = Regex.class) + public static boolean isRegex(final String s, int n) { + return false; + } + + public static @Regex String asRegex(String s, int n) { + return null; + } + + @EnsuresQualifierIf(result = true, expression = "#1", qualifier = Regex.class) + public static boolean isRegex(final String s) { + return false; + } + + public static @Regex String asRegex(String s) { + return null; + } +} + +// These methods shouldn't work. +class IllegalName { + public static boolean isRegex(String s, int n) { + return false; + } + + public static @Regex String asRegex(String s, int n) { + return null; + } +} + +// These methods shouldn't work. +class IllegalNameRegexUtil { + public static boolean isRegex(String s, int n) { + return false; + } + + public static @Regex String asRegex(String s, int n) { + return null; + } +} diff --git a/checker/tests/regex/RegexUtilTest.java b/checker/tests/regex/RegexUtilTest.java deleted file mode 100644 index 7d24089baf4f..000000000000 --- a/checker/tests/regex/RegexUtilTest.java +++ /dev/null @@ -1,97 +0,0 @@ -import org.checkerframework.checker.regex.qual.Regex; -import org.checkerframework.framework.qual.EnsuresQualifierIf; - -public class RegexUtilTest { - void fullyQualifiedRegexUtil(String s) { - if (org.checkerframework.checker.regex.RegexUtil.isRegex(s, 2)) { - @Regex(2) String s2 = s; - } - @Regex(2) String s2 = org.checkerframework.checker.regex.RegexUtil.asRegex(s, 2); - } - - void unqualifiedRegexUtil(String s) { - if (RegexUtil.isRegex(s, 2)) { - @Regex(2) String s2 = s; - } - @Regex(2) String s2 = RegexUtil.asRegex(s, 2); - } - - void fullyQualifiedRegexUtilNoParamsArg(String s) { - if (org.checkerframework.checker.regex.RegexUtil.isRegex(s)) { - @Regex String s2 = s; - @Regex(0) String s3 = s; - } - @Regex String s2 = org.checkerframework.checker.regex.RegexUtil.asRegex(s); - @Regex(0) String s3 = org.checkerframework.checker.regex.RegexUtil.asRegex(s); - } - - void unqualifiedRegexUtilNoParamsArg(String s) { - if (RegexUtil.isRegex(s)) { - @Regex String s2 = s; - @Regex(0) String s3 = s; - } - @Regex String s2 = RegexUtil.asRegex(s, 2); - @Regex(0) String s3 = RegexUtil.asRegex(s, 2); - } - - void illegalName(String s) { - if (IllegalName.isRegex(s, 2)) { - // :: error: (assignment.type.incompatible) - @Regex(2) String s2 = s; - } - // :: error: (assignment.type.incompatible) - @Regex(2) String s2 = IllegalName.asRegex(s, 2); - } - - void illegalNameRegexUtil(String s) { - if (IllegalNameRegexUtil.isRegex(s, 2)) { - // :: error: (assignment.type.incompatible) - @Regex(2) String s2 = s; - } - // :: error: (assignment.type.incompatible) - @Regex(2) String s2 = IllegalNameRegexUtil.asRegex(s, 2); - } -} - -// A dummy RegexUtil class to make sure RegexUtil in no package works. -class RegexUtil { - @EnsuresQualifierIf(result = true, expression = "#1", qualifier = Regex.class) - public static boolean isRegex(final String s, int n) { - return false; - } - - public static @Regex String asRegex(String s, int n) { - return null; - } - - @EnsuresQualifierIf(result = true, expression = "#1", qualifier = Regex.class) - public static boolean isRegex(final String s) { - return false; - } - - public static @Regex String asRegex(String s) { - return null; - } -} - -// These methods shouldn't work. -class IllegalName { - public static boolean isRegex(String s, int n) { - return false; - } - - public static @Regex String asRegex(String s, int n) { - return null; - } -} - -// These methods shouldn't work. -class IllegalNameRegexUtil { - public static boolean isRegex(String s, int n) { - return false; - } - - public static @Regex String asRegex(String s, int n) { - return null; - } -} diff --git a/checker/tests/regex/SimpleRegex.java b/checker/tests/regex/SimpleRegex.java index f480da1018c5..989e2b970936 100644 --- a/checker/tests/regex/SimpleRegex.java +++ b/checker/tests/regex/SimpleRegex.java @@ -1,6 +1,7 @@ -import java.util.regex.Pattern; import org.checkerframework.checker.regex.qual.Regex; +import java.util.regex.Pattern; + public class SimpleRegex { void regString() { @@ -89,56 +90,6 @@ void testCharConcatenation() { @Regex String s7 = 'r' + "ege("; // error } - // TODO: Uncomment this once isValidUse works better. See RegexChecker.isValidUse for details. - // class TestAllowedTypes { - // @Regex CharSequence cs; - // @Regex String s11; - // @Regex StringBuilder sb; - // @Regex Segment s21; - // @Regex char c; - // @Regex Pattern p; - // @Regex Matcher m; - // - // // :: error: (type.invalid) - // @Regex Object o; // error - // // :: error: (type.invalid) - // @Regex List l; // error - // // :: error: (type.invalid) - // ArrayList<@Regex Double> al; // error - // // :: error: (type.invalid) - // @Regex int i; // error - // // :: error: (type.invalid) - // @Regex boolean b; // error - // } - - // TODO: This is not supported until the checker supports getting explicit - // annotations from local variables (instead of just fields.) - // void testAllowedTypes() { - // @Regex CharSequence cs; - // @Regex String s11; - // @Regex StringBuilder sb; - // @Regex Segment s21; - // @Regex char c; - // - // // :: error: (type.invalid) - // @Regex Object o; // error - // // :: error: (type.invalid) - // @Regex List l; // error - // // :: error: (type.invalid) - // ArrayList<@Regex Double> al; // error - // // :: error: (type.invalid) - // @Regex int i; // error - // // :: error: (type.invalid) - // @Regex boolean b; // error - // - // @Regex String regex = "a"; - // // :: error: (argument.type.incompatible) - // regex += "("; - // - // String nonRegex = "a"; - // nonRegex += "("; - // } - void testPatternLiteral() { Pattern.compile("non(", Pattern.LITERAL); Pattern.compile(foo("regex"), Pattern.LITERAL); diff --git a/checker/tests/regex/TestIsRegex.java b/checker/tests/regex/TestIsRegex.java index 457bb2df25a3..71b27d195f7a 100644 --- a/checker/tests/regex/TestIsRegex.java +++ b/checker/tests/regex/TestIsRegex.java @@ -1,9 +1,10 @@ +import org.checkerframework.checker.regex.qual.*; +import org.checkerframework.checker.regex.util.RegexUtil; + import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.checkerframework.checker.regex.RegexUtil; -import org.checkerframework.checker.regex.qual.*; -class TestIsRegex { +public class TestIsRegex { void test1(String str1) throws Exception { if (!RegexUtil.isRegex(str1)) { throw new Exception(); diff --git a/checker/tests/regex/TestRegex.java b/checker/tests/regex/TestRegex.java index 0d8712877891..f932b12e7a7e 100644 --- a/checker/tests/regex/TestRegex.java +++ b/checker/tests/regex/TestRegex.java @@ -13,7 +13,7 @@ public void Concatenation2() { // test-case for issue 148 class Search { public static void main(String[] args) { - if (!org.checkerframework.checker.regex.RegexUtil.isRegex(args[0], 4)) { + if (!org.checkerframework.checker.regex.util.RegexUtil.isRegex(args[0], 4)) { return; } @Regex(4) String regex = args[0]; diff --git a/checker/tests/regex/TypeParamSubtype.java b/checker/tests/regex/TypeParamSubtype.java index 35ab3af6b2ed..372ba987a9be 100644 --- a/checker/tests/regex/TypeParamSubtype.java +++ b/checker/tests/regex/TypeParamSubtype.java @@ -1,7 +1,8 @@ -import java.util.Collection; import org.checkerframework.checker.regex.qual.Regex; -class TypeParamSubtype { +import java.util.Collection; + +public class TypeParamSubtype { // These are legal because null has type @Regex String // void nullRegexSubtype(Collection col) { // // :: error: (argument.type.incompatible) diff --git a/checker/tests/regex/WildcardInvoke.java b/checker/tests/regex/WildcardInvoke.java index 4c4cde756a95..de0135bb944a 100644 --- a/checker/tests/regex/WildcardInvoke.java +++ b/checker/tests/regex/WildcardInvoke.java @@ -1,4 +1,4 @@ -class WildcardInvoke { +public class WildcardInvoke { class Demo { void call(T p) {} } diff --git a/checker/tests/regex_poly/StringBuilderToStringPolyRegex.java b/checker/tests/regex_poly/StringBuilderToStringPolyRegex.java new file mode 100644 index 000000000000..82b3636b09c7 --- /dev/null +++ b/checker/tests/regex_poly/StringBuilderToStringPolyRegex.java @@ -0,0 +1,13 @@ +// Test case for issue #58: https://tinyurl.com/cfissue/58 +// This does not test what #58 wanted. See +// https://github.com/eisop/checker-framework/issues/242 +// for follow-up discussions. + +import org.checkerframework.checker.regex.qual.Regex; + +class StringBuilderToStringPolyRegex { + + void createPattern(final @Regex(1) StringBuilder regex) { + @Regex(1) String s = regex.toString(); + } +} diff --git a/checker/tests/resourceleak-customignoredexceptions/BasicTest.java b/checker/tests/resourceleak-customignoredexceptions/BasicTest.java new file mode 100644 index 000000000000..a9d7987c3184 --- /dev/null +++ b/checker/tests/resourceleak-customignoredexceptions/BasicTest.java @@ -0,0 +1,60 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +abstract class BasicTest { + + abstract Closeable alloc(); + + abstract void method(); + + public void runtimeExceptionManuallyThrown() throws IOException { + // this code is obviously wrong + // ::error: (required.method.not.called) + Closeable r = alloc(); + if (true) { + throw new RuntimeException(); + } + r.close(); + } + + public void runtimeExceptionFromMethod() throws IOException { + // method() may throw RuntimeException, so this code is not OK + // ::error: (required.method.not.called) + Closeable r = alloc(); + method(); + r.close(); + } + + // Note that even just constructing an instance of NullPointerException can throw all kinds + // of exceptions: ClassCircularityError, OutOfMemoryError, etc. Even RuntimeException is + // possible in theory. So, to really test what we're trying to test, we have to isolate + // the construction of the exception out here. + static final NullPointerException NPE = new NullPointerException(); + + public void ignoreNPE() throws IOException { + // this code is obviously wrong, but it is allowed because our ignored exceptions list + // includes NullPointerException + Closeable r = alloc(); + if (true) { + throw NPE; + } + r.close(); + } + + static class CustomNPESubtype extends NullPointerException { + static final CustomNPESubtype INSTANCE = new CustomNPESubtype(); + } + + public void doNotIgnoreNPESubtype() throws IOException { + // Only NullPointerException should be ignored, not its subtypes, since the options + // specified "=java.lang.NullPointerException". + // ::error: (required.method.not.called) + Closeable r = alloc(); + if (true) { + throw CustomNPESubtype.INSTANCE; + } + r.close(); + } +} diff --git a/checker/tests/resourceleak-extraignoredexceptions/BasicTest.java b/checker/tests/resourceleak-extraignoredexceptions/BasicTest.java new file mode 100644 index 000000000000..5d1942c30d2b --- /dev/null +++ b/checker/tests/resourceleak-extraignoredexceptions/BasicTest.java @@ -0,0 +1,37 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +abstract class BasicTest { + + abstract Closeable alloc(); + + abstract void method(); + + public void runtimeExceptionManuallyThrown() throws IOException { + // this code is obviously wrong, but RuntimeException is ignored by default + Closeable r = alloc(); + if (true) { + throw new RuntimeException(); + } + r.close(); + } + + public void runtimeExceptionFromMethod() throws IOException { + // method() may throw RuntimeException, but RuntimeException is ignored by default + Closeable r = alloc(); + method(); + r.close(); + } + + public void ignoreIllegalStateException() throws IOException { + // this code is obviously wrong, but it is allowed because our ignored exceptions list + // includes IllegalStateException + Closeable r = alloc(); + if (true) { + throw new IllegalStateException(); + } + r.close(); + } +} diff --git a/checker/tests/resourceleak-nocreatesmustcallfor/ConnectingServerSockets.java b/checker/tests/resourceleak-nocreatesmustcallfor/ConnectingServerSockets.java new file mode 100644 index 000000000000..76de8c024ae6 --- /dev/null +++ b/checker/tests/resourceleak-nocreatesmustcallfor/ConnectingServerSockets.java @@ -0,0 +1,51 @@ +// a set of test cases that demonstrate that errors are actually insued in appropriate +// places when ServerSockets are connected + +// This version of the test expects that the obligation creation support (i.e. the +// @CreatesMustCallFor annotation +// and its accompanying logic) has been disabled. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.net.*; + +class ConnectingServerSockets { + + static void simple_ss_test(SocketAddress sa) throws Exception { + // :: error: (required.method.not.called) + ServerSocket s = new ServerSocket(); + s.bind(sa); + } + + static void simple_ss_test2(SocketAddress sa) throws Exception { + // :: error: (required.method.not.called) + ServerSocket s = new ServerSocket(); + // s.bind(sa); + } + + static void simple_ss_test4(SocketAddress sa, int to) throws Exception { + // :: error: (required.method.not.called) + ServerSocket s = new ServerSocket(); + s.bind(sa, to); + } + + static @MustCall({}) ServerSocket makeUnconnected() throws Exception { + // :: error: (return.type.incompatible) + return new ServerSocket(); + } + + static void simple_ss_test5(SocketAddress sa) throws Exception { + ServerSocket s = makeUnconnected(); + s.bind(sa); + } + + static void simple_ss_test6(SocketAddress sa) throws Exception { + ServerSocket s = makeUnconnected(); + // s.bind(sa); + } + + static void simple_ss_test8(SocketAddress sa, int to) throws Exception { + ServerSocket s = makeUnconnected(); + s.bind(sa, to); + } +} diff --git a/checker/tests/resourceleak-nocreatesmustcallfor/ConnectingSockets.java b/checker/tests/resourceleak-nocreatesmustcallfor/ConnectingSockets.java new file mode 100644 index 000000000000..d57057a1c73c --- /dev/null +++ b/checker/tests/resourceleak-nocreatesmustcallfor/ConnectingSockets.java @@ -0,0 +1,58 @@ +// a set of test cases that demonstrate that errors are actually issued in appropriate +// places when Sockets are connected + +import org.checkerframework.checker.mustcall.qual.*; + +import java.net.*; + +class ConnectingSockets { + + static void simple_ns_test(SocketAddress sa) throws Exception { + // :: error: (required.method.not.called) + Socket s = new Socket(); + s.bind(sa); + } + + static void simple_ns_test2(SocketAddress sa) throws Exception { + // :: error: (required.method.not.called) + Socket s = new Socket(); + // s.bind(sa); + } + + static void simple_ns_test3(SocketAddress sa) throws Exception { + // :: error: (required.method.not.called) + Socket s = new Socket(); + s.connect(sa); + } + + static void simple_ns_test4(SocketAddress sa, int to) throws Exception { + // :: error: (required.method.not.called) + Socket s = new Socket(); + s.connect(sa, to); + } + + static @MustCall({}) Socket makeUnconnected() throws Exception { + // :: error: (return.type.incompatible) + return new Socket(); + } + + static void simple_ns_test5(SocketAddress sa) throws Exception { + Socket s = makeUnconnected(); + s.bind(sa); + } + + static void simple_ns_test6(SocketAddress sa) throws Exception { + Socket s = makeUnconnected(); + // s.bind(sa); + } + + static void simple_ns_test7(SocketAddress sa) throws Exception { + Socket s = makeUnconnected(); + s.connect(sa); + } + + static void simple_ns_test8(SocketAddress sa, int to) throws Exception { + Socket s = makeUnconnected(); + s.connect(sa, to); + } +} diff --git a/checker/tests/resourceleak-nocreatesmustcallfor/CreatesMustCallForSimpler.java b/checker/tests/resourceleak-nocreatesmustcallfor/CreatesMustCallForSimpler.java new file mode 100644 index 000000000000..cdb72d010ca8 --- /dev/null +++ b/checker/tests/resourceleak-nocreatesmustcallfor/CreatesMustCallForSimpler.java @@ -0,0 +1,31 @@ +// A simpler test that @CreatesMustCallFor works as intended wrt the Object Construction Checker. + +// This test has been modified to expect that CreatesMustCallFor is feature-flagged to off. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall("a") +class CreatesMustCallForSimpler { + + @CreatesMustCallFor + void reset() {} + + @CreatesMustCallFor("this") + void resetThis() {} + + void a() {} + + static @MustCall({}) CreatesMustCallForSimpler makeNoMC() { + // :: error: (return.type.incompatible) + return new CreatesMustCallForSimpler(); + } + + static void test1() { + CreatesMustCallForSimpler cos = makeNoMC(); + @MustCall({}) CreatesMustCallForSimpler a = cos; + cos.reset(); + @CalledMethods({"reset"}) CreatesMustCallForSimpler b = cos; + @CalledMethods({}) CreatesMustCallForSimpler c = cos; + } +} diff --git a/checker/tests/resourceleak-nocreatesmustcallfor/DifferentSWKeys.java b/checker/tests/resourceleak-nocreatesmustcallfor/DifferentSWKeys.java new file mode 100644 index 000000000000..8b0a5b0edded --- /dev/null +++ b/checker/tests/resourceleak-nocreatesmustcallfor/DifferentSWKeys.java @@ -0,0 +1,26 @@ +// A test case that the RLC's -AnoCreatesMustCallFor argument doesn't change +// warning suppression behavior for the Must Call Checker. + +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.Owning; + +@SuppressWarnings("required.method.not.called") +class DifferentSWKeys { + void test(@Owning @MustCall("foo") Object obj) { + // :: warning: unneeded.suppression + @SuppressWarnings("mustcall") + @MustCall("foo") Object bar = obj; + } + + void test2(@Owning @MustCall("foo") Object obj) { + // actually needed suppression + @SuppressWarnings("mustcall") + @MustCall({}) Object bar = obj; + } + + void test3(@Owning @MustCall("foo") Object obj) { + // test that the option-specific suppression key works + @SuppressWarnings("mustcallnocreatesmustcallfor") + @MustCall({}) Object bar = obj; + } +} diff --git a/checker/tests/resourceleak-nocreatesmustcallfor/SocketContainer.java b/checker/tests/resourceleak-nocreatesmustcallfor/SocketContainer.java new file mode 100644 index 000000000000..5d2e4c3858b8 --- /dev/null +++ b/checker/tests/resourceleak-nocreatesmustcallfor/SocketContainer.java @@ -0,0 +1,35 @@ +// A simple class that has a Socket as an owning field. +// This is a modified version of tests/socket/SocketContainer.java +// for checking that without CO support we can't assign to non-final owning fields at all. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.net.*; + +@InheritableMustCall("close") +class SocketContainer { + @Owning Socket sock; + + public SocketContainer(String host, int port) throws Exception { + // Assignments to owning fields should not be permitted. + // :: error: required.method.not.called + sock = new Socket(host, port); + } + + // No missing create obligation error is issued, since CO is disabled... + public void reassign(String host, int port) throws Exception { + sock.close(); + // For the RHS, because the field can't take ownership + // :: error: required.method.not.called + Socket sr = new Socket(host, port); + // No warning for overwriting the field, since it can't take ownership! + sock = sr; + } + + @EnsuresCalledMethods(value = "this.sock", methods = "close") + public void close() throws IOException { + sock.close(); + } +} diff --git a/checker/tests/resourceleak-nolightweightownership/ACOwning.java b/checker/tests/resourceleak-nolightweightownership/ACOwning.java new file mode 100644 index 000000000000..ddbbbe611033 --- /dev/null +++ b/checker/tests/resourceleak-nolightweightownership/ACOwning.java @@ -0,0 +1,60 @@ +// This copy of the ACOwning.java test from the mustcall tests +// has expected errors as if ownership transfer can only happen based +// on defaults - that is, that @Owning and @NotOwning annotations are +// ignored. + +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +class ACOwning { + + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + Foo makeFoo() { + return new Foo(); + } + + static void takeOwnership(@Owning Foo foo) { + foo.a(); + } + + static void noOwnership(Foo foo) {} + + static void takeOwnershipWrong(@Owning Foo foo) {} + + static @NotOwning Foo getNonOwningFoo() { + return new Foo(); + } + + static void callGetNonOwningFoo() { + // :: error: (required.method.not.called) + getNonOwningFoo(); + } + + static void ownershipInCallee() { + // :: error: (required.method.not.called) + Foo f = new Foo(); + takeOwnership(f); + // :: error: (required.method.not.called) + Foo g = new Foo(); + noOwnership(g); + } + + @Owning + public Foo owningAtReturn() { + return new Foo(); + } + + void owningAtReturnTest() { + // :: error: (required.method.not.called) + Foo f = owningAtReturn(); + } + + void ownershipTest() { + // :: error: (required.method.not.called) + takeOwnership(new Foo()); + } +} diff --git a/checker/tests/resourceleak-noresourcealiases/MustCallAliasExamples.java b/checker/tests/resourceleak-noresourcealiases/MustCallAliasExamples.java new file mode 100644 index 000000000000..946bd3e826a1 --- /dev/null +++ b/checker/tests/resourceleak-noresourcealiases/MustCallAliasExamples.java @@ -0,0 +1,63 @@ +// Simple tests of @MustCallAlias functionality on wrapper streams. +// This version has been modified to expect that @MustCallAlias annotations +// are always ignored, as if running with -AnoResourceAliases. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.io.IOException; +import java.net.*; + +class MustCallAliasExamples { + + void test_two_locals(String address) { + Socket socket = null; + try { + socket = new Socket(address, 8000); + // :: error: required.method.not.called + DataInputStream d = new DataInputStream(socket.getInputStream()); + } catch (IOException e) { + + } finally { + closeSocket(socket); + } + } + + // :: error: required.method.not.called + void test_close_wrapper(@Owning InputStream b) throws IOException { + DataInputStream d = new DataInputStream(b); + d.close(); + } + + void test_close_nonwrapper(@Owning InputStream b) throws IOException { + // :: error: required.method.not.called + DataInputStream d = new DataInputStream(b); + b.close(); + } + + // :: error: required.method.not.called + void test_no_close(@Owning InputStream b) { + // :: error: required.method.not.called + DataInputStream d = new DataInputStream(b); + } + + // :: error: required.method.not.called + void test_no_assign(@Owning InputStream b) { + // :: error: required.method.not.called + new DataInputStream( + // :: error: required.method.not.called + new BufferedInputStream(b)); + } + + @EnsuresCalledMethods(value = "#1", methods = "close") + void closeSocket(Socket sock) { + try { + if (sock != null) { + sock.close(); + } + } catch (IOException e) { + + } + } +} diff --git a/checker/tests/resourceleak-noresourcealiases/MustCallAliasPassthroughLocal.java b/checker/tests/resourceleak-noresourcealiases/MustCallAliasPassthroughLocal.java new file mode 100644 index 000000000000..12ec04fe6394 --- /dev/null +++ b/checker/tests/resourceleak-noresourcealiases/MustCallAliasPassthroughLocal.java @@ -0,0 +1,26 @@ +// A test that passing a local to an MCA super constructor is allowed. +// This version has been modified to expect errors, as if running under +// -AnoResourceAliases - so @MustCallAlias annotations are ignored. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class MustCallAliasPassthroughLocal extends FilterInputStream { + MustCallAliasPassthroughLocal(File f) throws Exception { + // This is safe - this MCA constructor of FilterInputStream means that the result of this + // constructor - i.e. the caller - is taking ownership of this newly-created output stream. + // :: error: required.method.not.called + super(new FileInputStream(f)); + } + + static void test(File f) throws Exception { + // :: error: required.method.not.called + new MustCallAliasPassthroughLocal(f); + } + + static void test_ok(File f) throws Exception { + new MustCallAliasPassthroughLocal(f).close(); + } +} diff --git a/checker/tests/resourceleak-permitinitializationleak/InstanceInitializer.java b/checker/tests/resourceleak-permitinitializationleak/InstanceInitializer.java new file mode 100644 index 000000000000..17c268604a2c --- /dev/null +++ b/checker/tests/resourceleak-permitinitializationleak/InstanceInitializer.java @@ -0,0 +1,54 @@ +// A test that the checker is sound in the presence of instance initializer blocks. +// In the resourceleak-permitinitializationleak/ directory, it's a test that the +// checker is unsound with the -ApermitInitializationLeak command-line argument. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.net.Socket; + +class InstanceInitializer { + // :: error: required.method.not.called + private @Owning Socket s; + + private final int DEFAULT_PORT = 5; + private final String DEFAULT_ADDR = "localhost"; + + { + try { + // This assignment is OK, because it's the first assignment. + s = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } catch (Exception e) { + } + } + + { + try { + // This assignment is not OK, because it's a reassignment without satisfying the + // mustcall obligations of the previous value of `s`. + // With -ApermitInitializationLeak, the Resource Leak Checker unsoundly permits it. + s = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } catch (Exception e) { + } + } + + { + try { + // :: error: required.method.not.called + Socket s1 = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } catch (Exception e) { + } + } + + { + Socket s1 = null; + try { + s1 = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } catch (Exception e) { + } + s1.close(); + } + + public InstanceInitializer() throws Exception { + s = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } +} diff --git a/checker/tests/resourceleak-permitinitializationleak/SocketContainer.java b/checker/tests/resourceleak-permitinitializationleak/SocketContainer.java new file mode 100644 index 000000000000..96e6c96effc4 --- /dev/null +++ b/checker/tests/resourceleak-permitinitializationleak/SocketContainer.java @@ -0,0 +1,30 @@ +// A simple class that has a Socket as an owning field. +// This test exists to check that we gracefully handle assignments 1) +// in the constructor and 2) to null. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.net.*; + +@InheritableMustCall("close") +class SocketContainer { + @Owning Socket sock; + + public SocketContainer(String host, int port) throws Exception { + sock = new Socket(host, port); + try { + sock = new Socket(host, port); + } catch (Exception ignored) { + } + } + + @EnsuresCalledMethods(value = "this.sock", methods = "close") + public void close() throws IOException { + sock.close(); + // It's okay to assign a field to null after its obligations have been fulfilled, + // without inducing a reset. + sock = null; + } +} diff --git a/checker/tests/resourceleak-permitstaticowning/StaticOwningField.java b/checker/tests/resourceleak-permitstaticowning/StaticOwningField.java new file mode 100644 index 000000000000..9016c5d1ebec --- /dev/null +++ b/checker/tests/resourceleak-permitstaticowning/StaticOwningField.java @@ -0,0 +1,64 @@ +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.Owning; + +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintStream; + +@InheritableMustCall("close") +class StaticOwningField implements Closeable { + + // Instance field + + private @Owning @MustCall("close") PrintStream ps_instance; + + @CreatesMustCallFor("this") + void m_instance() throws IOException { + ps_instance.close(); + ps_instance = new PrintStream("filename.txt"); + } + + @EnsuresCalledMethods(value = "ps_instance", methods = "close") + @Override + public void close() { + ps_instance.close(); + } + + // Static field + + private static @Owning @MustCall("close") PrintStream ps_static; + + static void m_static() throws IOException { + ps_static.close(); + ps_static = new PrintStream("filename.txt"); + } + + private static @Owning @MustCall("close") PrintStream ps_static_initialized1 = + newPrintStreamWithoutExceptions(); + + private static @Owning @MustCall("close") PrintStream ps_static_initialized2; + + static { + ps_static_initialized2 = newPrintStreamWithoutExceptions(); + } + + private static final @Owning @MustCall("close") PrintStream ps_static_final_initialized1 = + newPrintStreamWithoutExceptions(); + + private static final @Owning @MustCall("close") PrintStream ps_static_final_initialized2; + + static { + ps_static_final_initialized2 = newPrintStreamWithoutExceptions(); + } + + public static PrintStream newPrintStreamWithoutExceptions() { + try { + return new PrintStream("filename.txt"); + } catch (Exception e) { + throw new Error(e); + } + } +} diff --git a/checker/tests/resourceleak-permitstaticowning/StaticOwningFieldOtherClass.java b/checker/tests/resourceleak-permitstaticowning/StaticOwningFieldOtherClass.java new file mode 100644 index 000000000000..6bddb48357c2 --- /dev/null +++ b/checker/tests/resourceleak-permitstaticowning/StaticOwningFieldOtherClass.java @@ -0,0 +1,21 @@ +import org.checkerframework.checker.mustcall.qual.Owning; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.UncheckedIOException; + +public class StaticOwningFieldOtherClass {} + +abstract class HasStaticOwningField { + public static @Owning FileWriter log = null; +} + +class TestUtils { + public static void setLog(String filename) { + try { + HasStaticOwningField.log = new FileWriter(filename); + } catch (IOException ioe) { + throw new UncheckedIOException("Cannot write file " + filename, ioe); + } + } +} diff --git a/checker/tests/resourceleak/ACExceptionalExitPointTest.java b/checker/tests/resourceleak/ACExceptionalExitPointTest.java new file mode 100644 index 000000000000..d547df215834 --- /dev/null +++ b/checker/tests/resourceleak/ACExceptionalExitPointTest.java @@ -0,0 +1,39 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +class ACExceptionalExitPointTest { + + @InheritableMustCall("a") + class Foo { + void a() {} + + @This Foo b() { + return this; + } + + void c() {} + } + + Foo makeFoo() { + return new Foo(); + } + + @CalledMethods({"a"}) Foo makeFoo2() { + Foo f = new Foo(); + f.a(); + return f; + } + + void exceptionalExitWrong() throws Exception { + // :: error: required.method.not.called + Foo fw = makeFoo(); + throw new Exception(); + } + + void exceptionalExitCorrect() throws Exception { + Foo fw = new Foo(); + fw.a(); + throw new Exception(); + } +} diff --git a/checker/tests/resourceleak/ACMethodInvocationTest.java b/checker/tests/resourceleak/ACMethodInvocationTest.java new file mode 100644 index 000000000000..5d1850e24d7e --- /dev/null +++ b/checker/tests/resourceleak/ACMethodInvocationTest.java @@ -0,0 +1,98 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +class ACMethodInvocationTest { + + @InheritableMustCall("a") + class Foo { + void a() {} + + @This Foo b() { + return this; + } + + void c() {} + } + + @Owning + Foo makeFoo() { + return new Foo(); + } + + @CalledMethods({"a"}) Foo makeFooFinalize() { + Foo f = new Foo(); + f.a(); + return f; + } + + @Owning + @CalledMethods({"b"}) Foo makeFooFinalize2() { + Foo f = new Foo(); + f.b(); + return f; + } + + void CallMethodsInSequence() { + makeFoo().a(); + } + + void CallMethodsInSequence2() { + makeFoo().b().a(); + } + + void testFluentAPIWrong() { + // :: error: required.method.not.called + makeFoo().b(); + } + + void testFluentAPIWrong2() { + // :: error: required.method.not.called + makeFoo(); + } + + void invokeMethodWithCallA() { + makeFooFinalize(); + } + + void invokeMethodWithCallBWrong() { + // :: error: required.method.not.called + makeFooFinalize2(); + } + + void invokeMethodAndCallCWrong() { + // :: error: required.method.not.called + makeFoo().c(); + } + + Foo returnMakeFoo() { + return makeFoo(); + } + + Foo testField1; + Foo testField2; + Foo testField3; + + void testStoringInField() { + // :: error: required.method.not.called + testField1 = makeFoo(); + // :: error: required.method.not.called + testField2 = new Foo(); + + testField3 = makeFooFinalize(); + } + + void tryCatchFinally() { + Foo f = null; + try { + f = new Foo(); + try { + throw new RuntimeException(); + } catch (Exception e) { + + } + } finally { + f.a(); + } + } +} diff --git a/checker/tests/resourceleak/ACOwning.java b/checker/tests/resourceleak/ACOwning.java new file mode 100644 index 000000000000..2b5f0da9363f --- /dev/null +++ b/checker/tests/resourceleak/ACOwning.java @@ -0,0 +1,81 @@ +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +class ACOwning { + + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + Foo makeFoo() { + return new Foo(); + } + + static void takeOwnership(@Owning Foo foo, Foo f) { + foo.a(); + } + + static void noOwnership(Foo foo) {} + + // :: error: required.method.not.called + static void takeOwnershipWrong(@Owning Foo foo) {} + + static @NotOwning Foo getNonOwningFoo() { + // :: error: required.method.not.called + return new Foo(); + } + + static void callGetNonOwningFoo() { + getNonOwningFoo(); + } + + static void ownershipInCallee() { + Foo f = new Foo(); + // :: error: required.method.not.called + takeOwnership(f, new Foo()); + // :: error: required.method.not.called + Foo g = new Foo(); + noOwnership(g); + } + + // make sure enum doesn't crash things + static enum TestEnum { + CASE1, + CASE2, + CASE3 + } + + @Owning + public Foo owningAtReturn() { + return new Foo(); + } + + void owningAtReturnTest() { + // :: error: required.method.not.called + Foo f = owningAtReturn(); + } + + void ownershipTest() { + // :: error: required.method.not.called + takeOwnership(new Foo(), makeFoo()); + } + + @InheritableMustCall({}) + // :: error: super.invocation.invalid :: error: inconsistent.mustcall.subtype + private class SubFoo extends Foo { + + void test() { + SubFoo f = new SubFoo(); + } + + void test2() { + // :: error: required.method.not.called + Foo f = new Foo(); + } + + void test3() { + Foo f = new SubFoo(); + } + } +} diff --git a/checker/tests/resourceleak/ACRegularExitPointTest.java b/checker/tests/resourceleak/ACRegularExitPointTest.java new file mode 100644 index 000000000000..16f19a8693a6 --- /dev/null +++ b/checker/tests/resourceleak/ACRegularExitPointTest.java @@ -0,0 +1,309 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +import java.io.IOException; +import java.util.function.Function; + +class ACRegularExitPointTest { + + @InheritableMustCall("a") + class Foo { + void a() {} + + @This Foo b() { + return this; + } + + void c(@CalledMethods("a") Foo this) {} + } + + class SubFoo extends Foo {} + + Foo makeFoo() { + return new Foo(); + } + + @CalledMethods("a") Foo makeFooCallA() { + Foo f = new Foo(); + f.a(); + return f; + } + + @EnsuresCalledMethods(value = "#1", methods = "a") + void callA(Foo f) { + f.a(); + } + + void makeFooFinalize() { + Foo f = new Foo(); + f.a(); + } + + void makeFooFinalizeWrong() { + Foo m; + // :: error: required.method.not.called + m = new Foo(); + // :: error: required.method.not.called + Foo f = new Foo(); + f.b(); + } + + void testStoringInLocalWrong() { + // :: error: required.method.not.called + Foo foo = makeFoo(); + } + + void testStoringInLocalWrong2() { + Foo f; + // :: error: required.method.not.called + f = makeFoo(); + } + + void testStoringInLocal() { + Foo foo = makeFooCallA(); + } + + void testStoringInLocalWrong3() { + // :: error: required.method.not.called + Foo foo = new Foo(); + } + + void emptyFuncWithFormalPram(Foo f) {} + + void innerFunc(Foo f) { + Runnable r = + new Runnable() { + public void run() { + Foo f; + } + ; + }; + r.run(); + } + + void innerFuncWrong(Foo f) { + Runnable r = + new Runnable() { + public void run() { + // :: error: required.method.not.called + Foo g = new Foo(); + } + ; + }; + r.run(); + } + + void innerFunc2(Foo f) { + Runnable r = + new Runnable() { + public void run() { + Foo g = makeFoo(); + g.a(); + } + ; + }; + r.run(); + } + + void innerfunc3() { + + Foo f = makeFoo(); + f.a(); + Function<@MustCall Foo, @CalledMethods("a") @MustCall Foo> innerfunc = + st -> { + // :: error: required.method.not.called + Foo fn1 = new Foo(); + Foo fn2 = makeFoo(); + fn2.a(); + // The need for this cast is undesirable, but is a consequence of our approach + // to generic types. In this case, this cast is clearly safe (a() has already + // been called, so the obligation is satisfied on the returned value, as + // intended). + // :: warning: cast.unsafe + return ((@MustCall Foo) fn2); + }; + + innerfunc.apply(f); + } + + void ifElse(boolean b) { + if (b) { + Foo f1 = new Foo(); + f1.a(); + } else { + // :: error: required.method.not.called + Foo f2 = new Foo(); + } + } + + Foo ifElseWithReturnExit(boolean b, boolean c) { + // :: error: required.method.not.called + Foo f1 = makeFoo(); + // :: error: required.method.not.called + Foo f3 = new Foo(); + // :: error: required.method.not.called + Foo f4 = new Foo(); + + if (b) { + // :: error: required.method.not.called + Foo f2 = new Foo(); + if (c) { + f4.a(); + } else { + f4.b(); + } + return f1; + } else { + // :: error: required.method.not.called + Foo f2 = new Foo(); + f2 = new Foo(); + f2.a(); + } + return f3; + } + + void ifElseWithDeclaration(boolean b) { + Foo f1; + Foo f2; + if (b) { + f1 = new Foo(); + f1.a(); + } else { + // :: error: required.method.not.called + f2 = new Foo(); + } + } + + void ifElseWithInitialization(boolean b) { + // :: error: required.method.not.called + Foo f2 = new Foo(); + Foo f11 = null; + if (b) { + f11 = makeFoo(); + f11.a(); + } else { + // :: error: required.method.not.called + f2 = new Foo(); + } + } + + void ifWithInitialization(boolean b) { + // :: error: required.method.not.called + Foo f1 = new Foo(); + // :: error: required.method.not.called + Foo f2 = new Foo(); + if (b) { + f1.a(); + } + } + + void variableGoesOutOfScope(boolean b) { + if (b) { + Foo f1 = new Foo(); + f1.a(); + } + } + + void ifWithNullInitialization(boolean b) { + Foo f1 = null; + Foo f2 = null; + if (b) { + f1 = new Foo(); + f1.a(); + } else { + // :: error: required.method.not.called + f2 = new Foo(); + } + } + + void variableInitializedWithNull() { + Foo f = null; + } + + void testLoop() { + Foo f = null; + while (true) { + // :: error: required.method.not.called + f = new Foo(); + } + } + + void overWrittingVarInLoop() { + // :: error: required.method.not.called + Foo f = new Foo(); + while (true) { + // :: error: required.method.not.called + f = new Foo(); + } + } + + void loopWithNestedBranches(boolean b) { + Foo frodo = null; + while (true) { + if (b) { + // :: error: required.method.not.called + frodo = new Foo(); + } else { + // this is a known false positive, due to lack of path sensitivity in the + // Called Methods Checker + // :: error: required.method.not.called + frodo = new Foo(); + frodo.a(); + } + } + } + + void replaceVarWithNull(boolean b, boolean c) { + // :: error: required.method.not.called + Foo f = new Foo(); + if (b) { + f = null; + } else if (c) { + f = null; + } else { + + } + } + + void ownershipTransfer() { + Foo f1 = new Foo(); + Foo f2 = f1; + Foo f3 = f2.b(); + f3.a(); + } + + void ownershipTransfer2() { + Foo f1 = null; + Foo f2 = f1; + } + + void testECM() { + Foo f = new Foo(); + callA(f); + } + + void testFinallyBlock(boolean b) { + Foo f = null; + try { + f = new Foo(); + if (true) { + throw new IOException(); + } + } catch (IOException e) { + + } finally { + f.a(); + } + } + + void testSubFoo() { + // :: error: required.method.not.called + Foo f = new SubFoo(); + } + + void testSubFoo2() { + // :: error: required.method.not.called + SubFoo f = new SubFoo(); + } +} diff --git a/checker/tests/resourceleak/ACSocketTest.java b/checker/tests/resourceleak/ACSocketTest.java new file mode 100644 index 000000000000..ca7cb03e3504 --- /dev/null +++ b/checker/tests/resourceleak/ACSocketTest.java @@ -0,0 +1,453 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.net.*; +import java.nio.channels.*; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +import javax.net.ssl.*; + +public class ACSocketTest { + + @Owning + Socket makeSocket(String address, int port) { + + try { + Socket socket = new Socket(address, port); + return socket; + } catch (IOException i) { + return null; + } + } + + void basicTest(String address, int port) { + try { + // :: error: required.method.not.called + Socket socket2 = new Socket(address, port); + Socket specialSocket = new Socket(address, port); + specialSocket.close(); + } catch (IOException i) { + } + } + + void tryWithResourcesTest(String address, int port) throws IOException { + try (Socket s = new Socket(address, port)) {} + } + + void callMakeSocketAndClose(String address, int port) { + Socket socket = makeSocket(address, port); + try { + socket.close(); + } catch (IOException i) { + } + } + + void callMakeSocket(String address, int port) { + // :: error: required.method.not.called + Socket socket = makeSocket(address, port); + } + + void ifElseWithDeclaration(String address, int port, boolean b) { + Socket s1; + Socket s2; + try { + if (b) { + s1 = new Socket(address, port); + s1.close(); + } else { + // :: error: required.method.not.called + s2 = new Socket(address, port + 1); + } + } catch (IOException i) { + + } + } + + void testLoop(String address, int port) { + Socket s = null; + while (true) { + try { + s = new Socket(address, port); + s.close(); + } catch (IOException e) { + + } + } + } + + void overWrittingVarInLoop(String address, int port) { + // :: error: required.method.not.called + Socket s = makeSocket(address, port); + while (true) { + try { + // :: error: required.method.not.called + s = new Socket(address, port); + } catch (IOException e) { + + } + } + } + + void loopWithNestedBranches(String address, int port, boolean b) { + Socket s = null; + while (true) { + if (b) { + // :: error: required.method.not.called + s = makeSocket(address, port); + } else { + // :: error: required.method.not.called + s = makeSocket(address, port); + } + } + } + + void replaceVarWithNull(String address, int port, boolean b, boolean c) { + Socket s; + try { + // :: error: required.method.not.called + s = new Socket(address, port); + } catch (IOException e) { + + } + if (b) { + s = null; + } else if (c) { + s = null; + } else { + + } + } + + void ownershipTransfer(String address, int port) { + Socket s1 = null; + try { + // :: error: required.method.not.called + s1 = new Socket(address, port); + } catch (IOException e) { + + } + // It is equally correct to report an error here. + Socket s2 = s1; + if (true) { + closeSocket(s2); + } + } + + void test(String address, int port) { + try { + // :: error: required.method.not.called + Socket socket = new Socket(address, 80); + PrintStream out = new PrintStream(socket.getOutputStream()); + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + in.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + protected Socket sock; + + // This type.argument error is undesirable, but is a necessary consquence of our approach to + // handling generics in the Must Call Checker, which prevents containers from having + // @MustCall("close") type arguments without errors (in exchange for avoiding many false + // positives on containers that do not have must-call obligations on their component types). + // :: error: type.argument.type.incompatible + void connectToLeader(AtomicReference socket) throws IOException { + // :: error: required.method.not.called + if (socket.get() == null) { + throw new IOException("Failed connect to "); + } else { + // :: error: required.method.not.called + sock = socket.get(); + } + } + + Socket createSocket(boolean b, String address, int port) throws IOException { + Socket sock; + if (b) { + // :: error: required.method.not.called + sock = new Socket(address, port); + } else { + // :: error: required.method.not.called + sock = new Socket(address, port); + } + + sock.setSoTimeout(10000); + closeSocket(sock); + return sock; + } + + // @EnsuresCalledMethodsIf(expression = "#1", methods = {"close"}, result = true) + // void closeSocket(Socket sock) { + //// if (sock == null) { + //// return; + //// } + // + // try { + // sock.close(); + // } catch (IOException ie) { + // + // } + // } + + public static void ruok(String host, int port) { + Socket s = null; + try { + s = new Socket(host, port); + } catch (IOException e) { + + } finally { + + try { + s.close(); + } catch (IOException e) { + + } + } + } + + @EnsuresCalledMethods(value = "#1", methods = "close") + void closeSocket(Socket sock) { + try { + if (sock != null) { + sock.close(); + } + } catch (IOException e) { + + } + } + + @EnsuresCalledMethods(value = "#1", methods = "close") + void closeServerSocket(ServerSocket sock) { + try { + if (sock != null) { + sock.close(); + } + } catch (IOException e) { + + } + } + + void useCloseSocket(String address, int port) throws IOException { + Socket sock = new Socket(address, port); + Socket s = getSocket(sock); + closeSocket(sock); + } + + void setSockOpts(Socket sock) throws SocketException { + sock.setTcpNoDelay(true); + sock.setKeepAlive(true); + sock.setSoTimeout(1000); + } + + void initiateConnection( + SocketAddress endpoint, int timeout, SSLContext context, final Long sid) { + Socket sock = null; + try { + sock = context.getSocketFactory().createSocket(); + setSockOpts(sock); + sock.connect(endpoint, timeout); + if (sock instanceof SSLSocket) { + SSLSocket sslSock = (SSLSocket) sock; + sslSock.startHandshake(); + } + } catch (ClassCastException e) { + closeSocket(sock); + return; + } catch (IOException e) { + closeSocket(sock); + return; + } + + try { + startConnection(sock); + } catch (IOException e) { + closeSocket(sock); + } + } + + private boolean startConnection(@Owning Socket s) throws IOException { + closeSocket(s); + return true; + } + + private boolean startConnection(@Owning SSLSocket s) throws IOException { + closeSocket(s); + return true; + } + + @MustCall({"close"}) class PrependableSocket extends Socket { + + public PrependableSocket(SocketImpl base) throws IOException { + super(base); + } + } + + void makePrependableSocket() throws IOException { + // :: error: required.method.not.called + final PrependableSocket prependableSocket = new PrependableSocket(null); + } + + // private void acceptConnections() { + // int numRetries = 0; + // Socket client = null; + // + // while ((!shutdown) && (portBindMaxRetry == 0 || numRetries < portBindMaxRetry)) { + // try { + // serverSocket = createNewServerSocket(); + // LOG.info("{} is accepting connections now, my election bind port: {}", + // QuorumCnxManager.this.mySid, address.toString()); + // while (!shutdown) { + // try { + // client = serverSocket.accept(); + // setSockOpts(client); + // LOG.info("Received connection request from {}", + // client.getRemoteSocketAddress()); + // // Receive and handle the connection request + // // asynchronously if the quorum sasl authentication is + // // enabled. This is required because sasl server + // // authentication process may take few seconds to finish, + // // this may delay next peer connection requests. + // if (quorumSaslAuthEnabled) { + // receiveConnectionAsync(client); + // } else { + // receiveConnection(client); + // } + // numRetries = 0; + // } catch (SocketTimeoutException e) { + // LOG.warn("The socket is listening for the election accepted " + // + "and it timed out unexpectedly, but will retry." + // + "see ZOOKEEPER-2836"); + // } + // } + // } catch (IOException e) { + // if (shutdown) { + // break; + // } + // + // LOG.error("Exception while listening", e); + // + // if (e instanceof SocketException) { + // socketException.set(true); + // } + // + // numRetries++; + // try { + // close(); + // Thread.sleep(1000); + // } catch (IOException ie) { + // LOG.error("Error closing server socket", ie); + // } catch (InterruptedException ie) { + // LOG.error("Interrupted while sleeping. Ignoring exception", ie); + // } + // closeSocket(client); + // } + // } + // if (!shutdown) { + // LOG.error( + // "Leaving listener thread for address {} after {} errors. Use {} property + // to + // increase retry count.", + // formatInetAddr(address), + // numRetries, + // ELECTION_PORT_BIND_RETRY); + // } + // } + + void createNewServerSocket(InetSocketAddress address, boolean b, boolean c) throws IOException { + ServerSocket socket; + + if (b) { + socket = new ServerSocket(); + } else if (c) { + socket = new ServerSocket(); + } else { + socket = new ServerSocket(); + } + + socket.setReuseAddress(true); + socket.bind(address); + closeServerSocket(socket); + } + + @Owning + public SSLServerSocket createSSLServerSocket(SSLContext sslContext) throws IOException { + SSLServerSocket sslServerSocket = + (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket(); + return configureSSLServerSocket(sslServerSocket); + } + + private SSLServerSocket nonOwningSSField; + + void assignToNonOwningViaCast(SSLContext sslContext) throws IOException { + nonOwningSSField = + (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket(); + } + + private SSLServerSocket configureSSLServerSocket(@Owning SSLServerSocket socket) { + return socket; + } + + public SSLSocket createSSLSocket( + @Owning Socket socket, byte[] pushbackBytes, SSLContext sslContext) throws IOException { + SSLSocket sslSocket; + if (pushbackBytes != null && pushbackBytes.length > 0) { + sslSocket = + (SSLSocket) + sslContext + .getSocketFactory() + .createSocket(socket, null, socket.getPort(), true); + } else { + sslSocket = + (SSLSocket) + sslContext + .getSocketFactory() + .createSocket(socket, null, socket.getPort(), true); + } + return configureSSLSocket(sslSocket, false); + } + + private SSLSocket configureSSLSocket(@Owning SSLSocket socket, boolean isClientSocket) { + SSLParameters sslParameters = socket.getSSLParameters(); + // configureSslParameters(sslParameters, isClientSocket); + socket.setSSLParameters(sslParameters); + socket.setUseClientMode(isClientSocket); + return socket; + } + + private void updateSocketAddresses(SelectionKey sockKey) { + // no error here as SelectionKey.channel()'s return is @NotOwning + Socket socket = ((SocketChannel) sockKey.channel()).socket(); + SocketAddress localSocketAddress = socket.getLocalSocketAddress(); + SocketAddress remoteSocketAddress = socket.getRemoteSocketAddress(); + } + + private void recieverParameterWithCasting(@Owning SelectableChannel channel1) + throws IOException { + try { + ((SocketChannel) channel1).socket(); + } finally { + channel1.close(); + } + } + + @NotOwning + Socket getSocket(Socket s) { + return s; + } + + private ServerSocket testMCAParamInReturn() throws IOException { + ServerSocketChannel chan = ServerSocketChannel.open(); + return chan.socket(); + } + + private void testMCAParamInReturn2() throws IOException { + ServerSocket chan = ServerSocketChannel.open().socket(); + } +} diff --git a/checker/tests/resourceleak/AccumulationValueFieldTest.java b/checker/tests/resourceleak/AccumulationValueFieldTest.java new file mode 100644 index 000000000000..e145aa32426d --- /dev/null +++ b/checker/tests/resourceleak/AccumulationValueFieldTest.java @@ -0,0 +1,40 @@ +// This test checks the accumulation value for a field with a wildcard type. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +public class AccumulationValueFieldTest { + + @InheritableMustCall({"a"}) + class MCAB { + void a() {} + + void b() {} + } + + @InheritableMustCall({"a"}) + class FieldTest { + + @Owning + @MustCall({"a"}) T m = null; + + FieldTest(@Owning @MustCall({"a"}) T mcab) { + m = mcab; + } + + @RequiresCalledMethods( + value = {"this.m"}, + methods = {"a"}) + @CreatesMustCallFor("this") + void overwriteMCorrect(@Owning @MustCall({"a"}) T mcab) { + this.m = mcab; + } + + @EnsuresCalledMethods( + value = {"this.m"}, + methods = {"a"}) + void a() { + m.a(); + } + } +} diff --git a/checker/tests/resourceleak/AccumulationValueTest.java b/checker/tests/resourceleak/AccumulationValueTest.java new file mode 100644 index 000000000000..ddd99fdf11d7 --- /dev/null +++ b/checker/tests/resourceleak/AccumulationValueTest.java @@ -0,0 +1,97 @@ +// This test checks that the leastUpperBound and mostSpecific methods in AccumulationValue.java +// behave as expected, in the context of resource leak checking (which is their first client). + +import org.checkerframework.checker.mustcall.qual.*; + +public class AccumulationValueTest { + + @InheritableMustCall({"a", "b"}) + class MCAB { + void a() {} + + void b() {} + + void c() {} + } + + void simple1(@Owning @MustCall({"a", "b"}) T mcab) { + // test that an accumulation value can accumulate more than one item + mcab.a(); + mcab.b(); + } + + // :: error: required.method.not.called + void simple2(@Owning @MustCall({"a", "b"}) T mcab) { + // test that the RLC handles missing call to a() + mcab.b(); + } + + void simple3(@Owning @MustCall({"a", "b"}) T mcab) { + // test that an accumulation value can accumulate extra items without issue (this tests + // mostSpecific) + mcab.a(); + mcab.b(); + mcab.c(); + } + + // :: error: required.method.not.called + void lub1(@Owning @MustCall({"a", "b"}) T mcab, boolean b) { + // tests lubbing two AccumulationValue at a join + if (b) { + mcab.a(); + } + mcab.b(); + } + + void lub2(@Owning @MustCall({"a", "b"}) T mcab, boolean b) { + // tests lubbing two AccumulationValue at a join + if (b) mcab.a(); + else mcab.a(); + mcab.b(); + } + + // :: error: required.method.not.called + void lub3(@Owning @MustCall({"a", "b"}) T mcab, boolean b) { + // tests lubbing two AccumulationValue at a join if both are non-empty but non-intersecting + if (b) { + mcab.a(); + } else { + mcab.b(); + } + } + + // :: error: required.method.not.called + void lub4(@Owning @MustCall({"a", "b"}) T mcab, boolean b) { + // tests lubbing two AccumulationValue at a join if both are non-empty but intersecting + if (b) { + mcab.a(); + mcab.c(); + } else { + mcab.a(); + mcab.b(); + } + } + + void lub5(@Owning @MustCall({"a", "b"}) T mcab, boolean b) { + // tests lubbing two AccumulationValue at a join if both are non-empty but intersecting + if (b) { + mcab.a(); + mcab.b(); + mcab.c(); + } else { + mcab.a(); + mcab.b(); + } + } + + // These two paired methods show what happens when the @MustCall type is "too small": + // errors at call sites. + void wrongMCAnno(@Owning @MustCall({"a"}) T mcab) { + mcab.a(); + } + + void wrongMCAnnoUse(@Owning MCAB mcab) { + // :: error: argument.type.incompatible + wrongMCAnno(mcab); + } +} diff --git a/checker/tests/resourceleak/BindChannel.java b/checker/tests/resourceleak/BindChannel.java new file mode 100644 index 000000000000..5db5525fabc0 --- /dev/null +++ b/checker/tests/resourceleak/BindChannel.java @@ -0,0 +1,40 @@ +// A test for code encountered by Narges. + +import java.io.*; +import java.net.*; +import java.nio.channels.*; + +class BindChannel { + static void test(InetSocketAddress addr, boolean b) { + try { + // This channel is bound - so even with unconnected socket support, we need to + // treat either this channel or the .socket() expression as must-close. + // + // Even though there's now a temporary in the Must Call Checker for the value that + // has the reset method (bind) called on it below, we can't successfully translate + // the reset expression to that temporary, since all we have is a string (from the + // reset annotation) and so we have to go through the type factory's parsing facility, + // which doesn't know about the temporaries and so doesn't return them. We're therefore + // limited to issuing the reset.not.owning error below, + // instead of the preferable required.method.not.called error on this line - as in + // the method below, which extracts the socket into a local variable, which can be + // parsed as an CO target. + ServerSocketChannel httpChannel = ServerSocketChannel.open(); + // :: error: reset.not.owning + httpChannel.socket().bind(addr); + } catch (IOException io) { + + } + } + + static void test_lv(InetSocketAddress addr, boolean b) { + try { + ServerSocketChannel httpChannel = ServerSocketChannel.open(); + // :: error: required.method.not.called + ServerSocket httpSock = httpChannel.socket(); + httpSock.bind(addr); + } catch (IOException io) { + + } + } +} diff --git a/checker/tests/resourceleak/COAnonymousClass.java b/checker/tests/resourceleak/COAnonymousClass.java new file mode 100644 index 000000000000..2efbccf4a231 --- /dev/null +++ b/checker/tests/resourceleak/COAnonymousClass.java @@ -0,0 +1,60 @@ +// Test case for https://github.com/kelloggm/object-construction-checker/issues/368 + +import org.checkerframework.checker.mustcall.qual.*; + +class COAnonymousClass { + @InheritableMustCall("foo") + static class Foo { + + void foo() {} + + @CreatesMustCallFor("this") + void resetFoo() {} + + void other() { + + Runnable r = + new Runnable() { + @Override + @CreatesMustCallFor("Foo.this") + // :: error: creates.mustcall.for.invalid.target + // :: error: creates.mustcall.for.override.invalid + public void run() { + // [The following explanation is incorrect. The problem is a bug in + // creating implicit "this" expressions.] + // Ideally, we would not issue the following error. However, the Checker + // Framework's JavaExpression support + // (https://eisop.github.io/cf/manual/#java-expressions-as-arguments) + // treats all versions of "this" (including "Foo.this") as referring to + // the object that directly contains the annotation, so we treat this + // call to resetFoo as not permitted. + // :: error: (reset.not.owning) + resetFoo(); + } + }; + call_run(r); + } + + void other2() { + + Runnable r = + new Runnable() { + @Override + @CreatesMustCallFor("this") + // :: error: creates.mustcall.for.invalid.target + // :: error: creates.mustcall.for.override.invalid + public void run() { + // This error definitely must be issued, since Foo.this != this. + // :: error: reset.not.owning + resetFoo(); + } + }; + call_run(r); + } + + // If this call to run() were permitted with no errors, this would be unsound. + void call_run(Runnable r) { + r.run(); + } + } +} diff --git a/checker/tests/resourceleak/COInSubtype.java b/checker/tests/resourceleak/COInSubtype.java new file mode 100644 index 000000000000..13ff2d5b52fa --- /dev/null +++ b/checker/tests/resourceleak/COInSubtype.java @@ -0,0 +1,29 @@ +// A test for a bad interaction between CO and subtyping +// that could happen if CO was unsound. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class COInSubtype { + static class Foo { + + void foo() {} + + // This is not supported, even though a sub-class may have must-call obligations. + // This pattern is not used in realistic code, and supporting it hurts checker performance. + @CreatesMustCallFor("this") + // :: error: creates.mustcall.for.invalid.target + void resetFoo() {} + } + + @InheritableMustCall("a") + static class Bar extends Foo { + void a() {} + } + + static void test() { + // :: error: required.method.not.called + @MustCall("a") Foo f = new Bar(); + f.resetFoo(); + } +} diff --git a/checker/tests/resourceleak/CheckFields.java b/checker/tests/resourceleak/CheckFields.java new file mode 100644 index 000000000000..62e1872d99b0 --- /dev/null +++ b/checker/tests/resourceleak/CheckFields.java @@ -0,0 +1,171 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +class CheckFields { + + @InheritableMustCall("a") + static class Foo { + void a() {} + + void c() {} + } + + Foo makeFoo() { + return new Foo(); + } + + @InheritableMustCall("b") + static class FooField { + private final @Owning Foo finalOwningFoo; + // :: error: required.method.not.called + private final @Owning Foo finalOwningFooWrong; + private final Foo finalNotOwningFoo; + private @Owning Foo owningFoo; + private @Owning @MustCall({}) Foo owningEmptyMustCallFoo; + private Foo notOwningFoo; + + public FooField() { + this.finalOwningFoo = new Foo(); + this.finalOwningFooWrong = new Foo(); + // :: error: required.method.not.called + this.finalNotOwningFoo = new Foo(); + } + + @CreatesMustCallFor + void assingToOwningFieldWrong() { + Foo f = new Foo(); + // :: error: required.method.not.called + this.owningFoo = f; + } + + @CreatesMustCallFor + void assignToOwningFieldWrong2() { + // :: error: required.method.not.called + this.owningFoo = new Foo(); + } + + @CreatesMustCallFor + void assingToOwningField() { + // this is a safe re-assignment. + if (this.owningFoo == null) { + Foo f = new Foo(); + this.owningFoo = f; + } + } + + void assingToFinalNotOwningField() { + // :: error: required.method.not.called + Foo f = new Foo(); + this.notOwningFoo = f; + } + + Foo getOwningFoo() { + return this.owningFoo; + } + + @EnsuresCalledMethods( + value = {"this.finalOwningFoo", "this.owningFoo"}, + methods = {"a"}) + void b() { + this.finalOwningFoo.a(); + this.finalOwningFoo.c(); + this.owningFoo.a(); + } + } + + void testField() { + FooField fooField = new FooField(); + fooField.b(); + } + + void testAccessField() { + FooField fooField = new FooField(); + // :: error: required.method.not.called + fooField.owningFoo = new Foo(); + fooField.b(); + } + + void testAccessField2() { + FooField fooField = new FooField(); + if (fooField.owningFoo == null) { + fooField.owningFoo = new Foo(); + } + fooField.b(); + } + + void testAccessFieldWrong() { + // :: error: required.method.not.called + FooField fooField = new FooField(); + // :: error: required.method.not.called + fooField.owningFoo = new Foo(); + // :: error: required.method.not.called + fooField.notOwningFoo = new Foo(); + } + + @CreatesMustCallFor("#1") + void testAccessField_param(FooField fooField) { + // :: error: required.method.not.called + fooField.owningFoo = new Foo(); + fooField.b(); + } + + // :: error: missing.creates.mustcall.for + void testAccessField_param_no_co(FooField fooField) { + // :: error: required.method.not.called + fooField.owningFoo = new Foo(); + fooField.b(); + } + + static class NestedWrong { + + // Non-final owning fields also require the surrounding class to have an appropriate MC + // annotation. + // :: error: required.method.not.called + @Owning Foo foo; + + @CreatesMustCallFor("this") + // :: error: creates.mustcall.for.invalid.target + void initFoo() { + if (this.foo == null) { + this.foo = new Foo(); + } + } + } + + @InheritableMustCall("f") + static class NestedWrong2 { + // Non-final owning fields also require the surrounding class to have an appropriate MC + // annotation. + // :: error: required.method.not.called + @Owning Foo foo; + + @CreatesMustCallFor("this") + void initFoo() { + if (this.foo == null) { + this.foo = new Foo(); + } + } + + void f() {} + } + + @InheritableMustCall("f") + static class NestedRight { + // Non-final owning fields also require the surrounding class to have an appropriate MC + // annotation. + @Owning Foo foo; + + @CreatesMustCallFor("this") + void initFoo() { + if (this.foo == null) { + this.foo = new Foo(); + } + } + + @EnsuresCalledMethods(value = "this.foo", methods = "a") + void f() { + this.foo.a(); + } + } +} diff --git a/checker/tests/resourceleak/CloseSuper.java b/checker/tests/resourceleak/CloseSuper.java new file mode 100644 index 000000000000..3636436a79db --- /dev/null +++ b/checker/tests/resourceleak/CloseSuper.java @@ -0,0 +1,40 @@ +// Test case for https://github.com/typetools/checker-framework/issues/6204 + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.Owning; + +import java.io.Closeable; +import java.io.IOException; + +public class CloseSuper { + + public static class A implements Closeable { + private final @Owning Closeable resource; + + public A(@Owning Closeable resource) { + this.resource = resource; + } + + @Override + @EnsuresCalledMethods( + value = "resource", + methods = {"close"}) + public void close() throws IOException { + resource.close(); + } + } + + public static class B extends A { + public B(@Owning Closeable resource) { + super(resource); + } + + @Override + @EnsuresCalledMethods( + value = "resource", + methods = {"close"}) + public void close() throws IOException { + super.close(); + } + } +} diff --git a/checker/tests/resourceleak/CloseableAndMore.java b/checker/tests/resourceleak/CloseableAndMore.java new file mode 100644 index 000000000000..7237d45ab3c2 --- /dev/null +++ b/checker/tests/resourceleak/CloseableAndMore.java @@ -0,0 +1,31 @@ +// A test that when a class implements autocloseable and has another must-call obligation, +// errors are still issued about the other obligation even when it used as a resource variable. + +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; + +import java.io.IOException; + +@SuppressWarnings( + "declaration.inconsistent.with.implements.clause") // stronger @InheritableMustCall +@InheritableMustCall({"close", "foo"}) +public class CloseableAndMore implements AutoCloseable { + void foo() {} + + @Override + public void close() throws IOException {} + + public static void test_bad() { + // :: error: required.method.not.called + try (CloseableAndMore c = new CloseableAndMore()) { + // empty body + } catch (Exception e) { + } + } + + public static void test_good() { + try (CloseableAndMore c = new CloseableAndMore()) { + c.foo(); + } catch (Exception e) { + } + } +} diff --git a/checker/tests/resourceleak/CommonModuleCrash.java b/checker/tests/resourceleak/CommonModuleCrash.java new file mode 100644 index 000000000000..cf3eb5703cb8 --- /dev/null +++ b/checker/tests/resourceleak/CommonModuleCrash.java @@ -0,0 +1,11 @@ +import java.net.*; + +class CommonModuleCrash { + Socket bar = new Socket(); + + static void baz(Socket s) {} + + static { + baz(new Socket()); + } +} diff --git a/checker/tests/resourceleak/ConnectingServerSockets.java b/checker/tests/resourceleak/ConnectingServerSockets.java new file mode 100644 index 000000000000..1856d26b31dc --- /dev/null +++ b/checker/tests/resourceleak/ConnectingServerSockets.java @@ -0,0 +1,47 @@ +// a set of test cases that demonstrate that errors are actually insued in appropriate +// places when ServerSockets are connected + +import org.checkerframework.checker.mustcall.qual.*; + +import java.net.*; + +class ConnectingServerSockets { + + static void simple_ss_test(SocketAddress sa) throws Exception { + // :: error: required.method.not.called + ServerSocket s = new ServerSocket(); + s.bind(sa); + } + + static void simple_ss_test2(SocketAddress sa) throws Exception { + ServerSocket s = new ServerSocket(); + // s.bind(sa); + } + + static void simple_ss_test4(SocketAddress sa, int to) throws Exception { + // :: error: required.method.not.called + ServerSocket s = new ServerSocket(); + s.bind(sa, to); + } + + static @MustCall({}) ServerSocket makeUnconnected() throws Exception { + return new ServerSocket(); + } + + static void simple_ss_test5(SocketAddress sa) throws Exception { + // :: error: required.method.not.called + ServerSocket s = makeUnconnected(); + s.bind(sa); + } + + static void simple_ss_test6(SocketAddress sa) throws Exception { + ServerSocket s = makeUnconnected(); + // s.bind(sa); + } + + static void simple_ss_test8(SocketAddress sa, int to) throws Exception { + // :: error: required.method.not.called + ServerSocket s = makeUnconnected(); + s.bind(sa, to); + } +} diff --git a/checker/tests/resourceleak/ConnectingSockets.java b/checker/tests/resourceleak/ConnectingSockets.java new file mode 100644 index 000000000000..d91c6b04a25d --- /dev/null +++ b/checker/tests/resourceleak/ConnectingSockets.java @@ -0,0 +1,59 @@ +// a set of test cases that demonstrate that errors are actually insued in appropriate +// places when Sockets are connected + +import org.checkerframework.checker.mustcall.qual.*; + +import java.net.*; + +class ConnectingSockets { + + static void simple_ns_test(SocketAddress sa) throws Exception { + // :: error: required.method.not.called + Socket s = new Socket(); + s.bind(sa); + } + + static void simple_ns_test2(SocketAddress sa) throws Exception { + Socket s = new Socket(); + // s.bind(sa); + } + + static void simple_ns_test3(SocketAddress sa) throws Exception { + // :: error: required.method.not.called + Socket s = new Socket(); + s.connect(sa); + } + + static void simple_ns_test4(SocketAddress sa, int to) throws Exception { + // :: error: required.method.not.called + Socket s = new Socket(); + s.connect(sa, to); + } + + static @MustCall({}) Socket makeUnconnected() throws Exception { + return new Socket(); + } + + static void simple_ns_test5(SocketAddress sa) throws Exception { + // :: error: required.method.not.called + Socket s = makeUnconnected(); + s.bind(sa); + } + + static void simple_ns_test6(SocketAddress sa) throws Exception { + Socket s = makeUnconnected(); + // s.bind(sa); + } + + static void simple_ns_test7(SocketAddress sa) throws Exception { + // :: error: required.method.not.called + Socket s = makeUnconnected(); + s.connect(sa); + } + + static void simple_ns_test8(SocketAddress sa, int to) throws Exception { + // :: error: required.method.not.called + Socket s = makeUnconnected(); + s.connect(sa, to); + } +} diff --git a/checker/tests/resourceleak/ConnectingSockets2.java b/checker/tests/resourceleak/ConnectingSockets2.java new file mode 100644 index 000000000000..529af682426d --- /dev/null +++ b/checker/tests/resourceleak/ConnectingSockets2.java @@ -0,0 +1,16 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5739 . + +import java.io.IOException; +import java.net.*; + +class ConnectingSockets2 { + + void run(InetSocketAddress isa) { + try (Socket serverSocket = new Socket()) { + serverSocket.close(); + serverSocket.connect(isa); + } catch (IOException e) { + // do nothing + } + } +} diff --git a/checker/tests/resourceleak/ConstructorAddsMustCall.java b/checker/tests/resourceleak/ConstructorAddsMustCall.java new file mode 100644 index 000000000000..319139d2e3ac --- /dev/null +++ b/checker/tests/resourceleak/ConstructorAddsMustCall.java @@ -0,0 +1,22 @@ +// A test that ensures that the Resource Leak Checker continues to track an +// Object if it's returned from a method with a non-empty @MustCall annotation. + +import org.checkerframework.checker.mustcall.qual.MustCall; + +public class ConstructorAddsMustCall { + static class Foo { + void a() {} + + Foo() {} + + @MustCall("a") Foo(String s) {} + } + + static void useFoo() { + // no obligation for this one + Foo f1 = new Foo(); + // obligation for this one + // :: error: required.method.not.called + Foo f2 = new Foo("hi"); + } +} diff --git a/checker/tests/resourceleak/CreatesMustCallForIndirect.java b/checker/tests/resourceleak/CreatesMustCallForIndirect.java new file mode 100644 index 000000000000..c941f6694b43 --- /dev/null +++ b/checker/tests/resourceleak/CreatesMustCallForIndirect.java @@ -0,0 +1,63 @@ +// A test that methods containing calls to other @CreatesMustCallFor methods work as intended. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall("a") +class CreatesMustCallForIndirect { + + @CreatesMustCallFor + void reset() {} + + void a() {} + + static @MustCall({}) CreatesMustCallForSimple makeNoMC() { + return null; + } + + public static void resetIndirect_no_anno(CreatesMustCallForIndirect r) { + // :: error: reset.not.owning + r.reset(); + } + + @CreatesMustCallFor("#1") + public static void resetIndirect_anno(CreatesMustCallForIndirect r) { + r.reset(); + } + + public static void reset_local() { + // :: error: required.method.not.called + CreatesMustCallForIndirect r = new CreatesMustCallForIndirect(); + r.reset(); + } + + public static void reset_local2() { + CreatesMustCallForIndirect r = new CreatesMustCallForIndirect(); + r.reset(); + r.a(); + } + + public static void reset_local3() { + // :: error: required.method.not.called + CreatesMustCallForIndirect r = new CreatesMustCallForIndirect(); + // Ideally, we'd issue a reset.not.owning error on the next line instead, but not being able + // to parse the case and requiring it to be in a local var is okay too. + // :: error: createsmustcallfor.target.unparseable + ((CreatesMustCallForIndirect) r).reset(); + } + + // :: error: required.method.not.called + public static void test(@Owning CreatesMustCallForIndirect r) { + resetIndirect_anno(r); + } + + public static void test2(CreatesMustCallForIndirect r) { + // :: error: reset.not.owning + resetIndirect_anno(r); + } + + public static void test3(@Owning CreatesMustCallForIndirect r) { + resetIndirect_anno(r); + r.a(); + } +} diff --git a/checker/tests/resourceleak/CreatesMustCallForInnerClass.java b/checker/tests/resourceleak/CreatesMustCallForInnerClass.java new file mode 100644 index 000000000000..f230f4dc739a --- /dev/null +++ b/checker/tests/resourceleak/CreatesMustCallForInnerClass.java @@ -0,0 +1,33 @@ +// Test case for https://github.com/kelloggm/object-construction-checker/issues/368 + +import org.checkerframework.checker.mustcall.qual.*; + +class CreatesMustCallForInnerClass { + @InheritableMustCall("foo") + static class Foo { + + void foo() {} + + @CreatesMustCallFor("this") + void resetFoo() {} + + /** non-static inner class */ + class Bar { + @CreatesMustCallFor + // :: error: creates.mustcall.for.invalid.target + void bar() { + // :: error: reset.not.owning + resetFoo(); + } + } + + void callBar() { + Bar b = new Bar(); + // If this call to bar() were permitted with no errors, this would be unsound. + // b is in fact an owning pointer, but we don't track it as such because + // Bar objects cannot have must-call obligations created for them. + // :: error: reset.not.owning + b.bar(); + } + } +} diff --git a/checker/tests/resourceleak/CreatesMustCallForOverride.java b/checker/tests/resourceleak/CreatesMustCallForOverride.java new file mode 100644 index 000000000000..698b7feca4c6 --- /dev/null +++ b/checker/tests/resourceleak/CreatesMustCallForOverride.java @@ -0,0 +1,32 @@ +// A test case that a create-obligation method cannot be called via dynamic dispatch +// without resetting the obligation. + +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall("a") +class CreatesMustCallForOverride { + @CreatesMustCallFor + @Override + // :: error: creates.mustcall.for.override.invalid + public String toString() { + return "this method could re-assign a field or do something else it shouldn't"; + } + + public void a() {} + + public static void test_no_cast() { + // :: error: required.method.not.called + CreatesMustCallForOverride co = new CreatesMustCallForOverride(); + co.a(); + co.toString(); + } + + public static void test_cast() { + // it would be ideal if the checker issued an error directly here, but the best we can do is + // issue the error above when the offending version of toString() is defined + CreatesMustCallForOverride co = new CreatesMustCallForOverride(); + co.a(); + Object o = co; + o.toString(); + } +} diff --git a/checker/tests/resourceleak/CreatesMustCallForOverride2.java b/checker/tests/resourceleak/CreatesMustCallForOverride2.java new file mode 100644 index 000000000000..28d3d3596877 --- /dev/null +++ b/checker/tests/resourceleak/CreatesMustCallForOverride2.java @@ -0,0 +1,176 @@ +// This test checks that 1) writing a CO annotation on an overridden method that's also +// CO is permitted, and 2) that all overridden versions of a CO method are always also CO. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class CreatesMustCallForOverride2 { + + @InheritableMustCall("a") + static class Foo { + + @CreatesMustCallFor + public void b() {} + + public void a() {} + } + + static class Bar extends Foo { + + @Override + @CreatesMustCallFor + public void b() {} + } + + static class Baz extends Foo {} + + static class Qux extends Foo { + @Override + public void b() {} + } + + static class Razz extends Foo { + + public @Owning Foo myFoo; + + @Override + @EnsuresCalledMethods(value = "this.myFoo", methods = "a") + public void a() { + super.a(); + myFoo.a(); + } + + // this version isn't permitted, since it adds a new obligation + @Override + @CreatesMustCallFor("this.myFoo") + // :: error: creates.mustcall.for.override.invalid + public void b() {} + } + + static class Thud extends Foo { + + public @Owning Foo myFoo; + + @Override + @EnsuresCalledMethods(value = "this.myFoo", methods = "a") + public void a() { + super.a(); + myFoo.a(); + } + + // this method isn't permitted, since it's also adding a new obligation + @Override + @CreatesMustCallFor("this.myFoo") + @CreatesMustCallFor("this") + // :: error: creates.mustcall.for.override.invalid + public void b() {} + } + + static class Thudless extends Thud { + // this method override is also NOT permitted, because the @CreatesMustCallFor("this.myFoo") + // annotation from Thud is inherited! + @Override + @CreatesMustCallFor("this") + // :: error: creates.mustcall.for.override.invalid + public void b() {} + } + + static void test1() { + // :: error: required.method.not.called + Foo foo = new Foo(); + foo.a(); + foo.b(); + } + + static void test2() { + // :: error: required.method.not.called + Foo foo = new Bar(); + foo.a(); + foo.b(); + } + + static void test3() { + // :: error: required.method.not.called + Foo foo = new Baz(); + foo.a(); + foo.b(); + } + + static void test4() { + // :: error: required.method.not.called + Foo foo = new Qux(); + foo.a(); + foo.b(); + } + + static void test5() { + // :: error: required.method.not.called + Bar foo = new Bar(); + foo.a(); + foo.b(); + } + + static void test6() { + // :: error: required.method.not.called + Baz foo = new Baz(); + foo.a(); + foo.b(); + } + + static void test7() { + // :: error: required.method.not.called + Qux foo = new Qux(); + foo.a(); + foo.b(); + } + + static void test8() { + // :: error: required.method.not.called + Foo foo = new Razz(); + foo.a(); + foo.b(); + } + + static void test9() { + // No error is issued here, because Razz#b is *only* @CreatesMustCallFor("this.myFoo"), not + // @CreatesMustCallFor("this"). An error is issued at the declaration of Razz#b instead. + Razz foo = new Razz(); + foo.a(); + foo.b(); + } + + static void test10() { + // :: error: required.method.not.called + Foo foo = new Thud(); + foo.a(); + foo.b(); + } + + static void test11() { + // :: error: required.method.not.called + Thud foo = new Thud(); + foo.a(); + foo.b(); + } + + static void test12() { + // :: error: required.method.not.called + Foo foo = new Thudless(); + foo.a(); + foo.b(); + } + + static void test13() { + // :: error: required.method.not.called + Thud foo = new Thudless(); + foo.a(); + foo.b(); + } + + static void test14() { + // :: error: required.method.not.called + Thudless foo = new Thudless(); + foo.a(); + foo.b(); + } +} diff --git a/checker/tests/resourceleak/CreatesMustCallForRepeat.java b/checker/tests/resourceleak/CreatesMustCallForRepeat.java new file mode 100644 index 000000000000..d1c9a849f61e --- /dev/null +++ b/checker/tests/resourceleak/CreatesMustCallForRepeat.java @@ -0,0 +1,71 @@ +// A simple test that @CreatesMustCallFor is repeatable and works as intended. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall("a") +class CreatesMustCallForRepeat { + + @CreatesMustCallFor("this") + @CreatesMustCallFor("#1") + void reset(CreatesMustCallForRepeat r) {} + + void a() {} + + static @MustCall({}) CreatesMustCallForRepeat makeNoMC() { + return null; + } + + static void test1() { + // :: error: required.method.not.called + CreatesMustCallForRepeat cos1 = makeNoMC(); + // :: error: required.method.not.called + CreatesMustCallForRepeat cos2 = makeNoMC(); + @MustCall({}) CreatesMustCallForRepeat a = cos2; + @MustCall({}) CreatesMustCallForRepeat a2 = cos2; + cos2.a(); + cos1.reset(cos2); + // :: error: assignment.type.incompatible + @CalledMethods({"reset"}) CreatesMustCallForRepeat b = cos1; + @CalledMethods({}) CreatesMustCallForRepeat c = cos1; + @CalledMethods({}) CreatesMustCallForRepeat d = cos2; + // :: error: assignment.type.incompatible + @CalledMethods({"a"}) CreatesMustCallForRepeat e = cos2; + } + + static void test3() { + // :: error: required.method.not.called + CreatesMustCallForRepeat cos = new CreatesMustCallForRepeat(); + // :: error: required.method.not.called + CreatesMustCallForRepeat cos2 = new CreatesMustCallForRepeat(); + cos.a(); + cos.reset(cos2); + } + + static void test4() { + CreatesMustCallForRepeat cos = new CreatesMustCallForRepeat(); + // :: error: required.method.not.called + CreatesMustCallForRepeat cos2 = new CreatesMustCallForRepeat(); + cos.a(); + cos.reset(cos2); + cos.a(); + } + + static void test5() { + // :: error: required.method.not.called + CreatesMustCallForRepeat cos = new CreatesMustCallForRepeat(); + CreatesMustCallForRepeat cos2 = new CreatesMustCallForRepeat(); + cos.a(); + cos.reset(cos2); + cos2.a(); + } + + static void test6() { + CreatesMustCallForRepeat cos = new CreatesMustCallForRepeat(); + CreatesMustCallForRepeat cos2 = new CreatesMustCallForRepeat(); + cos.a(); + cos.reset(cos2); + cos2.a(); + cos.a(); + } +} diff --git a/checker/tests/resourceleak/CreatesMustCallForSimple.java b/checker/tests/resourceleak/CreatesMustCallForSimple.java new file mode 100644 index 000000000000..4f5829dd5c07 --- /dev/null +++ b/checker/tests/resourceleak/CreatesMustCallForSimple.java @@ -0,0 +1,77 @@ +// A simple test that @CreatesMustCallFor works as intended wrt the Object Construction Checker. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall("a") +class CreatesMustCallForSimple { + + @CreatesMustCallFor + void reset() {} + + @CreatesMustCallFor("this") + void resetThis() {} + + void a() {} + + static @MustCall({}) CreatesMustCallForSimple makeNoMC() { + return null; + } + + static void test1() { + // :: error: required.method.not.called + CreatesMustCallForSimple cos = makeNoMC(); + @MustCall({}) CreatesMustCallForSimple a = cos; + cos.reset(); + // :: error: assignment.type.incompatible + @CalledMethods({"reset"}) CreatesMustCallForSimple b = cos; + @CalledMethods({}) CreatesMustCallForSimple c = cos; + } + + static void test2() { + // :: error: required.method.not.called + CreatesMustCallForSimple cos = makeNoMC(); + @MustCall({}) CreatesMustCallForSimple a = cos; + cos.resetThis(); + // :: error: assignment.type.incompatible + @CalledMethods({"resetThis"}) CreatesMustCallForSimple b = cos; + @CalledMethods({}) CreatesMustCallForSimple c = cos; + } + + static void test3() { + // :: error: required.method.not.called + CreatesMustCallForSimple cos = new CreatesMustCallForSimple(); + cos.a(); + cos.resetThis(); + } + + static void test4() { + CreatesMustCallForSimple cos = new CreatesMustCallForSimple(); + cos.a(); + cos.resetThis(); + cos.a(); + } + + static void test5() { + CreatesMustCallForSimple cos = new CreatesMustCallForSimple(); + cos.resetThis(); + cos.a(); + } + + static void test6(boolean b) { + CreatesMustCallForSimple cos = new CreatesMustCallForSimple(); + if (b) { + cos.resetThis(); + } + cos.a(); + } + + static void test7(boolean b) { + // :: error: required.method.not.called + CreatesMustCallForSimple cos = new CreatesMustCallForSimple(); + cos.a(); + if (b) { + cos.resetThis(); + } + } +} diff --git a/checker/tests/resourceleak/CreatesMustCallForSimpler.java b/checker/tests/resourceleak/CreatesMustCallForSimpler.java new file mode 100644 index 000000000000..c5b2a6a8ed68 --- /dev/null +++ b/checker/tests/resourceleak/CreatesMustCallForSimpler.java @@ -0,0 +1,30 @@ +// A simpler test that @CreatesMustCallFor works as intended wrt the Object Construction Checker. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall("a") +class CreatesMustCallForSimpler { + + @CreatesMustCallFor + void reset() {} + + @CreatesMustCallFor("this") + void resetThis() {} + + void a() {} + + static @MustCall({}) CreatesMustCallForSimpler makeNoMC() { + return null; + } + + static void test1() { + // :: error: required.method.not.called + CreatesMustCallForSimpler cos = makeNoMC(); + @MustCall({}) CreatesMustCallForSimpler a = cos; + cos.reset(); + // :: error: assignment.type.incompatible + @CalledMethods({"reset"}) CreatesMustCallForSimpler b = cos; + @CalledMethods({}) CreatesMustCallForSimpler c = cos; + } +} diff --git a/checker/tests/resourceleak/CreatesMustCallForTargets.java b/checker/tests/resourceleak/CreatesMustCallForTargets.java new file mode 100644 index 000000000000..e809b865878a --- /dev/null +++ b/checker/tests/resourceleak/CreatesMustCallForTargets.java @@ -0,0 +1,90 @@ +// A test that errors are correctly issued when re-assignments don't match the +// create obligation annotation on a method. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +@InheritableMustCall("a") +class CreatesMustCallForTargets { + @Owning InputStream is1; + + @CreatesMustCallFor + // :: error: createsmustcallfor.target.unparseable + // :: error: incompatible.creates.mustcall.for + static void resetObj1(CreatesMustCallForTargets r) throws Exception { + if (r.is1 == null) { + r.is1 = new FileInputStream("foo.txt"); + } + } + + @CreatesMustCallFor("#2") + // :: error: incompatible.creates.mustcall.for + static void resetObj2(CreatesMustCallForTargets r, CreatesMustCallForTargets other) + throws Exception { + if (r.is1 == null) { + r.is1 = new FileInputStream("foo.txt"); + } + } + + @CreatesMustCallFor("#1") + static void resetObj3(CreatesMustCallForTargets r, CreatesMustCallForTargets other) + throws Exception { + if (r.is1 == null) { + r.is1 = new FileInputStream("foo.txt"); + } + } + + @CreatesMustCallFor + void resetObj4(CreatesMustCallForTargets this, CreatesMustCallForTargets other) + throws Exception { + if (is1 == null) { + is1 = new FileInputStream("foo.txt"); + } + } + + @CreatesMustCallFor + // :: error: incompatible.creates.mustcall.for + void resetObj5(CreatesMustCallForTargets this, CreatesMustCallForTargets other) + throws Exception { + if (other.is1 == null) { + other.is1 = new FileInputStream("foo.txt"); + } + } + + @CreatesMustCallFor("#2") + // :: error: createsmustcallfor.target.unparseable + // :: error: incompatible.creates.mustcall.for + void resetObj6(CreatesMustCallForTargets this, CreatesMustCallForTargets other) + throws Exception { + if (other.is1 == null) { + other.is1 = new FileInputStream("foo.txt"); + } + } + + @CreatesMustCallFor("#1") + void resetObj7(CreatesMustCallForTargets this, CreatesMustCallForTargets other) + throws Exception { + if (other.is1 == null) { + other.is1 = new FileInputStream("foo.txt"); + } + } + + @EnsuresCalledMethods(value = "this.is1", methods = "close") + void a() throws Exception { + is1.close(); + } + + @CreatesMustCallFor("#1") + // :: error: creates.mustcall.for.invalid.target + static void testBadCreates(Object o) {} + + static class BadCreatesField { + @Owning Object o; + + @CreatesMustCallFor("this.o") + // :: error: creates.mustcall.for.invalid.target + void badCreatesOnField() {} + } +} diff --git a/checker/tests/resourceleak/CreatesMustCallForTwoAliases.java b/checker/tests/resourceleak/CreatesMustCallForTwoAliases.java new file mode 100644 index 000000000000..80c3025af5b4 --- /dev/null +++ b/checker/tests/resourceleak/CreatesMustCallForTwoAliases.java @@ -0,0 +1,52 @@ +// Test case for taking the least upper bound of obligations. + +import org.checkerframework.checker.mustcall.qual.*; + +public class CreatesMustCallForTwoAliases { + @InheritableMustCall("a") + static class Foo { + + @SuppressWarnings("mustcall") + @MustCall() Foo() { + // unconnected socket like + } + + @CreatesMustCallFor("this") + void reset() {} + + void a() {} + } + + public static void test1() { + Foo a = new Foo(); + // :: error: required.method.not.called + Foo b = a; + b.reset(); + } + + @CreatesMustCallFor("#1") + public static void sneakyReset(Foo f) { + f.reset(); + } + + public static void test2() { + Foo a = new Foo(); + // :: error: required.method.not.called + Foo b = a; + sneakyReset(b); + } + + public static void test3(Foo b) { + Foo a = new Foo(); + // :: error: required.method.not.called + b = a; + sneakyReset(b); + } + + public static void test4(Foo b) { + // :: error: required.method.not.called + Foo a = new Foo(); + b = a; + sneakyReset(a); + } +} diff --git a/checker/tests/resourceleak/DifferentSWKeys.java b/checker/tests/resourceleak/DifferentSWKeys.java new file mode 100644 index 000000000000..4bb74911abe0 --- /dev/null +++ b/checker/tests/resourceleak/DifferentSWKeys.java @@ -0,0 +1,27 @@ +// A test case that the RLC's -AnoCreatesMustCallFor argument doesn't change +// warning suppression behavior for the Must Call Checker. + +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.Owning; + +@SuppressWarnings("required.method.not.called") +class DifferentSWKeys { + void test(@Owning @MustCall("foo") Object obj) { + // :: warning: unneeded.suppression + @SuppressWarnings("mustcall") + @MustCall("foo") Object bar = obj; + } + + void test2(@Owning @MustCall("foo") Object obj) { + // actually needed suppression + @SuppressWarnings("mustcall") + @MustCall({}) Object bar = obj; + } + + void test3(@Owning @MustCall("foo") Object obj) { + // test that the option-specific suppression key doesn't work + @SuppressWarnings("mustcallnocreatesmustcallfor") + // :: error: assignment.type.incompatible + @MustCall({}) Object bar = obj; + } +} diff --git a/checker/tests/resourceleak/DoubleIf.java b/checker/tests/resourceleak/DoubleIf.java new file mode 100644 index 000000000000..7c4163d09f31 --- /dev/null +++ b/checker/tests/resourceleak/DoubleIf.java @@ -0,0 +1,55 @@ +// Based on an FP in ZK. Only #parse threw an error; +// removing either if statement made the code verifiable. +// Adding an @CalledMethods annotation, like in parse4, also +// makes this code verifiable... + +import org.checkerframework.checker.calledmethods.qual.CalledMethods; + +import java.io.*; + +class DoubleIf { + + String fn; + + public void parse(boolean b, boolean c) throws Exception { + if (c) { + FileInputStream fis1 = new FileInputStream(fn); + try { + } finally { + fis1.close(); + } + if (b) {} + } + } + + public void parse2(boolean c) throws Exception { + if (c) { + FileInputStream fis2 = new FileInputStream(fn); + try { + } finally { + fis2.close(); + } + } + } + + public void parse3(boolean b) throws Exception { + FileInputStream fis3 = new FileInputStream(fn); + try { + } finally { + fis3.close(); + } + if (b) {} + } + + public void parse4(boolean b, boolean c) throws Exception { + if (c) { + FileInputStream fis4 = new FileInputStream(fn); + try { + } finally { + fis4.close(); + } + if (b) {} + @CalledMethods("close") FileInputStream fis24 = fis4; + } + } +} diff --git a/checker/tests/resourceleak/DuplicateError.java b/checker/tests/resourceleak/DuplicateError.java new file mode 100644 index 000000000000..6fb0b88d92fa --- /dev/null +++ b/checker/tests/resourceleak/DuplicateError.java @@ -0,0 +1,22 @@ +import org.checkerframework.checker.nullness.qual.KeyForBottom; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.UnknownKeyFor; + +import java.util.List; +import java.util.function.Function; + +public class DuplicateError { + void m(List values) { + @SuppressWarnings("lambda.param.type.incompatible") + List stringVals = DECollectionsPlume.mapList((Object o) -> (String) o, values); + } +} + +class DECollectionsPlume { + public static < + @KeyForBottom FROM extends @Nullable @UnknownKeyFor Object, + @KeyForBottom TO extends @Nullable @UnknownKeyFor Object> + List mapList(Function f, Iterable iterable) { + return null; + } +} diff --git a/checker/tests/resourceleak/Enclosing.java b/checker/tests/resourceleak/Enclosing.java new file mode 100644 index 000000000000..eccdb4ff03ba --- /dev/null +++ b/checker/tests/resourceleak/Enclosing.java @@ -0,0 +1,23 @@ +// test case for https://github.com/kelloggm/object-construction-checker/issues/366 + +import org.checkerframework.checker.mustcall.qual.*; + +class Enclosing { + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + static class Nested { + // :: error: required.method.not.called + @Owning Foo foo; + + @CreatesMustCallFor("this") + // :: error: creates.mustcall.for.invalid.target + void initFoo() { + if (this.foo == null) { + this.foo = new Foo(); + } + } + } +} diff --git a/checker/tests/resourceleak/EnhancedFor.java b/checker/tests/resourceleak/EnhancedFor.java new file mode 100644 index 000000000000..d8e8f84f2223 --- /dev/null +++ b/checker/tests/resourceleak/EnhancedFor.java @@ -0,0 +1,67 @@ +// Based on some false positives I found in ZK. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.IOException; +import java.net.Socket; +import java.util.List; + +class EnhancedFor { + void test(List<@MustCall Socket> list) { + for (Socket s : list) { + try { + s.close(); + } catch (IOException i) { + } + } + } + + void test2(List<@MustCall Socket> list) { + for (int i = 0; i < list.size(); i++) { + Socket s = list.get(i); + try { + s.close(); + } catch (IOException io) { + } + } + } + + // :: error: (type.argument.type.incompatible) + void test3(List list) { + // This error is issued because `s` is a local variable, and + // the foreach loop under the hood assigns the result of a call + // to Iterator#next into it (which is owning by default, because it's + // a method return type). Both this error and the type.argument error + // above can be suppressed by writing @MustCall on the Socket type, as in + // test4 below (but note that this will make call sites difficult to verify). + // :: error: (required.method.not.called) + for (Socket s : list) {} + } + + void test4(List<@MustCall Socket> list) { + for (Socket s : list) {} + } + + void test5(List list) { + for (Socket s : list) {} + } + + void test6(List list) { + for (Socket s : list) { + try { + s.close(); + } catch (IOException i) { + } + } + } + + void test7(List list) { + for (int i = 0; i < list.size(); i++) { + Socket s = list.get(i); + try { + s.close(); + } catch (IOException io) { + } + } + } +} diff --git a/checker/tests/resourceleak/FileDescriptorTest.java b/checker/tests/resourceleak/FileDescriptorTest.java new file mode 100644 index 000000000000..50aad87d5f2d --- /dev/null +++ b/checker/tests/resourceleak/FileDescriptorTest.java @@ -0,0 +1,58 @@ +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.io.IOException; +import java.net.*; + +public abstract class FileDescriptorTest { + // This is the original test case. It fails because `in.close()` might throw an exception, which + // is + // not caught; therefore, file might still be open. + public static void readPropertiesFile(File from) throws IOException { + // This is a false positive. + // :: error: required.method.not.called + RandomAccessFile file = new RandomAccessFile(from, "rws"); + FileInputStream in = null; + try { + in = new FileInputStream(file.getFD()); + file.seek(0); + } finally { + if (in != null) { + in.close(); + } + file.close(); + } + } + + abstract Socket createSocket(); + + // This is a similar test to the above, but without using the indirection through getFD(). + // This test case demonstrates that the problem is not related to getFD(). + // This warning is a false positive, and should be resolved at the same time as the warning + // above. + public void sameScenario_noFD() throws IOException { + // :: error: (required.method.not.called) + Socket sock = createSocket(); + InputStream in = null; + try { + in = sock.getInputStream(); + } finally { + if (in != null) { + in.close(); + } + sock.close(); + } + } + + // This version, written by Narges, does not issue a false positive. + public static void readPropertiesFile_noFP(File from) throws IOException { + RandomAccessFile file = new RandomAccessFile(from, "rws"); + FileInputStream in = null; + try { + in = new FileInputStream(file.getFD()); + in.close(); + } catch (IOException e) { + file.close(); + } + } +} diff --git a/checker/tests/resourceleak/FilesTest.java b/checker/tests/resourceleak/FilesTest.java new file mode 100644 index 000000000000..cdcd41c87bbb --- /dev/null +++ b/checker/tests/resourceleak/FilesTest.java @@ -0,0 +1,18 @@ +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +public class FilesTest { + + void bad(Path p) throws IOException { + // :: error: (required.method.not.called) + Stream s = Files.list(p); + } + + void good(Path p) throws IOException { + try (Stream s = Files.list(p)) { + // empty body + } + } +} diff --git a/checker/tests/resourceleak/GetChannelOnLocks.java b/checker/tests/resourceleak/GetChannelOnLocks.java new file mode 100644 index 000000000000..aafa376a502d --- /dev/null +++ b/checker/tests/resourceleak/GetChannelOnLocks.java @@ -0,0 +1,40 @@ +// Based on a false positive in hdfs +// A test that shows we are handling the sequence of method invocations correctly + +import java.io.IOException; +import java.nio.channels.FileLock; + +class GetChannelOnLocks { + public boolean isLockSupported(FileLock lock, FileLock lock1, FileLock lock2) + throws IOException { + FileLock firstLock = null; + FileLock secondLock = null; + try { + firstLock = lock1; + if (firstLock == null) { + return true; + } + secondLock = lock2; + if (secondLock == null) { + return true; + } + } finally { + if (firstLock != null && firstLock != lock) { + firstLock.release(); + firstLock.channel().close(); + } + if (secondLock != null) { + secondLock.release(); + secondLock.channel().close(); + } + } + return false; + } + + public void isLockSupported2(FileLock lock, FileLock firstLock) throws IOException { + if (firstLock != null && firstLock != lock) { + firstLock.release(); + firstLock.channel().close(); + } + } +} diff --git a/checker/tests/resourceleak/HBaseReport1.java b/checker/tests/resourceleak/HBaseReport1.java new file mode 100644 index 000000000000..d08960e8de98 --- /dev/null +++ b/checker/tests/resourceleak/HBaseReport1.java @@ -0,0 +1,36 @@ +// Based on a HBase false positive +// In this example fstream is @MCA with out and close is called on all +// exit paths but we randomly report a warning for this test case +// @skip-test + +import java.io.*; +import java.util.*; + +class HBaseReport1 { + + public static void test(String fileName) { + FileWriter fstream; + try { + // :: error: required.method.not.called + fstream = new FileWriter(fileName); + } catch (IOException e) { + return; + } + + BufferedWriter out = new BufferedWriter(fstream); + + try { + try { + out.write(fileName + "\n"); + } finally { + try { + out.close(); + } finally { + fstream.close(); + } + } + } catch (IOException e) { + + } + } +} diff --git a/checker/tests/resourceleak/HDFSReport.java b/checker/tests/resourceleak/HDFSReport.java new file mode 100644 index 000000000000..f141eef21905 --- /dev/null +++ b/checker/tests/resourceleak/HDFSReport.java @@ -0,0 +1,24 @@ +class Handler extends Thread { + boolean running; + + static class Call { + boolean isResponseDeferred() { + return true; + } + } + + @Override + public void run() { + while (running) { + Call call = null; + try { + if (running) { + continue; + } + } catch (Exception e) { + } finally { + String s = call.isResponseDeferred() ? ", deferred" : ""; + } + } + } +} diff --git a/checker/tests/resourceleak/HDFSReport2.java b/checker/tests/resourceleak/HDFSReport2.java new file mode 100644 index 000000000000..d5cea231798b --- /dev/null +++ b/checker/tests/resourceleak/HDFSReport2.java @@ -0,0 +1,22 @@ +import java.io.Closeable; + +class RamDiskAsyncLazyPersistService { + + public interface FsVolumeReference extends Closeable {} + + class ReplicaLazyPersistTask implements Runnable { + private final FsVolumeReference targetVolume; + + ReplicaLazyPersistTask(FsVolumeReference targetVolume) { + this.targetVolume = targetVolume; + } + + @Override + public void run() { + try (FsVolumeReference ref = this.targetVolume) { + + } catch (Exception e) { + } + } + } +} diff --git a/checker/tests/resourceleak/HdfsReport3.java b/checker/tests/resourceleak/HdfsReport3.java new file mode 100644 index 000000000000..358550eb5fb4 --- /dev/null +++ b/checker/tests/resourceleak/HdfsReport3.java @@ -0,0 +1,28 @@ +// Based on a false positive in hdfs + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +import java.io.*; +import java.net.*; +import java.nio.*; +import java.nio.file.*; +import java.security.*; +import java.util.*; + +import javax.net.ssl.*; + +class HdfsReport3 { + private StringBuffer nonObligationTest(int id) { + final StringWriter out = new StringWriter(); + dumpTreeRecursively(new PrintWriter(out, true), new StringBuilder(), id); + return out.getBuffer(); + } + + public void dumpTreeRecursively(PrintWriter out, StringBuilder prefix, int snapshotId) {} + + // StringBuilder doesn't implement closeable + private final StringBuilder sb = new StringBuilder(); + private final Formatter formatter = new Formatter(sb); +} diff --git a/checker/tests/resourceleak/IOUtilsTest.java b/checker/tests/resourceleak/IOUtilsTest.java new file mode 100644 index 000000000000..eaa1721c8fae --- /dev/null +++ b/checker/tests/resourceleak/IOUtilsTest.java @@ -0,0 +1,17 @@ +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class IOUtilsTest { + static void test1(@Owning InputStream inputStream) { + org.apache.commons.io.IOUtils.closeQuietly(inputStream); + } + + static void test2(@Owning InputStream inputStream) throws IOException { + try { + InputStream other = org.apache.commons.io.IOUtils.toBufferedInputStream(inputStream); + } finally { + inputStream.close(); + } + } +} diff --git a/checker/tests/resourceleak/IgnoredExceptionECM.java b/checker/tests/resourceleak/IgnoredExceptionECM.java new file mode 100644 index 000000000000..4aabe7988ac1 --- /dev/null +++ b/checker/tests/resourceleak/IgnoredExceptionECM.java @@ -0,0 +1,20 @@ +// A test that the Resource Leak Checker ignores exceptions in destructors the same way that it +// does in the consistency checker. + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall("foo") +class IgnoredExceptionECM { + + @Owning + @MustCall("toString") Object obj; + + @EnsuresCalledMethods(value = "this.obj", methods = "toString") + void foo() { + // This line will produce an exception, + // which the RLC should ignore and verify the method. + int y = 5 / 0; + this.obj.toString(); + } +} diff --git a/checker/tests/resourceleak/IndexMode.java b/checker/tests/resourceleak/IndexMode.java new file mode 100644 index 000000000000..d5235c3539bf --- /dev/null +++ b/checker/tests/resourceleak/IndexMode.java @@ -0,0 +1,84 @@ +// A test for a new false positive issued in release 3.36.0 but not 3.35.0. +// Reported as part of https://github.com/typetools/checker-framework/issues/6077. + +import org.checkerframework.checker.mustcall.qual.MustCall; + +import java.io.InputStream; +import java.util.Map; + +public class IndexMode { + public static Object getMode(Map indexOptions) { + // Try-catch is needed, otherwise no FP. + try { + String literalOption = indexOptions.get("is_literal"); + } catch (Exception e) { + } + + // Actual return type rather than void is needed, otherwise no FP. + return null; + } + + // This copy of getMode() adds an explicit `@MustCall` annotation to the String. + public static Object getMode2(Map indexOptions) { + try { + // TODO: a required.method.not.called error should be issued on this line, but currently + // it is not. The reason is an interaction between type variable defaulting, + // local dataflow, and the rules that the RLC uses for choosing a variable's must-call + // obligations: local inference defaults literalOption to @MustCallUnknown (i.e., the + // top MustCall type) even though the RHS expression's type is @MustCall("hashCode"). + // Then, the rule for obligations says that if a variable has the top must-call type, + // use the type's default must-call type instead. For String, this is @MustCall({}), + // so no error is issued. This rule is important to avoid false positives in realistic + // code (such as the first getMode() method in this class). + String literalOption = indexOptions.get("is_literal"); + } catch (Exception e) { + } + + return null; + } + + // This copy of getMode() adds an explicit `@MustCall` annotation to the String and to + // the local variable. This version currently works as expected, unlike getMode2(). + public static Object getMode2a(Map indexOptions) { + try { + // :: error: required.method.not.called + @MustCall("hashCode") String literalOption = indexOptions.get("is_literal"); + } catch (Exception e) { + } + + return null; + } + + // This copy of getMode() adds an explicit `@MustCall` annotation to the String and removes + // the try-catch. + public static Object getMode3(Map indexOptions) { + // :: error: required.method.not.called + String literalOption = indexOptions.get("is_literal"); + return null; + } + + // This copy of getMode() adds an explicit `@MustCall` annotation to the String, removes + // the try-catch, and makes the return type void. + public static void getMode4(Map indexOptions) { + // :: error: required.method.not.called + String literalOption = indexOptions.get("is_literal"); + } + + // This copy of getMode() removes the try-catch and makes the return type void. + public static void getMode5(Map indexOptions) { + String literalOption = indexOptions.get("is_literal"); + } + + // This variant uses an InputStream (which has a MustCall type by default) as the + // value type in the map. + // :: error: type.argument.type.incompatible + public static Object getModeIS(Map indexOptions) { + try { + // :: error: required.method.not.called + InputStream literalOption = indexOptions.get("is_literal"); + } catch (Exception e) { + } + + return null; + } +} diff --git a/checker/tests/resourceleak/InheritanceStream.java b/checker/tests/resourceleak/InheritanceStream.java new file mode 100644 index 000000000000..893319b7dfe6 --- /dev/null +++ b/checker/tests/resourceleak/InheritanceStream.java @@ -0,0 +1,22 @@ +// A test that checks that an empty @MustCall annotation in a stub on a subclass overrides +// a non-empty one on a superclass (that is being inherited). + +import java.io.*; + +class InheritanceStream { + void testBAIS(byte[] buf) { + new ByteArrayInputStream(buf); + } + + void testBAOS() { + new ByteArrayOutputStream(); + } + + void testSR(String buf) { + new StringReader(buf); + } + + void testSW() { + new StringWriter(); + } +} diff --git a/checker/tests/resourceleak/InitializationAfterSuperTest.java b/checker/tests/resourceleak/InitializationAfterSuperTest.java new file mode 100644 index 000000000000..c723c79e3e36 --- /dev/null +++ b/checker/tests/resourceleak/InitializationAfterSuperTest.java @@ -0,0 +1,27 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5762 + +// @skip-test until the bug is fixed + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.mustcall.qual.Owning; + +import java.io.IOException; +import java.net.Socket; + +@InheritableMustCall("close") +public class InitializationAfterSuperTest implements AutoCloseable { + + @Owning Socket mySocket; + + public InitializationAfterSuperTest(@Owning Socket mySocket) { + super(); + this.mySocket = mySocket; + } + + @EnsuresCalledMethods(value = "mySocket", methods = "close") + @Override + public void close() throws IOException { + mySocket.close(); + } +} diff --git a/checker/tests/resourceleak/InputOutputStreams.java b/checker/tests/resourceleak/InputOutputStreams.java new file mode 100644 index 000000000000..9d45ae2f9d5a --- /dev/null +++ b/checker/tests/resourceleak/InputOutputStreams.java @@ -0,0 +1,188 @@ +// A MustCallAlias test wrt sockets. Also coincidentally tests that MCA sets can be larger than two. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.io.IOException; +import java.net.*; + +abstract class InputOutputStreams { + abstract Socket newSocket(); + + void test_close_sock(@Owning Socket sock) throws IOException { + try { + InputStream is = sock.getInputStream(); + OutputStream os = sock.getOutputStream(); + } catch (IOException e) { + + } finally { + sock.close(); + } + } + + // getInputStream()/getOutputStream() can throw IOException in three different scenarios: + // 1) The underlying socket is already closed + // 2) The underlying socket is not connected + // 3) The underlysing socket input is shutdown + // In the first case our checker always reports a false positive but for the second case + // and third case our checker has to verify that close is called on the underlying resource. + // So, because sock.getInputStream() can throw IOException, "is" can be null, then sock will + // remain open. So, it's a true positive warning. + // :: error: required.method.not.called + void test_close_is(@Owning Socket sock) throws IOException { + InputStream is = null; + OutputStream os = null; + try { + is = sock.getInputStream(); + os = sock.getOutputStream(); + } catch (IOException e) { + + } finally { + is.close(); + } + } + + // NOTE (2023/12/8): Previously the RLC reported required.method.not.called + // on this method. However, the code is actually safe because we are not + // obligated to close @Owning parameters when a method throws an exception + // (see https://github.com/typetools/checker-framework/pull/6241). + void test_close_os_old(@Owning Socket sock) throws IOException { + InputStream is = sock.getInputStream(); + OutputStream os = sock.getOutputStream(); + os.close(); + } + + void test_close_os() throws IOException { + // :: error: (required.method.not.called) + Socket sock = newSocket(); + InputStream is = sock.getInputStream(); + OutputStream os = sock.getOutputStream(); + os.close(); + } + + // :: error: required.method.not.called + void test_close_os2(@Owning Socket sock) throws IOException { + OutputStream os = null; + InputStream is = null; + try { + is = sock.getInputStream(); + } catch (IOException e) { + try { + os = sock.getOutputStream(); + } catch (IOException ee) { + } + } finally { + os.close(); + } + } + + // NOTE (2023/12/8): Previously the RLC reported required.method.not.called + // on this method. However, the code is actually safe because we are not + // obligated to close @Owning parameters when a method throws an exception + // (see https://github.com/typetools/checker-framework/pull/6241). + void test_close_os3_old(@Owning Socket sock) throws IOException { + OutputStream os = null; + try { + InputStream is = sock.getInputStream(); + } catch (IOException e) { + } + try { + os = sock.getOutputStream(); + } finally { + os.close(); + } + } + + void test_close_os3() throws IOException { + // :: error: (required.method.not.called) + Socket sock = newSocket(); + OutputStream os = null; + try { + InputStream is = sock.getInputStream(); + } catch (IOException e) { + } + try { + os = sock.getOutputStream(); + } finally { + os.close(); + } + } + + // NOTE (2023/12/8): Previously the RLC reported required.method.not.called + // on this method. However, the code is actually safe because we are not + // obligated to close @Owning parameters when a method throws an exception + // (see https://github.com/typetools/checker-framework/pull/6241). + void test_close_os4_old(@Owning Socket sock) throws IOException { + OutputStream os = null; + try { + InputStream is = sock.getInputStream(); + } catch (IOException e) { + } + try { + os = sock.getOutputStream(); + } finally { + if (os != null) { + os.close(); + } else { + sock.close(); + } + } + } + + // TODO this case requires more general tracking of additional boolean conditions + // If getOutputStream() throws an IOException, then the os variable remains definitely null. + // When our worklist analysis gets to the finally block along the corresponding CFG edge, + // it does not know that os is definitely null, and it is only tracking sock as a name for + // the resource. So, it analyzes a path through the "then" branch of the conditional, + // where sock.close() is not invoked, and reports an error. + void test_close_os4() throws IOException { + // :: error: (required.method.not.called) + Socket sock = newSocket(); + OutputStream os = null; + try { + InputStream is = sock.getInputStream(); + } catch (IOException e) { + } + try { + os = sock.getOutputStream(); + } finally { + if (os != null) { + os.close(); + } else { + sock.close(); + } + } + } + + // :: error: required.method.not.called + void test_close_buff(@Owning Socket sock) throws IOException { + BufferedOutputStream buff = null; + try { + InputStream is = sock.getInputStream(); + OutputStream os = sock.getOutputStream(); + buff = new BufferedOutputStream(os); + } catch (IOException e) { + + } finally { + buff.close(); + } + } + + void test_write(String host, int port) { + Socket sock = null; + try { + sock = new Socket(host, port); + sock.getOutputStream().write("isro".getBytes()); + } catch (Exception e) { + System.err.println("write failed: " + e); + } finally { + if (sock != null) { + try { + sock.close(); + } catch (IOException e) { + System.err.println("couldn't close socket!"); + } + } + } + } +} diff --git a/checker/tests/resourceleak/InstanceInitializer.java b/checker/tests/resourceleak/InstanceInitializer.java new file mode 100644 index 000000000000..4bb2a951ed42 --- /dev/null +++ b/checker/tests/resourceleak/InstanceInitializer.java @@ -0,0 +1,57 @@ +// A test that the checker is sound in the presence of instance initializer blocks. +// In the resourceleak-permitinitializationleak/ directory, it's a test that the +// checker is unsound with the -ApermitInitializationLeak command-line argument. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.net.Socket; + +class InstanceInitializer { + // :: error: required.method.not.called + private @Owning Socket s; + + private final int DEFAULT_PORT = 5; + private final String DEFAULT_ADDR = "localhost"; + + { + try { + // This assignment is OK, because it's the first assignment. + // However, the Resource Leak Checker issues a false positive warning. + // :: error: required.method.not.called + s = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } catch (Exception e) { + } + } + + { + try { + // This assignment is not OK, because it's a reassignment without satisfying the + // mustcall obligations of the previous value of `s`. + // :: error: required.method.not.called + s = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } catch (Exception e) { + } + } + + { + try { + // :: error: required.method.not.called + Socket s1 = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } catch (Exception e) { + } + } + + { + Socket s1 = null; + try { + s1 = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } catch (Exception e) { + } + s1.close(); + } + + public InstanceInitializer() throws Exception { + // :: error: required.method.not.called + s = new Socket(DEFAULT_ADDR, DEFAULT_PORT); + } +} diff --git a/checker/tests/resourceleak/IsClosed.java b/checker/tests/resourceleak/IsClosed.java new file mode 100644 index 000000000000..207027f646e9 --- /dev/null +++ b/checker/tests/resourceleak/IsClosed.java @@ -0,0 +1,29 @@ +// A test that the EnsuresCalledMethodsIf annotations in +// the Socket stub files are respected. + +import org.checkerframework.checker.mustcall.qual.Owning; + +import java.io.*; +import java.net.*; + +class IsClosed { + void test_socket(@Owning Socket sock) { + if (!sock.isClosed()) { + try { + sock.close(); + } catch (IOException io) { + + } + } + } + + void test_server_socket(@Owning ServerSocket sock) { + if (!sock.isClosed()) { + try { + sock.close(); + } catch (IOException io) { + + } + } + } +} diff --git a/checker/tests/resourceleak/Issue4815.java b/checker/tests/resourceleak/Issue4815.java new file mode 100644 index 000000000000..212d7cdeb824 --- /dev/null +++ b/checker/tests/resourceleak/Issue4815.java @@ -0,0 +1,18 @@ +// Test case for https://tinyurl.com/cfissue/4815 + +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.Owning; + +import java.util.List; + +public class Issue4815 { + public void initialize( + List list, @Owning @MustCall("initialize") T object) { + object.initialize(); + list.add(object); + } + + private static class Component { + void initialize() {} + } +} diff --git a/checker/tests/resourceleak/Issue6030.java b/checker/tests/resourceleak/Issue6030.java new file mode 100644 index 000000000000..7ae668c44427 --- /dev/null +++ b/checker/tests/resourceleak/Issue6030.java @@ -0,0 +1,30 @@ +// Test case for https://github.com/typetools/checker-framework/issues/6030 + +import org.checkerframework.checker.mustcall.qual.Owning; + +import java.util.*; + +public class Issue6030 { + + interface CloseableIterator extends Iterator, java.io.Closeable {} + + static class MyScanner> implements CloseableIterator { + @Owning I iterator; + + // :: error: missing.creates.mustcall.for + public boolean hasNext() { + if (iterator == null) iterator = createIterator(); + return iterator.hasNext(); + } + + public T next() { + return null; + } + + private I createIterator() { + return null; + } + + public void close() {} + } +} diff --git a/checker/tests/resourceleak/JavaEETest.java b/checker/tests/resourceleak/JavaEETest.java new file mode 100644 index 000000000000..b4fc459de04c --- /dev/null +++ b/checker/tests/resourceleak/JavaEETest.java @@ -0,0 +1,7 @@ +// Test for https://github.com/typetools/checker-framework/issues/5472 + +class JavaEETest { + static void foo(javax.servlet.ServletResponse s) throws java.io.IOException { + s.getWriter(); + } +} diff --git a/checker/tests/resourceleak/LemmaStack.java b/checker/tests/resourceleak/LemmaStack.java new file mode 100644 index 000000000000..2aaeae52e773 --- /dev/null +++ b/checker/tests/resourceleak/LemmaStack.java @@ -0,0 +1,49 @@ +// Test case for issue #5175: https://github.com/typetools/checker-framework/issues/5175 + +// The Resource Leak Checker issues the following error: +// LemmaStack.java:40: error: [reset.not.owning] Calling method startProver resets the must-call +// obligations of the expression this, which is non-owning. Either annotate its declaration with an +// @Owning annotation, extract it into a local variable, or write a corresponding +// @CreatesMustCallFor annotation on the method that encloses this statement. +// startProver(); +// ^ +// 1 error + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.Owning; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; + +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UncheckedIOException; + +@MustCall("close") public class LemmaStack implements Closeable { + + private @Owning @MustCall("close") PrintWriter session; + + @CreatesMustCallFor("this") + @EnsuresNonNull("session") + private void startProver() { + try { + if (session != null) { + session.close(); + } + session = new PrintWriter("filename.txt"); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public LemmaStack() { + startProver(); + } + + @EnsuresCalledMethods(value = "session", methods = "close") + @Override + public void close(LemmaStack this) { + session.close(); + } +} diff --git a/checker/tests/resourceleak/LhsArrayCast.java b/checker/tests/resourceleak/LhsArrayCast.java new file mode 100644 index 000000000000..452fe70d1e2d --- /dev/null +++ b/checker/tests/resourceleak/LhsArrayCast.java @@ -0,0 +1,8 @@ +class LhsArrayCast { + + void populateWithSamples(Object[] currentSample) { + int j = 22; + int k = 42; + ((String[]) currentSample[j])[k] = "hello"; + } +} diff --git a/checker/tests/resourceleak/LineNumberReaderTest.java b/checker/tests/resourceleak/LineNumberReaderTest.java new file mode 100644 index 000000000000..011c0df7daf6 --- /dev/null +++ b/checker/tests/resourceleak/LineNumberReaderTest.java @@ -0,0 +1,38 @@ +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.mustcall.qual.Owning; +import org.checkerframework.checker.nullness.qual.RequiresNonNull; + +import java.io.*; + +class LineNumberReaderTest implements Closeable { + + private final @Owning LineNumberReader reader; + + LineNumberReaderTest(File toRead) { + LineNumberReader reader = null; + try { + reader = new LineNumberReader(new FileReader(toRead)); + this.reader = reader; + advance(); + } catch (IOException e) { + if (reader != null) { + try { + reader.close(); + } catch (Exception exceptionOnClose) { + e.addSuppressed(exceptionOnClose); + } + } + throw new RuntimeException(e); + } + } + + @RequiresNonNull("reader") + protected void advance(@UnknownInitialization LineNumberReaderTest this) throws IOException {} + + @Override + @EnsuresCalledMethods(value = "reader", methods = "close") + public void close() throws IOException { + reader.close(); + } +} diff --git a/checker/tests/resourceleak/MCANotOwningField.java b/checker/tests/resourceleak/MCANotOwningField.java new file mode 100644 index 000000000000..5015283c5a16 --- /dev/null +++ b/checker/tests/resourceleak/MCANotOwningField.java @@ -0,0 +1,17 @@ +// A test case that a MustCallAlias value with a non-owning field with a must-call obligation +// does not lead to a false positive. + +import java.net.Socket; + +class MCANotOwningField { + + final Socket s; + + MCANotOwningField(Socket s) throws Exception { + this.s = s; + } + + void simple() throws Exception { + s.getInputStream(); + } +} diff --git a/checker/tests/resourceleak/MCAOwningField.java b/checker/tests/resourceleak/MCAOwningField.java new file mode 100644 index 000000000000..4173742c8cf7 --- /dev/null +++ b/checker/tests/resourceleak/MCAOwningField.java @@ -0,0 +1,26 @@ +// A test case for a common pattern in Zookeeper: something is must-call-alias +// with an owning field, and therefore a false positive was issued. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.net.Socket; + +@InheritableMustCall("stop") +class MCAOwningField { + + @Owning final Socket s; + + MCAOwningField() throws Exception { + s = new Socket(); + } + + void simple() throws Exception { + s.getInputStream(); + } + + @EnsuresCalledMethods(value = "s", methods = "close") + void stop() throws Exception { + s.close(); + } +} diff --git a/checker/tests/resourceleak/MCAWithThis.java b/checker/tests/resourceleak/MCAWithThis.java new file mode 100644 index 000000000000..ae76db6fbb28 --- /dev/null +++ b/checker/tests/resourceleak/MCAWithThis.java @@ -0,0 +1,18 @@ +// A test that when a must-call class with MustCallAlias methods +// is extended, those methods can be used without false positives. + +import java.net.Socket; + +class MCAWithThis extends Socket { + public MCAWithThis() { + super(); + } + + public void test() throws Exception { + this.getInputStream(); + } + + public void test2() throws Exception { + getInputStream(); + } +} diff --git a/checker/tests/resourceleak/ManualMustCallEmptyOnConstructor.java b/checker/tests/resourceleak/ManualMustCallEmptyOnConstructor.java new file mode 100644 index 000000000000..f15c12984c0c --- /dev/null +++ b/checker/tests/resourceleak/ManualMustCallEmptyOnConstructor.java @@ -0,0 +1,26 @@ +// test for https://github.com/kelloggm/object-construction-checker/issues/326 + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +import java.io.InputStream; + +class ManualMustCallEmptyOnConstructor { + + // Test that writing @MustCall({}) on a constructor results in an error + @InheritableMustCall("a") + static class Foo { + final @Owning InputStream is; + + // :: error: inconsistent.constructor.type + @MustCall({}) Foo(@Owning InputStream is) { + this.is = is; + } + + @EnsuresCalledMethods(value = "this.is", methods = "close") + void a() throws Exception { + is.close(); + } + } +} diff --git a/checker/tests/resourceleak/MultipleIdenticalReturns.java b/checker/tests/resourceleak/MultipleIdenticalReturns.java new file mode 100644 index 000000000000..ba24c6fde736 --- /dev/null +++ b/checker/tests/resourceleak/MultipleIdenticalReturns.java @@ -0,0 +1,37 @@ +import java.io.IOException; +import java.io.InputStream; + +/** + * Tests that the detector works properly in the presence of multiple identical return statements in + * a method + */ +class MultipleIdenticalReturns { + static class Repro { + private java.lang.ClassLoader loader; + + public Object loadClass(final String className) throws ClassNotFoundException { + final String classFile = className.replace('.', '/'); + Object RC = null; + if (RC != null) { + return RC; + } + try (InputStream is = loader.getResourceAsStream(classFile + ".class")) { + // no warning here, since parse() is invoked in parser + ClassParser parser = new ClassParser(); + RC = parser.parse(); + return RC; + } catch (final IOException e) { + throw new ClassNotFoundException(className + " not found: " + e, e); + } + } + } + + @org.checkerframework.checker.mustcall.qual.InheritableMustCall("parse") + static class ClassParser { + public ClassParser() {} + + public Object parse() { + return null; + } + } +} diff --git a/checker/tests/resourceleak/MultipleMethodParamsMustCallAliasTest.java b/checker/tests/resourceleak/MultipleMethodParamsMustCallAliasTest.java new file mode 100644 index 000000000000..7a7ee24c056c --- /dev/null +++ b/checker/tests/resourceleak/MultipleMethodParamsMustCallAliasTest.java @@ -0,0 +1,92 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +import java.io.*; +import java.net.*; + +class MultipleMethodParamsMustCallAliasTest { + + void testMultiMethodParamsCorrect1(@Owning InputStream in1, @Owning InputStream in2) + throws IOException { + + ReplicaInputStreams r = new ReplicaInputStreams(in1, in2); + + r.close(); + } + + void testMultiMethodParamsCorrect2(@Owning InputStream in1, @Owning InputStream in2) + throws IOException { + + ReplicaInputStreams r = new ReplicaInputStreams(in1, in2); + + try { + in1.close(); + } catch (IOException e) { + } finally { + in2.close(); + } + } + + void testMultiMethodParamsCorrect3(@Owning InputStream in1, @Owning InputStream in2) + throws IOException { + + ReplicaInputStreams r = new ReplicaInputStreams(in1, in2); + + try { + in1.close(); + } finally { + in2.close(); + } + } + + // :: error: required.method.not.called + void testMultiMethodParamsWrong1(@Owning InputStream in1, @Owning InputStream in2) + throws IOException { + + ReplicaInputStreams r = new ReplicaInputStreams(in1, in2); + + in1.close(); + } + + // :: error: required.method.not.called + void testMultiMethodParamsWrong2(@Owning InputStream in1, @Owning InputStream in2) + throws IOException { + + ReplicaInputStreams r = new ReplicaInputStreams(in1, in2); + + in2.close(); + } + + // :: error: required.method.not.called + void testMultiMethodParamsWrong3(@Owning InputStream in1) throws IOException { + // :: error: required.method.not.called + Socket socket = new Socket("address", 12); + ReplicaInputStreams r = new ReplicaInputStreams(in1, socket.getInputStream()); + } + + class ReplicaInputStreams implements Closeable { + + private final @Owning InputStream in1; + private final @Owning InputStream in2; + + public @MustCallAlias ReplicaInputStreams( + // This class is unsafe: calling close on i1 doesn't result in calling close on i2, + // so this MustCallAlias relationship shouldn't be verified. + // :: error: mustcallalias.out.of.scope + @MustCallAlias InputStream i1, @MustCallAlias InputStream i2) { + this.in1 = i1; + this.in2 = i2; + } + + @Override + @EnsuresCalledMethods( + value = {"this.in1", "this.in2"}, + methods = {"close"}) + // :: error: (contracts.exceptional.postcondition.not.satisfied) + public void close() throws IOException { + in1.close(); + in2.close(); + } + } +} diff --git a/checker/tests/resourceleak/MultipleOwnedResources.java b/checker/tests/resourceleak/MultipleOwnedResources.java new file mode 100644 index 000000000000..5ae8e56825b0 --- /dev/null +++ b/checker/tests/resourceleak/MultipleOwnedResources.java @@ -0,0 +1,29 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5911 + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class MultipleOwnedResources implements Closeable { + + private final @Owning Closeable r1; + private final @Owning Closeable r2; + + public MultipleOwnedResources(@Owning Closeable r1, @Owning Closeable r2) { + this.r1 = r1; + this.r2 = r2; + } + + @Override + @EnsuresCalledMethods( + value = {"r1", "r2"}, + methods = {"close"}) + public void close() throws IOException { + try { + r1.close(); + } finally { + r2.close(); + } + } +} diff --git a/checker/tests/resourceleak/MultipleOwnedResourcesOfDifferentTypes.java b/checker/tests/resourceleak/MultipleOwnedResourcesOfDifferentTypes.java new file mode 100644 index 000000000000..1478d1f46769 --- /dev/null +++ b/checker/tests/resourceleak/MultipleOwnedResourcesOfDifferentTypes.java @@ -0,0 +1,44 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5911 + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +class MultipleOwnedResourcesOfDifferentTypes { + + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + @InheritableMustCall("b") + static class Bar { + void b() {} + } + + @InheritableMustCall("finalizer") + static class OwningField { + + private final @Owning Foo owningFoo; + + private final @Owning Bar owningBar; + + public OwningField() { + this.owningFoo = new Foo(); + this.owningBar = new Bar(); + } + + @EnsuresCalledMethods( + value = {"owningBar"}, + methods = {"b"}) + @EnsuresCalledMethods( + value = {"this.owningFoo"}, + methods = {"a"}) + void finalizer() { + try { + this.owningFoo.a(); + } finally { + this.owningBar.b(); + } + } + } +} diff --git a/checker/tests/resourceleak/MustCallAliasDifferentMethodNames.java b/checker/tests/resourceleak/MustCallAliasDifferentMethodNames.java new file mode 100644 index 000000000000..4e61e6c94fae --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasDifferentMethodNames.java @@ -0,0 +1,46 @@ +// Test case to check that a wrapper type can have a @MustCall method with a different name than +// the @MustCall method of the type it wraps. +// See https://github.com/typetools/checker-framework/issues/4947 + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.mustcall.qual.MustCallAlias; +import org.checkerframework.checker.mustcall.qual.Owning; + +class MustCallAliasDifferentMethodNames { + + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + @InheritableMustCall("b") + static class FooField { + private final @Owning Foo finalOwningFoo; + + public @MustCallAlias FooField(@MustCallAlias Foo f) { + this.finalOwningFoo = f; + } + + @EnsuresCalledMethods( + value = {"this.finalOwningFoo"}, + methods = {"a"}) + void b() { + this.finalOwningFoo.a(); + } + } + + void testField1() { + Foo f = new Foo(); + FooField fooFieldWrapper = new FooField(f); + // Either calling f.a() or fooFieldWrapper.b() satisfies the obligation + fooFieldWrapper.b(); + } + + void testField2() { + Foo f = new Foo(); + FooField fooFieldWrapper = new FooField(f); + // Either calling f.a() or fooFieldWrapper.b() satisfies the obligation + f.a(); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasExamples.java b/checker/tests/resourceleak/MustCallAliasExamples.java new file mode 100644 index 000000000000..d21d5908c6e3 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasExamples.java @@ -0,0 +1,54 @@ +// Simple tests of @MustCallAlias functionality on wrapper streams. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.io.IOException; +import java.net.*; + +class MustCallAliasExamples { + + void test_two_locals(String address) { + Socket socket = null; + try { + socket = new Socket(address, 8000); + DataInputStream d = new DataInputStream(socket.getInputStream()); + } catch (IOException e) { + + } finally { + closeSocket(socket); + } + } + + void test_close_wrapper(@Owning InputStream b) throws IOException { + DataInputStream d = new DataInputStream(b); + d.close(); + } + + void test_close_nonwrapper(@Owning InputStream b) throws IOException { + DataInputStream d = new DataInputStream(b); + b.close(); + } + + // :: error: required.method.not.called + void test_no_close(@Owning InputStream b) { + DataInputStream d = new DataInputStream(b); + } + + // :: error: required.method.not.called + void test_no_assign(@Owning InputStream b) { + new DataInputStream(new BufferedInputStream(b)); + } + + @EnsuresCalledMethods(value = "#1", methods = "close") + void closeSocket(Socket sock) { + try { + if (sock != null) { + sock.close(); + } + } catch (IOException e) { + + } + } +} diff --git a/checker/tests/resourceleak/MustCallAliasImpl.java b/checker/tests/resourceleak/MustCallAliasImpl.java new file mode 100644 index 000000000000..91f17a4b9156 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasImpl.java @@ -0,0 +1,24 @@ +// A simple test that the extra obligations that MustCallAlias imposes are +// respected. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +public class MustCallAliasImpl implements Closeable { + + final @Owning Closeable foo; + + public @MustCallAlias MustCallAliasImpl(@MustCallAlias Closeable foo) { + this.foo = foo; + } + + @Override + @EnsuresCalledMethods( + value = {"this.foo"}, + methods = {"close"}) + public void close() throws IOException { + this.foo.close(); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasImplWrong1.java b/checker/tests/resourceleak/MustCallAliasImplWrong1.java new file mode 100644 index 000000000000..7d07c69e6ba5 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasImplWrong1.java @@ -0,0 +1,26 @@ +// A simple test that the extra obligations that MustCallAlias imposes are +// respected. This version gets it wrong by not assigning the MCA param +// to a field. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +public class MustCallAliasImplWrong1 implements Closeable { + + final @Owning Closeable foo; + + // :: error: mustcallalias.out.of.scope + public @MustCallAlias MustCallAliasImplWrong1(@MustCallAlias Closeable foo) { + this.foo = null; + } + + @Override + @EnsuresCalledMethods( + value = {"this.foo"}, + methods = {"close"}) + public void close() throws IOException { + this.foo.close(); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasImplWrong2.java b/checker/tests/resourceleak/MustCallAliasImplWrong2.java new file mode 100644 index 000000000000..e031e88eeb23 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasImplWrong2.java @@ -0,0 +1,25 @@ +// A simple test that the extra obligations that MustCallAlias imposes are +// respected. This version gets it wrong by assigning the MCA param to a non-owning +// field. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +public class MustCallAliasImplWrong2 implements Closeable { + + final /*@Owning*/ Closeable foo; + + // :: error: (mustcallalias.out.of.scope) + public @MustCallAlias MustCallAliasImplWrong2(@MustCallAlias Closeable foo) { + // The following error isn't really desirable, but occurs because the special case + // in the Must Call Checker for assigning @MustCallAlias parameters to @Owning fields + // is not triggered. + // :: error: (assignment.type.incompatible) + this.foo = foo; + } + + @Override + public void close() {} +} diff --git a/checker/tests/resourceleak/MustCallAliasInitializeWithOwningParameter.java b/checker/tests/resourceleak/MustCallAliasInitializeWithOwningParameter.java new file mode 100644 index 000000000000..187018627724 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasInitializeWithOwningParameter.java @@ -0,0 +1,33 @@ +// Test that methods like `allocateAndInitializeService` are accepted by +// the resource leak checker. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +public class MustCallAliasInitializeWithOwningParameter { + + public Service allocateAndInitializeService(@Owning Closeable resource) throws IOException { + Service service = new Service(resource); + service.initialize(); + return service; + } + + private static class Service implements Closeable { + + private final @Owning Closeable wrappedResource; + + public @MustCallAlias Service(@MustCallAlias Closeable resource) { + this.wrappedResource = resource; + } + + public void initialize() throws IOException {} + + @Override + @EnsuresCalledMethods(value = "wrappedResource", methods = "close") + public void close() throws IOException { + wrappedResource.close(); + } + } +} diff --git a/checker/tests/resourceleak/MustCallAliasLayeredStreams.java b/checker/tests/resourceleak/MustCallAliasLayeredStreams.java new file mode 100644 index 000000000000..29303050e635 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasLayeredStreams.java @@ -0,0 +1,23 @@ +// Test case based on an MCA situation in Zookeeper. +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class MustCallAliasLayeredStreams { + InputStream cache; + + public InputStream createInputStream(String filename) throws FileNotFoundException { + if (cache == null) { + // The real version of this uses a mix of JDK and custom streams, so it makes more + // sense... + // TODO we shouldn't report a warning here and the code is okay because the cache is + // non-owning, and the caller of createInputStream is the owner of all of these streams. + cache = + new DataInputStream( + // :: error: required.method.not.called + new BufferedInputStream(new FileInputStream(new File(filename)))); + } + return cache; + } +} diff --git a/checker/tests/resourceleak/MustCallAliasLocal.java b/checker/tests/resourceleak/MustCallAliasLocal.java new file mode 100644 index 000000000000..86b60579bdd7 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasLocal.java @@ -0,0 +1,27 @@ +// Test case for a set of false positives caused by the Must Call Checker's handling +// of assigning @MustCallAlias parameters to @Owning fields as a special case. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +public class MustCallAliasLocal implements Closeable { + + final @Owning Closeable foo; + + public @MustCallAlias MustCallAliasLocal(@MustCallAlias Closeable foo) { + Closeable local = foo; + // The error on the following line is a false positive: + // :: error: (assignment.type.incompatible) + this.foo = local; + } + + @Override + @EnsuresCalledMethods( + value = {"this.foo"}, + methods = {"close"}) + public void close() throws IOException { + this.foo.close(); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasNormalExit.java b/checker/tests/resourceleak/MustCallAliasNormalExit.java new file mode 100644 index 000000000000..103d341ac41f --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasNormalExit.java @@ -0,0 +1,74 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5597 + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +@InheritableMustCall("close") +public class MustCallAliasNormalExit { + + final @Owning InputStream is; + + @MustCallAlias MustCallAliasNormalExit(@MustCallAlias InputStream p, boolean b) throws Exception { + if (b) { + throw new Exception("an exception!"); + } + this.is = p; + } + + @EnsuresCalledMethods(value = "this.is", methods = "close") + public void close() throws IOException { + this.is.close(); + } + + public static @MustCallAlias MustCallAliasNormalExit mcaneFactory(InputStream is) + throws Exception { + return new MustCallAliasNormalExit(is, false); + } + + // :: error: required.method.not.called + public static void testUse1(@Owning InputStream inputStream) throws IOException { + MustCallAliasNormalExit mcane = null; + try { + mcane = new MustCallAliasNormalExit(inputStream, true); // at run time, this WILL throw + } catch (Exception e) { + // At run time would fail (NPE), but for illustrative purposes this is fine. + // This absolutely must not cause inputStream to be considered closed because of the + // MustCallAlias + // relationship. + mcane.close(); + } + } + + // :: error: required.method.not.called + public static void testUse2(@Owning InputStream inputStream) throws IOException { + MustCallAliasNormalExit mcane = null; + try { + mcane = mcaneFactory(inputStream); + } catch (Exception e) { + mcane.close(); + } + } + + // :: error: required.method.not.called + public static void testUse3(@Owning InputStream inputStream) throws Exception { + // if mcaneFactory throws, then inputStream goes out of scope w/o being closed + MustCallAliasNormalExit mcane = mcaneFactory(inputStream); + mcane.close(); + } + + // TODO: this appears to be a false positive, but the RLC doesn't handle it correctly because + // close() is called on different aliases on different branches. + // :: error: required.method.not.called + public static void testUse4(@Owning InputStream inputStream) throws Exception { + MustCallAliasNormalExit mcane = null; + try { + mcane = mcaneFactory(inputStream); + } catch (Exception e) { + // this makes it safe + inputStream.close(); + } + mcane.close(); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasNotThis.java b/checker/tests/resourceleak/MustCallAliasNotThis.java new file mode 100644 index 000000000000..8082b9aa61c2 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasNotThis.java @@ -0,0 +1,56 @@ +// A test case with examples of code that shouldn't pass the checker where an @MustCallAlias +// parameter is passed to an owning field, but not an owning field of this. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +public class MustCallAliasNotThis implements Closeable { + + @Owning Closeable foo; + + // Both of these constructors are wrong: the first assigns to the owning field of another + // object of the same class, but not the "this" object; the second assigns to another class' + // owning field entirely. Both of these assignments would require @CreatesMustCallFor + // annotations + // to verify, so it's okay that the @MustCallAlias annotations are verified here (because it + // is impossible to write the required @CreatesMustCallFor annotations, since they can only be + // written on methods, not on constructors). If we ever permit @CreatesMustCallFor annotations + // on constructors, this test should be revisited: it might be necessary to make a corresponding + // change in the rules for verifying @MustCallAlias. + + // :: error: missing.creates.mustcall.for + public @MustCallAlias MustCallAliasNotThis( + @MustCallAlias Closeable foo, MustCallAliasNotThis other) throws IOException { + other.close(); + other.foo = foo; + } + + // :: error: missing.creates.mustcall.for + public @MustCallAlias MustCallAliasNotThis(@MustCallAlias Closeable foo, Bar other) + throws IOException { + other.close(); + other.baz = foo; + } + + @Override + @EnsuresCalledMethods( + value = {"this.foo"}, + methods = {"close"}) + public void close() throws IOException { + this.foo.close(); + } + + class Bar implements Closeable { + @Owning Closeable baz; + + @Override + @EnsuresCalledMethods( + value = {"this.baz"}, + methods = {"close"}) + public void close() throws IOException { + this.baz.close(); + } + } +} diff --git a/checker/tests/resourceleak/MustCallAliasNullConstructor.java b/checker/tests/resourceleak/MustCallAliasNullConstructor.java new file mode 100644 index 000000000000..d9add720829a --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasNullConstructor.java @@ -0,0 +1,27 @@ +// test for https://github.com/typetools/checker-framework/issues/5777 + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.Closeable; +import java.io.IOException; +import java.net.Socket; + +public class MustCallAliasNullConstructor implements Closeable { + @Owning Socket socket; + + @MustCallAlias MustCallAliasNullConstructor(@MustCallAlias Socket s) throws IOException { + if (this.socket != null) { + this.socket.close(); + } + this.socket = s; + // :: error: required.method.not.called + this.socket = null; + } + + @Override + @EnsuresCalledMethods(value = "this.socket", methods = "close") + public void close() throws IOException { + this.socket.close(); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasOwningField.java b/checker/tests/resourceleak/MustCallAliasOwningField.java new file mode 100644 index 000000000000..955098052954 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasOwningField.java @@ -0,0 +1,30 @@ +// Based on a MustCallAlias scenario in Zookeeper. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +public @InheritableMustCall("shutdown") class MustCallAliasOwningField { + + private final @Owning BufferedInputStream input; + + public MustCallAliasOwningField(@Owning BufferedInputStream input, boolean b) { + this.input = input; + if (b) { + DataInputStream d = new DataInputStream(input); + authenticate(d); + } + } + + @EnsuresCalledMethods(value = "this.input", methods = "close") + public void shutdown() throws IOException { + input.close(); + } + + public static void authenticate(InputStream is) {} + + public void wrapField() { + DataInputStream dis = new DataInputStream(input); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasPassthrough.java b/checker/tests/resourceleak/MustCallAliasPassthrough.java new file mode 100644 index 000000000000..d3dd4bcbef8a --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasPassthrough.java @@ -0,0 +1,13 @@ +// A test that a class can extend another class with an MCA constructor, +// and have its own constructor be MCA as well. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class MustCallAliasPassthrough extends FilterInputStream { + @MustCallAlias MustCallAliasPassthrough(@MustCallAlias InputStream is) { + super(is); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasPassthroughChain.java b/checker/tests/resourceleak/MustCallAliasPassthroughChain.java new file mode 100644 index 000000000000..4bebae6aa295 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasPassthroughChain.java @@ -0,0 +1,54 @@ +// This test checks that a chain of several MCA methods can be verified, and that messing up the +// chain +// leads to errors. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class MustCallAliasPassthroughChain { + + static @MustCallAlias InputStream withMCA(@MustCallAlias InputStream is) { + return is; + } + + static @MustCallAlias InputStream chain1(@MustCallAlias InputStream is) { + return withMCA(is); + } + + static @MustCallAlias InputStream chain2(@MustCallAlias InputStream is) { + InputStream s = withMCA(is); + return s; + } + + static @MustCallAlias InputStream chain3(@MustCallAlias InputStream is) { + return withMCA(chain1(is)); + } + + static @MustCallAlias InputStream chain4(@MustCallAlias InputStream is) { + return withMCA(chain1(chain3(is))); + } + + static @MustCallAlias InputStream chain5(@MustCallAlias InputStream is) { + InputStream s = withMCA(chain1(is)); + return s; + } + + // :: error: mustcallalias.out.of.scope + static @MustCallAlias InputStream chain_bad1(@MustCallAlias InputStream is) { + InputStream s = withMCA(chain1(is)); + return null; + } + + // :: error: mustcallalias.out.of.scope + static @MustCallAlias InputStream chain_bad2(@MustCallAlias InputStream is) { + withMCA(chain1(is)); + return null; + } + + // :: error: mustcallalias.out.of.scope + static @MustCallAlias InputStream chain_bad3(@MustCallAlias InputStream is, boolean b) { + return b ? null : withMCA(chain1(is)); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasPassthroughLocal.java b/checker/tests/resourceleak/MustCallAliasPassthroughLocal.java new file mode 100644 index 000000000000..f51266173cf4 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasPassthroughLocal.java @@ -0,0 +1,23 @@ +// A test that passing a local to an MCA super constructor is allowed. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class MustCallAliasPassthroughLocal extends FilterInputStream { + MustCallAliasPassthroughLocal(File f) throws Exception { + // This is safe - this MCA constructor of FilterInputStream means that the result of this + // constructor - i.e. the caller - is taking ownership of this newly-created output stream. + super(new FileInputStream(f)); + } + + static void test(File f) throws Exception { + // :: error: required.method.not.called + new MustCallAliasPassthroughLocal(f); + } + + static void test_ok(File f) throws Exception { + new MustCallAliasPassthroughLocal(f).close(); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasPassthroughThis.java b/checker/tests/resourceleak/MustCallAliasPassthroughThis.java new file mode 100644 index 000000000000..3ecb02e1d113 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasPassthroughThis.java @@ -0,0 +1,16 @@ +// A test that a class can have multiple MCA constructors. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class MustCallAliasPassthroughThis extends FilterInputStream { + @MustCallAlias MustCallAliasPassthroughThis(@MustCallAlias InputStream is) { + super(is); + } + + @MustCallAlias MustCallAliasPassthroughThis(@MustCallAlias InputStream is, int x) { + this(is); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasPassthroughWrong1.java b/checker/tests/resourceleak/MustCallAliasPassthroughWrong1.java new file mode 100644 index 000000000000..063f8a9b4273 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasPassthroughWrong1.java @@ -0,0 +1,15 @@ +// A test that a class can extend another class with an MCA constructor, +// and have its own constructor be MCA as well. +// This version just throws away the input rather than passing it to the super constructor. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class MustCallAliasPassthroughWrong1 extends FilterInputStream { + // :: error: mustcallalias.out.of.scope + @MustCallAlias MustCallAliasPassthroughWrong1(@MustCallAlias InputStream is) { + super(null); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasPassthroughWrong2.java b/checker/tests/resourceleak/MustCallAliasPassthroughWrong2.java new file mode 100644 index 000000000000..f5a8741d06ec --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasPassthroughWrong2.java @@ -0,0 +1,27 @@ +// A test that a class can extend another class with an MCA constructor, +// and have its own constructor be MCA as well. +// This version passes the MCA param to another method instead of the passthrough constructor. +// This is sort of okay - the stream does get closed, if it needs to be closed - though the +// MCA annotation on the return type is super misleading and will lead to FPs. It would be better +// to annotate code like this with @Owning on the constructor. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class MustCallAliasPassthroughWrong2 extends FilterInputStream { + // :: error: mustcallalias.out.of.scope + @MustCallAlias MustCallAliasPassthroughWrong2(@MustCallAlias InputStream is) throws Exception { + super(null); + // The following error isn't really desirable, but occurs because the special case + // in the Must Call Checker for assigning @MustCallAlias parameters to @Owning fields + // is not triggered, and @MustCallAlias is treated as @PolyMustCall otherwise. + // :: error: (argument.type.incompatible) + closeIS(is); + } + + void closeIS(@Owning InputStream is) throws Exception { + is.close(); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasPassthroughWrong3.java b/checker/tests/resourceleak/MustCallAliasPassthroughWrong3.java new file mode 100644 index 000000000000..7191b2fe4385 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasPassthroughWrong3.java @@ -0,0 +1,29 @@ +// This is a test for what happens when there's a missing MCA return type. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class MustCallAliasPassthroughWrong3 { + + static InputStream missingMCA(@MustCallAlias InputStream is) { + // :: error: (return.type.incompatible) + return is; + } + + static @MustCallAlias InputStream withMCA(@MustCallAlias InputStream is) { + return is; + } + + // :: error: (required.method.not.called) + void use_bad(@Owning InputStream is) throws Exception { + InputStream is2 = missingMCA(is); + is2.close(); + } + + void use_good(@Owning InputStream is) throws Exception { + InputStream is2 = withMCA(is); + is2.close(); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasPassthroughWrong4.java b/checker/tests/resourceleak/MustCallAliasPassthroughWrong4.java new file mode 100644 index 000000000000..1be0c7cfda33 --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasPassthroughWrong4.java @@ -0,0 +1,18 @@ +// A test that a class can extend another class with an MCA constructor, +// and have its own constructor be MCA as well. +// This version just closes the MCA parameter, which isn't wrong so much as weird but I wanted a +// test for it. Issuing an error here is appropriate, because no aliasing relationship +// actually exists after the constructor returns. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class MustCallAliasPassthroughWrong4 extends FilterInputStream { + // :: error: mustcallalias.out.of.scope + @MustCallAlias MustCallAliasPassthroughWrong4(@MustCallAlias InputStream is) throws Exception { + super(null); + is.close(); + } +} diff --git a/checker/tests/resourceleak/MustCallAliasSocketException.java b/checker/tests/resourceleak/MustCallAliasSocketException.java new file mode 100644 index 000000000000..ef5dffc989af --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasSocketException.java @@ -0,0 +1,28 @@ +// Based on a false positive in Zookeeper's SaslQuorumAuthLearner. + +import java.io.*; +import java.net.*; + +class MustCallAliasSocketException { + + public boolean quorumRequireSasl; + + // This socket isn't owning, so we shouldn't warn on it. + public void authenticate(Socket sock, String hostName) throws IOException { + if (!quorumRequireSasl) { + // I kept this block in the test case because it demonstrates + // that sock is definitely non-owning. + System.out.println("Skipping SASL authentication as"); + return; + } + try { + DataOutputStream dout = new DataOutputStream(sock.getOutputStream()); + // Before MCA was implemented, the call to getInputStream() below triggered + // a false positive warning that dout had not been closed. + DataInputStream din = new DataInputStream(sock.getInputStream()); + // ~30 lines omitted... + } finally { + // do some other things that are definitely not closing sock + } + } +} diff --git a/checker/tests/resourceleak/MustCallAliasSubstitution.java b/checker/tests/resourceleak/MustCallAliasSubstitution.java new file mode 100644 index 000000000000..2b1d08ec6e2e --- /dev/null +++ b/checker/tests/resourceleak/MustCallAliasSubstitution.java @@ -0,0 +1,26 @@ +// A test case that checks that resolving the obligations of one object and then +// substituting a fresh object is not counted as an alias, for the purpose of MustCallAlias +// verification. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.net.Socket; + +class MustCallAliasSubstitution { + + // :: error: mustcallalias.out.of.scope + static @MustCallAlias Closeable example(@MustCallAlias Closeable p) throws IOException { + p.close(); + return new Socket("localhost", 5000); + } + + // This method demonstrates how a false negative could occur, if no error was issued + // on example(). + void use(Closeable c) throws IOException { + // s never gets closed, but the checker permits this code, because it believes + // that s and c are aliased. + Closeable s = example(c); + c.close(); + } +} diff --git a/checker/tests/resourceleak/MustCallNullStore.java b/checker/tests/resourceleak/MustCallNullStore.java new file mode 100644 index 000000000000..07551a3c19d6 --- /dev/null +++ b/checker/tests/resourceleak/MustCallNullStore.java @@ -0,0 +1,20 @@ +import java.nio.ByteBuffer; + +// input exposing a bug where store after the return is null +class MustCallNullStore { + + int inflateDirect(ByteBuffer src, ByteBuffer dst) throws java.io.IOException { + + ByteBuffer presliced = dst; + if (dst.position() > 0) { + dst = dst.slice(); + } + + int n = 0; + try { + presliced.position(presliced.position() + n); + } finally { + } + return n; + } +} diff --git a/checker/tests/resourceleak/MustCloseIntoObject.java b/checker/tests/resourceleak/MustCloseIntoObject.java new file mode 100644 index 000000000000..200568364a51 --- /dev/null +++ b/checker/tests/resourceleak/MustCloseIntoObject.java @@ -0,0 +1,11 @@ +// Test that assigning a "must close" value into a class without a mustCall annotation +// still results in an error. + +import java.net.Socket; + +class MustCloseIntoObject { + void test() throws Exception { + // :: error: required.method.not.called + Object o = new Socket("", 0); + } +} diff --git a/checker/tests/resourceleak/NonFinalFieldOnlyOverwrittenIfNull.java b/checker/tests/resourceleak/NonFinalFieldOnlyOverwrittenIfNull.java new file mode 100644 index 000000000000..bf2dea2d533d --- /dev/null +++ b/checker/tests/resourceleak/NonFinalFieldOnlyOverwrittenIfNull.java @@ -0,0 +1,89 @@ +// A test that must-call close errors are not issued when overwriting a field +// if the field is definitely null. + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.mustcall.qual.Owning; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +import java.io.*; + +@InheritableMustCall("close") +class NonFinalFieldOnlyOverwrittenIfNull { + @Owning @MonotonicNonNull InputStream is; + + @CreatesMustCallFor + void set(String fn) throws FileNotFoundException { + if (is == null) { + is = new FileInputStream(fn); + } + } + + @CreatesMustCallFor + void set_after_close(String fn, boolean b) throws IOException { + if (b) { + is.close(); + is = new FileInputStream(fn); + } + } + + @CreatesMustCallFor + void set_error(String fn, boolean b) throws FileNotFoundException { + if (b) { + // :: error: required.method.not.called + is = new FileInputStream(fn); + } + } + + // These three methods are copies of the three above, without the appropriate annotation. + // :: error: missing.creates.mustcall.for + void set2(String fn) throws FileNotFoundException { + if (is == null) { + is = new FileInputStream(fn); + } + } + + // :: error: missing.creates.mustcall.for + void set_after_close2(String fn, boolean b) throws IOException { + if (b) { + is.close(); + is = new FileInputStream(fn); + } + } + + // :: error: missing.creates.mustcall.for + void set_error2(String fn, boolean b) throws FileNotFoundException { + if (b) { + // :: error: required.method.not.called + is = new FileInputStream(fn); + } + } + + /* This version of close() doesn't verify, because in the `catch` block + `is` isn't @CalledMethods("close"). TODO: investigate that in the CM checker + @EnsuresCalledMethods(value="this.is", methods="close") + void close_real() { + if (is != null) { + try { + is.close(); + } catch (Exception ie) { + + } + } + } */ + + @EnsuresCalledMethods(value = "this.is", methods = "close") + void close() throws Exception { + if (is != null) { + is.close(); + } + } + + public static void test_leak() throws Exception { + // :: error: required.method.not.called + NonFinalFieldOnlyOverwrittenIfNull n = new NonFinalFieldOnlyOverwrittenIfNull(); + n.close(); + n.set_after_close("bar.txt", true); + } +} diff --git a/checker/tests/resourceleak/NonFinalFieldOnlyOverwrittenIfNull2.java b/checker/tests/resourceleak/NonFinalFieldOnlyOverwrittenIfNull2.java new file mode 100644 index 000000000000..c06aeba8a9c1 --- /dev/null +++ b/checker/tests/resourceleak/NonFinalFieldOnlyOverwrittenIfNull2.java @@ -0,0 +1,68 @@ +// Another test that must-call close errors are not issued when overwriting a field +// if the field is definitely null. + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.mustcall.qual.Owning; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +import java.io.*; + +@InheritableMustCall("close") +class NonFinalFieldOnlyOverwrittenIfNull2 { + @Owning @MonotonicNonNull InputStream is; + + @CreatesMustCallFor + void set(String fn) throws FileNotFoundException { + if (is == null) { + is = new FileInputStream(fn); + } + } + + @CreatesMustCallFor + void set_after_close(String fn, boolean b) throws IOException { + if (b) { + is.close(); + is = new FileInputStream(fn); + } + } + + @CreatesMustCallFor + void set_error(String fn, boolean b) throws FileNotFoundException { + if (b) { + // :: error: required.method.not.called + is = new FileInputStream(fn); + } + } + + /* This version of close() doesn't verify, because in the `catch` block + `is` isn't @CalledMethods("close"). TODO: investigate that in the CM checker + @EnsuresCalledMethods(value="this.is", methods="close") + void close_real() { + if (is != null) { + try { + is.close(); + } catch (Exception ie) { + + } + } + } */ + + @EnsuresCalledMethods(value = "this.is", methods = "close") + @CreatesMustCallFor + void close() throws Exception { + if (is != null) { + is.close(); + is = null; + } + } + + public static void test_leak() throws Exception { + // :: error: required.method.not.called + NonFinalFieldOnlyOverwrittenIfNull2 n = new NonFinalFieldOnlyOverwrittenIfNull2(); + n.set("foo.txt"); + n.close(); + n.set("bar.txt"); + } +} diff --git a/checker/tests/resourceleak/OptionalSocket.java b/checker/tests/resourceleak/OptionalSocket.java new file mode 100644 index 000000000000..60f726e8c7f7 --- /dev/null +++ b/checker/tests/resourceleak/OptionalSocket.java @@ -0,0 +1,26 @@ +// A test case for some false positives that came up in Zookeeper. +// These are sort-of a must call alias situation, but it's not as clear. +// I suspect our MCA implementation won't actually handle these cases, in +// which case it's fine with me to skip this test for now. - Martin +// @skip-test + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.net.*; +import java.util.*; + +class OptionalSocket { + void test_close_get_null(@Owning Optional sock) throws IOException { + // TODO can't pass this + if (sock.get() != null) { + // TODO can't pass this + sock.get().close(); + } + } + + void test_close_get(@Owning Optional sock) throws IOException { + // TODO can't pass this + sock.get().close(); + } +} diff --git a/checker/tests/resourceleak/OwnershipTransferAtReassignment.java b/checker/tests/resourceleak/OwnershipTransferAtReassignment.java new file mode 100644 index 000000000000..85ddab1d576f --- /dev/null +++ b/checker/tests/resourceleak/OwnershipTransferAtReassignment.java @@ -0,0 +1,38 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5971 + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.mustcall.qual.Owning; +import org.checkerframework.checker.nullness.qual.Nullable; + +@InheritableMustCall("disconnect") +public class OwnershipTransferAtReassignment { + + private @Owning @Nullable Node head = null; + + @CreatesMustCallFor("this") + public boolean add() { + head = new Node(head); + return true; + } + + @EnsuresCalledMethods(value = "this.head", methods = "disconnect") + public void disconnect() { + head.disconnect(); + } + + @InheritableMustCall("disconnect") + private static class Node { + @Owning private final @Nullable Node next; + + public Node(@Owning @Nullable Node next) { + this.next = next; + } + + @EnsuresCalledMethods(value = "this.next", methods = "disconnect") + public void disconnect() { + next.disconnect(); + } + } +} diff --git a/checker/tests/resourceleak/OwnershipWithExceptions.java b/checker/tests/resourceleak/OwnershipWithExceptions.java new file mode 100644 index 000000000000..f9403669387e --- /dev/null +++ b/checker/tests/resourceleak/OwnershipWithExceptions.java @@ -0,0 +1,266 @@ +// Test case for https://github.com/typetools/checker-framework/issues/6179 +// (and other rules regarding @Owning and exceptions) + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.dataflow.qual.SideEffectFree; + +import java.io.Closeable; +import java.io.IOException; +import java.net.Socket; + +abstract class OwnershipWithExceptions { + + static class ManualExample1 { + void example(String myHost, int myPort) throws IOException { + Socket s = new Socket(myHost, myPort); + closeSocket(s); + } + + void closeSocket(@Owning @MustCall("close") Socket t) { + try { + t.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + static class ManualExample2 { + void example(String myHost, int myPort) throws Exception { + // Error: `s` is not closed on all paths + // ::error: (required.method.not.called) + Socket s = new Socket(myHost, myPort); + + // `closeSocket` does not have to close `s` when it throws IOException. + // Instead, this method has to catch the exception and close `s`. + closeSocket(s); + } + + void closeSocket(@Owning Socket t) throws IOException { + throw new IOException(); + } + } + + abstract @Owning Closeable alloc(); + + @SideEffectFree + abstract boolean arbitraryChoice(); + + abstract void transfer(@Owning Closeable resource) throws IOException; + + void transferAndPropagateException(@Owning Closeable resource) throws IOException { + transfer(resource); + } + + void transferHasNoObligationsOnException(@Owning Closeable resource) throws IOException { + throw new IOException(); + } + + // :: error: (required.method.not.called) + void transferAndIgnoreExceptionWithoutClosing(@Owning Closeable zzz) { + try { + transfer(zzz); + } catch (IOException ignored) { + } + } + + boolean transferAndIgnoreExceptionCorrectly(@Owning Closeable resource) { + try { + transfer(resource); + return true; + } catch (Exception e) { + try { + resource.close(); + } catch (Exception other) { + } + return false; + } + } + + // Passing an argument as an @Owning parameter does not transfer ownership if + // the called method throws. So, this is not correct: if transfer(resource) + // throws an exception, it leaks the resource. + void noExceptionHandling() throws IOException { + // ::error: (required.method.not.called) + Closeable resource = alloc(); + // ::error: (assignment.type.incompatible) + @CalledMethods("close") Closeable a = resource; + transfer(resource); + // ::error: (assignment.type.incompatible) + @CalledMethods("close") Closeable b = resource; + } + + class FinalOwnedField implements Closeable { + + final @Owning Closeable resource; + + FinalOwnedField() throws IOException { + // Field assignments in constructors are special. When the constructor + // exits by exception, the field becomes permanently inaccessible, and + // therefore the allocated resource is leaked. + // :: error: (required.method.not.called) + resource = alloc(); + if (arbitraryChoice()) { + throw new IOException(); + } + } + + FinalOwnedField(@Owning Closeable resource) throws IOException { + // Although, when the resource was passed by a caller, then we can be + // more relaxed. On exception, ownership remains with the caller. + this.resource = resource; + if (arbitraryChoice()) { + throw new IOException(); + } + } + + FinalOwnedField(@Owning Closeable resource, boolean arg) throws IOException { + // Same as the previous constructor, but in the other order. + if (arbitraryChoice()) { + throw new IOException(); + } + this.resource = resource; + } + + FinalOwnedField(int ignored) throws IOException { + // Same as the 0-argument constructor, but handled correctly (algorithm 1). + resource = alloc(); + try { + if (arbitraryChoice()) { + throw new IOException(); + } + } catch (Exception e) { + resource.close(); + throw e; + } + } + + FinalOwnedField(float ignored) throws IOException { + // Same as the 0-argument constructor, but handled correctly (algorithm 2). + Closeable r = alloc(); + resource = r; + try { + if (arbitraryChoice()) { + throw new IOException(); + } + } catch (Exception e) { + r.close(); + throw e; + } + } + + // Not allowed: destructors have to close @Owning fields even on exception. + @Override + @EnsuresCalledMethods( + value = "this.resource", + methods = {"close"}) + // ::error: (contracts.exceptional.postcondition.not.satisfied) + public void close() throws IOException { + throw new IOException(); + } + } + + // Classes with >1 owned field are treated slightly differently + // (see ./TwoOwningMCATest.java) + class TwoOwnedFields implements Closeable { + + final @Owning Closeable unused = null; + + final @Owning Closeable resource; + + TwoOwnedFields() throws IOException { + // Field assignments in constructors are special. When the constructor + // exits by exception, the field becomes permanently inaccessible, and + // therefore the allocated resource is leaked. + // :: error: (required.method.not.called) + resource = alloc(); + if (arbitraryChoice()) { + throw new IOException(); + } + } + + TwoOwnedFields(@Owning Closeable resource) throws IOException { + // Although, when the resource was passed by a caller, then we can be + // more relaxed. On exception, ownership remains with the caller. + this.resource = resource; + if (arbitraryChoice()) { + throw new IOException(); + } + } + + TwoOwnedFields(@Owning Closeable resource, boolean arg) throws IOException { + // Same as the previous constructor, but in the other order. + if (arbitraryChoice()) { + throw new IOException(); + } + this.resource = resource; + } + + TwoOwnedFields(int ignored) throws IOException { + // Same as the 0-argument constructor, but handled correctly (algorithm 1). + resource = alloc(); + try { + if (arbitraryChoice()) { + throw new IOException(); + } + } catch (Exception e) { + resource.close(); + throw e; + } + } + + TwoOwnedFields(float ignored) throws IOException { + // Same as the 0-argument constructor, but handled correctly (algorithm 2). + Closeable r = alloc(); + resource = r; + try { + if (arbitraryChoice()) { + throw new IOException(); + } + } catch (Exception e) { + r.close(); + throw e; + } + } + + // Not allowed: destructors have to close @Owning fields even on exception. + // In this case, the exception from `unused.close()` can prematurely stop the method. + @Override + @EnsuresCalledMethods( + value = {"this.resource", "this.unused"}, + methods = {"close"}) + // ::error: (contracts.exceptional.postcondition.not.satisfied) + public void close() throws IOException { + if (unused != null) unused.close(); + if (resource != null) resource.close(); + } + } + + class MutableOwnedField implements Closeable { + + @Owning Closeable resource; + + @RequiresCalledMethods( + value = "this.resource", + methods = {"close"}) + @CreatesMustCallFor("this") + void realloc() throws IOException { + // Unlike in a constructor, field assignments in normal methods are not + // leaked when the method exits with an exception, since the reciever + // is still accessible to the caller. + resource = alloc(); + if (arbitraryChoice()) { + throw new IOException(); + } + } + + @Override + @EnsuresCalledMethods( + value = "this.resource", + methods = {"close"}) + public void close() throws IOException { + resource.close(); + } + } +} diff --git a/checker/tests/resourceleak/OwningAndEnsuresCalledMethodsOnException.java b/checker/tests/resourceleak/OwningAndEnsuresCalledMethodsOnException.java new file mode 100644 index 000000000000..c5066ff802a6 --- /dev/null +++ b/checker/tests/resourceleak/OwningAndEnsuresCalledMethodsOnException.java @@ -0,0 +1,72 @@ +// Test case for a Resource Leak manual example that involves interaction +// between @Owning and @EnsuresCalledMethodsOnException. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class OwningAndEnsuresCalledMethodsOnException implements Closeable { + + private final @Owning Closeable resource; + + // Good constructor, as illustrated in the manual. + @EnsuresCalledMethodsOnException(value = "#1", methods = "close") + public OwningAndEnsuresCalledMethodsOnException(@Owning Closeable resource) throws IOException { + this.resource = resource; + try { + initialize(resource); + } catch (Exception e) { + resource.close(); + throw e; + } + } + + // Alternative constructor with a weaker contract (specifically, the default + // contract for @Owning: only takes ownership on normal return) + public OwningAndEnsuresCalledMethodsOnException(@Owning Closeable resource, int ignored) + throws IOException { + this.resource = resource; + initialize(resource); + } + + public OwningAndEnsuresCalledMethodsOnException() throws IOException { + // OK: the good delegate constructor will either take ownership or close the argument + // This will issue a false positive warning due to + // https://github.com/typetools/checker-framework/issues/6270 + // ::error: (required.method.not.called) + this(new Resource()); + } + + public OwningAndEnsuresCalledMethodsOnException(int x) throws IOException { + // WRONG: the bad delegate constructor does not close the argument on exception + // ::error: (required.method.not.called) + this(new Resource(), x); + } + + static void exampleUseInNormalMethod1() throws IOException { + // OK: the constructor will either take ownership or close the argument + // This will issue a false positive warning due to + // https://github.com/typetools/checker-framework/issues/6270 + // ::error: (required.method.not.called) + new OwningAndEnsuresCalledMethodsOnException(new Resource()); + } + + static void exampleUseInNormalMethod2() throws IOException { + // WRONG: the bad constructor does not close the argument on exception + // ::error: (required.method.not.called) + new OwningAndEnsuresCalledMethodsOnException(new Resource(), 0); + } + + static void initialize(Closeable resource) throws IOException {} + + @EnsuresCalledMethods(value = "resource", methods = "close") + public void close() throws IOException { + resource.close(); + } + + private static class Resource implements Closeable { + @Override + public void close() throws IOException {} + } +} diff --git a/checker/tests/resourceleak/OwningEnsuresCalledMethods.java b/checker/tests/resourceleak/OwningEnsuresCalledMethods.java new file mode 100644 index 000000000000..bac8ab3a404b --- /dev/null +++ b/checker/tests/resourceleak/OwningEnsuresCalledMethods.java @@ -0,0 +1,37 @@ +// A test that @Owning on a parameter does not imply @EnsuresCalledMethods. +// +// This was originally: +// +// A test that the RLC understands that @Owning on a parameter is effectively +// a stronger version of the @EnsuresCalledMethods annotation +// +// However, that behavior has since been changed. An @Owning parameter can be +// satisfied by assigning the value to an @Owning field, in which case the +// methods have not been called. In other words, @Owning promises to call the +// must-call methods at some point in the future through a different alias; it +// does not promise to call those methods before returning. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.net.Socket; + +@InheritableMustCall("dispose") +public class OwningEnsuresCalledMethods { + + @Owning Socket con; + + @EnsuresCalledMethods(value = "this.con", methods = "close") + // ::error: (contracts.postcondition.not.satisfied) + void dispose() { + closeCon(con); + } + + static void closeCon(@Owning Socket con) { + try { + con.close(); + } catch (IOException e) { + } + } +} diff --git a/checker/tests/resourceleak/OwningFieldStringComparison.java b/checker/tests/resourceleak/OwningFieldStringComparison.java new file mode 100644 index 000000000000..3865402b1f69 --- /dev/null +++ b/checker/tests/resourceleak/OwningFieldStringComparison.java @@ -0,0 +1,33 @@ +// Test case for https://github.com/typetools/checker-framework/issues/6276 + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.net.Socket; + +@InheritableMustCall("a") +public class OwningFieldStringComparison { + + // :: error: required.method.not.called + @Owning Socket s; + + // important to the bug: the name of this field must contain + // the name of the owning socket + /* @NotOwning */ Socket s2; + + // Note this "destructor" closes the wrong socket + @EnsuresCalledMethods(value = "this.s2", methods = "close") + public void a() { + try { + this.s2.close(); + } catch (Exception e) { + + } finally { + try { + this.s2.close(); + } catch (Exception e) { + + } + } + } +} diff --git a/checker/tests/resourceleak/OwningMCU.java b/checker/tests/resourceleak/OwningMCU.java new file mode 100644 index 000000000000..226ce1ae6a44 --- /dev/null +++ b/checker/tests/resourceleak/OwningMCU.java @@ -0,0 +1,15 @@ +// a test case that @Owning @MustCallUnknown fields do not cause a crash. Of course, +// such fields should never exist. But, better to be safe. Relevant GitHub issue with +// discussion: https://github.com/typetools/checker-framework/issues/6030. + +import org.checkerframework.checker.mustcall.qual.*; + +public class OwningMCU { + + @Owning @MustCallUnknown Object foo; + + // :: error: missing.creates.mustcall.for + void test() { + foo = new Object(); + } +} diff --git a/checker/tests/resourceleak/OwningOverride.java b/checker/tests/resourceleak/OwningOverride.java new file mode 100644 index 000000000000..b5bb749c757f --- /dev/null +++ b/checker/tests/resourceleak/OwningOverride.java @@ -0,0 +1,29 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5722 + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.net.*; + +public class OwningOverride { + abstract static class A { + public abstract void closeStream(@Owning InputStream s) throws IOException; + } + + static class B extends A { + @Override + // :: error: owning.override.param + public void closeStream(InputStream s) {} + } + + static void main(String[] args) throws IOException { + // no resource leak reported for x + InputStream x = new FileInputStream("foo.txt"); + A a = new B(); + try { + a.closeStream(x); + } catch (Exception e) { + x.close(); + } + } +} diff --git a/checker/tests/resourceleak/OwningOverride2.java b/checker/tests/resourceleak/OwningOverride2.java new file mode 100644 index 000000000000..ea4ce962ee59 --- /dev/null +++ b/checker/tests/resourceleak/OwningOverride2.java @@ -0,0 +1,22 @@ +// More tests for the problem described at +// https://github.com/typetools/checker-framework/issues/5722 +// Test that an owning return cannot override a non-owning return + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.net.*; + +public class OwningOverride2 { + abstract static class A { + public abstract @NotOwning Socket get(); + } + + static class B extends A { + @Override + // :: error: owning.override.return + public Socket get() { + return null; + } + } +} diff --git a/checker/tests/resourceleak/PaperExample.java b/checker/tests/resourceleak/PaperExample.java new file mode 100644 index 000000000000..0838504d7ace --- /dev/null +++ b/checker/tests/resourceleak/PaperExample.java @@ -0,0 +1,16 @@ +import java.net.Socket; + +class PaperExample { + void test(String myHost, int myPort) throws Exception { + Socket s = null; + try { + s = new Socket(myHost, myPort); /* 1 */ + } catch (Exception e) { + } finally { + if (s != null) { + /* 2 */ + s.close(); + } + } + } +} diff --git a/checker/tests/resourceleak/PrimitiveCast.java b/checker/tests/resourceleak/PrimitiveCast.java new file mode 100644 index 000000000000..b28d7375ea1f --- /dev/null +++ b/checker/tests/resourceleak/PrimitiveCast.java @@ -0,0 +1,38 @@ +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.MustCallUnknown; + +import java.util.List; + +public class PrimitiveCast { + + char foo(List values) { + for (Object o : values) { + return (char) o; + } + return 'A'; + } + + char toChar1(@MustCall("hashCode") Character c) { + return (char) c; + } + + char toChar2(@MustCallUnknown Character c) { + return (char) c; + } + + char toChar3(@MustCall Character c) { + return (char) c; + } + + @MustCall("hashCode") Character toCharacter1(char c) { + return (char) c; + } + + @MustCallUnknown Character toCharacter2(char c) { + return (char) c; + } + + @MustCall Character toCharacter3(char c) { + return (char) c; + } +} diff --git a/checker/tests/resourceleak/ReassignmentWithMCA.java b/checker/tests/resourceleak/ReassignmentWithMCA.java new file mode 100644 index 000000000000..360839acbc76 --- /dev/null +++ b/checker/tests/resourceleak/ReassignmentWithMCA.java @@ -0,0 +1,50 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.security.*; + +public class ReassignmentWithMCA { + void testReassignment(File newFile, MessageDigest digester) throws IOException { + FileOutputStream fout = new FileOutputStream(newFile); + DigestOutputStream fos = new DigestOutputStream(fout, digester); + DataOutputStream out = new DataOutputStream(fos); + try { + out = new DataOutputStream(new BufferedOutputStream(fos)); + fout.getChannel(); + } finally { + out.close(); + } + } + + void testReassignmentWithoutMCA( + @Owning FileOutputStream fout1, @Owning FileOutputStream fout2, MessageDigest digester) + throws IOException { + DigestOutputStream fos1 = new DigestOutputStream(fout1, digester); + DataOutputStream out = new DataOutputStream(fos1); + try { + DigestOutputStream fos2 = new DigestOutputStream(fout2, digester); + out = new DataOutputStream(new BufferedOutputStream(fos2)); + fout1.getChannel(); + } finally { + callClose(fout1); + callClose(fout2); + } + } + + void testReassignmentSetSizeOne(@Owning FilterOutputStream out) throws IOException { + out = new DataOutputStream(out); + out.close(); + } + + @EnsuresCalledMethods(value = "#1", methods = "close") + void callClose(Closeable c) { + try { + if (c != null) { + c.close(); + } + } catch (IOException e) { + + } + } +} diff --git a/checker/tests/resourceleak/ReplicaInputStreams.java b/checker/tests/resourceleak/ReplicaInputStreams.java new file mode 100644 index 000000000000..c72b00c52431 --- /dev/null +++ b/checker/tests/resourceleak/ReplicaInputStreams.java @@ -0,0 +1,29 @@ +// A test case for https://github.com/typetools/checker-framework/issues/4838. + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.Owning; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; + +class ReplicaInputStreams implements Closeable { + + private final @Owning InputStream in1; + private final @Owning InputStream in2; + + public ReplicaInputStreams(@Owning InputStream i1, @Owning InputStream i2) { + this.in1 = i1; + this.in2 = i2; + } + + @Override + @EnsuresCalledMethods( + value = {"this.in1", "this.in2"}, + methods = {"close"}) + // :: error: (contracts.exceptional.postcondition.not.satisfied) + public void close() throws IOException { + in1.close(); + in2.close(); + } +} diff --git a/checker/tests/resourceleak/ReplicaInputStreams2.java b/checker/tests/resourceleak/ReplicaInputStreams2.java new file mode 100644 index 000000000000..089f4eaec83c --- /dev/null +++ b/checker/tests/resourceleak/ReplicaInputStreams2.java @@ -0,0 +1,32 @@ +// A test case for https://github.com/typetools/checker-framework/issues/4838. +// This variant uses a try-finally in the destructor, so it is correct. + +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.Owning; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; + +class ReplicaInputStreams2 implements Closeable { + + private final @Owning InputStream in1; + private final @Owning InputStream in2; + + public ReplicaInputStreams2(@Owning InputStream i1, @Owning InputStream i2) { + this.in1 = i1; + this.in2 = i2; + } + + @Override + @EnsuresCalledMethods( + value = {"this.in1", "this.in2"}, + methods = {"close"}) + public void close() throws IOException { + try { + in1.close(); + } finally { + in2.close(); + } + } +} diff --git a/checker/tests/resourceleak/RequiresCalledMethodsTest.java b/checker/tests/resourceleak/RequiresCalledMethodsTest.java new file mode 100644 index 000000000000..3178e6a2329d --- /dev/null +++ b/checker/tests/resourceleak/RequiresCalledMethodsTest.java @@ -0,0 +1,54 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +public class RequiresCalledMethodsTest { + + @InheritableMustCall("a") + static class Foo { + void a() {} + + void c() {} + } + + @InheritableMustCall("releaseFoo") + static class FooField { + private @Owning Foo foo = null; + + @RequiresCalledMethods( + value = {"this.foo"}, + methods = {"a"}) + @CreatesMustCallFor("this") + void overwriteFooCorrect() { + this.foo = new Foo(); + } + + @CreatesMustCallFor("this") + void overwriteFooWrong() { + // :: error: required.method.not.called + this.foo = new Foo(); + } + + @CreatesMustCallFor("this") + void overwriteFooWithoutReleasing() { + // :: error: contracts.precondition.not.satisfied + overwriteFooCorrect(); + } + + void releaseThenOverwriteFoo() { + releaseFoo(); + // :: error: reset.not.owning + overwriteFooCorrect(); + } + + @EnsuresCalledMethods( + value = {"this.foo"}, + methods = {"a"}) + void releaseFoo() { + if (this.foo != null) { + foo.a(); + } + } + } +} diff --git a/checker/tests/resourceleak/ReturnOwningObject.java b/checker/tests/resourceleak/ReturnOwningObject.java new file mode 100644 index 000000000000..20d3fa208fa2 --- /dev/null +++ b/checker/tests/resourceleak/ReturnOwningObject.java @@ -0,0 +1,23 @@ +// A test that ensures that the Resource Leak Checker continues to track an +// Object if it's returned from a method with a non-empty @MustCall annotation. + +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.mustcall.qual.MustCall; + +public class ReturnOwningObject { + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + // This is unsatisfiable without a cast, but + // for soundness the RLC still needs to track it. + public static @MustCall("a") Object getFoo() { + return new Foo(); + } + + public static void useGetFoo() { + // :: error: required.method.not.called + Object obj = getFoo(); + } +} diff --git a/checker/tests/resourceleak/RlcThisTest.java b/checker/tests/resourceleak/RlcThisTest.java new file mode 100644 index 000000000000..113b02daa5b2 --- /dev/null +++ b/checker/tests/resourceleak/RlcThisTest.java @@ -0,0 +1,64 @@ +// @skip-test remove when the bug is fixed + +import org.checkerframework.checker.mustcall.qual.InheritableMustCall; +import org.checkerframework.checker.mustcall.qual.MustCallAlias; +import org.checkerframework.common.returnsreceiver.qual.This; + +class RlcThisTest { + + @InheritableMustCall("a") + private class Foo { + + void a() {} + + @This Foo b1(Foo this) { + return this; + } + + @MustCallAlias Foo b2(@MustCallAlias Foo this) { + return this; + } + } + + void test1() { + Foo f = new Foo(); + f.b1(); + f.a(); + } + + void test2() { + Foo f = new Foo(); + f.b2(); + f.a(); + } + + void test3() { + Foo f = new Foo(); + Foo ff = f.b1(); + ff.a(); + } + + void test4() { + Foo f = new Foo(); + Foo ff = f.b2(); + ff.a(); + } + + void testA() { + Foo f = new Foo(); + f.b1(); // RLC reports a FP at this line + f.a(); + + f = new Foo(); + f.a(); + } + + void testB() { + Foo f = new Foo(); + f.b2(); // RLC reports a FP at this line + f.a(); + + f = new Foo(); + f.a(); + } +} diff --git a/checker/tests/resourceleak/SSLSocketFactoryTest.java b/checker/tests/resourceleak/SSLSocketFactoryTest.java new file mode 100644 index 000000000000..c7108731abde --- /dev/null +++ b/checker/tests/resourceleak/SSLSocketFactoryTest.java @@ -0,0 +1,21 @@ +// A test for a bug that came up while porting the Resource Leak Checker into the +// Checker Framework proper. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.IOException; +import java.net.Socket; + +import javax.net.ssl.*; + +class SSLSocketFactoryTest { + public SSLSocket createSSLSocket(@Owning Socket socket, SSLContext sslContext) + throws IOException { + SSLSocket sslSocket = + (SSLSocket) + sslContext + .getSocketFactory() + .createSocket(socket, null, socket.getPort(), true); + return sslSocket; + } +} diff --git a/checker/tests/resourceleak/SelfAssign.java b/checker/tests/resourceleak/SelfAssign.java new file mode 100644 index 000000000000..d3c9fa5205b9 --- /dev/null +++ b/checker/tests/resourceleak/SelfAssign.java @@ -0,0 +1,39 @@ +// test assignments of the same variable to itself + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +class SelfAssign { + + static void test0() throws IOException { + InputStream selfAssignIn0 = new FileInputStream("file.txt"); + try { + selfAssignIn0 = selfAssignIn0; + } finally { + selfAssignIn0.close(); + } + } + + static void test1(boolean b) throws IOException { + InputStream selfAssignIn = new FileInputStream("file.txt"); + try { + selfAssignIn = + selfAssignIn.markSupported() + ? selfAssignIn + : new BufferedInputStream(selfAssignIn); + } finally { + selfAssignIn.close(); + } + } + + static void test2(boolean b) throws IOException { + InputStream in = new FileInputStream("file.txt"); + try { + in = new BufferedInputStream(in); + } finally { + in.close(); + } + } +} diff --git a/checker/tests/resourceleak/SimpleSocketExample.java b/checker/tests/resourceleak/SimpleSocketExample.java new file mode 100644 index 000000000000..b52976246598 --- /dev/null +++ b/checker/tests/resourceleak/SimpleSocketExample.java @@ -0,0 +1,17 @@ +// A simple socket example for debugging. + +import java.io.IOException; +import java.net.Socket; + +class SimpleSocketExample { + void basicTest(String address, int port) { + try { + // :: error: required.method.not.called + Socket socket2 = new Socket(address, port); + Socket specialSocket = new Socket(address, port); + specialSocket.close(); + } catch (IOException i) { + + } + } +} diff --git a/checker/tests/resourceleak/SneakyDestructor.java b/checker/tests/resourceleak/SneakyDestructor.java new file mode 100644 index 000000000000..5437af3d1af9 --- /dev/null +++ b/checker/tests/resourceleak/SneakyDestructor.java @@ -0,0 +1,26 @@ +// Test case that shows that the fix for +// https://github.com/typetools/checker-framework/issues/6276 +// is not fooled by a must-call method that closes the owned field +// of another instance of the class. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; + +@InheritableMustCall("close") +public class SneakyDestructor { + // :: error: required.method.not.called + private final @Owning Closeable resource; + + public SneakyDestructor(Closeable r) { + this.resource = r; + } + + // ... + + @EnsuresCalledMethods(value = "#1.resource", methods = "close") + public void close(SneakyDestructor other) throws IOException { + other.resource.close(); + } +} diff --git a/checker/tests/resourceleak/SneakyDrop.java b/checker/tests/resourceleak/SneakyDrop.java new file mode 100644 index 000000000000..cc81c0b216d8 --- /dev/null +++ b/checker/tests/resourceleak/SneakyDrop.java @@ -0,0 +1,65 @@ +// Test case for https://github.com/typetools/checker-framework/issues/5908 + +import org.checkerframework.checker.mustcall.qual.*; + +class Resource implements java.io.Closeable { + @Override + public void close() {} +} + +public class SneakyDrop { + + public static void sneakyDrop(@Owning T value) {} + + public static void main(String[] args) throws Exception { + Resource x = new Resource(); + // :: error: (type.argument.type.incompatible) + sneakyDrop(x); + } + + // :: error: (required.method.not.called) + public static void sneakyDrop2(@Owning @MustCall("close") T value) {} + + public static void main2(String[] args) throws Exception { + Resource x = new Resource(); + sneakyDrop2(x); + } + + // :: error: (required.method.not.called) + public static void sneakyDrop3(@Owning T value) {} + + public static void main3(String[] args) throws Exception { + Resource x = new Resource(); + sneakyDrop3(x); + } + + public static void sneakyDrop4(@Owning T value) {} + + public static void main4(String[] args) throws Exception { + Resource x = new Resource(); + // :: error: (type.argument.type.incompatible) + sneakyDrop4(x); + } + + // :: error: (required.method.not.called) + public static void sneakyDrop5(@Owning T value) {} + + public static void main5(String[] args) throws Exception { + Resource x = new Resource(); + sneakyDrop5(x); + } + + public static void sneakyDropCorrect( + @Owning @MustCall("close") T value) throws Exception { + value.close(); + } + + public static void main6(String[] args) throws Exception { + Resource x = new Resource(); + try { + sneakyDropCorrect(x); + } catch (Exception e) { + x.close(); + } + } +} diff --git a/checker/tests/resourceleak/SocketContainer.java b/checker/tests/resourceleak/SocketContainer.java new file mode 100644 index 000000000000..302e597a325f --- /dev/null +++ b/checker/tests/resourceleak/SocketContainer.java @@ -0,0 +1,32 @@ +// A simple class that has a Socket as an owning field. +// This test exists to check that we gracefully handle assignments 1) +// in the constructor and 2) to null. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.net.*; + +@InheritableMustCall("close") +class SocketContainer { + @Owning Socket sock; + + public SocketContainer(String host, int port) throws Exception { + // It should be okay to assign to uninitialized owning fields in the constructor. + // But it isn't! Why? + // :: error: required.method.not.called + sock = new Socket(host, port); + // It's definitely not okay to do it twice! + // :: error: required.method.not.called + sock = new Socket(host, port); + } + + @EnsuresCalledMethods(value = "this.sock", methods = "close") + public void close() throws IOException { + sock.close(); + // It's okay to assign a field to null after its obligations have been fulfilled, + // without inducing a reset. + sock = null; + } +} diff --git a/checker/tests/resourceleak/SocketContainer2.java b/checker/tests/resourceleak/SocketContainer2.java new file mode 100644 index 000000000000..fc4dfd7a1806 --- /dev/null +++ b/checker/tests/resourceleak/SocketContainer2.java @@ -0,0 +1,25 @@ +// A simple class that has a Socket as an owning field. +// This test exists to check that we gracefully handle assignments to it. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.net.*; + +@InheritableMustCall("close") +class SocketContainer2 { + + @Owning Socket sock = new Socket(); + + public SocketContainer2(String host, int port) throws Exception { + // This assignment is safe, because the only possible value of sock here is the unconnected + // socket in the field initializer. + sock = new Socket(host, port); + } + + @EnsuresCalledMethods(value = "this.sock", methods = "close") + public void close() throws IOException { + sock.close(); + } +} diff --git a/checker/tests/resourceleak/SocketContainer3.java b/checker/tests/resourceleak/SocketContainer3.java new file mode 100644 index 000000000000..bcb212ebc5eb --- /dev/null +++ b/checker/tests/resourceleak/SocketContainer3.java @@ -0,0 +1,22 @@ +// A simple class that has a Socket as an owning field. +// This test exists to check that we gracefully handle assignments. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.net.*; + +@InheritableMustCall("close") +class SocketContainer3 { + @Owning Socket sock = null; + + public SocketContainer3(String host, int port) throws Exception { + sock = new Socket(host, port); + } + + @EnsuresCalledMethods(value = "this.sock", methods = "close") + public void close() throws IOException { + sock.close(); + } +} diff --git a/checker/tests/resourceleak/SocketField.java b/checker/tests/resourceleak/SocketField.java new file mode 100644 index 000000000000..29a505f109f5 --- /dev/null +++ b/checker/tests/resourceleak/SocketField.java @@ -0,0 +1,67 @@ +// test case for https://github.com/kelloggm/object-construction-checker/issues/381 + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.dataflow.qual.Pure; + +import java.io.IOException; +import java.net.Socket; + +@InheritableMustCall("closeSocket") +class SocketField { + protected @Owning Socket socket = null; + + @CreatesMustCallFor("this") + protected void setupConnection(javax.net.SocketFactory socketFactory) throws IOException { + // This is the original test case. Before this issue was fixed, an error was issued on the + // second line. + this.socket.close(); + this.socket = socketFactory.createSocket(); + } + + @CreatesMustCallFor("this") + protected void setupConnectionWithLocal(javax.net.SocketFactory socketFactory) + throws IOException { + // This is the original test case, modified to include an assignment to a local that + // demonstrates that + // the correct value was in the store at some point. + this.socket.close(); + @CalledMethods("close") Socket s = this.socket; + this.socket = socketFactory.createSocket(); + } + + @CreatesMustCallFor("this") + protected void setupConnectionWithConstructor(javax.net.SocketFactory socketFactory) + throws IOException { + // This is the original test case, modified to replace the call to createSocket() with a new + // Socket() call. + // This version succeeded, even before the bug was fixed. + this.socket.close(); + this.socket = new Socket(); + } + + @CreatesMustCallFor("this") + protected void setupConnection2(javax.net.SocketFactory socketFactory) throws IOException { + this.socket.close(); + // This version succeeds, because getSocket is @Pure, so no side-effects can occur. + this.socket = getSocket(socketFactory); + } + + @CreatesMustCallFor("this") + protected void setupConnection3(javax.net.SocketFactory socketFactory) throws IOException { + // This version demonstrates a work-around. + Socket s = socketFactory.createSocket(); + this.socket.close(); + this.socket = s; + } + + @Pure + private Socket getSocket(javax.net.SocketFactory socketFactory) throws IOException { + return socketFactory.createSocket(); + } + + @EnsuresCalledMethods(value = "this.socket", methods = "close") + private void closeSocket() throws IOException { + this.socket.close(); + } +} diff --git a/checker/tests/resourceleak/SocketIntoList.java b/checker/tests/resourceleak/SocketIntoList.java new file mode 100644 index 000000000000..d56e87fa0440 --- /dev/null +++ b/checker/tests/resourceleak/SocketIntoList.java @@ -0,0 +1,46 @@ +// This test case demonstrates that the checker does issue a warning when +// a socket with an obligation is stored into a List (which cannot be +// owning). + +import org.checkerframework.checker.mustcall.qual.*; + +import java.net.*; +import java.util.List; + +public class SocketIntoList { + public void test1(List<@MustCall({}) Socket> l) { + // s is unconnected, so no error is expected when it's stored into a list + Socket s = new Socket(); + l.add(s); + } + + // :: error: type.argument.type.incompatible + public void test2(List l) { + // s is unconnected, so no error is expected when it's stored into the list. + // But, if the list is unannotated, we do get an error at its declaration site + // (as expected, due to #5912). + Socket s = new Socket(); + l.add(s); + } + + public void test3(List<@MustCall({}) Socket> l) throws Exception { + // :: error: required.method.not.called + Socket s = new Socket(); + s.bind(new InetSocketAddress("192.168.0.1", 0)); + l.add(s); + } + + // This input list might have been produced by e.g., test1() + public void test4(List<@MustCall({}) Socket> l) throws Exception { + // :: error: required.method.not.called + Socket s = l.get(0); + s.bind(new InetSocketAddress("192.168.0.1", 0)); + } + + // This input list might have been produced by e.g., test1() + public void test5(List<@MustCall({}) Socket> l) throws Exception { + // No error is expected here, because the socket should be @MustCall({}) when + // it is retrieved from the list. (Equivalently, the list is not owning). + Socket s = l.get(0); + } +} diff --git a/checker/tests/resourceleak/SocketNullOverwrite.java b/checker/tests/resourceleak/SocketNullOverwrite.java new file mode 100644 index 000000000000..72b6e1b93cfe --- /dev/null +++ b/checker/tests/resourceleak/SocketNullOverwrite.java @@ -0,0 +1,16 @@ +// Taken from ACSocketTest and put here to ease debugging. + +import java.io.IOException; +import java.net.Socket; + +class SocketNullOverwrite { + void replaceVarWithNull(String address, int port) { + try { + // :: error: required.method.not.called + Socket s = new Socket(address, port); + s = null; + } catch (IOException e) { + + } + } +} diff --git a/checker/tests/resourceleak/StaticOwningField.java b/checker/tests/resourceleak/StaticOwningField.java new file mode 100644 index 000000000000..2da76b120cb2 --- /dev/null +++ b/checker/tests/resourceleak/StaticOwningField.java @@ -0,0 +1,67 @@ +import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods; +import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor; +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.Owning; + +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintStream; + +@MustCall("close") class StaticOwningField implements Closeable { + + // Instance field + + private @Owning @MustCall("close") PrintStream ps_instance; + + @CreatesMustCallFor("this") + void m_instance() throws IOException { + ps_instance.close(); + ps_instance = new PrintStream("filename.txt"); + } + + @EnsuresCalledMethods(value = "ps_instance", methods = "close") + @Override + public void close() { + ps_instance.close(); + } + + // Static field + + // :: error: (required.method.not.called) + private static @Owning @MustCall("close") PrintStream ps_static; + + // :: error: (missing.creates.mustcall.for) + static void m_static() throws IOException { + ps_static.close(); + ps_static = new PrintStream("filename.txt"); + } + + // :: error: (required.method.not.called) + private static @Owning @MustCall("close") PrintStream ps_static_initialized1 = + newPrintStreamWithoutExceptions(); + + // :: error: (required.method.not.called) + private static @Owning @MustCall("close") PrintStream ps_static_initialized2; + + static { + // :: error: (required.method.not.called) + ps_static_initialized2 = newPrintStreamWithoutExceptions(); + } + + private static final @Owning @MustCall("close") PrintStream ps_static_final_initialized1 = + newPrintStreamWithoutExceptions(); + + private static final @Owning @MustCall("close") PrintStream ps_static_final_initialized2; + + static { + ps_static_final_initialized2 = newPrintStreamWithoutExceptions(); + } + + public static PrintStream newPrintStreamWithoutExceptions() { + try { + return new PrintStream("filename.txt"); + } catch (Exception e) { + throw new Error(e); + } + } +} diff --git a/checker/tests/resourceleak/StaticOwningFieldOtherClass.java b/checker/tests/resourceleak/StaticOwningFieldOtherClass.java new file mode 100644 index 000000000000..1999a06499af --- /dev/null +++ b/checker/tests/resourceleak/StaticOwningFieldOtherClass.java @@ -0,0 +1,24 @@ +import org.checkerframework.checker.mustcall.qual.Owning; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.UncheckedIOException; + +public class StaticOwningFieldOtherClass {} + +abstract class HasStaticOwningField { + // :: error: (required.method.not.called) + public static @Owning FileWriter log = null; +} + +class TestUtils { + // :: error: (missing.creates.mustcall.for) + public static void setLog(String filename) { + try { + // :: error: (required.method.not.called) + HasStaticOwningField.log = new FileWriter(filename); + } catch (IOException ioe) { + throw new UncheckedIOException("Cannot write file " + filename, ioe); + } + } +} diff --git a/checker/tests/resourceleak/StringConcatenation.java b/checker/tests/resourceleak/StringConcatenation.java new file mode 100644 index 000000000000..017a8b105b7f --- /dev/null +++ b/checker/tests/resourceleak/StringConcatenation.java @@ -0,0 +1,17 @@ +public class StringConcatenation { + public final V1 first; + public final V2 second; + + private StringConcatenation(V1 v1, V2 v2) { + this.first = v1; + this.second = v2; + } + + public static StringConcatenation of(V1 v1, V2 v2) { + return new StringConcatenation<>(v1, v2); + } + + public String toString() { + return "StringConcatenation(" + first + ", " + second + ")"; + } +} diff --git a/checker/tests/resourceleak/StringFromObject.java b/checker/tests/resourceleak/StringFromObject.java new file mode 100644 index 000000000000..0d108848d24a --- /dev/null +++ b/checker/tests/resourceleak/StringFromObject.java @@ -0,0 +1,20 @@ +// Tests that converting to a String from an object doesn't create +// phantom must call obligations. Taken from some code in Zookeeper that +// caused a false positive. + +import java.util.Map; +import java.util.Map.Entry; + +class StringFromObject { + boolean test(Map map) { + boolean isHierarchical = false; + for (Entry entry : map.entrySet()) { + String key = entry.getKey().toString().trim(); + if (key.startsWith("group") || key.startsWith("weight")) { + isHierarchical = true; + break; + } + } + return isHierarchical; + } +} diff --git a/checker/tests/resourceleak/TernaryExpressions.java b/checker/tests/resourceleak/TernaryExpressions.java new file mode 100644 index 000000000000..710e18ced666 --- /dev/null +++ b/checker/tests/resourceleak/TernaryExpressions.java @@ -0,0 +1,121 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +class TernaryExpressions { + + @InheritableMustCall("a") + class Foo { + void a() {} + + @This Foo b() { + return this; + } + + void c(@CalledMethods("a") Foo this) {} + } + + Foo makeFoo() { + return new Foo(); + } + + static void takeOwnership(@Owning Foo foo) { + foo.a(); + } + + /** cases where ternary expressions are assigned to a variable */ + void testTernaryAssigned(boolean b) { + Foo ternary1 = b ? new Foo() : makeFoo(); + ternary1.a(); + + // :: error: required.method.not.called + Foo ternary2 = b ? new Foo() : makeFoo(); + + // :: error: required.method.not.called + Foo x = new Foo(); + Foo ternary3 = b ? new Foo() : x; + ternary3.a(); + + Foo y = new Foo(); + Foo ternary4 = b ? y : y; + ternary4.a(); + + takeOwnership(b ? new Foo() : makeFoo()); + + // :: error: required.method.not.called + Foo x2 = new Foo(); + takeOwnership(b ? x2 : null); + + int i = 10; + Foo ternaryInLoop = null; + while (i > 0) { + // :: error: required.method.not.called + ternaryInLoop = b ? null : new Foo(); + i--; + } + ternaryInLoop.a(); + + (b ? new Foo() : makeFoo()).a(); + } + + /** + * tests where ternary and cast expressions (possibly nested) may or may not be assigned to a + * variable + */ + void testTernaryCastUnassigned(boolean b) { + // :: error: required.method.not.called + if ((b ? new Foo() : null) != null) { + b = !b; + } + + // :: error: required.method.not.called + if ((b ? makeFoo() : null) != null) { + b = !b; + } + + Foo x = new Foo(); + if ((b ? x : null) != null) { + b = !b; + } + x.a(); + + // :: error: required.method.not.called + if (((Foo) new Foo()) != null) { + b = !b; + } + + // double cast; no error + Foo doubleCast = (Foo) ((Foo) makeFoo()); + doubleCast.a(); + + // nesting casts and ternary expressions; no error + Foo deepNesting = (b ? (!b ? makeFoo() : (Foo) makeFoo()) : ((Foo) new Foo())); + deepNesting.a(); + } + + @Owning + Foo testTernaryReturnOk(boolean b) { + return b ? new Foo() : makeFoo(); + } + + @Owning + Foo testTernaryReturnBad(boolean b) { + // :: error: required.method.not.called + Foo x = new Foo(); + return b ? x : makeFoo(); + } + + @InheritableMustCall("toString") + static class Sub1 extends Object {} + + @InheritableMustCall("clone") + static class Sub2 extends Object {} + + static void testTernarySubtyping(boolean b) { + // :: error: required.method.not.called + Object toStringAndClone = b ? new Sub1() : new Sub2(); + // at this point, for soundness, we should be responsible for calling both toString and + // clone on obj... + toStringAndClone.toString(); + } +} diff --git a/checker/tests/resourceleak/TryWithResourcesDeclaration.java b/checker/tests/resourceleak/TryWithResourcesDeclaration.java new file mode 100644 index 000000000000..ebf7baf54b68 --- /dev/null +++ b/checker/tests/resourceleak/TryWithResourcesDeclaration.java @@ -0,0 +1,43 @@ +// Tests for try-with-resources that declare local variables in the resource declaration. + +import java.io.*; +import java.net.Socket; +import java.nio.channels.*; +import java.util.*; + +class TryWithResourcesDeclaration { + static void test(String address, int port) { + try (Socket socket = new Socket(address, port)) { + + } catch (Exception e) { + + } + } + + public boolean isPreUpgradableLayout(File oldF) throws IOException { + + if (!oldF.exists()) { + return false; + } + // check the layout version inside the storage file + // Lock and Read old storage file + try (RandomAccessFile oldFile = new RandomAccessFile(oldF, "rws"); + FileLock oldLock = oldFile.getChannel().tryLock()) { + if (null == oldLock) { + throw new OverlappingFileLockException(); + } + oldFile.seek(0); + int oldVersion = oldFile.readInt(); + return false; + } + } + + public void testNestedTryWithResourcesDecls(Properties prop, ClassLoader cl, String propfile) + throws Exception { + try (InputStream in = cl.getResourceAsStream(propfile)) { + try (InputStream fis = new FileInputStream(propfile)) { + prop.load(fis); + } + } + } +} diff --git a/checker/tests/resourceleak/TryWithResourcesFP.java b/checker/tests/resourceleak/TryWithResourcesFP.java new file mode 100644 index 000000000000..b49932f95f05 --- /dev/null +++ b/checker/tests/resourceleak/TryWithResourcesFP.java @@ -0,0 +1,29 @@ +// Based on a false positive reported on the BibTeX project + +import org.plumelib.util.EntryReader; +import org.plumelib.util.UtilPlume; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; + +@SuppressWarnings("deprecation") +public final class TryWithResourcesFP { + public static void main(String[] args) { + for (String filename : args) { + File inFile = new File(filename); + File outFile = new File(inFile.getName()); // in current directory + // Delete the file to work around a bug. Files.newBufferedWriter (which is called by + // UtilPlume.bufferedFileWriter) seems to have a bug where it does not correctly + // truncate the file first. If the target file already exists, then characters beyond + // what is written remain in the file. + outFile.delete(); + try (PrintWriter out = + new PrintWriter(UtilPlume.bufferedFileWriter(outFile.toString())); + EntryReader er = new EntryReader(filename)) { + } catch (IOException e) { + + } + } + } +} diff --git a/checker/tests/resourceleak/TryWithResourcesMultiResources.java b/checker/tests/resourceleak/TryWithResourcesMultiResources.java new file mode 100644 index 000000000000..de39bfc03994 --- /dev/null +++ b/checker/tests/resourceleak/TryWithResourcesMultiResources.java @@ -0,0 +1,43 @@ +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.IOException; +import java.net.Socket; + +public class TryWithResourcesMultiResources { + + class OuterResource implements java.io.Closeable { + private final @Owning Socket socket; + + public @MustCallAlias OuterResource(@MustCallAlias Socket sock) throws IOException { + this.socket = sock; + } + + @Override + @EnsuresCalledMethods( + value = {"this.socket"}, + methods = {"close"}) + public void close() throws IOException { + this.socket.close(); + } + } + + // If "new OuterResource" throws an exception, then the socket won't be released. + public void multiResourcesWrong(String address, int port) { + // :: error: required.method.not.called + try (OuterResource outer = new OuterResource(new Socket(address, port))) { + + } catch (Exception e) { + + } + } + + public void multiResourcesCorrect(String address, int port) { + try (Socket s = new Socket(address, port); + OuterResource outer = new OuterResource(s)) { + + } catch (Exception e) { + + } + } +} diff --git a/checker/tests/resourceleak/TryWithResourcesVariable.java b/checker/tests/resourceleak/TryWithResourcesVariable.java new file mode 100644 index 000000000000..34568261fba6 --- /dev/null +++ b/checker/tests/resourceleak/TryWithResourcesVariable.java @@ -0,0 +1,127 @@ +// Test for try-with-resources where the resource declaration uses an existing variable + +// @below-java9-jdk-skip-test + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.net.*; + +class TryWithResourcesVariable { + static void test1() throws Exception { + Socket socket = new Socket("127.0.0.1", 5050); + try (socket) { + + } catch (Exception e) { + + } + } + + static void test2(@Owning Socket socket) { + try (socket) { + + } catch (Exception e) { + + } + } + + static void test3(InetSocketAddress isa) { + Socket socket = new Socket(); + try (socket) { + socket.connect(isa); + } catch (Exception e) { + + } + } + + // :: error: (required.method.not.called) + static void test4(@Owning InputStream i1, @Owning InputStream i2) { + try { + try (i2) {} + // This will not run if i2.close() throws an IOException + i1.close(); + } catch (Exception e) { + + } + } + + static void test4Fixed(@Owning InputStream i1, @Owning InputStream i2) throws IOException { + try { + try (i2) {} + } catch (Exception e) { + } + i1.close(); + } + + @InheritableMustCall("disposer") + static class FinalResourceField { + final @Owning Socket socketField; + + FinalResourceField() { + try { + socketField = new Socket("127.0.0.1", 5050); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @EnsuresCalledMethods(value = "this.socketField", methods = "close") + void disposer() { + try (socketField) { + + } catch (Exception e) { + + } + } + } + + static void closeFinalFieldUnsupported() throws Exception { + // This is a false positive (i.e., there is no resource leak), but our checker reports a + // warning since it does not support this coding pattern. + // :: error: (required.method.not.called) + FinalResourceField finalResourceField = new FinalResourceField(); + try (finalResourceField.socketField) {} + } + + @InheritableMustCall("disposer") + static class FinalResourceFieldWrapper { + + final @Owning FinalResourceField frField = new FinalResourceField(); + + @EnsuresCalledMethods(value = "this.frField", methods = "disposer") + void disposer() { + this.frField.disposer(); + } + } + + static void closeWrapperUnsupported() throws Exception { + // This is a false positive (i.e., there is no resource leak), but our checker reports a + // warning since it does not support this coding pattern. + // :: error: (required.method.not.called) + FinalResourceFieldWrapper finalResourceFieldWrapper = new FinalResourceFieldWrapper(); + try (finalResourceFieldWrapper.frField.socketField) {} + } + + @InheritableMustCall("disposer") + static class TwoFinalResourceFields { + final @Owning Socket socketField1; + final @Owning Socket socketField2; + + TwoFinalResourceFields(@Owning Socket socket1, @Owning Socket socket2) { + socketField1 = socket1; + socketField2 = socket2; + } + + @EnsuresCalledMethods(value = "this.socketField1", methods = "close") + @EnsuresCalledMethods(value = "this.socketField2", methods = "close") + void disposer() { + try (socketField1; + socketField2) { + + } catch (Exception e) { + + } + } + } +} diff --git a/checker/tests/resourceleak/TwoConstructorsCloseable.java b/checker/tests/resourceleak/TwoConstructorsCloseable.java new file mode 100644 index 000000000000..47a20424728a --- /dev/null +++ b/checker/tests/resourceleak/TwoConstructorsCloseable.java @@ -0,0 +1,19 @@ +// A test case for false positives that I encountered in Zookeeper. + +import java.io.Closeable; + +public class TwoConstructorsCloseable implements Closeable { + public TwoConstructorsCloseable(Object obj) {} + + public TwoConstructorsCloseable() { + this(null); + } + + public void close() {} + + class Derivative extends TwoConstructorsCloseable { + Derivative() { + super(null); + } + } +} diff --git a/checker/tests/resourceleak/TwoOwningMCATest.java b/checker/tests/resourceleak/TwoOwningMCATest.java new file mode 100644 index 000000000000..5166c73253dd --- /dev/null +++ b/checker/tests/resourceleak/TwoOwningMCATest.java @@ -0,0 +1,38 @@ +// Test case for issue 5453: https://github.com/typetools/checker-framework/issues/5453 + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +@InheritableMustCall({"finish1", "finish2"}) +class TwoOwningMCATest { + + @Owning private final Foo f1 = new Foo(); + + @Owning private final Foo f2; + + @MustCallAlias + // :: error: mustcallalias.out.of.scope + TwoOwningMCATest(@MustCallAlias Foo g) { + this.f2 = g; + } + + @EnsuresCalledMethods(value = "this.f1", methods = "a") + void finish1() { + this.f1.a(); + } + + @EnsuresCalledMethods(value = "this.f2", methods = "a") + void finish2() { + this.f2.a(); + } + + @InheritableMustCall("a") + static class Foo { + void a() {} + } + + public static void test(Foo f) { + TwoOwningMCATest t = new TwoOwningMCATest(f); + f.a(); + } +} diff --git a/checker/tests/resourceleak/TwoResourcesECM.java b/checker/tests/resourceleak/TwoResourcesECM.java new file mode 100644 index 000000000000..aea952475664 --- /dev/null +++ b/checker/tests/resourceleak/TwoResourcesECM.java @@ -0,0 +1,42 @@ +// A test case for https://github.com/typetools/checker-framework/issues/4838. +// +// This test that shows that no unsoundess occurs when a single close() method is responsible +// for closing two resources. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.IOException; +import java.net.Socket; + +@InheritableMustCall("dispose") +class TwoResourcesECM { + @Owning Socket s1, s2; + + // The contracts.postcondition error below is thrown because s1 is not final, + // and therefore might theoretically be side-effected by the call to s2.close() + // even on the non-exceptional path. See ReplicaInputStreams.java for a variant + // of this test where such an error is not issued. Because this method can leak + // along both regular and exceptional exits, both errors are issued. + // + // The contracts.exceptional.postcondition.not.satisfied error is thrown because destructors + // have to close their resources even on exception. If s1.close() throws an exception, then + // s2.close() will not be called. + @EnsuresCalledMethods( + value = {"this.s1", "this.s2"}, + methods = {"close"}) + // :: error: (contracts.postcondition.not.satisfied) + // :: error: (contracts.exceptional.postcondition.not.satisfied) + public void dispose() throws IOException { + s1.close(); + s2.close(); + } + + static void test1(TwoResourcesECM obj) { + try { + obj.dispose(); + } catch (IOException ioe) { + + } + } +} diff --git a/checker/tests/resourceleak/TwoSocketContainer.java b/checker/tests/resourceleak/TwoSocketContainer.java new file mode 100644 index 000000000000..f290e89aaf0d --- /dev/null +++ b/checker/tests/resourceleak/TwoSocketContainer.java @@ -0,0 +1,38 @@ +// A test that a class with two owned sockets cannot be @MustCallAliased with both of them. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.net.Socket; + +@InheritableMustCall({"close1", "close2"}) +public class TwoSocketContainer { + @Owning private final Socket s1, s2; + + // :: error: mustcallalias.out.of.scope + public @MustCallAlias TwoSocketContainer(@MustCallAlias Socket s1, @MustCallAlias Socket s2) { + this.s1 = s1; + this.s2 = s2; + } + + @EnsuresCalledMethods( + value = "this.s1", + methods = {"close"}) + public void close1() throws java.io.IOException { + s1.close(); + } + + @EnsuresCalledMethods( + value = "this.s2", + methods = {"close"}) + public void close2() throws java.io.IOException { + s2.close(); + } + + // The following error should be thrown about at least sock2 + // :: error: required.method.not.called + public static void test(@Owning Socket sock1, @Owning Socket sock2) throws java.io.IOException { + TwoSocketContainer tsc = new TwoSocketContainer(sock1, sock2); + sock1.close(); + } +} diff --git a/checker/tests/resourceleak/TwoSocketContainerSafe.java b/checker/tests/resourceleak/TwoSocketContainerSafe.java new file mode 100644 index 000000000000..6e1482a1e761 --- /dev/null +++ b/checker/tests/resourceleak/TwoSocketContainerSafe.java @@ -0,0 +1,43 @@ +// This is the safe version of TwoSocketContainer.java. + +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; + +import java.net.Socket; + +@InheritableMustCall({"close1", "close2"}) +public class TwoSocketContainerSafe { + @Owning private final Socket s1, s2; + + public TwoSocketContainerSafe(@Owning Socket s1, @Owning Socket s2) { + this.s1 = s1; + this.s2 = s2; + } + + @EnsuresCalledMethods( + value = "this.s1", + methods = {"close"}) + public void close1() throws java.io.IOException { + s1.close(); + } + + @EnsuresCalledMethods( + value = "this.s2", + methods = {"close"}) + public void close2() throws java.io.IOException { + s2.close(); + } + + public static void test(@Owning Socket sock1, @Owning Socket sock2) throws java.io.IOException { + TwoSocketContainerSafe tsc = new TwoSocketContainerSafe(sock1, sock2); + try { + tsc.close1(); + } catch (Exception io) { + } finally { + try { + tsc.close2(); + } catch (Exception io2) { + } + } + } +} diff --git a/checker/tests/resourceleak/TypeProcessError.java b/checker/tests/resourceleak/TypeProcessError.java new file mode 100644 index 000000000000..4db149eee1c2 --- /dev/null +++ b/checker/tests/resourceleak/TypeProcessError.java @@ -0,0 +1,33 @@ +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.mustcall.qual.Owning; + +import java.io.IOException; +import java.io.PrintStream; + +public class TypeProcessError { + + @SuppressWarnings("required.method.not.called") + @Owning + @MustCall("close") PrintStream ps_instance; + + @SuppressWarnings("required.method.not.called") + private static @Owning @MustCall("close") PrintStream ps_static; + + @SuppressWarnings("missing.creates.mustcall.for") + static void m_static() throws IOException { + ps_static.close(); + ps_static = new PrintStream("filename.txt"); + } +} + +class TypeProcessError2 extends TypeProcessError { + @SuppressWarnings("required.method.not.called") + @Owning + @MustCall("close") PrintStream ps_instance; + + @SuppressWarnings("missing.creates.mustcall.for") + void m() throws IOException { + super.ps_instance.close(); + super.ps_instance = new PrintStream("filename.txt"); + } +} diff --git a/checker/tests/resourceleak/TypevarDefault.java b/checker/tests/resourceleak/TypevarDefault.java new file mode 100644 index 000000000000..e6641b69a3a3 --- /dev/null +++ b/checker/tests/resourceleak/TypevarDefault.java @@ -0,0 +1,48 @@ +// Minimized test case from InitializationVisitor. + +import org.checkerframework.checker.mustcall.qual.*; + +class IATF< + Value extends CFAV, + Store extends IS, + Transfer extends IT, + Flow extends CFAA> + extends GATF {} + +class CFAV> {} + +class IS, S extends IS> extends CFAS {} + +class IT, T extends IT, S extends IS> extends CFAT {} + +class CFAA, S extends CFAS, T extends CFAT> {} + +class CFAT, S extends CFAS, T extends CFAT> {} + +class CFAS, S extends CFAS> {} + +class GATF< + Value extends CFAV, + Store extends CFAS, + TransferFunction extends CFAT, + FlowAnalysis extends CFAA> { + + public @MustCall({}) Store getRegularExitStore() { + return null; + } +} + +class BTV> {} + +class IV< + Factory extends IATF, + Value extends CFAV, + Store extends IS> + extends BTV { + + Factory atypefactory; + + public void test() { + Store store = this.atypefactory.getRegularExitStore(); + } +} diff --git a/checker/tests/resourceleak/TypevarSimple.java b/checker/tests/resourceleak/TypevarSimple.java new file mode 100644 index 000000000000..639ec7a80cb9 --- /dev/null +++ b/checker/tests/resourceleak/TypevarSimple.java @@ -0,0 +1,11 @@ +// Simple test case to ensure that the CMC can infer facts about parameters with +// type-variable types. + +import org.checkerframework.checker.mustcall.qual.*; + +public class TypevarSimple { + public static void sneakyDropCorrect( + @Owning @MustCall("close") T value1) throws Exception { + value1.close(); + } +} diff --git a/checker/tests/resourceleak/UnconnectedSocketAlias.java b/checker/tests/resourceleak/UnconnectedSocketAlias.java new file mode 100644 index 000000000000..7e1bc0b1bb3f --- /dev/null +++ b/checker/tests/resourceleak/UnconnectedSocketAlias.java @@ -0,0 +1,14 @@ +// A test case for an interaction between CO and aliasing that could +// lead to a soundness bug if handled wrong. + +import java.net.*; + +class UnconnectedSocketAlias { + void test(SocketAddress sa) throws Exception { + // :: error: required.method.not.called + Socket s = new Socket(); + Socket t = s; + t.close(); + s.connect(sa); + } +} diff --git a/checker/tests/resourceleak/WrapperStream.java b/checker/tests/resourceleak/WrapperStream.java new file mode 100644 index 000000000000..ad3c9e90908d --- /dev/null +++ b/checker/tests/resourceleak/WrapperStream.java @@ -0,0 +1,10 @@ +// A simple test that must-call as a type annotation fixes the simplest version +// of the wrapper stream problem. + +import java.io.*; + +class WrapperStream { + void test(byte[] buf) { + InputStream is = new ByteArrayInputStream(buf); + } +} diff --git a/checker/tests/resourceleak/WrapperStreamPoly.java b/checker/tests/resourceleak/WrapperStreamPoly.java new file mode 100644 index 000000000000..7a3eafbd897e --- /dev/null +++ b/checker/tests/resourceleak/WrapperStreamPoly.java @@ -0,0 +1,19 @@ +// A simple test that must-call as a type annotation makes it so that so-called +// "polymorphic" streams like DataInputStream and DataOutputStream are treated as +// their constituent stream. + +import org.checkerframework.checker.mustcall.qual.Owning; + +import java.io.*; + +class WrapperStreamPoly { + void test_no_close_needed(@Owning ByteArrayInputStream b) { + // b doesn't need to be closed, so neither does this stream. + DataInputStream d = new DataInputStream(b); + } + + // :: error: required.method.not.called + void test_close_needed(@Owning InputStream b) { + DataInputStream d = new DataInputStream(b); + } +} diff --git a/checker/tests/resourceleak/ZookeeperByteBufferInputStream.java b/checker/tests/resourceleak/ZookeeperByteBufferInputStream.java new file mode 100644 index 000000000000..0cb36deee57a --- /dev/null +++ b/checker/tests/resourceleak/ZookeeperByteBufferInputStream.java @@ -0,0 +1,63 @@ +// This is a test that the correct errors are thrown on this class, which is a copy of one defined +// by Zookeeper. Earlier versions of our code to handle this issued many duplicate +// inconsistent.mustcall.subtype +// errors. This test case doesn't actually test for that, but it's useful for debugging and as a +// regression +// test that at least one error is still issued. + +import org.checkerframework.checker.mustcall.qual.MustCall; + +import java.io.*; +import java.nio.ByteBuffer; + +@MustCall({}) +// :: error: inconsistent.mustcall.subtype +public class ZookeeperByteBufferInputStream extends InputStream { + + ByteBuffer bb; + + // :: error: super.invocation.invalid + public ZookeeperByteBufferInputStream(ByteBuffer bb) { + this.bb = bb; + } + + @Override + public int read() throws IOException { + if (bb.remaining() == 0) { + return -1; + } + return bb.get() & 0xff; + } + + @Override + public int available() throws IOException { + return bb.remaining(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (bb.remaining() == 0) { + return -1; + } + if (len > bb.remaining()) { + len = bb.remaining(); + } + bb.get(b, off, len); + return len; + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public long skip(long n) throws IOException { + if (n < 0L) { + return 0; + } + n = Math.min(n, bb.remaining()); + bb.position(bb.position() + (int) n); + return n; + } +} diff --git a/checker/tests/resourceleak/ZookeeperReport1.java b/checker/tests/resourceleak/ZookeeperReport1.java new file mode 100644 index 000000000000..a38b2be1a212 --- /dev/null +++ b/checker/tests/resourceleak/ZookeeperReport1.java @@ -0,0 +1,72 @@ +// Based on a Zookeeper false positive that requires unconnected socket support. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.IOException; +import java.net.Socket; +import java.net.SocketAddress; + +class ZookeeperReport1 { + + static int tickTime, initLimit; + + protected static @MustCall({}) Socket createSocket() throws IOException { + Socket sock; + sock = new Socket(); + sock.setSoTimeout(tickTime * initLimit); + return sock; + } + + protected static @MustCall({}) Socket createSocket2() throws IOException { + Socket sock; + sock = createCustomSocket(); + sock.setSoTimeout(tickTime * initLimit); + return sock; + } + + // This is the full version of case 1. + protected static @MustCall({}) Socket createSocket3(boolean b) throws IOException { + Socket sock; + if (b) { + sock = createCustomSocket(); + } else { + sock = new Socket(); + } + sock.setSoTimeout(tickTime * initLimit); + return sock; + } + + private static @MustCall({}) Socket createCustomSocket() { + return new Socket(); + } + + static void use1() throws IOException { + Socket s = createSocket(); + } + + static void use2(SocketAddress endpoint) throws IOException { + // :: error: required.method.not.called + Socket s = createSocket(); + s.connect(endpoint); + } + + static void use3() throws IOException { + Socket s = createSocket2(); + } + + static void use4(SocketAddress endpoint) throws IOException { + // :: error: required.method.not.called + Socket s = createSocket2(); + s.connect(endpoint); + } + + static void use5(boolean b) throws IOException { + Socket s = createSocket3(b); + } + + static void use6(SocketAddress endpoint, boolean b) throws IOException { + // :: error: required.method.not.called + Socket s = createSocket3(b); + s.connect(endpoint); + } +} diff --git a/checker/tests/resourceleak/ZookeeperReport1a.java b/checker/tests/resourceleak/ZookeeperReport1a.java new file mode 100644 index 000000000000..5462d47ad3ba --- /dev/null +++ b/checker/tests/resourceleak/ZookeeperReport1a.java @@ -0,0 +1,73 @@ +// Based on a Zookeeper false positive that requires unconnected socket support. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.IOException; +import java.net.Socket; +import java.net.SocketAddress; + +// Like ZookeeperReport1, but using "@MustCall()" instead of "@MustCall({})" +class ZookeeperReport1a { + + static int tickTime, initLimit; + + protected static @MustCall() Socket createSocket() throws IOException { + Socket sock; + sock = new Socket(); + sock.setSoTimeout(tickTime * initLimit); + return sock; + } + + protected static @MustCall() Socket createSocket2() throws IOException { + Socket sock; + sock = createCustomSocket(); + sock.setSoTimeout(tickTime * initLimit); + return sock; + } + + // This is the full version of case 1. + protected static @MustCall() Socket createSocket3(boolean b) throws IOException { + Socket sock; + if (b) { + sock = createCustomSocket(); + } else { + sock = new Socket(); + } + sock.setSoTimeout(tickTime * initLimit); + return sock; + } + + private static @MustCall() Socket createCustomSocket() { + return new Socket(); + } + + static void use1() throws IOException { + Socket s = createSocket(); + } + + static void use2(SocketAddress endpoint) throws IOException { + // :: error: required.method.not.called + Socket s = createSocket(); + s.connect(endpoint); + } + + static void use3() throws IOException { + Socket s = createSocket2(); + } + + static void use4(SocketAddress endpoint) throws IOException { + // :: error: required.method.not.called + Socket s = createSocket2(); + s.connect(endpoint); + } + + static void use5(boolean b) throws IOException { + Socket s = createSocket3(b); + } + + static void use6(SocketAddress endpoint, boolean b) throws IOException { + // :: error: required.method.not.called + Socket s = createSocket3(b); + s.connect(endpoint); + } +} diff --git a/checker/tests/resourceleak/ZookeeperReport3.java b/checker/tests/resourceleak/ZookeeperReport3.java new file mode 100644 index 000000000000..099519e95fd7 --- /dev/null +++ b/checker/tests/resourceleak/ZookeeperReport3.java @@ -0,0 +1,73 @@ +// Based on a Zookeeper false positive that requires unconnected socket support. +// This example is functionally equivalent to example #4, so I included it in this file +// ("createNewServerSocket"). This version of the test is incomplete: the real version +// of this test also requires support for Optional. See ZookeeperTest3WithOptional.java. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.net.*; + +class ZookeeperReport3 { + + // This is a simpler version of case 3. + ServerSocket createServerSocket_easy( + InetSocketAddress address, boolean portUnification, boolean sslQuorum) { + ServerSocket serverSocket; + try { + serverSocket = new ServerSocket(); + serverSocket.setReuseAddress(true); + serverSocket.bind(address); + return serverSocket; + } catch (IOException e) { + System.err.println("Couldn't bind to " + address.toString() + e); + } + return null; + } + + ServerSocket createServerSocket( + InetSocketAddress address, boolean portUnification, boolean sslQuorum) { + ServerSocket serverSocket; + try { + if (portUnification || sslQuorum) { + serverSocket = new UnifiedServerSocket(portUnification); + } else { + serverSocket = new ServerSocket(); + } + serverSocket.setReuseAddress(true); + serverSocket.bind(address); + return serverSocket; + } catch (IOException e) { + System.err.println("Couldn't bind to " + address.toString() + e); + } + return null; + } + + private ServerSocket createNewServerSocket( + SocketAddress address, boolean portUnification, boolean sslQuorum) throws IOException { + ServerSocket socket; + + if (portUnification) { + System.out.println("Creating TLS-enabled quorum server socket"); + socket = new UnifiedServerSocket(true); + } else if (sslQuorum) { + System.out.println("Creating TLS-only quorum server socket"); + socket = new UnifiedServerSocket(false); + } else { + socket = new ServerSocket(); + } + + socket.setReuseAddress(true); + socket.bind(address); + + return socket; + } + + class UnifiedServerSocket extends ServerSocket { + // A human has to verify that this constructor actually does produce an unconnected socket. + @SuppressWarnings("inconsistent.constructor.type") + public @MustCall({}) UnifiedServerSocket(boolean b) throws IOException { + super(); + } + } +} diff --git a/checker/tests/resourceleak/ZookeeperReport3WithOptional.java b/checker/tests/resourceleak/ZookeeperReport3WithOptional.java new file mode 100644 index 000000000000..7a2fa20ca70c --- /dev/null +++ b/checker/tests/resourceleak/ZookeeperReport3WithOptional.java @@ -0,0 +1,53 @@ +// Based on a Zookeeper false positive that requires unconnected socket support +// and support for java.util.Optional. + +// @skip-test until Optional is supported. For now, users should use null instead. + +import org.checkerframework.checker.mustcall.qual.*; + +import java.io.*; +import java.net.*; +import java.util.Optional; + +class ZookeeperReport3WithOptional { + + // This is a simpler version of case 3. + Optional createServerSocket_easy( + InetSocketAddress address, boolean portUnification, boolean sslQuorum) { + ServerSocket serverSocket; + try { + serverSocket = new ServerSocket(); + serverSocket.setReuseAddress(true); + serverSocket.bind(address); + return Optional.of(serverSocket); + } catch (IOException e) { + System.err.println("Couldn't bind to " + address.toString() + e); + } + return Optional.empty(); + } + + Optional createServerSocket( + InetSocketAddress address, boolean portUnification, boolean sslQuorum) { + ServerSocket serverSocket; + try { + if (portUnification || sslQuorum) { + serverSocket = new UnifiedServerSocket(portUnification); + } else { + serverSocket = new ServerSocket(); + } + serverSocket.setReuseAddress(true); + serverSocket.bind(address); + return Optional.of(serverSocket); + } catch (IOException e) { + System.err.println("Couldn't bind to " + address.toString() + e); + } + return Optional.empty(); + } + + class UnifiedServerSocket extends ServerSocket { + // A human has to verify that this constructor actually does produce an unconnected socket. + public @MustCall({}) UnifiedServerSocket(boolean b) throws IOException { + super(); + } + } +} diff --git a/checker/tests/resourceleak/ZookeeperReport6.java b/checker/tests/resourceleak/ZookeeperReport6.java new file mode 100644 index 000000000000..3adef15563db --- /dev/null +++ b/checker/tests/resourceleak/ZookeeperReport6.java @@ -0,0 +1,15 @@ +// Based on a Zookeeper false positive that requires unconnected socket support. + +import java.io.IOException; +import java.nio.channels.SocketChannel; + +class ZookeeperReport6 { + SocketChannel createSock() throws IOException { + SocketChannel sock; + sock = SocketChannel.open(); + sock.configureBlocking(false); + sock.socket().setSoLinger(false, -1); + sock.socket().setTcpNoDelay(true); + return sock; + } +} diff --git a/checker/tests/resourceleak/ZookeeperTernaryCrash.java b/checker/tests/resourceleak/ZookeeperTernaryCrash.java new file mode 100644 index 000000000000..62df18e405d1 --- /dev/null +++ b/checker/tests/resourceleak/ZookeeperTernaryCrash.java @@ -0,0 +1,76 @@ +import org.checkerframework.checker.mustcall.qual.MustCall; + +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.*; + +final class ZookeeperTernaryCrash { + + private static List getSubjectAltNames(final X509Certificate cert) { + try { + final Collection> entries = cert.getSubjectAlternativeNames(); + if (entries == null) { + return Collections.emptyList(); + } + final List result = new ArrayList(); + for (List entry : entries) { + // the need to add this annotation is annoying, but it's better than the + // alternative, which would be to prevent boxed primitives from having must-call + // types at all. + final Integer type = + entry.size() >= 2 ? (@MustCall({}) Integer) entry.get(0) : null; + if (type != null) { + if (type == SubjectName.DNS || type == SubjectName.IP) { + final Object o = entry.get(1); + if (o instanceof String) { + result.add(new SubjectName((String) o, type)); + } else if (o instanceof byte[]) { + // TODO ASN.1 DER encoded form + } + } + } + } + return result; + } catch (final CertificateParsingException ignore) { + return Collections.emptyList(); + } + } + + private static final class SubjectName { + + static final int DNS = 2; + static final int IP = 7; + + private final String value; + private final int type; + + static SubjectName IP(final String value) { + return new SubjectName(value, IP); + } + + static SubjectName DNS(final String value) { + return new SubjectName(value, DNS); + } + + SubjectName(final String value, final int type) { + if (type != DNS && type != IP) { + throw new IllegalArgumentException("Invalid type: " + type); + } + this.value = Objects.requireNonNull(value); + this.type = type; + } + + public int getType() { + return type; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } + } +} diff --git a/checker/tests/resourceleak/java17/SwitchExpressions.java b/checker/tests/resourceleak/java17/SwitchExpressions.java new file mode 100644 index 000000000000..f5a4521578f9 --- /dev/null +++ b/checker/tests/resourceleak/java17/SwitchExpressions.java @@ -0,0 +1,185 @@ +// @below-java17-jdk-skip-test +import org.checkerframework.checker.calledmethods.qual.*; +import org.checkerframework.checker.mustcall.qual.*; +import org.checkerframework.common.returnsreceiver.qual.*; + +class SwitchExpressions { + + @InheritableMustCall("a") + class Foo { + void a() {} + + @This Foo b() { + return this; + } + + void c(@CalledMethods("a") Foo this) {} + } + + Foo makeFoo() { + return new Foo(); + } + + static void takeOwnership(@Owning Foo foo) { + foo.a(); + } + + /** cases where switch expressions are assigned to a variable */ + void testSwitchAssigned(int i) { + Foo switch1 = + switch (i) { + case 3 -> new Foo(); + default -> makeFoo(); + }; + switch1.a(); + + // :: error: required.method.not.called + Foo switch2 = + switch (i) { + case 3 -> new Foo(); + default -> makeFoo(); + }; + + // :: error: required.method.not.called + Foo x = new Foo(); + Foo switch3 = + switch (i) { + case 3 -> new Foo(); + default -> x; + }; + switch3.a(); + + Foo y = new Foo(); + Foo switch4 = + switch (i) { + case 3 -> y; + default -> y; + }; + switch4.a(); + + takeOwnership( + switch (i) { + case 3 -> new Foo(); + default -> makeFoo(); + }); + + // :: error: required.method.not.called + Foo x2 = new Foo(); + takeOwnership( + switch (i) { + case 3 -> x2; + default -> null; + }); + + int j = 10; + Foo switchInLoop = null; + while (j > 0) { + // :: error: required.method.not.called + switchInLoop = + switch (i) { + case 3 -> null; + default -> new Foo(); + }; + j--; + } + switchInLoop.a(); + + (switch (i) { + case 3 -> new Foo(); + default -> makeFoo(); + }) + .a(); + } + + /** + * tests where switch and cast expressions (possibly nested) may or may not be assigned to a + * variable + */ + void testSwitchCastUnassigned(int i) { + // :: error: required.method.not.called + if ((switch (i) { + case 3 -> new Foo(); + default -> null; + }) + != null) { + i = -i; + } + + // :: error: required.method.not.called + if (switch (i) { + case 3 -> makeFoo(); + default -> null; + } + != null) { + i = -i; + } + + Foo x = new Foo(); + if (switch (i) { + case 3 -> x; + default -> null; + } + != null) { + i = -i; + } + x.a(); + + // :: error: required.method.not.called + if (((Foo) new Foo()) != null) { + i = -i; + } + + // double cast; no error + Foo doubleCast = (Foo) ((Foo) makeFoo()); + doubleCast.a(); + + // nesting casts and switch expressions; no error + Foo deepNesting = + (switch (i) { + case 3 -> + (switch (-i) { + case -3 -> makeFoo(); + default -> (Foo) makeFoo(); + }); + default -> ((Foo) new Foo()); + }); + deepNesting.a(); + } + + @Owning + Foo testSwitchReturnOk(int i) { + return switch (i) { + case 3 -> new Foo(); + default -> makeFoo(); + }; + } + + @Owning + Foo testSwitchReturnBad(int i) { + // :: error: required.method.not.called + Foo x = new Foo(); + return switch (i) { + case 3 -> x; + default -> makeFoo(); + }; + } + + @InheritableMustCall("toString") + static class Sub1 extends Object {} + + @InheritableMustCall("clone") + static class Sub2 extends Object {} + + static void testSwitchSubtyping(int i) { + // :: error: required.method.not.called + Object toStringAndClone = + switch (i) { + case 3 -> new Sub1(); + default -> new Sub2(); + }; + // at this point, for soundness, we should be responsible for calling both toString and + // clone on + // obj... + toStringAndClone.toString(); + } +} diff --git a/checker/tests/signature/ArraysAsList.java b/checker/tests/signature/ArraysAsList.java index 6a0e829c2bce..cd20e5d5cfa8 100644 --- a/checker/tests/signature/ArraysAsList.java +++ b/checker/tests/signature/ArraysAsList.java @@ -1,6 +1,7 @@ +import org.checkerframework.checker.signature.qual.*; + import java.util.Arrays; import java.util.List; -import org.checkerframework.checker.signature.qual.*; public class ArraysAsList { diff --git a/checker/tests/signature/CanonicalNameNonEmptyTest.java b/checker/tests/signature/CanonicalNameNonEmptyTest.java new file mode 100644 index 000000000000..fd7a3eac882a --- /dev/null +++ b/checker/tests/signature/CanonicalNameNonEmptyTest.java @@ -0,0 +1,29 @@ +import org.checkerframework.checker.signature.qual.*; + +public class CanonicalNameNonEmptyTest { + + @CanonicalName String nonEmpty1(@CanonicalNameOrEmpty String s) { + if (s.isEmpty()) { + return null; + } else { + return s; + } + } + + @CanonicalName String nonEmpty2(@CanonicalNameOrEmpty String s) { + if (!s.isEmpty()) { + return s; + } else { + return null; + } + } + + @CanonicalName String nonEmpty3(@FullyQualifiedName String s) { + if (s.isEmpty()) { + return null; + } else { + // :: error: (return.type.incompatible) + return s; + } + } +} diff --git a/checker/tests/signature/ClassGetNameBinaryName.java b/checker/tests/signature/ClassGetNameBinaryName.java new file mode 100644 index 000000000000..544031cef8fd --- /dev/null +++ b/checker/tests/signature/ClassGetNameBinaryName.java @@ -0,0 +1,120 @@ +import org.checkerframework.checker.signature.qual.CanonicalNameAndBinaryName; +import org.checkerframework.checker.signature.qual.DotSeparatedIdentifiers; +import org.checkerframework.checker.signature.qual.PrimitiveType; + +public class ClassGetNameBinaryName { + + static class Nested {} + + class Inner {} + + class TestGetName { + + @DotSeparatedIdentifiers String s1 = ClassGetNameBinaryName.class.getName(); + + @DotSeparatedIdentifiers String s2a = Integer.class.getName(); + + @DotSeparatedIdentifiers String s2b = java.lang.Integer.class.getName(); + + @DotSeparatedIdentifiers String s4a = Boolean.class.getName(); + + // :: error: (assignment.type.incompatible) + @PrimitiveType String s4b = Boolean.class.getName(); + + // :: error: (assignment.type.incompatible) + @DotSeparatedIdentifiers String s12 = Nested.class.getName(); + + // :: error: (assignment.type.incompatible) + @DotSeparatedIdentifiers String s13 = Inner.class.getName(); + + /// Primitive types + + @PrimitiveType String prim1 = int.class.getName(); + + // :: error: (assignment.type.incompatible) + @DotSeparatedIdentifiers String prim2 = int.class.getName(); + + @PrimitiveType String prim3 = boolean.class.getName(); + + // :: error: (assignment.type.incompatible) + @DotSeparatedIdentifiers String prim4 = boolean.class.getName(); + + // :: error: (assignment.type.incompatible) + @DotSeparatedIdentifiers String prim5 = void.class.getName(); + + // :: error: (assignment.type.incompatible) + @PrimitiveType String prim6 = void.class.getName(); + + /// Arrays + + // :: error: (assignment.type.incompatible) + @DotSeparatedIdentifiers String s6 = int[].class.getName(); + + // :: error: (assignment.type.incompatible) + @DotSeparatedIdentifiers String s7 = int[][].class.getName(); + + // :: error: (assignment.type.incompatible) + @DotSeparatedIdentifiers String s8 = boolean[].class.getName(); + + // :: error: (assignment.type.incompatible) + @DotSeparatedIdentifiers String s9 = Integer[].class.getName(); + + // :: error: (assignment.type.incompatible) + @DotSeparatedIdentifiers String s10 = Boolean[].class.getName(); + } + + class TestGetCanonicalName { + + @CanonicalNameAndBinaryName String s1 = ClassGetNameBinaryName.class.getCanonicalName(); + + @CanonicalNameAndBinaryName String s2a = Integer.class.getCanonicalName(); + + @CanonicalNameAndBinaryName String s2b = java.lang.Integer.class.getCanonicalName(); + + @CanonicalNameAndBinaryName String s4a = Boolean.class.getCanonicalName(); + + // :: error: (assignment.type.incompatible) + @PrimitiveType String s4b = Boolean.class.getCanonicalName(); + + // :: error: (assignment.type.incompatible) + @CanonicalNameAndBinaryName String s12 = Nested.class.getCanonicalName(); + + // :: error: (assignment.type.incompatible) + @CanonicalNameAndBinaryName String s13 = Inner.class.getName(); + + /// Primitive types + + @PrimitiveType String prim1 = int.class.getCanonicalName(); + + // :: error: (assignment.type.incompatible) + @CanonicalNameAndBinaryName String prim2 = int.class.getCanonicalName(); + + @PrimitiveType String prim3 = boolean.class.getCanonicalName(); + + // :: error: (assignment.type.incompatible) + @CanonicalNameAndBinaryName String prim4 = boolean.class.getCanonicalName(); + + // :: error: (assignment.type.incompatible) + @CanonicalNameAndBinaryName String prim5 = void.class.getCanonicalName(); + + // :: error: (assignment.type.incompatible) + @PrimitiveType String prim6 = void.class.getCanonicalName(); + + /// Arrays + + // :: error: (assignment.type.incompatible) + @CanonicalNameAndBinaryName String s6 = int[].class.getCanonicalName(); + + // :: error: (assignment.type.incompatible) + @CanonicalNameAndBinaryName String s7 = int[][].class.getCanonicalName(); + + // :: error: (assignment.type.incompatible) + @CanonicalNameAndBinaryName String s8 = boolean[].class.getCanonicalName(); + + // :: error: (assignment.type.incompatible) + @CanonicalNameAndBinaryName String s9 = Integer[].class.getCanonicalName(); + + // :: error: (assignment.type.incompatible) + @CanonicalNameAndBinaryName String s10 = Boolean[].class.getCanonicalName(); + } +} diff --git a/checker/tests/signature/DiamondTest.java b/checker/tests/signature/DiamondTest.java index 8f9fbe60a9eb..04402b728fde 100644 --- a/checker/tests/signature/DiamondTest.java +++ b/checker/tests/signature/DiamondTest.java @@ -1,6 +1,7 @@ -import java.util.ArrayList; import org.checkerframework.checker.signature.qual.*; +import java.util.ArrayList; + public class DiamondTest { void m() { diff --git a/checker/tests/signature/FakeOverridePoly.java b/checker/tests/signature/FakeOverridePoly.java new file mode 100644 index 000000000000..b46a947e7341 --- /dev/null +++ b/checker/tests/signature/FakeOverridePoly.java @@ -0,0 +1,12 @@ +// @skip-test until fake overrides affect formal parameter types as well as return types + +import org.checkerframework.checker.signature.qual.CanonicalName; + +import javax.lang.model.element.Name; + +public class FakeOverridePoly { + + void m(@CanonicalName Name n) { + @CanonicalName String s = n.toString(); + } +} diff --git a/checker/tests/signature/PolySignatureTest2.java b/checker/tests/signature/PolySignatureTest2.java index 3b0ff886c487..2dc859add405 100644 --- a/checker/tests/signature/PolySignatureTest2.java +++ b/checker/tests/signature/PolySignatureTest2.java @@ -1,13 +1,14 @@ // Test for stub files and https://tinyurl.com/cfissue/658 . // Commented in part because that issue is not yet fixed. +import org.checkerframework.checker.signature.qual.*; + import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; -import org.checkerframework.checker.signature.qual.*; public class PolySignatureTest2 { - @DotSeparatedIdentifiers Name m1(TypeElement e) { + @CanonicalNameOrEmpty Name m1(TypeElement e) { return e.getQualifiedName(); } diff --git a/checker/tests/signature/SignatureLiteralTest.java b/checker/tests/signature/SignatureLiteralTest.java new file mode 100644 index 000000000000..66cf1d73ad83 --- /dev/null +++ b/checker/tests/signature/SignatureLiteralTest.java @@ -0,0 +1,7 @@ +import org.checkerframework.checker.signature.qual.FullyQualifiedName; + +public class SignatureLiteralTest { + + protected static final @FullyQualifiedName String FORMAT_NAME = + "org.checkerframework.checker.formatter.qual.Format"; +} diff --git a/checker/tests/signature/SignatureTypeFactoryTest.java b/checker/tests/signature/SignatureTypeFactoryTest.java index 9917b6483f32..017fa38a4deb 100644 --- a/checker/tests/signature/SignatureTypeFactoryTest.java +++ b/checker/tests/signature/SignatureTypeFactoryTest.java @@ -130,7 +130,6 @@ void m() { fbn = s2; us = s3; - // :: error: (assignment.type.incompatible) fqn = s3; cgn = s3; // :: error: (assignment.type.incompatible) @@ -243,7 +242,6 @@ void m() { fbn = s11; us = s12; - // :: error: (assignment.type.incompatible) fqn = s12; // :: error: (assignment.type.incompatible) cgn = s12; @@ -701,7 +699,6 @@ void m() { fbn = t27; us = t26; - // :: error: (assignment.type.incompatible) fqn = t26; cgn = t26; // :: error: (assignment.type.incompatible) @@ -730,7 +727,6 @@ void m() { fbn = t32; us = t30; - // :: error: (assignment.type.incompatible) fqn = t30; // :: error: (assignment.type.incompatible) cgn = t30; @@ -759,7 +755,6 @@ void m() { fbn = t31; us = t34; - // :: error: (assignment.type.incompatible) fqn = t34; cgn = t34; // :: error: (assignment.type.incompatible) @@ -768,9 +763,7 @@ void m() { iform = t34; // :: error: (assignment.type.incompatible) sn = t34; - // :: error: (assignment.type.incompatible) bn = t34; - // :: error: (assignment.type.incompatible) fbn = t34; us = t17; @@ -799,7 +792,6 @@ void m() { bn = t18; // t18 is pakkage.Outer.Inner[] us = t19; - // :: error: (assignment.type.incompatible) fqn = t19; cgn = t19; // :: error: (assignment.type.incompatible) @@ -812,7 +804,6 @@ void m() { fbn = t19; us = t21; - // :: error: (assignment.type.incompatible) fqn = t21; // :: error: (assignment.type.incompatible) cgn = t21; diff --git a/checker/tests/signedness-initialized-fields/SignednessFields.java b/checker/tests/signedness-initialized-fields/SignednessFields.java new file mode 100644 index 000000000000..4113c0b3d9dd --- /dev/null +++ b/checker/tests/signedness-initialized-fields/SignednessFields.java @@ -0,0 +1,10 @@ +import org.checkerframework.checker.signedness.qual.Signed; + +public class SignednessFields { + + int intField; + @Signed int signedField; + double doubleField; + Object o; + boolean b; +} diff --git a/checker/tests/signedness-unchecked-defaults/TestUncheckedByteCode.java b/checker/tests/signedness-unchecked-defaults/TestUncheckedByteCode.java index f90df0468723..4d1dcba7dca2 100644 --- a/checker/tests/signedness-unchecked-defaults/TestUncheckedByteCode.java +++ b/checker/tests/signedness-unchecked-defaults/TestUncheckedByteCode.java @@ -1,7 +1,8 @@ -import testlib.lib.UncheckedByteCode; +import org.checkerframework.checker.signedness.qual.UnknownSignedness; +import org.checkerframework.framework.testchecker.lib.UncheckedByteCode; public class TestUncheckedByteCode { - Object field; + @UnknownSignedness Object field; void test(UncheckedByteCode param, Integer i) { field = param.getCT(); diff --git a/checker/tests/signedness/AdditionWithChar.java b/checker/tests/signedness/AdditionWithChar.java new file mode 100644 index 000000000000..92bcd8b3ebaa --- /dev/null +++ b/checker/tests/signedness/AdditionWithChar.java @@ -0,0 +1,7 @@ +public class AdditionWithChar { + int i2; + + void additionWithChar(int i1, char c) { + i2 = i1 + c; + } +} diff --git a/checker/tests/signedness/Arrays.java b/checker/tests/signedness/Arrays.java new file mode 100644 index 000000000000..c60c1966c6e0 --- /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/BooleansTest.java b/checker/tests/signedness/BooleansTest.java new file mode 100644 index 000000000000..46ab2c34c96f --- /dev/null +++ b/checker/tests/signedness/BooleansTest.java @@ -0,0 +1,25 @@ +import org.checkerframework.checker.signedness.qual.UnknownSignedness; + +public final class BooleansTest { + + private static int indexOf(boolean[] array, boolean target, int start, int end) { + for (int i = start; i < end; i++) { + if (array[i] == target) { + return i; + } + } + return -1; + } + + static class BooleanArrayAsList { + boolean[] array = new boolean[] {}; + + int start = 0; + int end = 0; + + public boolean contains(@UnknownSignedness Object target) { + return (target instanceof Boolean) + && BooleansTest.indexOf(array, (Boolean) target, start, end) != -1; + } + } +} diff --git a/checker/tests/signedness/BoxedPrimitives.java b/checker/tests/signedness/BoxedPrimitives.java new file mode 100644 index 000000000000..a182725f6073 --- /dev/null +++ b/checker/tests/signedness/BoxedPrimitives.java @@ -0,0 +1,84 @@ +import org.checkerframework.checker.signedness.qual.Signed; +import org.checkerframework.checker.signedness.qual.Unsigned; + +import java.util.LinkedList; + +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/Cast.java b/checker/tests/signedness/Cast.java new file mode 100644 index 000000000000..40122c62fc7e --- /dev/null +++ b/checker/tests/signedness/Cast.java @@ -0,0 +1,22 @@ +import org.checkerframework.checker.signedness.qual.*; + +public class Cast { + + static final Object object = 1; + + void client() { + objectiveParameter(object); + } + + void objectiveParameter(Object object) { + integralParameter((Integer) object); + } + + // This passes when object is initialized within objectiveArgument(). + void objectiveArgument() { + Object object = -3; + integralParameter((Integer) object); + } + + void integralParameter(int x) {} +} diff --git a/checker/tests/signedness/CastedShifts.java b/checker/tests/signedness/CastedShifts.java index 89dfd16db88d..a703a9e0c5a7 100644 --- a/checker/tests/signedness/CastedShifts.java +++ b/checker/tests/signedness/CastedShifts.java @@ -35,37 +35,6 @@ public void CastedIntShifts(@Unsigned int unsigned, @Signed int signed) { byteRes = (@Signed byte) (signed >>> 0); byteRes = (@Signed byte) (signed >> 0); - // Cast to char. - @UnknownSignedness 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); - - // :: 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,35 +204,25 @@ 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); + charRes = (char) (unsigned >>> 55); + charRes = (char) (unsigned >> 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); + charRes = (char) (unsigned >>> 56); + charRes = (char) (unsigned >> 56); // Shifting right by 57, now the MSB matters. - charRes = (@Unsigned char) (unsigned >>> 57); + charRes = (char) (unsigned >>> 57); // :: error: (shift.signed) - charRes = (@Unsigned char) (unsigned >> 57); - - // :: error: (shift.unsigned) - charRes = (@Signed char) (signed >>> 57); - charRes = (@Signed char) (signed >> 57); + charRes = (char) (unsigned >> 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); + charRes = (char) (unsigned >>> 0); + charRes = (char) (unsigned >> 0); // Cast to short. @UnknownSignedness short shortRes; diff --git a/checker/tests/signedness/CharCast.java b/checker/tests/signedness/CharCast.java new file mode 100644 index 000000000000..3d83353bfbc5 --- /dev/null +++ b/checker/tests/signedness/CharCast.java @@ -0,0 +1,29 @@ +import org.checkerframework.checker.signedness.qual.SignedPositive; + +public class CharCast { + + void m(@SignedPositive int i) { + char c = (char) i; + } + + void m1(short s) { + int x = s; + char c = (char) x; + } + + void m2(int i) { + int x = (short) i; + char c = (char) x; + } + + void m3() { + int x = (short) 1; + char c = (char) x; + } + + void m4() { + short x = 1; + int y = x; + char c = (char) y; + } +} diff --git a/checker/tests/signedness/CharCastedToInt.java b/checker/tests/signedness/CharCastedToInt.java new file mode 100644 index 000000000000..25cc6924c233 --- /dev/null +++ b/checker/tests/signedness/CharCastedToInt.java @@ -0,0 +1,8 @@ +public class CharCastedToInt { + int charCastToInt(char c) { + intParameter((int) c); + return (int) c; + } + + void intParameter(int x) {} +} diff --git a/checker/tests/signedness/CharComparisons.java b/checker/tests/signedness/CharComparisons.java new file mode 100644 index 000000000000..c44551f233a4 --- /dev/null +++ b/checker/tests/signedness/CharComparisons.java @@ -0,0 +1,30 @@ +import org.checkerframework.checker.signedness.qual.Unsigned; + +public class CharComparisons { + char c; + @Unsigned byte b; + + void unsignedComparison(char c, @Unsigned byte b) { + // :: error: (comparison.unsignedrhs) + boolean res = c > b; + // :: error: (comparison.unsignedrhs) + res = c >= b; + // :: error: (comparison.unsignedrhs) + res = c < b; + // :: error: (comparison.unsignedrhs) + res = c <= b; + res = c == b; + } + + void unsignedComparisonFields() { + // :: error: (comparison.unsignedrhs) + boolean res = this.c > this.b; + // :: error: (comparison.unsignedrhs) + res = this.c >= this.b; + // :: error: (comparison.unsignedrhs) + res = this.c < this.b; + // :: error: (comparison.unsignedrhs) + res = this.c <= this.b; + res = this.c == this.b; + } +} diff --git a/checker/tests/signedness/CharSignedObject.java b/checker/tests/signedness/CharSignedObject.java new file mode 100644 index 000000000000..e4fd6e79bfc8 --- /dev/null +++ b/checker/tests/signedness/CharSignedObject.java @@ -0,0 +1,5 @@ +public final class CharSignedObject { + void m(int ttype) { + System.out.printf(" bad ttype %c%n", (char) ttype); + } +} diff --git a/checker/tests/signedness/CharToFloat.java b/checker/tests/signedness/CharToFloat.java new file mode 100644 index 000000000000..1caaea9f8f0f --- /dev/null +++ b/checker/tests/signedness/CharToFloat.java @@ -0,0 +1,17 @@ +// Test case for issue #3711: https://github.com/typetools/checker-framework/issues/3711 + +public class CharToFloat { + void castCharacter(Object o) { + floatParameter((Character) o); + doubleParameter((Character) o); + } + + void passCharacter(Character c) { + floatParameter(c); + doubleParameter(c); + } + + void floatParameter(float f) {} + + void doubleParameter(double d) {} +} diff --git a/checker/tests/signedness/CombinationIterator.java b/checker/tests/signedness/CombinationIterator.java new file mode 100644 index 000000000000..d37c8f8e2d19 --- /dev/null +++ b/checker/tests/signedness/CombinationIterator.java @@ -0,0 +1,21 @@ +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +public class CombinationIterator implements Iterator> { + public CombinationIterator(Collection> collectionsOfCandidates) { + ArrayList> listOfCollectionsOfCanditates = + new ArrayList<>(collectionsOfCandidates); + } + + @Override + public boolean hasNext() { + return false; + } + + @Override + public List next() { + return null; + } +} diff --git a/checker/tests/signedness/CompareChars.java b/checker/tests/signedness/CompareChars.java new file mode 100644 index 000000000000..bbacec7b5bfb --- /dev/null +++ b/checker/tests/signedness/CompareChars.java @@ -0,0 +1,17 @@ +// Test case for issue 3668: +// https://github.com/typetools/checker-framework/issues/3669 + +public class CompareChars { + void compareUnsignedChars(char c2) { + char c1 = 'a'; + boolean res = c1 > c2; + res = c1 >= c2; + res = c1 < c2; + res = c1 <= c2; + } + + // Test case for issue #5166: https://tinyurl.com/cfissue/5166 + private static boolean isWhitespace(char c) { + return c <= '\u0020'; + } +} diff --git a/checker/tests/signedness/CompoundAssignments.java b/checker/tests/signedness/CompoundAssignments.java deleted file mode 100644 index ebbc80749676..000000000000 --- a/checker/tests/signedness/CompoundAssignments.java +++ /dev/null @@ -1,166 +0,0 @@ -import org.checkerframework.checker.signedness.qual.*; - -public class CompoundAssignments { - - public void DivModTest( - @Unsigned int unsigned, - @PolySigned int polysigned, - @UnknownSignedness int unknown, - @SignednessGlb int constant) { - - // :: error: (compound.assignment.unsigned.expression) - unknown /= unsigned; - - // :: error: (compound.assignment.unsigned.variable) - // :: error: (compound.assignment.type.incompatible) - unsigned /= unknown; - - // :: error: (compound.assignment.unsigned.variable) - unsigned /= constant; - - // :: error: (compound.assignment.unsigned.expression) - // :: error: (compound.assignment.type.incompatible) - constant /= unsigned; - - // :: error: (compound.assignment.unsigned.expression) - unknown /= polysigned; - - // :: error: (compound.assignment.unsigned.variable) - // :: error: (compound.assignment.type.incompatible) - polysigned /= unknown; - - // :: error: (compound.assignment.unsigned.variable) - // :: error: (compound.assignment.type.incompatible) - polysigned /= constant; - - // :: error: (compound.assignment.unsigned.expression) - // :: error: (compound.assignment.type.incompatible) - constant /= polysigned; - - // :: error: (compound.assignment.unsigned.expression) - unknown %= unsigned; - - // :: error: (compound.assignment.unsigned.variable) - // :: error: (compound.assignment.type.incompatible) - unsigned %= unknown; - - // :: error: (compound.assignment.unsigned.expression) - unknown %= polysigned; - - // :: error: (compound.assignment.unsigned.variable) - // :: error: (compound.assignment.type.incompatible) - polysigned %= unknown; - - // :: error: (compound.assignment.unsigned.variable) - unsigned %= constant; - - // :: error: (compound.assignment.unsigned.expression) - // :: error: (compound.assignment.type.incompatible) - constant %= unsigned; - - // :: error: (compound.assignment.unsigned.variable) - // :: error: (compound.assignment.type.incompatible) - polysigned %= constant; - - // :: error: (compound.assignment.unsigned.expression) - // :: error: (compound.assignment.type.incompatible) - constant %= polysigned; - } - - public void SignedRightShiftTest( - @Unsigned int unsigned, - @PolySigned int polysigned, - @UnknownSignedness int unknown, - @SignednessGlb int constant) { - - // :: error: (compound.assignment.shift.signed) - unsigned >>= constant; - - constant >>= unsigned; - - // :: error: (compound.assignment.shift.signed) - polysigned >>= constant; - - constant >>= polysigned; - - // :: error: (compound.assignment.shift.signed) - unsigned >>= unknown; - - unknown >>= unsigned; - - // :: error: (compound.assignment.shift.signed) - polysigned >>= unknown; - - unknown >>= polysigned; - } - - public void UnsignedRightShiftTest( - @Signed int signed, - @PolySigned int polysigned, - @UnknownSignedness int unknown, - @SignednessGlb int constant) { - - // :: error: (compound.assignment.shift.unsigned) - signed >>>= constant; - - constant >>>= signed; - - // :: error: (compound.assignment.shift.unsigned) - signed >>>= unknown; - - unknown >>>= signed; - - // :: error: (compound.assignment.shift.unsigned) - polysigned >>>= constant; - - constant >>>= polysigned; - - // :: error: (compound.assignment.shift.unsigned) - polysigned >>>= unknown; - - unknown >>>= polysigned; - } - - public void LeftShiftTest( - @Signed int signed, - @Unsigned int unsigned, - @PolySigned int polysigned, - @UnknownSignedness int unknown, - @SignednessGlb int constant) { - - signed <<= constant; - - constant <<= signed; - - signed <<= unknown; - - unknown <<= signed; - - unsigned <<= constant; - - constant <<= unsigned; - - unsigned <<= unknown; - - unknown <<= unsigned; - - polysigned <<= constant; - - constant <<= polysigned; - - polysigned <<= unknown; - - unknown <<= polysigned; - } - - public void mixedTest(@Unsigned int unsigned, @Signed int signed) { - - // :: error: (compound.assignment.mixed.unsigned.variable) - // :: error: (compound.assignment.type.incompatible) - unsigned += signed; - - // :: error: (compound.assignment.mixed.unsigned.expression) - // :: error: (compound.assignment.type.incompatible) - signed += unsigned; - } -} diff --git a/checker/tests/signedness/CompoundAssignmentsSignedness.java b/checker/tests/signedness/CompoundAssignmentsSignedness.java new file mode 100644 index 000000000000..62dccb004a13 --- /dev/null +++ b/checker/tests/signedness/CompoundAssignmentsSignedness.java @@ -0,0 +1,166 @@ +import org.checkerframework.checker.signedness.qual.*; + +public class CompoundAssignmentsSignedness { + + public void DivModTest( + @Unsigned int unsigned, + @PolySigned int polysigned, + @UnknownSignedness int unknown, + @SignednessGlb int constant) { + + // :: error: (compound.assignment.unsigned.expression) + unknown /= unsigned; + + // :: error: (compound.assignment.unsigned.variable) + // :: error: (compound.assignment.type.incompatible) + unsigned /= unknown; + + // :: error: (compound.assignment.unsigned.variable) + unsigned /= constant; + + // :: error: (compound.assignment.unsigned.expression) + // :: error: (compound.assignment.type.incompatible) + constant /= unsigned; + + // :: error: (compound.assignment.unsigned.expression) + unknown /= polysigned; + + // :: error: (compound.assignment.unsigned.variable) + // :: error: (compound.assignment.type.incompatible) + polysigned /= unknown; + + // :: error: (compound.assignment.unsigned.variable) + // :: error: (compound.assignment.type.incompatible) + polysigned /= constant; + + // :: error: (compound.assignment.unsigned.expression) + // :: error: (compound.assignment.type.incompatible) + constant /= polysigned; + + // :: error: (compound.assignment.unsigned.expression) + unknown %= unsigned; + + // :: error: (compound.assignment.unsigned.variable) + // :: error: (compound.assignment.type.incompatible) + unsigned %= unknown; + + // :: error: (compound.assignment.unsigned.expression) + unknown %= polysigned; + + // :: error: (compound.assignment.unsigned.variable) + // :: error: (compound.assignment.type.incompatible) + polysigned %= unknown; + + // :: error: (compound.assignment.unsigned.variable) + unsigned %= constant; + + // :: error: (compound.assignment.unsigned.expression) + // :: error: (compound.assignment.type.incompatible) + constant %= unsigned; + + // :: error: (compound.assignment.unsigned.variable) + // :: error: (compound.assignment.type.incompatible) + polysigned %= constant; + + // :: error: (compound.assignment.unsigned.expression) + // :: error: (compound.assignment.type.incompatible) + constant %= polysigned; + } + + public void SignedRightShiftTest( + @Unsigned int unsigned, + @PolySigned int polysigned, + @UnknownSignedness int unknown, + @SignednessGlb int constant) { + + // :: error: (compound.assignment.shift.signed) + unsigned >>= constant; + + constant >>= unsigned; + + // :: error: (compound.assignment.shift.signed) + polysigned >>= constant; + + constant >>= polysigned; + + // :: error: (compound.assignment.shift.signed) + unsigned >>= unknown; + + unknown >>= unsigned; + + // :: error: (compound.assignment.shift.signed) + polysigned >>= unknown; + + unknown >>= polysigned; + } + + public void UnsignedRightShiftTest( + @Signed int signed, + @PolySigned int polysigned, + @UnknownSignedness int unknown, + @SignednessGlb int constant) { + + // :: error: (compound.assignment.shift.unsigned) + signed >>>= constant; + + constant >>>= signed; + + // :: error: (compound.assignment.shift.unsigned) + signed >>>= unknown; + + unknown >>>= signed; + + // :: error: (compound.assignment.shift.unsigned) + polysigned >>>= constant; + + constant >>>= polysigned; + + // :: error: (compound.assignment.shift.unsigned) + polysigned >>>= unknown; + + unknown >>>= polysigned; + } + + public void LeftShiftTest( + @Signed int signed, + @Unsigned int unsigned, + @PolySigned int polysigned, + @UnknownSignedness int unknown, + @SignednessGlb int constant) { + + signed <<= constant; + + constant <<= signed; + + signed <<= unknown; + + unknown <<= signed; + + unsigned <<= constant; + + constant <<= unsigned; + + unsigned <<= unknown; + + unknown <<= unsigned; + + polysigned <<= constant; + + constant <<= polysigned; + + polysigned <<= unknown; + + unknown <<= polysigned; + } + + public void mixedTest(@Unsigned int unsigned, @Signed int signed) { + + // :: error: (compound.assignment.mixed.unsigned.variable) + // :: error: (compound.assignment.type.incompatible) + unsigned += signed; + + // :: error: (compound.assignment.mixed.unsigned.expression) + // :: error: (compound.assignment.type.incompatible) + signed += unsigned; + } +} diff --git a/checker/tests/signedness/CompoundAssignmentsSignedness2.java b/checker/tests/signedness/CompoundAssignmentsSignedness2.java new file mode 100644 index 000000000000..1710a3516319 --- /dev/null +++ b/checker/tests/signedness/CompoundAssignmentsSignedness2.java @@ -0,0 +1,63 @@ +// Test case for issue #3709: https://github.com/typetools/checker-framework/issues/3709 + +public class CompoundAssignmentsSignedness2 { + void additionWithCompoundAssignment(char c, int i1) { + i1 += c; + } + + void additionWithoutCompoundAssignment1(char c, int i1) { + i1 = (int) (i1 + c); + } + + void additionWithoutCompoundAssignment2(char c, int i1) { + i1 = i1 + c; + } + + void subtractionWithCompoundAssignment(char c, int i1) { + i1 -= c; + } + + void subtractionWithoutCompoundAssignment1(char c, int i1) { + i1 = (int) (i1 - c); + } + + void subtractionWithoutCompoundAssignment2(char c, int i1) { + i1 = i1 - c; + } + + void multiplicationWithCompoundAssignment(char c, int i1) { + i1 *= c; + } + + void multiplicationWithoutCompoundAssignment1(char c, int i1) { + i1 = (int) (i1 * c); + } + + void multiplicationWithoutCompoundAssignment2(char c, int i1) { + i1 = i1 * c; + } + + void divisionWithCompoundAssignment(char c, int i1) { + i1 /= c; + } + + void divisionWithoutCompoundAssignment1(char c, int i1) { + i1 = (int) (i1 / c); + } + + void divisionWithoutCompoundAssignment2(char c, int i1) { + i1 = i1 / c; + } + + void modulusWithCompoundAssignment(char c, int i1) { + i1 %= c; + } + + void modulusWithoutCompoundAssignment1(char c, int i1) { + i1 = (int) (i1 % c); + } + + void modulusWithoutCompoundAssignment2(char c, int i1) { + i1 = i1 % c; + } +} diff --git a/checker/tests/signedness/ConstantTests.java b/checker/tests/signedness/ConstantTests.java index f5704ebe1fb3..313ec42050a5 100644 --- a/checker/tests/signedness/ConstantTests.java +++ b/checker/tests/signedness/ConstantTests.java @@ -1,12 +1,14 @@ // Tests of constants -// @skip-test - import org.checkerframework.checker.signedness.qual.UnknownSignedness; import org.checkerframework.checker.signedness.qual.Unsigned; public class ConstantTests { + @Unsigned int uint_negative_one = (@Unsigned int) -1; + + @Unsigned int u1lit = 0xFFFFFFFE; // unsigned: 2^32 - 2, signed: -2 + void m() { int s = -2 / -1; @@ -23,9 +25,8 @@ void m() { @UnknownSignedness int y = s1 / 2; // :: error: (operation.unsignedlhs) - @UnknownSignedness int z = ((@Unsigned int) -1) / -2; + @UnknownSignedness int z = (uint_negative_one) / -2; - @Unsigned int u1lit = 0xFFFFFFFE; // unsigned: 2^32 - 2, signed: -2 // :: error: (operation.unsignedlhs) @UnknownSignedness int w = u1lit / 2; } diff --git a/checker/tests/signedness/DefaultsSignedness.java b/checker/tests/signedness/DefaultsSignedness.java index 518ac2465ced..8b19c1d08d14 100644 --- a/checker/tests/signedness/DefaultsSignedness.java +++ b/checker/tests/signedness/DefaultsSignedness.java @@ -47,17 +47,6 @@ public void ConstantTest() { // :: error: (assignment.type.incompatible) botLong = testLong; - - // Test chars with literal values - @SignednessGlb char conChar; - @SignednessBottom char botChar; - - char testChar = 'a'; - - conChar = testChar; - - // :: error: (assignment.type.incompatible) - botChar = testChar; } public void SignedTest( @@ -111,32 +100,17 @@ public void SignedTest( conLong = testLong; // Test floats + // :: error: (anno.on.irrelevant) @Signed float sinFloat; - @SignednessGlb float conFloat; sinFloat = testFloat; - // :: error: (assignment.type.incompatible) - conFloat = testFloat; - // Test doubles + // :: error: (anno.on.irrelevant) @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; @@ -188,15 +162,20 @@ public void SignednessBottom() { public void UnknownSignedness(Object testObj, @Unsigned int unsigned, @Signed int signed) { @UnknownSignedness Object unkObj; - @Signed Object sinObj; + @Unsigned Object unsinObj; unkObj = testObj; // :: error: (assignment.type.incompatible) - sinObj = testObj; + unsinObj = testObj; } public void booleanProblem(@Unsigned int unsigned, @Signed int signed) { boolean testBool = unsigned == 1 || signed > 1; } + + void method(Object[] obj_tags, int field_num) { + Object o = new DefaultsSignedness(); + obj_tags[field_num] = o; + } } diff --git a/checker/tests/signedness/Desugar.java b/checker/tests/signedness/Desugar.java new file mode 100644 index 000000000000..b7315e47c17d --- /dev/null +++ b/checker/tests/signedness/Desugar.java @@ -0,0 +1,32 @@ +import org.checkerframework.checker.signedness.qual.PolySigned; +import org.checkerframework.checker.signedness.qual.Signed; + +import java.util.Map; + +public class Desugar { + + void test(int x) { + int i = getI(); + Integer box = i; + @Signed Integer boxy = box; + @Signed Integer box2 = method(box); + } + + @PolySigned Integer method(@PolySigned Integer i) { + return i; + } + + @Signed int getI() { + return 0; + } + + void test2(Map nonceMap, String nextInvo) { + int invoNonce = calcNonce(nextInvo); + Integer key = invoNonce; + String enterInvo = nonceMap.get(key); + } + + private @Signed int calcNonce(String invocation) { + return 0; + } +} diff --git a/checker/tests/signedness/IrrelevantAnnotationsTest.java b/checker/tests/signedness/IrrelevantAnnotationsTest.java new file mode 100644 index 000000000000..bd701eb0400a --- /dev/null +++ b/checker/tests/signedness/IrrelevantAnnotationsTest.java @@ -0,0 +1,15 @@ +import org.checkerframework.checker.signedness.qual.Signed; +import org.checkerframework.checker.signedness.qual.Unsigned; + +public final class IrrelevantAnnotationsTest { + + // :: error: (anno.on.irrelevant) + @Signed Boolean b1; + + // :: error: (anno.on.irrelevant) + @Unsigned Boolean b2; + + @Signed Object o1; + + @Unsigned Object o2; +} diff --git a/checker/tests/signedness/Issue2482.java b/checker/tests/signedness/Issue2482.java index 78c1db2548a6..8872f3c91e3a 100644 --- a/checker/tests/signedness/Issue2482.java +++ b/checker/tests/signedness/Issue2482.java @@ -1,4 +1,4 @@ -class Issue2482 { +public class Issue2482 { void regularAssignment(byte[] b, int c) { int a = b.length; diff --git a/checker/tests/signedness/Issue2483.java b/checker/tests/signedness/Issue2483.java new file mode 100644 index 000000000000..a0abf1dbd297 --- /dev/null +++ b/checker/tests/signedness/Issue2483.java @@ -0,0 +1,8 @@ +import org.checkerframework.checker.signedness.qual.*; + +public class Issue2483 { + void foo(String a, byte[] b) { + @Unsigned int len = a.length(); + @Unsigned int len2 = b.length; + } +} diff --git a/checker/tests/signedness/Issue2534.java b/checker/tests/signedness/Issue2534.java index 732290bb91e6..742f7535bc67 100644 --- a/checker/tests/signedness/Issue2534.java +++ b/checker/tests/signedness/Issue2534.java @@ -1,7 +1,7 @@ import org.checkerframework.checker.signedness.qual.Unsigned; import org.checkerframework.common.value.qual.IntRange; -class Issue2534 { +public class Issue2534 { @IntRange(from = 0, to = Integer.MAX_VALUE) int field = 3; diff --git a/checker/tests/signedness/Issue2543.java b/checker/tests/signedness/Issue2543.java index 243e31d5bc28..704ca5b6b2df 100644 --- a/checker/tests/signedness/Issue2543.java +++ b/checker/tests/signedness/Issue2543.java @@ -5,11 +5,29 @@ public class Issue2543 { + public static @PolySigned int rotateRightPart1(@PolySigned int i, int distance) { + // :: error: (shift.unsigned) + return i >>> distance; + } + + public static @PolySigned int rotateRightPart2(@PolySigned int i, int distance) { + return i << -distance; + } + public static @PolySigned int rotateRight(@PolySigned int i, int distance) { // :: error: (shift.unsigned) return (i >>> distance) | (i << -distance); } + public static @Signed int rotateRightSignedPart1(@Signed int i, int distance) { + // :: error: (shift.unsigned) + return i >>> distance; + } + + public static @Signed int rotateRightSignedPart2(@Signed int i, int distance) { + return i << -distance; + } + public static @Signed int rotateRightSigned(@Signed int i, int distance) { // :: error: (shift.unsigned) return (i >>> distance) | (i << -distance); diff --git a/checker/tests/signedness/Issue3710.java b/checker/tests/signedness/Issue3710.java new file mode 100644 index 000000000000..138c32c95dba --- /dev/null +++ b/checker/tests/signedness/Issue3710.java @@ -0,0 +1,21 @@ +// Test case for issue #3711: https://github.com/typetools/checker-framework/issues/3710 + +public class Issue3710 { + int returnIntWithLocalVariable(char c) { + int i = c; + return i; + } + + long returnLongWithLocalVariable(char c) { + long l = c; + return l; + } + + int returnIntWithoutLocalVariable(char c) { + return c; + } + + long returnLongWithoutLocalVariable(char c) { + return c; + } +} diff --git a/checker/tests/signedness/Issue5256.java b/checker/tests/signedness/Issue5256.java new file mode 100644 index 000000000000..280115c1a445 --- /dev/null +++ b/checker/tests/signedness/Issue5256.java @@ -0,0 +1,16 @@ +final class Issue5256 { + char c; + + public int foo1() { + int x = 1; + x = x + (int) c; + return x; + } + + public int foo2() { + char c = 65535; + int x = 1; + x = x + (int) c; + return x; + } +} diff --git a/checker/tests/signedness/JdkConstantsTest.java b/checker/tests/signedness/JdkConstantsTest.java new file mode 100644 index 000000000000..2321d2ca4fb0 --- /dev/null +++ b/checker/tests/signedness/JdkConstantsTest.java @@ -0,0 +1,14 @@ +import org.checkerframework.checker.signedness.qual.PolySigned; + +public class JdkConstantsTest { + + static @PolySigned int integerMinValue(@PolySigned int value) { + // :: error: (return.type.incompatible) + return Integer.MIN_VALUE; + } + + static @PolySigned int flip(@PolySigned int value) { + // :: error: (return.type.incompatible) + return value ^ Integer.MIN_VALUE; + } +} diff --git a/checker/tests/signedness/LiteralCast.java b/checker/tests/signedness/LiteralCast.java new file mode 100644 index 000000000000..c1ec2552c684 --- /dev/null +++ b/checker/tests/signedness/LiteralCast.java @@ -0,0 +1,73 @@ +import org.checkerframework.checker.signedness.qual.Signed; +import org.checkerframework.checker.signedness.qual.Unsigned; +import org.checkerframework.checker.units.qual.m; + +import java.util.Arrays; + +public class LiteralCast { + + @Unsigned int u; + @Signed int s; + + void m() { + testCompile(2); + // manifest literals are treated as @SignednessGlb + testCompile(-2); + // :: error: (argument.type.incompatible) + testCompile((@Signed int) 2); + testCompile((@Unsigned int) 2); + testCompile((int) 2); + testCompile((@m int) 2); + + requireSigned((@Signed int) 2); + // :: error: (argument.type.incompatible) + requireSigned((@Unsigned int) 2); + requireSigned((int) 2); + requireSigned((@m int) 2); + // :: warning: (cast.unsafe) + requireSigned((@Signed int) u); + // :: error: (argument.type.incompatible) + requireSigned((@Unsigned int) u); + // :: error: (argument.type.incompatible) + requireSigned((int) u); + // :: error: (argument.type.incompatible) + requireSigned((@m int) u); + requireSigned((@Signed int) s); + // :: error: (argument.type.incompatible) :: warning: (cast.unsafe) + requireSigned((@Unsigned int) s); + requireSigned((int) s); + requireSigned((@m int) s); + + // :: error: (argument.type.incompatible) + requireUnsigned((@Signed int) 2); + requireUnsigned((@Unsigned int) 2); + requireUnsigned((int) 2); + requireUnsigned((@m int) 2); + // :: error: (argument.type.incompatible) :: warning: (cast.unsafe) + requireUnsigned((@Signed int) u); + requireUnsigned((@Unsigned int) u); + requireUnsigned((int) u); + requireUnsigned((@m int) u); + // :: error: (argument.type.incompatible) + requireUnsigned((@Signed int) s); + // :: warning: (cast.unsafe) + requireUnsigned((@Unsigned int) s); + // :: error: (argument.type.incompatible) + requireUnsigned((int) s); + // :: error: (argument.type.incompatible) + requireUnsigned((@m int) s); + } + + void requireSigned(@Signed int arg) {} + + void requireUnsigned(@Unsigned int arg) {} + + public static void testCompile(@Unsigned int x) { + @Unsigned int[] arr = {1, 2, 3, 4, 5, 56}; + + Arrays.fill(arr, x); + Arrays.fill(arr, (@Unsigned int) Integer.valueOf(-2)); + Arrays.fill(arr, Integer.valueOf(-2)); + Arrays.fill(arr, (@Unsigned int) Integer.valueOf(2)); + } +} diff --git a/checker/tests/signedness/LocalVarDefaults.java b/checker/tests/signedness/LocalVarDefaults.java new file mode 100644 index 000000000000..60efe1b10813 --- /dev/null +++ b/checker/tests/signedness/LocalVarDefaults.java @@ -0,0 +1,27 @@ +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: (anno.on.irrelevant) + void methodDouble(@Unsigned double unsigned, @Signed double signed) { + double local = unsigned; + double local2 = signed; + } + + void methodInteger(@Unsigned Integer unsignedInt, @Signed Integer signedInt) { + Integer local = unsignedInt; + Integer local2 = signedInt; + } + + // :: error: (anno.on.irrelevant) + void methodDoubleWrapper(@Unsigned Double unsigned, @Signed Double signed) { + Double local = unsigned; + Double local2 = signed; + } +} diff --git a/checker/tests/signedness/MaskedShifts.java b/checker/tests/signedness/MaskedShifts.java index d603ec207d7f..e4609faaf9fd 100644 --- a/checker/tests/signedness/MaskedShifts.java +++ b/checker/tests/signedness/MaskedShifts.java @@ -34,7 +34,7 @@ public void MaskedAndShifts(@Unsigned int unsigned, @Signed int signed) { testRes = (signed >>> 8) & 0x1FFFFFF; testRes = (signed >> 8) & 0x1FFFFFF; - // Use mask that doesn't render the MSB irrelevent, but does render the next 7 MSB_s + // Use mask that doesn't render the MSB irrelevant, but does render the next 7 MSB_s // irrelevant. // Now the left-most introduced bit matters @@ -47,7 +47,7 @@ public void MaskedAndShifts(@Unsigned int unsigned, @Signed int signed) { testRes = (signed >>> 8) & 0x90FFFFFF; testRes = (signed >> 8) & 0x90FFFFFF; - // Use mask that doesn't render any bits irrelevent + // Use mask that doesn't render any bits irrelevant testRes = (unsigned >>> 8) & 0xFFFFFFFF; @@ -68,7 +68,7 @@ public void MaskedAndShifts(@Unsigned int unsigned, @Signed int signed) { testRes = signed >>> 8 & 0xFFFFFF; testRes = signed >> 8 & 0xFFFFFF; - // Use mask that doesn't render any bits irrelevent + // Use mask that doesn't render any bits irrelevant testRes = unsigned >>> 8 & 0xFFFFFFFF; @@ -89,7 +89,7 @@ public void MaskedAndShifts(@Unsigned int unsigned, @Signed int signed) { testRes = ((signed >>> 8)) & 0xFFFFFF; testRes = ((signed >> 8)) & 0xFFFFFF; - // Use mask that doesn't render any bits irrelevent + // Use mask that doesn't render any bits irrelevant testRes = ((unsigned >>> 8)) & 0xFFFFFFFF; @@ -110,7 +110,7 @@ public void MaskedAndShifts(@Unsigned int unsigned, @Signed int signed) { testRes = 0xFFFFFF & (signed >>> 8); testRes = 0xFFFFFF & (signed >> 8); - // Use mask that doesn't render any bits irrelevent + // Use mask that doesn't render any bits irrelevant testRes = 0xFFFFFFFF & (unsigned >>> 8); @@ -131,7 +131,7 @@ public void MaskedAndShifts(@Unsigned int unsigned, @Signed int signed) { testRes = 0xFFFFFF & signed >>> 8; testRes = 0xFFFFFF & signed >> 8; - // Use mask that doesn't render any bits irrelevent + // Use mask that doesn't render any bits irrelevant testRes = 0xFFFFFFFF & unsigned >>> 8; @@ -152,7 +152,7 @@ public void MaskedAndShifts(@Unsigned int unsigned, @Signed int signed) { testRes = signed >>> 8 & (0xFFFFFF); testRes = signed >> 8 & (0xFFFFFF); - // Use mask that doesn't render any bits irrelevent + // Use mask that doesn't render any bits irrelevant testRes = unsigned >>> 8 & (0xFFFFFFFF); @@ -173,7 +173,7 @@ public void MaskedAndShifts(@Unsigned int unsigned, @Signed int signed) { testRes = signed >>> 8 & ((0xFFFFFF)); testRes = signed >> 8 & ((0xFFFFFF)); - // Use mask that doesn't render any bits irrelevent + // Use mask that doesn't render any bits irrelevant testRes = unsigned >>> 8 & ((0xFFFFFFFF)); @@ -217,7 +217,7 @@ public void MaskedOrShifts(@Unsigned int unsigned, @Signed int signed) { testRes = (signed >>> 8) | 0xFE000000; testRes = (signed >> 8) | 0xFE000000; - // Use mask that doesn't render the MSB irrelevent, but does render the next 7 MSB_s + // Use mask that doesn't render the MSB irrelevant, but does render the next 7 MSB_s // irrelevant. // Now the left-most introduced bit matters @@ -230,7 +230,7 @@ public void MaskedOrShifts(@Unsigned int unsigned, @Signed int signed) { testRes = (signed >>> 8) | 0x8F000000; testRes = (signed >> 8) | 0x8F000000; - // Use mask that doesn't render any bits irrelevent + // Use mask that doesn't render any bits irrelevant testRes = (unsigned >>> 8) | 0x0; @@ -251,7 +251,7 @@ public void MaskedOrShifts(@Unsigned int unsigned, @Signed int signed) { testRes = signed >>> 8 | 0xFF000000; testRes = signed >> 8 | 0xFF000000; - // Use mask that doesn't render any bits irrelevent + // Use mask that doesn't render any bits irrelevant testRes = unsigned >>> 8 | 0x0; @@ -272,7 +272,7 @@ public void MaskedOrShifts(@Unsigned int unsigned, @Signed int signed) { testRes = ((signed >>> 8)) | 0xFF000000; testRes = ((signed >> 8)) | 0xFF000000; - // Use mask that doesn't render any bits irrelevent + // Use mask that doesn't render any bits irrelevant testRes = ((unsigned >>> 8)) | 0x0; @@ -293,7 +293,7 @@ public void MaskedOrShifts(@Unsigned int unsigned, @Signed int signed) { testRes = 0xFF000000 | (signed >>> 8); testRes = 0xFF000000 | (signed >> 8); - // Use mask that doesn't render any bits irrelevent + // Use mask that doesn't render any bits irrelevant testRes = 0x0 | (unsigned >>> 8); @@ -314,7 +314,7 @@ public void MaskedOrShifts(@Unsigned int unsigned, @Signed int signed) { testRes = signed >>> 8 | (0xFF000000); testRes = signed >> 8 | (0xFF000000); - // Use mask that doesn't render any bits irrelevent + // Use mask that doesn't render any bits irrelevant testRes = unsigned >>> 8 | (0x0); @@ -335,7 +335,7 @@ public void MaskedOrShifts(@Unsigned int unsigned, @Signed int signed) { testRes = signed >>> 8 | ((0xFF000000)); testRes = signed >> 8 | ((0xFF000000)); - // Use mask that doesn't render any bits irrelevent + // Use mask that doesn't render any bits irrelevant testRes = unsigned >>> 8 | ((0x0)); diff --git a/checker/tests/signedness/ObjectCasts.java b/checker/tests/signedness/ObjectCasts.java new file mode 100644 index 000000000000..9537a197421a --- /dev/null +++ b/checker/tests/signedness/ObjectCasts.java @@ -0,0 +1,107 @@ +// Test case for issue 3668: +// https://github.com/typetools/checker-framework/issues/3668 + +import org.checkerframework.checker.signedness.qual.*; + +public class ObjectCasts { + + Integer castObjectToInteger1(Object o) { + return (Integer) o; + } + + Integer castObjectToInteger2(@Unsigned Object o) { + // :: error: (return.type.incompatible) + return (Integer) o; + } + + Integer castObjectToInteger3(@Signed Object o) { + return (Integer) o; + } + + @Signed Integer castObjectToInteger4(Object o) { + return (Integer) o; + } + + @Signed Integer castObjectToInteger5(@Unsigned Object o) { + // :: error: (return.type.incompatible) + return (Integer) o; + } + + @Signed Integer castObjectToInteger6(@Signed Object o) { + return (Integer) o; + } + + @Unsigned Integer castObjectToInteger7(Object o) { + // :: error: (return.type.incompatible) + return (Integer) o; + } + + @Unsigned Integer castObjectToInteger8(@Unsigned Object o) { + return (Integer) o; + } + + @Unsigned Integer castObjectToInteger9(@Signed Object o) { + // :: error: (return.type.incompatible) + return (Integer) o; + } + + Object castIntegerToObject1(Integer o) { + return (Object) o; + } + + Object castIntegerToObject2(@Unsigned Integer o) { + // :: error: (return.type.incompatible) + return (Object) o; + } + + Object castIntegerToObject3(@Signed Integer o) { + return (Object) o; + } + + @Signed Object castIntegerToObject4(Integer o) { + return (Object) o; + } + + @Signed Object castIntegerToObject5(@Unsigned Integer o) { + // :: error: (return.type.incompatible) + return (Object) o; + } + + @Signed Object castIntegerToObject6(@Signed Integer o) { + return (Object) o; + } + + @Unsigned Object castIntegerToObject7(Integer o) { + // :: error: (return.type.incompatible) + return (Object) o; + } + + @Unsigned Object castIntegerToObject8(@Unsigned Integer o) { + return (Object) o; + } + + @Unsigned Object castIntegerToObject9(@Signed Integer o) { + // :: error: (return.type.incompatible) + return (Object) o; + } + + void castObjectToBoxedVariants() { + byte b1 = 1; + short s1 = 1; + int i1 = 1; + long l1 = 1; + Object[] obj = new Object[] {b1, s1, i1, l1}; + byteParameter((Byte) obj[0]); + shortParameter((Short) obj[1]); + integralParameter((Integer) obj[2]); + longParameter((Long) obj[3]); + } + + void byteParameter(byte b) {} + + void shortParameter(short s) {} + + void integralParameter(int i) {} + + void longParameter(long l) {} +} diff --git a/checker/tests/signedness/PrimitiveCasts.java b/checker/tests/signedness/PrimitiveCasts.java new file mode 100644 index 000000000000..515fc2589ba6 --- /dev/null +++ b/checker/tests/signedness/PrimitiveCasts.java @@ -0,0 +1,40 @@ +import org.checkerframework.checker.signedness.qual.Unsigned; + +public class PrimitiveCasts { + + void shortToChar1(short s) { + 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) { + double d = (double) ui; + } + + void intToDouble2(@Unsigned int ui) { + double d = ui; + } + + double intToDouble3(@Unsigned int ui) { + return ui; + } + + void shortToDouble1(@Unsigned short ui) { + double d = (double) ui; + } + + void shortToDouble2(@Unsigned short ui) { + double d = ui; + } + + double shortToDouble3(@Unsigned short ui) { + return ui; + } +} diff --git a/checker/tests/signedness/RestrictedPolymorphism.java b/checker/tests/signedness/RestrictedPolymorphism.java new file mode 100644 index 000000000000..7658afc4f369 --- /dev/null +++ b/checker/tests/signedness/RestrictedPolymorphism.java @@ -0,0 +1,20 @@ +import org.checkerframework.checker.signedness.qual.PolySigned; +import org.checkerframework.checker.signedness.qual.Signed; +import org.checkerframework.checker.signedness.qual.Unsigned; + +public class RestrictedPolymorphism { + + @Signed Number sn; + @Unsigned Number un; + + public void foo(@PolySigned Object a, @PolySigned Object b) {} + + void client() { + foo(sn, sn); + // :: error: (argument.type.incompatible) + foo(sn, un); + // :: error: (argument.type.incompatible) + foo(un, sn); + foo(un, un); + } +} diff --git a/checker/tests/signedness/ShiftAndMask.java b/checker/tests/signedness/ShiftAndMask.java new file mode 100644 index 000000000000..f287dfafca76 --- /dev/null +++ b/checker/tests/signedness/ShiftAndMask.java @@ -0,0 +1,7 @@ +public class ShiftAndMask { + + void m(long longValue) { + byte b1 = (byte) ((longValue >>> 32) & 0xFF); + byte b2 = (byte) ((longValue >>> 40) & 0xFF); + } +} diff --git a/checker/tests/signedness/SignednessAnnotationError.java b/checker/tests/signedness/SignednessAnnotationError.java new file mode 100644 index 000000000000..6ce74cfd5ac1 --- /dev/null +++ b/checker/tests/signedness/SignednessAnnotationError.java @@ -0,0 +1,11 @@ +import org.checkerframework.checker.nullness.qual.KeyFor; + +import java.util.ArrayList; +import java.util.List; + +public class SignednessAnnotationError { + void test() { + List<@KeyFor("hello") String> s = new ArrayList<>(); + @KeyFor("hell") Object o = new Object(); + } +} diff --git a/checker/tests/signedness/SignednessAssignments.java b/checker/tests/signedness/SignednessAssignments.java new file mode 100644 index 000000000000..73d32d18cd70 --- /dev/null +++ b/checker/tests/signedness/SignednessAssignments.java @@ -0,0 +1,136 @@ +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; + + @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() { + // :: error: (assignment.type.incompatible) + @SignedPositive short i1 = sb; + // :: error: (assignment.type.incompatible) + @SignedPositive short i2 = ub; + // :: error: (assignment.type.incompatible) + @SignedPositive short i3 = sB; + // :: error: (assignment.type.incompatible) + @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() { + // :: error: (assignment.type.incompatible) + @SignedPositive int i1 = sb; + // :: error: (assignment.type.incompatible) + @SignedPositive int i2 = ub; + // :: error: (assignment.type.incompatible) + @SignedPositive int i3 = sB; + // :: error: (assignment.type.incompatible) + @SignedPositive int i4 = uB; + + // :: error: (assignment.type.incompatible) + @SignedPositive int i9 = ss; + // :: error: (assignment.type.incompatible) + @SignedPositive int i10 = us; + // :: error: (assignment.type.incompatible) + @SignedPositive int i11 = sS; + // :: error: (assignment.type.incompatible) + @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() { + // :: error: (assignment.type.incompatible) + @SignedPositive long i1 = sb; + // :: error: (assignment.type.incompatible) + @SignedPositive long i2 = ub; + // :: error: (assignment.type.incompatible) + @SignedPositive long i3 = sB; + // :: error: (assignment.type.incompatible) + @SignedPositive long i4 = uB; + + // :: error: (assignment.type.incompatible) + @SignedPositive long i9 = ss; + // :: error: (assignment.type.incompatible) + @SignedPositive long i10 = us; + // :: error: (assignment.type.incompatible) + @SignedPositive long i11 = sS; + // :: error: (assignment.type.incompatible) + @SignedPositive long i12 = uS; + + // :: error: (assignment.type.incompatible) + @SignedPositive long i13 = si; + // :: error: (assignment.type.incompatible) + @SignedPositive long i14 = ui; + // :: error: (assignment.type.incompatible) + @SignedPositive long i15 = sI; + // :: error: (assignment.type.incompatible) + @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/SignednessCast.java b/checker/tests/signedness/SignednessCast.java new file mode 100644 index 000000000000..36f2aef1f5e1 --- /dev/null +++ b/checker/tests/signedness/SignednessCast.java @@ -0,0 +1,39 @@ +import java.io.Serializable; + +@SuppressWarnings("deprecation") // newInstance is deprecated. +public class SignednessCast { + static class Instruction {} + + public Instruction createCast(String name) { + Instruction i = null; + try { + i = (Instruction) java.lang.Class.forName(name).newInstance(); + } catch (final Exception e) { + throw new IllegalArgumentException("Could not find instruction: " + name, e); + } + return i; + } + + static class SerializableInstruction implements Serializable {} + + SerializableInstruction other(String name) { + SerializableInstruction i = null; + try { + i = (SerializableInstruction) java.lang.Class.forName(name).newInstance(); + } catch (final Exception e) { + throw new IllegalArgumentException("Could not find instruction: " + name, e); + } + return i; + } + + Serializable maybeNumber(String name) { + Serializable i = null; + try { + i = (Serializable) java.lang.Class.forName(name).newInstance(); + } catch (final Exception e) { + throw new IllegalArgumentException("Could not find instruction: " + name, e); + } + // :: error: (return.type.incompatible) + return i; + } +} diff --git a/checker/tests/signedness/SignednessEquals.java b/checker/tests/signedness/SignednessEquals.java new file mode 100644 index 000000000000..fc34b7ed3bf2 --- /dev/null +++ b/checker/tests/signedness/SignednessEquals.java @@ -0,0 +1,97 @@ +import org.checkerframework.checker.signedness.qual.Signed; +import org.checkerframework.checker.signedness.qual.Unsigned; + +import java.util.Objects; + +public class SignednessEquals { + + @Signed Object so; + @Unsigned Object uo; + + @Signed Number sn; + @Unsigned Number un; + + @Signed byte sb; + @Unsigned byte ub; + @Signed Byte sB; + @Unsigned Byte uB; + + char uc; + 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 nonIntegralEquality() { + so.equals(sn); + // :: error: (comparison.mixed.unsignedrhs) + so.equals(un); + // :: error: (comparison.mixed.unsignedlhs) + uo.equals(sn); + uo.equals(un); + + Objects.equals(so, sn); + // :: error: (comparison.mixed.unsignedrhs) + Objects.equals(so, un); + // :: error: (comparison.mixed.unsignedlhs) + Objects.equals(uo, sn); + Objects.equals(uo, un); + + sI.equals(sn); + // :: error: (comparison.mixed.unsignedrhs) + sI.equals(un); + // :: error: (comparison.mixed.unsignedlhs) + uI.equals(sn); + uI.equals(un); + + Objects.equals(sI, sn); + // :: error: (comparison.mixed.unsignedrhs) + Objects.equals(sI, un); + // :: error: (comparison.mixed.unsignedlhs) + Objects.equals(uI, sn); + Objects.equals(uI, un); + } + + void integralEquality() { + + so.equals(sS); + // :: error: (comparison.mixed.unsignedrhs) + so.equals(uS); + // :: error: (comparison.mixed.unsignedlhs) + uo.equals(sS); + uo.equals(uS); + + Objects.equals(so, sS); + // :: error: (comparison.mixed.unsignedrhs) + Objects.equals(so, uS); + // :: error: (comparison.mixed.unsignedlhs) + Objects.equals(uo, sS); + Objects.equals(uo, uS); + + sB.equals(sS); + // :: error: (comparison.mixed.unsignedrhs) + sB.equals(uS); + // :: error: (comparison.mixed.unsignedlhs) + uB.equals(sS); + uB.equals(uS); + + Objects.equals(sB, sS); + // :: error: (comparison.mixed.unsignedrhs) + Objects.equals(sB, uS); + // :: error: (comparison.mixed.unsignedlhs) + Objects.equals(uB, sS); + Objects.equals(uB, uS); + } +} diff --git a/checker/tests/signedness/SignednessNumberCasts.java b/checker/tests/signedness/SignednessNumberCasts.java new file mode 100644 index 000000000000..5fc3bf3d600b --- /dev/null +++ b/checker/tests/signedness/SignednessNumberCasts.java @@ -0,0 +1,17 @@ +import org.checkerframework.checker.signedness.qual.Signed; + +public class SignednessNumberCasts { + Double d2; + + void test(MyClass o, MyClass signed) { + // :: error: (assignment.type.incompatible) + @Signed int i = (Integer) o.get(); + @Signed int i2 = (Integer) signed.get(); + Double d = (Double) o.get(); + d2 = (Double) signed.get(); + } + + static interface MyClass { + T get(); + } +} diff --git a/checker/tests/signedness/SignednessRangeTest.java b/checker/tests/signedness/SignednessRangeTest.java new file mode 100644 index 000000000000..eab0658df223 --- /dev/null +++ b/checker/tests/signedness/SignednessRangeTest.java @@ -0,0 +1,10 @@ +import org.checkerframework.checker.signedness.qual.Unsigned; + +class SignednessRangeTest { + + private static final @Unsigned int UINT8_MAX = 255; + private static final @Unsigned int UINT16_MAX = 65_535; + private static final @Unsigned byte SOFT_MAX = (@Unsigned byte) UINT8_MAX; + private static final @Unsigned byte SOFT_MAX_2 = (@Unsigned byte) 255; + private static final @Unsigned short DISTANCE_MAX = (@Unsigned short) UINT16_MAX; +} diff --git a/checker/tests/signedness/StringConcat.java b/checker/tests/signedness/StringConcat.java new file mode 100644 index 000000000000..bc13a9af2abb --- /dev/null +++ b/checker/tests/signedness/StringConcat.java @@ -0,0 +1,8 @@ +import org.checkerframework.checker.signedness.qual.Unsigned; + +public class StringConcat { + public String doConcat(@Unsigned int i, String s) { + // :: error: (unsigned.concat) + return s + i; + } +} diff --git a/checker/tests/signedness/ToHexString.java b/checker/tests/signedness/ToHexString.java new file mode 100644 index 000000000000..8ef65748442b --- /dev/null +++ b/checker/tests/signedness/ToHexString.java @@ -0,0 +1,16 @@ +import org.checkerframework.checker.signedness.qual.Signed; +import org.checkerframework.checker.signedness.qual.Unsigned; + +public class ToHexString { + void toHexString(int x) { + Integer.toHexString(x); + } + + void toHexStringU(@Unsigned int x) { + Integer.toHexString(x); + } + + void toHexStringS(@Signed int x) { + Integer.toHexString(x); + } +} diff --git a/checker/tests/signedness/UnsignedConcat.java b/checker/tests/signedness/UnsignedConcat.java new file mode 100644 index 000000000000..df21c73bb975 --- /dev/null +++ b/checker/tests/signedness/UnsignedConcat.java @@ -0,0 +1,57 @@ +import org.checkerframework.checker.signedness.qual.Signed; +import org.checkerframework.checker.signedness.qual.UnknownSignedness; +import org.checkerframework.checker.signedness.qual.Unsigned; + +public class UnsignedConcat { + @UnknownSignedness int unknownInt = -3; + @Unsigned short unsignedShort = -2; + @Unsigned int unsignedInt = -2; + @Signed short signedShort = -2; + @Signed int signedInt = -2; + + void test1(char c, Character charObj) { + // :: error: (unsigned.concat) + String s1 = "" + unsignedShort; + // :: error: (unsigned.concat) + String s2 = "" + unsignedInt; + // :: error: (unsigned.concat) + String s1b = unsignedShort + ""; + // :: error: (unsigned.concat) + String s2b = "" + unsignedInt + ""; + String s3 = "" + signedShort; + String s4 = "" + signedInt; + // :: error: (unsigned.concat) + String s5 = "" + unknownInt; + String s6 = "" + -1; + + String s7 = "" + c; + String s8 = "" + charObj; + } + + void test2(String s, char c, Character charObj) { + // :: error: (unsigned.concat) + s += unsignedShort; + // :: error: (unsigned.concat) + s += +unsignedInt; + s += "" + signedShort; + s += signedInt; + // :: error: (unsigned.concat) + s += unknownInt; + s += 9; + s += c; + s += charObj; + } + + void test3() { + String a = "World"; + String s = "Hi " + (int) a.charAt(0); + } + + void test4(String s) { + Class sc = null; + if (s != null) { + sc = s.getClass(); + } + System.out.println("In: " + sc); + } +} diff --git a/checker/tests/signedness/UnsignedConcat2.java b/checker/tests/signedness/UnsignedConcat2.java new file mode 100644 index 000000000000..19e8d5f67693 --- /dev/null +++ b/checker/tests/signedness/UnsignedConcat2.java @@ -0,0 +1,23 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Set; + +public class UnsignedConcat2 { + + protected @Nullable T value; + + protected Set set; + + @Override + public String toString() { + switch (set.size()) { + case 0: + return "[]"; + case 1: + return "[" + value + "]"; + default: + return set.toString(); + } + } +} diff --git a/checker/tests/signedness/UnsignedRightShiftTest.java b/checker/tests/signedness/UnsignedRightShiftTest.java new file mode 100644 index 000000000000..cc51fd23df56 --- /dev/null +++ b/checker/tests/signedness/UnsignedRightShiftTest.java @@ -0,0 +1,50 @@ +// Test case for issue 3667: +// https://github.com/typetools/checker-framework/issues/3667 + +import org.checkerframework.checker.signedness.qual.Signed; +import org.checkerframework.checker.signedness.qual.Unsigned; + +public class UnsignedRightShiftTest { + int length; + + void unsignedRightShiftWithLiteral() { + int length = Integer.MAX_VALUE; + byte b = (byte) (length >>> 24); + } + + void unsignedRightShiftWithParameter(int length) { + byte b1 = (byte) (length >>> 24); + byte b2 = (@Signed byte) (length >>> 24); + byte b3 = (@Unsigned byte) (length >>> 24); + } + + void unsignedRightShiftWithField() { + byte b = (byte) (this.length >>> 24); + } + + void unsignedRightShiftComplex() { + int length = return12(); + byte[] byteArray = new byte[4]; + byteArray[0] = (byte) (length >>> 24); + byteArray[1] = (byte) (length >>> 16); + byteArray[2] = (byte) (length >>> 8); + byteArray[3] = (byte) length; + } + + void testWrite64(long x) { + write32((int) (x >>> 32)); + } + + void testWrite64() { + long myLong = Long.MAX_VALUE; + int z = (int) (myLong >>> 32); + int myInt = 2; + short w = (short) (myInt >>> 16); + } + + int return12() { + return 12; + } + + void write32(int x) {} +} diff --git a/checker/tests/signedness/Utils.java b/checker/tests/signedness/Utils.java index 15b47c8b51d5..304ff37486db 100644 --- a/checker/tests/signedness/Utils.java +++ b/checker/tests/signedness/Utils.java @@ -1,6 +1,7 @@ -import java.nio.ByteBuffer; -import org.checkerframework.checker.signedness.SignednessUtil; import org.checkerframework.checker.signedness.qual.*; +import org.checkerframework.checker.signedness.util.SignednessUtil; + +import java.nio.ByteBuffer; public class Utils { diff --git a/checker/tests/signedness/UtilsJava8.java b/checker/tests/signedness/UtilsJava8.java index 63f7512ef376..1421299ea9f1 100644 --- a/checker/tests/signedness/UtilsJava8.java +++ b/checker/tests/signedness/UtilsJava8.java @@ -108,7 +108,6 @@ public void annotatedJDKTests( resLong = Integer.toUnsignedLong(sint); - // :: error: (argument.type.incompatible) ulong = Integer.toUnsignedLong(uint); // :: error: (argument.type.incompatible) diff --git a/checker/tests/signedness/ValueIntegration.java b/checker/tests/signedness/ValueIntegration.java index 83a194ba8f81..ae4ecc860d71 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/signedness/WideningConversion.java b/checker/tests/signedness/WideningConversion.java new file mode 100644 index 000000000000..10e0dc177ae8 --- /dev/null +++ b/checker/tests/signedness/WideningConversion.java @@ -0,0 +1,100 @@ +import org.checkerframework.checker.signedness.qual.Signed; +import org.checkerframework.checker.signedness.qual.Unsigned; + +public class WideningConversion { + + char c1; + char c2; + int i1; + int i2; + @Signed int si1; + @Signed int si2; + @Unsigned int ui1; + @Unsigned int ui2; + @Unsigned short us1; + @Unsigned short us2; + + void compare() { + boolean b; + b = c1 > c2; + b = c1 > i2; + b = i1 > c2; + b = i1 > i2; + } + + void plus() { + // Not just "int si" because it's defaulted to TOP so every assignment would work. + @Signed int si; + si = c1 + c2; + si = c1 + i2; + si = i1 + c2; + si = i1 + i2; + + si = c1 + c2; + si = c1 + si2; + si = si1 + c2; + si = si1 + si2; + + si = c1 + c2; + // :: error: (assignment.type.incompatible) + si = c1 + ui2; + // :: error: (assignment.type.incompatible) + si = ui1 + c2; + // :: error: (assignment.type.incompatible) + si = ui1 + ui2; + + @Unsigned int ui; + ui = c1 + c2; + // :: error: (assignment.type.incompatible) + ui = c1 + i2; + // :: error: (assignment.type.incompatible) + ui = i1 + c2; + // :: error: (assignment.type.incompatible) + ui = i1 + i2; + + ui = c1 + c2; + // :: error: (assignment.type.incompatible) + ui = c1 + si2; + // :: error: (assignment.type.incompatible) + ui = si1 + c2; + // :: error: (assignment.type.incompatible) + ui = si1 + si2; + + ui = c1 + c2; + ui = c1 + ui2; + ui = ui1 + c2; + ui = ui1 + ui2; + + // All of these are illegal in Java, without an explicit cast. + // char c; + // c = c1 + c2; + // c = c1 + i2; + // c = i1 + c2; + // c = i1 + i2; + + char c; + c = (char) (c1 + c2); + c = (char) (c1 + i2); + c = (char) (i1 + c2); + c = (char) (i1 + i2); + + c = (char) (c1 + c2); + c = (char) (c1 + si2); + c = (char) (si1 + c2); + c = (char) (si1 + si2); + + c = (char) (c1 + c2); + c = (char) (c1 + ui2); + c = (char) (ui1 + c2); + c = (char) (ui1 + ui2); + } + + void to_string() { + // :: error: (unsigned.concat) + String s1 = "" + us1; + // :: error: (argument.type.incompatible) + String s2 = String.valueOf(us2); + // :: error: (argument.type.incompatible) + String s3 = Short.toString(us1); + } +} diff --git a/checker/tests/signedness/WideningFloat.java b/checker/tests/signedness/WideningFloat.java new file mode 100644 index 000000000000..11a4c5755202 --- /dev/null +++ b/checker/tests/signedness/WideningFloat.java @@ -0,0 +1,22 @@ +import org.checkerframework.checker.signedness.qual.UnknownSignedness; + +public class WideningFloat { + + void floatArg(float x) {} + + void m(Object arg) { + floatArg((Byte) arg); + } + + void m2(@UnknownSignedness Byte arg) { + floatArg(arg); + } + + void m3(@UnknownSignedness Byte arg) { + float f = arg; + } + + void m3(@UnknownSignedness byte arg) { + float f = arg; + } +} diff --git a/checker/tests/signedness/WideningInitialization.java b/checker/tests/signedness/WideningInitialization.java new file mode 100644 index 000000000000..3422848d8121 --- /dev/null +++ b/checker/tests/signedness/WideningInitialization.java @@ -0,0 +1,34 @@ +import org.checkerframework.checker.signedness.qual.Signed; + +public class WideningInitialization { + public int findLineNr(int pc, char startPC, char lineNr) { + int ln = 0; + for (int i = 0; i < 3; i++) { + if (startPC <= pc) { + ln = lineNr; + } else { + return ln; + } + } + return ln; + } + + public int findLineNr2(char lineNr) { + int ln = 0; + ln = lineNr; + return ln; + } + + public void findLineNr3a(char lineNr) { + @Signed int ln = lineNr; + } + + public int findLineNr3b(char lineNr) { + int ln = lineNr; + return ln; + } + + public int findLineNr4(char lineNr) { + return lineNr; + } +} diff --git a/checker/tests/signedness/java17/Issue6100.java b/checker/tests/signedness/java17/Issue6100.java new file mode 100644 index 000000000000..eaa84fb7cbe5 --- /dev/null +++ b/checker/tests/signedness/java17/Issue6100.java @@ -0,0 +1,18 @@ +// @below-java17-jdk-skip-test +// @infer-jaifs-skip-test The AFU's JAIF reading/writing libraries don't support records. + +import org.checkerframework.checker.index.qual.NonNegative; + +import java.util.List; + +public record Issue6100(List<@NonNegative Integer> bar) { + + public Issue6100 { + List<@NonNegative Integer> b = bar; + // :: error: (assignment.type.incompatible) + List b2 = bar; + if (bar.size() < 0) { + throw new IllegalArgumentException(); + } + } +} diff --git a/checker/tests/stubparser-tests/MultidimentionalArrayAnnotationTest.java b/checker/tests/stubparser-nullness/MultidimentionalArrayAnnotationTest.java similarity index 99% rename from checker/tests/stubparser-tests/MultidimentionalArrayAnnotationTest.java rename to checker/tests/stubparser-nullness/MultidimentionalArrayAnnotationTest.java index 9060c534fb7f..d2bec564ad87 100644 --- a/checker/tests/stubparser-tests/MultidimentionalArrayAnnotationTest.java +++ b/checker/tests/stubparser-nullness/MultidimentionalArrayAnnotationTest.java @@ -45,7 +45,6 @@ void callTomethod1() { obj6 = method1(); obj7 = method1(); obj8 = method1(); - ; } /* @@ -185,33 +184,33 @@ void callTomethod8() { Object[][][] method1() { return new Object[numb][numb][numb]; - }; + } Object @Nullable [][][] method2() { return new Object[numb][numb][numb]; - }; + } Object[] @Nullable [][] method3() { return new Object[numb][numb][numb]; - }; + } Object[][] @Nullable [] method4() { return new Object[numb][numb][numb]; - }; + } Object @Nullable [] @Nullable [][] method5() { return new Object[numb][numb][numb]; - }; + } Object @Nullable [][] @Nullable [] method6() { return new Object[numb][numb][numb]; - }; + } Object[] @Nullable [] @Nullable [] method7() { return new Object[numb][numb][numb]; - }; + } Object @Nullable [] @Nullable [] @Nullable [] method8() { return new Object[numb][numb][numb]; - }; + } } diff --git a/checker/tests/stubparser-nullness/NoExplicitAnnotations.astub b/checker/tests/stubparser-nullness/NoExplicitAnnotations.astub new file mode 100644 index 000000000000..85918685b7ae --- /dev/null +++ b/checker/tests/stubparser-nullness/NoExplicitAnnotations.astub @@ -0,0 +1,17 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +class NoExplicitAnnotationsSub1 { + // There is no explicit annotation here. + String method1(); +} + +class NoExplicitAnnotationsSub2 { + // There is an explicit annotation here. + @NonNull String method2(); +} + +class NoExplicitAnnotationsSub3 { + // There is an explicit annotation here. + @Nullable String method3(); +} diff --git a/checker/tests/stubparser-nullness/NoExplicitAnnotations.java b/checker/tests/stubparser-nullness/NoExplicitAnnotations.java new file mode 100644 index 000000000000..82e72708ca5b --- /dev/null +++ b/checker/tests/stubparser-nullness/NoExplicitAnnotations.java @@ -0,0 +1,64 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +public class NoExplicitAnnotations {} + +class NoExplicitAnnotationsSuper { + @Nullable String method1() { + return helper(); + } + + @Nullable String method2() { + return helper(); + } + + @Nullable String method3() { + return helper(); + } + + @Nullable String helper() { + return null; + } +} + +class NoExplicitAnnotationsSub1 extends NoExplicitAnnotationsSuper { + @Override + String helper() { + return "hello"; + } +} + +class NoExplicitAnnotationsSub2 extends NoExplicitAnnotationsSuper { + @Override + String helper() { + return "hello"; + } +} + +class NoExplicitAnnotationsSub3 extends NoExplicitAnnotationsSuper { + @Override + String helper() { + return "hello"; + } +} + +class NoExplicitAnnotationsUse { + @Nullable String nble = null; + @NonNull String nn = "hello"; + + void use( + NoExplicitAnnotationsSub1 sub1, + NoExplicitAnnotationsSub2 sub2, + NoExplicitAnnotationsSub3 sub3) { + nble = sub1.method1(); + nn = sub1.method1(); + nble = sub2.method2(); + nn = sub2.method2(); + nble = sub3.method3(); + // :: error: (assignment.type.incompatible) + nn = sub3.method3(); + + // :: error: (assignment.type.incompatible) + nn = nble; + } +} diff --git a/checker/tests/stubparser-tests/VarargConstructorParameterAnnotationTest.java b/checker/tests/stubparser-nullness/VarargConstructorParameterAnnotationTest.java similarity index 92% rename from checker/tests/stubparser-tests/VarargConstructorParameterAnnotationTest.java rename to checker/tests/stubparser-nullness/VarargConstructorParameterAnnotationTest.java index 797c8f6a91d5..fc027aa2567e 100644 --- a/checker/tests/stubparser-tests/VarargConstructorParameterAnnotationTest.java +++ b/checker/tests/stubparser-nullness/VarargConstructorParameterAnnotationTest.java @@ -3,7 +3,7 @@ /* * Tests parsing annotations on parameter represented by an array or vararg to the constructor. */ -class ProcessBuilding2 { +public class VarargConstructorParameterAnnotationTest { public void strArraysNonNull(@NonNull String[] parameter) { new ProcessBuilder(parameter); diff --git a/checker/tests/stubparser-records/PairRecord.astub b/checker/tests/stubparser-records/PairRecord.astub new file mode 100644 index 000000000000..0c9b18835933 --- /dev/null +++ b/checker/tests/stubparser-records/PairRecord.astub @@ -0,0 +1,5 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +record PairRecord(String key, @Nullable Object value) { + PairRecord(@Nullable String val); +} diff --git a/checker/tests/stubparser-records/PairRecord.java b/checker/tests/stubparser-records/PairRecord.java new file mode 100644 index 000000000000..0b868db501e6 --- /dev/null +++ b/checker/tests/stubparser-records/PairRecord.java @@ -0,0 +1,6 @@ +record PairRecord(String key, Object value) { + + PairRecord(String val) { + this("", val); + } +} diff --git a/checker/tests/stubparser-records/RecordStubbed.astub b/checker/tests/stubparser-records/RecordStubbed.astub new file mode 100644 index 000000000000..ca7fbbd96813 --- /dev/null +++ b/checker/tests/stubparser-records/RecordStubbed.astub @@ -0,0 +1,14 @@ +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * nxx is nullable in the record but non-null in constructor and accessor via stubs + * nsxx is nullable in the stubs but non-null in constructor and accessor via stubs + * xnn is de-facto non-null in record but nullable in constructor and accessor via stubs + */ +public record RecordStubbed(String nxx, @Nullable String nsxx, Integer xnn) { + RecordStubbed(@NonNull String nxx, @NonNull String nsxx, @Nullable Integer xnn); + @NonNull String nxx(); + @NonNull String nsxx(); + @Nullable Integer xnn(); +} diff --git a/checker/tests/stubparser-records/RecordStubbed.java b/checker/tests/stubparser-records/RecordStubbed.java new file mode 100644 index 000000000000..e04bc66620f7 --- /dev/null +++ b/checker/tests/stubparser-records/RecordStubbed.java @@ -0,0 +1,12 @@ +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * nxx is nullable in the record but non-null in constructor and accessor via stubs nsxx is nullable + * in the stubs but non-null in constructor and accessor via stubs xnn is de-facto non-null in + * record but nullable in constructor and accessor via stubs + */ +public record RecordStubbed(@Nullable String nxx, String nsxx, Integer xnn) { + RecordStubbed(Integer a, String b, String c) { + this(c, b, a); + } +} diff --git a/checker/tests/stubparser-records/RecordUsage.java b/checker/tests/stubparser-records/RecordUsage.java new file mode 100644 index 000000000000..228959e2660f --- /dev/null +++ b/checker/tests/stubparser-records/RecordUsage.java @@ -0,0 +1,24 @@ +import org.checkerframework.checker.nullness.qual.NonNull; + +class PairUsage { + public void makePairs() { + PairRecord a = new PairRecord("key", "value"); + PairRecord b = new PairRecord(null); + // :: error: (assignment.type.incompatible) + @NonNull Object o = a.value(); + PairRecord p = new PairRecord("key", null); + } + + public void makeStubbed() { + RecordStubbed r = new RecordStubbed("a", "b", 7); + RecordStubbed r1 = new RecordStubbed("a", "b", null); + // :: error: (argument.type.incompatible) + RecordStubbed r2 = new RecordStubbed((String) null, "b", null); + // :: error: (argument.type.incompatible) + RecordStubbed r3 = new RecordStubbed("a", null, null); + @NonNull Object o = r.nxx(); + @NonNull Object o2 = r.nsxx(); + // :: error: (assignment.type.incompatible) + @NonNull Object o3 = r.xnn(); + } +} diff --git a/checker/tests/stubparser-tainting/FakeOverrideFormalParameter.astub-TODO b/checker/tests/stubparser-tainting/FakeOverrideFormalParameter.astub-TODO new file mode 100644 index 000000000000..a0c127bc1f25 --- /dev/null +++ b/checker/tests/stubparser-tainting/FakeOverrideFormalParameter.astub-TODO @@ -0,0 +1,19 @@ +// TODO: Fake overrides only work for return types, not formal parameters yet. + +import org.checkerframework.checker.tainting.qual.Untainted; + +class FakeOverrideFPMid { + + void requiresTaintedIntWithFakeOverride(@Untainted int i) {} + + void requiresUntaintedIntWithFakeOverride(@Tainted int i) {} + + void requiresTaintedIntegerWithFakeOverride(@Untainted Integer i) {} + + void requiresUntaintedIntegerWithFakeOverride(@Tainted Integer i) {} + + void requiresTaintedFqIntegerWithFakeOverride(java.lang. @Untainted Integer i) {} + + void requiresUntaintedFqIntegerWithFakeOverride(java.lang. @Tainted Integer i) {} + +} diff --git a/checker/tests/stubparser-tainting/FakeOverrideFormalParameter.java-TODO b/checker/tests/stubparser-tainting/FakeOverrideFormalParameter.java-TODO new file mode 100644 index 000000000000..dc4d3085eb92 --- /dev/null +++ b/checker/tests/stubparser-tainting/FakeOverrideFormalParameter.java-TODO @@ -0,0 +1,86 @@ +// TODO: Fake overrides only work for return types, not formal parameters yet. + +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +// Define this class because it's the name of the file +public class FakeOverrideFormalParameter {} + +class FakeOverrideFPSuper { + + void requiresTaintedInt(@Tainted int i) {} + + void requiresUntaintedInt(@Untainted int i) {} + + void requiresTaintedIntWithFakeOverride(@Tainted int i) {} + + void requiresUntaintedIntWithFakeOverride(@Untainted int i) {} + + void requiresTaintedInteger(@Tainted Integer i) {} + + void requiresUntaintedInteger(@Untainted Integer i) {} + + void requiresTaintedIntegerWithFakeOverride(@Tainted Integer i) {} + + void requiresUntaintedIntegerWithFakeOverride(@Untainted Integer i) {} + + void requiresTaintedFqInteger(java.lang.@Tainted Integer i) {} + + void requiresUntaintedFqInteger(java.lang.@Untainted Integer i) {} + + void requiresTaintedFqIntegerWithFakeOverride(java.lang.@Tainted Integer i) {} + + void requiresUntaintedFqIntegerWithFakeOverride(java.lang.@Untainted Integer i) {} +} + +class FakeOverrideFPMid extends FakeOverrideFPSuper {} + +class FakeOverrideFPSub extends FakeOverrideFPMid {} + +class FakeOverrideFPClient { + + @Tainted int tf; + + @Untainted int uf; + + void m(@Tainted int t, @Untainted int u) { + + FakeOverrideFPSuper sup = new FakeOverrideFPSuper(); + FakeOverrideFPMid mid = new FakeOverrideFPMid(); + FakeOverrideFPSub sub = new FakeOverrideFPSub(); + + sup.requiresTaintedInt(t); + mid.requiresTaintedInt(t); + sub.requiresTaintedInt(t); + sup.requiresTaintedInt(u); + mid.requiresTaintedInt(u); + sub.requiresTaintedInt(u); + + // :: error: (argument.type.incompatible) + sup.requiresUntaintedInt(t); + // :: error: (argument.type.incompatible) + mid.requiresUntaintedInt(t); + // :: error: (argument.type.incompatible) + sub.requiresUntaintedInt(t); + sup.requiresUntaintedInt(u); + mid.requiresUntaintedInt(u); + sub.requiresUntaintedInt(u); + + sup.requiresTaintedIntWithFakeOverride(t); + // :: error: (argument.type.incompatible) + mid.requiresTaintedIntWithFakeOverride(t); + // :: error: (argument.type.incompatible) + sub.requiresTaintedIntWithFakeOverride(t); + sup.requiresTaintedIntWithFakeOverride(u); + mid.requiresTaintedIntWithFakeOverride(u); + sub.requiresTaintedIntWithFakeOverride(u); + + // :: error: (argument.type.incompatible) + sup.requiresUntaintedIntWithFakeOverride(t); + mid.requiresUntaintedIntWithFakeOverride(t); + sub.requiresUntaintedIntWithFakeOverride(t); + sup.requiresUntaintedIntWithFakeOverride(u); + mid.requiresUntaintedIntWithFakeOverride(u); + sub.requiresUntaintedIntWithFakeOverride(u); + } +} diff --git a/checker/tests/stubparser-tainting/FakeOverrideRMid.java b/checker/tests/stubparser-tainting/FakeOverrideRMid.java new file mode 100644 index 000000000000..ec9b9e4aad21 --- /dev/null +++ b/checker/tests/stubparser-tainting/FakeOverrideRMid.java @@ -0,0 +1 @@ +public class FakeOverrideRMid extends FakeOverrideRSuper {} diff --git a/checker/tests/stubparser-tainting/FakeOverrideRSub.java b/checker/tests/stubparser-tainting/FakeOverrideRSub.java new file mode 100644 index 000000000000..1e1e108bde27 --- /dev/null +++ b/checker/tests/stubparser-tainting/FakeOverrideRSub.java @@ -0,0 +1 @@ +public class FakeOverrideRSub extends FakeOverrideRMid {} diff --git a/checker/tests/stubparser-tainting/FakeOverrideRSuper.java b/checker/tests/stubparser-tainting/FakeOverrideRSuper.java new file mode 100644 index 000000000000..25a990c38a28 --- /dev/null +++ b/checker/tests/stubparser-tainting/FakeOverrideRSuper.java @@ -0,0 +1,32 @@ +// The type qualifier hierarchy is: @Tainted :> @Untainted +import org.checkerframework.checker.tainting.qual.PolyTainted; +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +@SuppressWarnings("tainting") +public class FakeOverrideRSuper { + + public @Tainted int returnsTaintedInt() { + return 0; + } + + public @Untainted int returnsUntaintedInt() { + return 0; + } + + public @Tainted int returnsTaintedIntWithFakeOverride() { + return 0; + } + + public @Untainted int returnsUntaintedIntWithFakeOverride() { + return 0; + } + + public @Untainted int returnsUntaintedIntWithFakeOverride2() { + return 0; + } + + public @PolyTainted int returnsPolyTaintedIntWithFakeOverride() { + return 0; + } +} diff --git a/checker/tests/stubparser-tainting/FakeOverrideReturn.astub b/checker/tests/stubparser-tainting/FakeOverrideReturn.astub new file mode 100644 index 000000000000..813ca9c5666d --- /dev/null +++ b/checker/tests/stubparser-tainting/FakeOverrideReturn.astub @@ -0,0 +1,15 @@ +import org.checkerframework.checker.tainting.qual.PolyTainted; +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +class FakeOverrideRMid { + + public @Untainted int returnsTaintedIntWithFakeOverride(); + + public @Tainted int returnsUntaintedIntWithFakeOverride(); + + public @PolyTainted int returnsUntaintedIntWithFakeOverride2(); + + public @Untainted int returnsPolyTaintedIntWithFakeOverride(); + +} diff --git a/checker/tests/stubparser-tainting/FakeOverrideReturn.java b/checker/tests/stubparser-tainting/FakeOverrideReturn.java new file mode 100644 index 000000000000..5af14e890e4e --- /dev/null +++ b/checker/tests/stubparser-tainting/FakeOverrideReturn.java @@ -0,0 +1,59 @@ +// The type qualifier hierarchy is: @Tainted :> @Untainted +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +public class FakeOverrideReturn { + + @Tainted int tf; + + @Untainted int uf; + + void m(@Tainted int t, @Untainted int u) { + + FakeOverrideRSuper sup = new FakeOverrideRSuper(); + FakeOverrideRMid mid = new FakeOverrideRMid(); + FakeOverrideRSub sub = new FakeOverrideRSub(); + + tf = sup.returnsTaintedInt(); + tf = mid.returnsTaintedInt(); + tf = sub.returnsTaintedInt(); + // :: error: (assignment.type.incompatible) + uf = sup.returnsTaintedInt(); + // :: error: (assignment.type.incompatible) + uf = mid.returnsTaintedInt(); + // :: error: (assignment.type.incompatible) + uf = sub.returnsTaintedInt(); + + tf = sup.returnsUntaintedInt(); + tf = mid.returnsUntaintedInt(); + tf = sub.returnsUntaintedInt(); + uf = sup.returnsUntaintedInt(); + uf = mid.returnsUntaintedInt(); + uf = sub.returnsUntaintedInt(); + + tf = sup.returnsTaintedIntWithFakeOverride(); + tf = mid.returnsTaintedIntWithFakeOverride(); + tf = sub.returnsTaintedIntWithFakeOverride(); + // :: error: (assignment.type.incompatible) + uf = sup.returnsTaintedIntWithFakeOverride(); + uf = mid.returnsTaintedIntWithFakeOverride(); + uf = sub.returnsTaintedIntWithFakeOverride(); + + tf = sup.returnsUntaintedIntWithFakeOverride(); + tf = mid.returnsUntaintedIntWithFakeOverride(); + tf = sub.returnsUntaintedIntWithFakeOverride(); + uf = sup.returnsUntaintedIntWithFakeOverride(); + // :: error: (assignment.type.incompatible) + uf = mid.returnsUntaintedIntWithFakeOverride(); + // :: error: (assignment.type.incompatible) + uf = sub.returnsUntaintedIntWithFakeOverride(); + } + + void poly() { + FakeOverrideRSuper sup = new FakeOverrideRSuper(); + FakeOverrideRMid mid = new FakeOverrideRMid(); + + @Untainted int j = mid.returnsUntaintedIntWithFakeOverride2(); + @Untainted int k = sup.returnsPolyTaintedIntWithFakeOverride(); + } +} diff --git a/checker/tests/stubparser-tainting/TypeParamWithInner.astub b/checker/tests/stubparser-tainting/TypeParamWithInner.astub new file mode 100644 index 000000000000..351e928d8fd0 --- /dev/null +++ b/checker/tests/stubparser-tainting/TypeParamWithInner.astub @@ -0,0 +1,12 @@ +import org.checkerframework.checker.tainting.qual.Untainted; + +// T extends @Untainted String in stub file. Tests bug where the presence of an inner class causes +// annotations on type variables to be forgotten. +public class TypeParamWithInner { + public class Inner { + } + + public void requiresUntainted(T param) { + @Untainated String s = param; + } +} diff --git a/checker/tests/stubparser-tainting/TypeParamWithInner.java b/checker/tests/stubparser-tainting/TypeParamWithInner.java new file mode 100644 index 000000000000..6a96162f7edb --- /dev/null +++ b/checker/tests/stubparser-tainting/TypeParamWithInner.java @@ -0,0 +1,11 @@ +import org.checkerframework.checker.tainting.qual.Untainted; + +// T extends @Untainted String in stub file. Tests bug where the presence of an inner class causes +// annotations on type variables to be forgotten. +public class TypeParamWithInner { + public class Inner {} + + public void requiresUntainted(T param) { + @Untainted String s = param; + } +} diff --git a/checker/tests/tainting/Buffer.java b/checker/tests/tainting/Buffer.java index c30c560a22c7..c3d2b1bc534e 100644 --- a/checker/tests/tainting/Buffer.java +++ b/checker/tests/tainting/Buffer.java @@ -1,10 +1,11 @@ -import java.util.ArrayList; -import java.util.List; 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; +import java.util.ArrayList; +import java.util.List; + @HasQualifierParameter(Tainted.class) public class Buffer { final List<@PolyTainted String> list = new ArrayList<>(); diff --git a/checker/tests/tainting/CaptureSubtype.java b/checker/tests/tainting/CaptureSubtype.java new file mode 100644 index 000000000000..086ace7e52c1 --- /dev/null +++ b/checker/tests/tainting/CaptureSubtype.java @@ -0,0 +1,17 @@ +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +public class CaptureSubtype { + + class MyGeneric {} + + class SubGeneric extends MyGeneric {} + + class UseMyGeneric { + SubGeneric wildcardUnbounded = + new SubGeneric<@Untainted Number>(); + + MyGeneric wildcardOutsideUB = wildcardUnbounded; + MyGeneric wildcardInsideUB2 = wildcardUnbounded; + } +} diff --git a/checker/tests/tainting/CaptureSubtype2.java b/checker/tests/tainting/CaptureSubtype2.java new file mode 100644 index 000000000000..e8080626ee23 --- /dev/null +++ b/checker/tests/tainting/CaptureSubtype2.java @@ -0,0 +1,28 @@ +import org.checkerframework.checker.tainting.qual.Untainted; + +import java.util.function.Function; + +public class CaptureSubtype2 { + + interface FFunction extends Function {} + + interface DInterface {} + + interface MInterface

    {} + + interface QInterface, V extends MInterface

    , P> {} + + FFunction> r; + + CaptureSubtype2( + FFunction< + String, + QInterface< + ? extends MInterface, + ? extends MInterface, + DInterface>> + r) { + // :: error: (assignment.type.incompatible) + this.r = r; + } +} diff --git a/checker/tests/tainting/ExtendHasQual.java b/checker/tests/tainting/ExtendHasQual.java index c9d321ec3e49..3b4f643f23bd 100644 --- a/checker/tests/tainting/ExtendHasQual.java +++ b/checker/tests/tainting/ExtendHasQual.java @@ -12,7 +12,6 @@ static class Super { @HasQualifierParameter(Tainted.class) static class Buffer extends Super {} - // :: error: (missing.has.qual.param) static class MyBuffer1 extends Buffer {} @HasQualifierParameter(Tainted.class) @@ -28,18 +27,15 @@ static class MyBuffer4 extends Buffer {} @HasQualifierParameter(Tainted.class) interface BufferInterface {} - // :: error: (missing.has.qual.param) static class ImplementsBufferInterface1 implements BufferInterface {} @HasQualifierParameter(Tainted.class) static class ImplementsBufferInterface2 implements BufferInterface {} - // :: error: (missing.has.qual.param) static class Both1 extends Buffer implements BufferInterface {} @HasQualifierParameter(Tainted.class) static class Both2 extends Buffer implements BufferInterface {} - // :: error: (missing.has.qual.param) static class Both3 extends Super implements BufferInterface {} } diff --git a/checker/tests/tainting/ExtendsAndAnnotation.java b/checker/tests/tainting/ExtendsAndAnnotation.java index 98fee6c61eba..f569bff3c1dc 100644 --- a/checker/tests/tainting/ExtendsAndAnnotation.java +++ b/checker/tests/tainting/ExtendsAndAnnotation.java @@ -5,7 +5,7 @@ import org.checkerframework.checker.tainting.qual.Untainted; import org.checkerframework.framework.qual.HasQualifierParameter; -class ExtendsAndAnnotation extends @Tainted Object { +public class ExtendsAndAnnotation extends @Tainted Object { void test(@Untainted ExtendsAndAnnotation c) { // :: warning: (cast.unsafe.constructor.invocation) Object o = new @Untainted ExtendsAndAnnotation(); @@ -13,6 +13,7 @@ void test(@Untainted ExtendsAndAnnotation c) { } @HasQualifierParameter(Tainted.class) + // :: error: (invalid.polymorphic.qualifier) // :: error: (declaration.inconsistent.with.extends.clause) static class Banana extends @PolyTainted Object {} } diff --git a/checker/tests/tainting/HasQualParamDefaults.java b/checker/tests/tainting/HasQualParamDefaults.java index e568139cac5f..d62dd15d03c3 100644 --- a/checker/tests/tainting/HasQualParamDefaults.java +++ b/checker/tests/tainting/HasQualParamDefaults.java @@ -1,10 +1,11 @@ -import java.util.ArrayList; -import java.util.List; 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; +import java.util.ArrayList; +import java.util.List; + public class HasQualParamDefaults { @HasQualifierParameter(Tainted.class) public class Buffer { @@ -44,6 +45,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 +109,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/InheritQualifierParameter.java b/checker/tests/tainting/InheritQualifierParameter.java new file mode 100644 index 000000000000..3da5f7da47f4 --- /dev/null +++ b/checker/tests/tainting/InheritQualifierParameter.java @@ -0,0 +1,31 @@ +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; +import org.checkerframework.framework.qual.HasQualifierParameter; +import org.checkerframework.framework.qual.NoQualifierParameter; + +@HasQualifierParameter(Tainted.class) +public class InheritQualifierParameter {} + +class SubHasQualifierParameter extends InheritQualifierParameter { + void test(@Untainted SubHasQualifierParameter arg) { + // :: error: (assignment.type.incompatible) + @Tainted SubHasQualifierParameter local = arg; + } +} + +@NoQualifierParameter(Tainted.class) +// :: error: (conflicting.qual.param) +class SubHasQualifierParameter1 extends InheritQualifierParameter {} + +@NoQualifierParameter(Tainted.class) +class InheritNoQualifierParameter {} + +class SubNoQualifierParameter extends InheritNoQualifierParameter { + void test(@Untainted SubNoQualifierParameter arg) { + @Tainted SubNoQualifierParameter local = arg; + } +} + +@HasQualifierParameter(Tainted.class) +// :: error: (conflicting.qual.param) +@Tainted class SubNoQualifierParameter1 extends InheritNoQualifierParameter {} diff --git a/checker/tests/tainting/InitializerDataflow.java b/checker/tests/tainting/InitializerDataflow.java new file mode 100644 index 000000000000..73d1f484e983 --- /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/checker/tests/tainting/InnerHasQualifierParameter.java b/checker/tests/tainting/InnerHasQualifierParameter.java new file mode 100644 index 000000000000..17b241271fe8 --- /dev/null +++ b/checker/tests/tainting/InnerHasQualifierParameter.java @@ -0,0 +1,18 @@ +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.framework.qual.HasQualifierParameter; + +@HasQualifierParameter(Tainted.class) +public class InnerHasQualifierParameter { + + @HasQualifierParameter(Tainted.class) + interface TestInterface { + public void testMethod(); + } + + public void test() { + TestInterface test = + new TestInterface() { + public void testMethod() {} + }; + } +} diff --git a/checker/tests/tainting/Issue1111.java b/checker/tests/tainting/Issue1111.java index a665026d6c06..9578f6ab5c26 100644 --- a/checker/tests/tainting/Issue1111.java +++ b/checker/tests/tainting/Issue1111.java @@ -2,11 +2,13 @@ // https://github.com/typetools/checker-framework/issues/1111 // Additional test case in framework/tests/all-systems/Issue1111.java -import java.util.List; import org.checkerframework.checker.tainting.qual.Untainted; +import java.util.List; + public class Issue1111 { void foo(Box box, List list) { + // :: error: (argument.type.incompatible) bar(box, list); } diff --git a/checker/tests/tainting/Issue1705.java b/checker/tests/tainting/Issue1705.java index 4ca7a0179619..871b788feb60 100644 --- a/checker/tests/tainting/Issue1705.java +++ b/checker/tests/tainting/Issue1705.java @@ -1,10 +1,11 @@ // Test case for Issue 1705 // https://github.com/typetools/checker-framework/issues/1705 -import java.util.function.Function; import org.checkerframework.checker.tainting.qual.PolyTainted; import org.checkerframework.checker.tainting.qual.Untainted; +import java.util.function.Function; + public class Issue1705 { static class MySecondClass { @PolyTainted MySecondClass doOnComplete(@PolyTainted MySecondClass this) { diff --git a/checker/tests/tainting/Issue1942.java b/checker/tests/tainting/Issue1942.java index ff6b70d8aff6..b9087f418f5e 100644 --- a/checker/tests/tainting/Issue1942.java +++ b/checker/tests/tainting/Issue1942.java @@ -1,6 +1,7 @@ -import java.util.List; import org.checkerframework.checker.tainting.qual.Untainted; +import java.util.List; + public class Issue1942 { public interface LoadableExpression {} diff --git a/checker/tests/tainting/Issue2156.java b/checker/tests/tainting/Issue2156.java index 2c7fdc07a598..2372d3b14c6f 100644 --- a/checker/tests/tainting/Issue2156.java +++ b/checker/tests/tainting/Issue2156.java @@ -11,10 +11,10 @@ enum SampleEnum { @Tainted SECOND; } -class Issue2156 { +public class Issue2156 { void test() { requireUntainted(SampleEnum.FIRST); - // :: error: assignment.type.incompatible + // :: error: (assignment.type.incompatible) requireUntainted(SampleEnum.SECOND); } diff --git a/checker/tests/tainting/Issue3033.java b/checker/tests/tainting/Issue3033.java new file mode 100644 index 000000000000..7abe60e7bda1 --- /dev/null +++ b/checker/tests/tainting/Issue3033.java @@ -0,0 +1,20 @@ +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +public class Issue3033 { + + void main() { + @Tainted String a = getTainted(); + // :: warning: (instanceof.unsafe) + if (a instanceof @Untainted String) { + // `a` is now refined to @Untainted String + isUntainted(a); + } + } + + static void isUntainted(@Untainted String a) {} + + static @Tainted String getTainted() { + return "hi"; + } +} diff --git a/checker/tests/tainting/Issue352.java b/checker/tests/tainting/Issue352.java index 51991427e1b8..73c42874f86a 100644 --- a/checker/tests/tainting/Issue352.java +++ b/checker/tests/tainting/Issue352.java @@ -1,13 +1,12 @@ -// @skip-test // Test case for Issue 352: // https://github.com/typetools/checker-framework/issues/352 import org.checkerframework.checker.tainting.qual.Untainted; -class Outer { +public class Issue352 { class Nested { - @Untainted Outer context(@Untainted Outer.@Untainted Nested this) { - return Outer.this; + @Untainted Issue352 context(@Untainted Issue352.@Untainted Nested this) { + return Issue352.this; } } } diff --git a/checker/tests/tainting/Issue3561.java b/checker/tests/tainting/Issue3561.java new file mode 100644 index 000000000000..2e4c44c9923f --- /dev/null +++ b/checker/tests/tainting/Issue3561.java @@ -0,0 +1,16 @@ +import org.checkerframework.checker.tainting.qual.*; + +public class Issue3561 { + void outerMethod(@Untainted Issue3561 this) {} + + class Inner { + void innerMethod(@Untainted Issue3561.@Untainted Inner this) { + Issue3561.this.outerMethod(); + } + + void innerMethod2(@Tainted Issue3561.@Untainted Inner this) { + // :: error: (method.invocation.invalid) + Issue3561.this.outerMethod(); + } + } +} diff --git a/checker/tests/tainting/Issue3562.java b/checker/tests/tainting/Issue3562.java new file mode 100644 index 000000000000..2600b9649b21 --- /dev/null +++ b/checker/tests/tainting/Issue3562.java @@ -0,0 +1,8 @@ +import org.checkerframework.checker.tainting.qual.*; + +public class Issue3562 { + // This used to issue type.invalid.conflicting.annos + @Tainted Issue3562.@Untainted Inner field; + + class Inner {} +} diff --git a/checker/tests/tainting/Issue3776.java b/checker/tests/tainting/Issue3776.java new file mode 100644 index 000000000000..302a09588a0f --- /dev/null +++ b/checker/tests/tainting/Issue3776.java @@ -0,0 +1,45 @@ +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +public class Issue3776 { + class MyInnerClass { + public MyInnerClass() {} + + public MyInnerClass(@Untainted String s) {} + + public MyInnerClass(int... i) {} + } + + static class MyClass { + public MyClass() {} + + public MyClass(@Untainted String s) {} + + public MyClass(int... i) {} + } + + void test(Issue3776 outer, @Tainted String tainted) { + new MyInnerClass("1") {}; + this.new MyInnerClass("2") {}; + new MyClass() {}; + // :: error: (argument.type.incompatible) + new MyClass(tainted) {}; + new MyClass(1, 2, 3) {}; + new MyClass(1) {}; + new MyInnerClass() {}; + // :: error: (argument.type.incompatible) + new MyInnerClass(tainted) {}; + new MyInnerClass(1) {}; + new MyInnerClass(1, 2, 3) {}; + this.new MyInnerClass() {}; + // :: error: (argument.type.incompatible) + this.new MyInnerClass(tainted) {}; + this.new MyInnerClass(1) {}; + this.new MyInnerClass(1, 2, 3) {}; + outer.new MyInnerClass() {}; + // :: error: (argument.type.incompatible) + outer.new MyInnerClass(tainted) {}; + outer.new MyInnerClass(1) {}; + outer.new MyInnerClass(1, 2, 3) {}; + } +} diff --git a/checker/tests/tainting/Issue4170.java b/checker/tests/tainting/Issue4170.java new file mode 100644 index 000000000000..0ce21cbefbb0 --- /dev/null +++ b/checker/tests/tainting/Issue4170.java @@ -0,0 +1,59 @@ +// @below-java10-jdk-skip-test + +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +import java.util.ArrayList; +import java.util.List; + +public class Issue4170 { + public void method1() { + var list = new ArrayList<@Untainted String>(); + ArrayList<@Untainted String> list2 = list; + // :: error: (assignment.type.incompatible) + ArrayList list3 = new ArrayList<@Untainted String>(); + ArrayList<@Tainted String> list4 = list3; + var stream = list.stream(); + } + + public void method2() { + var list = new ArrayList(); + var stream = list.stream(); + } + + public void method3() { + var list = new ArrayList<@Tainted String>(); + var stream = list.stream(); + } + + public ArrayList<@Untainted String> method4() { + var list = new ArrayList<@Untainted String>(); + return list; + } + + public ArrayList<@Tainted String> method5() { + var list = new ArrayList<@Untainted String>(); + // :: error: (return.type.incompatible) + return list; + } + + public ArrayList<@Untainted String> method6() { + var list = new ArrayList(); + // :: error: (return.type.incompatible) + return list; + } + + public void method7() { + var list = new ArrayList<@Untainted String>(); + method8(list); + } + + public void method8(ArrayList<@Untainted String> data) {} + + public void method9(List<@Tainted String> taintedlist, List<@Untainted String> untaintedList) { + var list1 = taintedlist; + List<@Tainted String> l = list1; + // :: error: (assignment.type.incompatible) + list1 = untaintedList; + } +} diff --git a/checker/tests/tainting/Issue5435.java b/checker/tests/tainting/Issue5435.java new file mode 100644 index 000000000000..61140363fc0b --- /dev/null +++ b/checker/tests/tainting/Issue5435.java @@ -0,0 +1,7 @@ +class Issue5435 { + public @interface A1 {} + + public @interface A2 { + A1[] m() default {@A1()}; + } +} diff --git a/checker/tests/tainting/Issue6110.java b/checker/tests/tainting/Issue6110.java new file mode 100644 index 000000000000..2e22c799320f --- /dev/null +++ b/checker/tests/tainting/Issue6110.java @@ -0,0 +1,23 @@ +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +import java.util.EnumSet; + +class Issue6110 { + enum TestEnum { + ONE, + @Untainted TWO + } + + static void test(Enum<@Untainted TestEnum> o) { + + @Tainted TestEnum e = TestEnum.ONE; + o.compareTo(TestEnum.ONE); + o.compareTo(TestEnum.TWO); + + EnumSet<@Tainted TestEnum> s1 = EnumSet.of(TestEnum.ONE); + // :: error: (assignment.type.incompatible) + EnumSet<@Untainted TestEnum> s2 = EnumSet.of(TestEnum.ONE); + EnumSet<@Untainted TestEnum> s3 = EnumSet.of(TestEnum.TWO); + } +} diff --git a/checker/tests/tainting/Issue6113.java b/checker/tests/tainting/Issue6113.java new file mode 100644 index 000000000000..f05089095bc7 --- /dev/null +++ b/checker/tests/tainting/Issue6113.java @@ -0,0 +1,10 @@ +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +class Issue6113 { + public void bar(List list) { + Collection c = + list != null ? list : Collections.emptyList(); // reported error here + } +} diff --git a/checker/tests/tainting/Issue6116.java b/checker/tests/tainting/Issue6116.java new file mode 100644 index 000000000000..c4367118cca1 --- /dev/null +++ b/checker/tests/tainting/Issue6116.java @@ -0,0 +1,14 @@ +import java.util.Iterator; +import java.util.function.Consumer; + +class Issue6116 { + private final Iterator it; + + public Issue6116(Iterator iterator) { + this.it = iterator; + } + + public void test(Consumer action) { + it.forEachRemaining(action); + } +} diff --git a/checker/tests/tainting/LambdaParameterDefaulting.java b/checker/tests/tainting/LambdaParameterDefaulting.java new file mode 100644 index 000000000000..f8dd5a1d1466 --- /dev/null +++ b/checker/tests/tainting/LambdaParameterDefaulting.java @@ -0,0 +1,38 @@ +import org.checkerframework.checker.tainting.qual.Untainted; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.checkerframework.framework.qual.TypeUseLocation; + +import java.util.function.Function; + +public class LambdaParameterDefaulting { + @DefaultQualifier(locations = TypeUseLocation.PARAMETER, value = Untainted.class) + void method() { + // :: error: (lambda.param.type.incompatible) + Function function = (String s) -> untainted(s); + Function<@Untainted String, String> function2 = (String s) -> untainted(s); + Function<@Untainted String, @Untainted String> function3 = (String s) -> untainted(s); + + // :: error: (argument.type.incompatible) + Function function4 = s -> untainted(s); + Function<@Untainted String, String> function5 = s -> untainted(s); + Function<@Untainted String, @Untainted String> function6 = s -> untainted(s); + } + + void method2() { + // :: error: (argument.type.incompatible) + Function function = (String s) -> untainted(s); + // :: error: (argument.type.incompatible) + Function<@Untainted String, String> function2 = (String s) -> untainted(s); + // :: error: (argument.type.incompatible) + Function<@Untainted String, @Untainted String> function3 = (String s) -> untainted(s); + + // :: error: (argument.type.incompatible) + Function function4 = s -> untainted(s); + Function<@Untainted String, String> function5 = s -> untainted(s); + Function<@Untainted String, @Untainted String> function6 = s -> untainted(s); + } + + @Untainted String untainted(@Untainted String s) { + return s; + } +} diff --git a/checker/tests/tainting/NestedAnonymous.java b/checker/tests/tainting/NestedAnonymous.java new file mode 100644 index 000000000000..4fe15a787e26 --- /dev/null +++ b/checker/tests/tainting/NestedAnonymous.java @@ -0,0 +1,14 @@ +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +class NestedAnonymous { + Object o1 = new @Tainted Object() {}; + Object o2 = new Outer.@Tainted Inner() {}; + + // :: error: (assignment.type.incompatible) + Outer.@Untainted Inner unt = new Outer.@Tainted Inner() {}; + + static class Outer { + static class Inner {} + } +} diff --git a/checker/tests/tainting/NestedTypeConstructor.java b/checker/tests/tainting/NestedTypeConstructor.java index 27e3b5abae81..ae6b50ba7cb6 100644 --- a/checker/tests/tainting/NestedTypeConstructor.java +++ b/checker/tests/tainting/NestedTypeConstructor.java @@ -3,7 +3,7 @@ // Test case for Issue 275 // https://github.com/typetools/checker-framework/issues/275 // Not tainting-specific, but a convenient location. -class NestedTypeConstructor { +public class NestedTypeConstructor { class Inner { @Tainted Inner() {} } diff --git a/checker/tests/tainting/PolyClassDecl.java b/checker/tests/tainting/PolyClassDecl.java new file mode 100644 index 000000000000..1cae9303f720 --- /dev/null +++ b/checker/tests/tainting/PolyClassDecl.java @@ -0,0 +1,34 @@ +import org.checkerframework.checker.tainting.qual.PolyTainted; + +import java.util.ArrayList; +import java.util.List; + +public class PolyClassDecl { + // :: error: (invalid.polymorphic.qualifier) + @PolyTainted static class Class1 {} + + // :: error: (invalid.polymorphic.qualifier) + static class Class2<@PolyTainted T> {} + + // :: error: (invalid.polymorphic.qualifier) + abstract static class Class3> {} + + // :: error: (invalid.polymorphic.qualifier) + interface Class4 extends List<@PolyTainted String> {} + + // :: error: (invalid.polymorphic.qualifier) + // :: error: (declaration.inconsistent.with.implements.clause) + interface Class5 extends @PolyTainted List {} + + // :: error: (invalid.polymorphic.qualifier) + abstract static class Class6 implements List<@PolyTainted String> {} + + void method() { + ArrayList<@PolyTainted String> s = new ArrayList<@PolyTainted String>() {}; + } + + // :: error: (invalid.polymorphic.qualifier) + <@PolyTainted T> T identity(T arg) { + return arg; + } +} diff --git a/checker/tests/tainting/SameTypeBounds.java b/checker/tests/tainting/SameTypeBounds.java new file mode 100644 index 000000000000..67c05f7703bf --- /dev/null +++ b/checker/tests/tainting/SameTypeBounds.java @@ -0,0 +1,46 @@ +package wildcards; + +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +public class SameTypeBounds { + static class MyGen {} + + void test1(MyGen p) { + // The upper and lower bound must have the same annotation because the bounds are collasped + // during capture conversion. + // :: error: (type.invalid.super.wildcard) + MyGen o = p; + // :: error: (assignment.type.incompatible) + p = o; + } + + void test2(MyGen p) { + // :: error: (assignment.type.incompatible) + MyGen<@Untainted ? super @Untainted Object> o = p; + // :: error: (assignment.type.incompatible) + p = o; + } + + void test3(MyGen<@Untainted Object> p) { + // :: error: (assignment.type.incompatible) + MyGen o = p; + // :: error: (assignment.type.incompatible) + p = o; + } + + static class MyClass {} + + static class MySubClass extends MyClass {} + + class Gen {} + + // :: error: (type.invalid.super.wildcard) + void test3(Gen p, Gen p2) { + // :: error: (type.invalid.super.wildcard) + Gen o = p; + o = p2; + // :: error: (assignment.type.incompatible) + p = p2; + } +} diff --git a/checker/tests/tainting/SimplePrims.java b/checker/tests/tainting/SimplePrims.java index 787879accc78..b784473601f9 100644 --- a/checker/tests/tainting/SimplePrims.java +++ b/checker/tests/tainting/SimplePrims.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.tainting.qual.Untainted; -class SimplePrims { +public class SimplePrims { void execute(@Untainted int s) {} diff --git a/checker/tests/tainting/SimpleTainting.java b/checker/tests/tainting/SimpleTainting.java index cf6fcdb98651..498b9c11933f 100644 --- a/checker/tests/tainting/SimpleTainting.java +++ b/checker/tests/tainting/SimpleTainting.java @@ -1,6 +1,6 @@ import org.checkerframework.checker.tainting.qual.Untainted; -class SimpleTainting { +public class SimpleTainting { void execute(@Untainted String s) {} diff --git a/checker/tests/tainting/SubClassHasQP.java b/checker/tests/tainting/SubClassHasQP.java index 8780451e61eb..8a7783c6096d 100644 --- a/checker/tests/tainting/SubClassHasQP.java +++ b/checker/tests/tainting/SubClassHasQP.java @@ -4,7 +4,7 @@ import org.checkerframework.framework.qual.HasQualifierParameter; // @skip-test https://github.com/typetools/checker-framework/issues/3400 -class SubClassHasQP { +public class SubClassHasQP { @HasQualifierParameter(Tainted.class) static class Buffer { void append(@PolyTainted Buffer this, @PolyTainted String s) {} diff --git a/checker/tests/tainting/TaintedIntersections.java b/checker/tests/tainting/TaintedIntersections.java new file mode 100644 index 000000000000..52510441e1c5 --- /dev/null +++ b/checker/tests/tainting/TaintedIntersections.java @@ -0,0 +1,49 @@ +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +public class TaintedIntersections { + interface MyInterface {} + + void test1() { + // null is @Untainted + @Untainted Object o1 = (@Untainted Object & @Untainted MyInterface) null; + // :: warning: (explicit.annotation.ignored) + @Untainted Object o2 = (@Untainted Object & @Tainted MyInterface) null; + // :: error: (assignment.type.incompatible) :: warning: (explicit.annotation.ignored) + @Untainted Object o3 = (@Tainted Object & @Untainted MyInterface) null; + // :: error: (assignment.type.incompatible) + @Untainted Object o4 = (@Tainted Object & @Tainted MyInterface) null; + } + + void test2() { + // null is @Untainted + @Untainted Object o1 = (@Untainted Object & MyInterface) null; + @Untainted Object o3 = (Object & @Untainted MyInterface) null; + // :: error: (assignment.type.incompatible) + @Untainted Object o2 = (Object & @Tainted MyInterface) null; + // :: error: (assignment.type.incompatible) + @Untainted Object o4 = (@Tainted Object & MyInterface) null; + } + + void test3(@Tainted MyInterface i) { + // :: warning: (cast.unsafe) + @Untainted Object o1 = (@Untainted Object & @Untainted MyInterface) i; + // :: warning: (explicit.annotation.ignored) :: warning: (cast.unsafe) + @Untainted Object o2 = (@Untainted Object & @Tainted MyInterface) i; + // :: error: (assignment.type.incompatible) :: warning: (explicit.annotation.ignored) + @Untainted Object o3 = (@Tainted Object & @Untainted MyInterface) i; + // :: error: (assignment.type.incompatible) + @Untainted Object o4 = (@Tainted Object & @Tainted MyInterface) i; + } + + void test4(@Tainted MyInterface i) { + // :: warning: (cast.unsafe) + @Untainted Object o1 = (@Untainted Object & MyInterface) i; + // :: warning: (cast.unsafe) + @Untainted Object o3 = (Object & @Untainted MyInterface) i; + // :: error: (assignment.type.incompatible) + @Untainted Object o2 = (Object & @Tainted MyInterface) i; + // :: error: (assignment.type.incompatible) + @Untainted Object o4 = (@Tainted Object & MyInterface) i; + } +} diff --git a/checker/tests/tainting/TaintingDiamondInference.java b/checker/tests/tainting/TaintingDiamondInference.java index fba1401d45c1..59ece7471a2f 100644 --- a/checker/tests/tainting/TaintingDiamondInference.java +++ b/checker/tests/tainting/TaintingDiamondInference.java @@ -1,8 +1,9 @@ // Test case for issue #660: https://github.com/typetools/checker-framework/issues/660 +import org.checkerframework.checker.tainting.qual.Untainted; + import java.util.Set; import java.util.TreeSet; -import org.checkerframework.checker.tainting.qual.Untainted; public class TaintingDiamondInference { diff --git a/checker/tests/tainting/TaintingIssue6025.java b/checker/tests/tainting/TaintingIssue6025.java new file mode 100644 index 000000000000..e0b8c2fa1d70 --- /dev/null +++ b/checker/tests/tainting/TaintingIssue6025.java @@ -0,0 +1,20 @@ +import org.checkerframework.checker.tainting.qual.Untainted; + +public class TaintingIssue6025 { + + public interface A {} + + private static final class B {} + + public interface C, T2 extends A> {} + + private final B, ? extends A>> one = new B<>(); + private final B, ? extends A>> two = new B<>(); + + void f(boolean b) { + // :: error: (assignment.type.incompatible) + B> three1 = one; + // :: error: (assignment.type.incompatible) + B> three = b ? two : one; + } +} diff --git a/checker/tests/tainting/TaintingIssue6060.java b/checker/tests/tainting/TaintingIssue6060.java new file mode 100644 index 000000000000..e2e6e6a8769e --- /dev/null +++ b/checker/tests/tainting/TaintingIssue6060.java @@ -0,0 +1,26 @@ +import org.checkerframework.checker.tainting.qual.Untainted; + +import java.util.Spliterator; +import java.util.function.Consumer; + +public interface TaintingIssue6060 extends Iterable<@Untainted R> { + + default Spliterator<@Untainted R> spliterator() { + return Iterable.super.spliterator(); + } + + default Spliterator spliterator2() { + // :: error: (return.type.incompatible) + return Iterable.super.spliterator(); + } + + default Spliterator spliterator3() { + // :: error: (return.type.incompatible) + return this.spliterator(); + } + + // :: error: (override.param.invalid) + default void forEach(Consumer action) { + Iterable.super.forEach(action); + } +} diff --git a/checker/tests/tainting/TaintingPolyFields.java b/checker/tests/tainting/TaintingPolyFields.java index 75e09fda0226..52fd672239fd 100644 --- a/checker/tests/tainting/TaintingPolyFields.java +++ b/checker/tests/tainting/TaintingPolyFields.java @@ -1,8 +1,9 @@ -import java.util.List; import org.checkerframework.checker.tainting.qual.PolyTainted; import org.checkerframework.checker.tainting.qual.Tainted; import org.checkerframework.checker.tainting.qual.Untainted; +import java.util.List; + public class TaintingPolyFields { // :: error: (invalid.polymorphic.qualifier.use) @PolyTainted Integer x; diff --git a/checker/tests/tainting/TestFieldPolymorphism.java b/checker/tests/tainting/TestFieldPolymorphism.java index 7fc21f477295..78d6cdea920d 100644 --- a/checker/tests/tainting/TestFieldPolymorphism.java +++ b/checker/tests/tainting/TestFieldPolymorphism.java @@ -4,7 +4,7 @@ import org.checkerframework.framework.qual.HasQualifierParameter; @HasQualifierParameter(Tainted.class) -class TestFieldPolymorphism { +public class TestFieldPolymorphism { @PolyTainted String field; @PolyTainted TestFieldPolymorphism(@PolyTainted String s) { diff --git a/checker/tests/tainting/TestNoQualifierParameterConflicting.java b/checker/tests/tainting/TestNoQualifierParameterConflicting.java index 363da54ba55e..250380a29388 100644 --- a/checker/tests/tainting/TestNoQualifierParameterConflicting.java +++ b/checker/tests/tainting/TestNoQualifierParameterConflicting.java @@ -11,6 +11,6 @@ public class TestNoQualifierParameterConflicting { static class Super {} @NoQualifierParameter(Tainted.class) - // :: error: (missing.has.qual.param) + // :: error: (conflicting.qual.param) static class Sup extends Super {} } diff --git a/checker/tests/tainting/TypeInvalid.java b/checker/tests/tainting/TypeInvalid.java index 1c91648094fe..65b4623a35fc 100644 --- a/checker/tests/tainting/TypeInvalid.java +++ b/checker/tests/tainting/TypeInvalid.java @@ -1,8 +1,7 @@ // Test case for Issue 292: // https://github.com/typetools/checker-framework/issues/292 -// TODO: ensure that type validation is consistently performed for each -// possible tree. +// TODO: ensure that type validation is consistently performed for each possible tree. // We should also add a jtreg version of this test to // ensure that each error is only output once and in the right place. @@ -12,6 +11,7 @@ abstract class TypeInvalid { // :: error: (type.invalid.conflicting.annos) static @Untainted @Tainted class Inner {} + // Duplication forbidden // :: error: (type.invalid.conflicting.annos) void bad(@Tainted @Untainted TypeInvalid c) { @@ -28,6 +28,7 @@ void bad(@Tainted @Untainted TypeInvalid c) { // :: error: (type.invalid.conflicting.annos) o = (new @Tainted @Untainted Object()) instanceof Object; // :: error: (type.invalid.conflicting.annos) + // :: warning: (instanceof.unsafe) o = o instanceof @Tainted @Untainted TypeInvalid; } diff --git a/checker/tests/tainting/WildcardArrayBound.java b/checker/tests/tainting/WildcardArrayBound.java new file mode 100644 index 000000000000..d17e55d9555d --- /dev/null +++ b/checker/tests/tainting/WildcardArrayBound.java @@ -0,0 +1,40 @@ +package wildcards; + +import org.checkerframework.checker.tainting.qual.Tainted; +import org.checkerframework.checker.tainting.qual.Untainted; + +import java.io.Serializable; + +public class WildcardArrayBound { + interface MyInterface {} + + abstract static class Other { + void use1( + Other x, + Other<@Tainted MyInterface @Untainted []> y) { + Other z = y; + // :: error: (assignment.type.incompatible) + x = y; + } + + void use( + Other x, + Other<@Tainted MyInterface @Untainted []> y) { + // :: error: (assignment.type.incompatible) + x = y; + @Untainted Serializable s = x.getU(); + @Untainted MyInterface @Untainted [] sw = x.getU(); + } + + abstract U getU(); + } + + abstract static class Another { + void use(Another x) { + Serializable s = x.getU(); + MyInterface[] sw = x.getU(); + } + + abstract U getU(); + } +} diff --git a/checker/tests/tainting/WildcardMethodArgument.java b/checker/tests/tainting/WildcardMethodArgument.java new file mode 100644 index 000000000000..1403ab76e4ac --- /dev/null +++ b/checker/tests/tainting/WildcardMethodArgument.java @@ -0,0 +1,14 @@ +import com.sun.source.tree.ExpressionTree; + +import java.util.ArrayList; +import java.util.List; + +public class WildcardMethodArgument { + abstract static class MyClass { + abstract List getArguments(); + } + + void method(MyClass myClass) { + List javacArgs = new ArrayList<>(myClass.getArguments()); + } +} diff --git a/checker/tests/tainting/java17/TaintingBindingVariable.java b/checker/tests/tainting/java17/TaintingBindingVariable.java new file mode 100644 index 000000000000..0cea4fb27f16 --- /dev/null +++ b/checker/tests/tainting/java17/TaintingBindingVariable.java @@ -0,0 +1,32 @@ +// @below-java16-jdk-skip-test +import org.checkerframework.checker.tainting.qual.Untainted; + +public class TaintingBindingVariable { + + void bar(@Untainted Object o) { + if (o instanceof @Untainted String s) { + @Untainted String f = s; + } + if (o instanceof String s) { + @Untainted String f2 = s; + } + } + + void bar2(Object o) { + // :: warning: (instanceof.pattern.unsafe) + if (o instanceof @Untainted String s) { + @Untainted String f = s; + } + if (o instanceof String s) { + // :: error: (assignment.type.incompatible) + @Untainted String f2 = s; + } + } + + void bar3(Object o, boolean b) { + // :: warning: (instanceof.pattern.unsafe) + if (b && o instanceof @Untainted String s) { + @Untainted String f = s; + } + } +} diff --git a/checker/tests/units/Addition.java b/checker/tests/units/Addition.java index 1a2a4be6ac8d..557911d16a44 100644 --- a/checker/tests/units/Addition.java +++ b/checker/tests/units/Addition.java @@ -1,5 +1,39 @@ -import org.checkerframework.checker.units.UnitsTools; -import org.checkerframework.checker.units.qual.*; +import org.checkerframework.checker.units.qual.A; +import org.checkerframework.checker.units.qual.Acceleration; +import org.checkerframework.checker.units.qual.Area; +import org.checkerframework.checker.units.qual.C; +import org.checkerframework.checker.units.qual.Current; +import org.checkerframework.checker.units.qual.Force; +import org.checkerframework.checker.units.qual.K; +import org.checkerframework.checker.units.qual.Length; +import org.checkerframework.checker.units.qual.Luminance; +import org.checkerframework.checker.units.qual.Mass; +import org.checkerframework.checker.units.qual.N; +import org.checkerframework.checker.units.qual.Substance; +import org.checkerframework.checker.units.qual.Temperature; +import org.checkerframework.checker.units.qual.Time; +import org.checkerframework.checker.units.qual.cd; +import org.checkerframework.checker.units.qual.g; +import org.checkerframework.checker.units.qual.h; +import org.checkerframework.checker.units.qual.kN; +import org.checkerframework.checker.units.qual.kg; +import org.checkerframework.checker.units.qual.km; +import org.checkerframework.checker.units.qual.km2; +import org.checkerframework.checker.units.qual.km3; +import org.checkerframework.checker.units.qual.kmPERh; +import org.checkerframework.checker.units.qual.m; +import org.checkerframework.checker.units.qual.m2; +import org.checkerframework.checker.units.qual.m3; +import org.checkerframework.checker.units.qual.mPERs; +import org.checkerframework.checker.units.qual.mPERs2; +import org.checkerframework.checker.units.qual.min; +import org.checkerframework.checker.units.qual.mm; +import org.checkerframework.checker.units.qual.mm2; +import org.checkerframework.checker.units.qual.mm3; +import org.checkerframework.checker.units.qual.mol; +import org.checkerframework.checker.units.qual.s; +import org.checkerframework.checker.units.qual.t; +import org.checkerframework.checker.units.util.UnitsTools; public class Addition { // Addition is legal when the operands have the same units. @@ -50,6 +84,11 @@ void good() { @km2 int bSquareKilometer = 5 * UnitsTools.km2; @km2 int sSquareKilometer = aSquareKilometer + bSquareKilometer; + // Cubic kilometer + @km3 int aCubicKilometer = 5 * UnitsTools.km3; + @km3 int bCubicKilometer = 5 * UnitsTools.km3; + @km3 int sCubicKilometer = aCubicKilometer + bCubicKilometer; + // Kilometer per hour @kmPERh int aKilometerPerHour = 5 * UnitsTools.kmPERh; @kmPERh int bKilometerPerHour = 5 * UnitsTools.kmPERh; @@ -65,6 +104,11 @@ void good() { @m2 int bSquareMeter = 5 * UnitsTools.m2; @m2 int sSquareMeter = aSquareMeter + bSquareMeter; + // Cubic meter + @m3 int aCubicMeter = 5 * UnitsTools.m3; + @m3 int bCubicMeter = 5 * UnitsTools.m3; + @m3 int sCubicMeter = aCubicMeter + bCubicMeter; + // Meter per second @mPERs int aMeterPerSecond = 5 * UnitsTools.mPERs; @mPERs int bMeterPerSecond = 5 * UnitsTools.mPERs; @@ -90,20 +134,34 @@ void good() { @mm2 int bSquareMillimeter = 5 * UnitsTools.mm2; @mm2 int sSquareMillimeter = aSquareMillimeter + bSquareMillimeter; + // Cubic millimeter + @mm3 int aCubicMillimeter = 5 * UnitsTools.mm3; + @mm3 int bCubicMillimeter = 5 * UnitsTools.mm3; + @mm3 int sCubicMillimeter = aCubicMillimeter + bCubicMillimeter; + // Mole @mol int aMole = 5 * UnitsTools.mol; @mol int bMole = 5 * UnitsTools.mol; @mol int sMole = aMole + bMole; + // Newton + @N int aNewton = 5 * UnitsTools.N; + @N int bNewton = 5 * UnitsTools.N; + @N int sNewton = aNewton + bNewton; + + // Kilonewton + @kN int aKilonewton = 5 * UnitsTools.kN; + @kN int bKilonewton = 5 * UnitsTools.kN; + @kN int sKilonewton = aKilonewton + bKilonewton; + // Second @s int aSecond = 5 * UnitsTools.s; @s int bSecond = 5 * UnitsTools.s; @s int sSecond = aSecond + bSecond; } - // Addition is illegal when the operands have different units or one - // is unqualified. In these tests, we cycle between the result and - // the first or second operand having an incorrect type. + // Addition is illegal when the operands have different units or one is unqualified. In these + // tests, we cycle between the result and the first or second operand having an incorrect type. void bad() { // Dimensions // Acceleration @@ -118,6 +176,10 @@ void bad() { @Current int aCurrent = 5 * UnitsTools.A; @Current int bCurrent = 5 * UnitsTools.A; + // Force + @Force int aForce = 5 * UnitsTools.N; + @Force int bForce = 5 * UnitsTools.N; + // Length @Length int aLength = 5 * UnitsTools.m; @Length int bLength = 5 * UnitsTools.mm; @@ -179,6 +241,10 @@ void bad() { // :: error: (assignment.type.incompatible) @Time int sTime = aArea + bTime; + // Force + // :: error: (assignment.type.incompatible) + sMass = aForce + bForce; + // Units // Amperes @A int aAmpere = 5 * UnitsTools.A; @@ -256,6 +322,18 @@ void bad() { @s int aSecond = 5 * UnitsTools.s; @s int bSecond = 5 * UnitsTools.s; + // Metric Ton + @t int aMetricTon = 5 * UnitsTools.t; + @t int bMetricTon = 5 * UnitsTools.t; + + // Newton + @N int aNewton = 5 * UnitsTools.N; + @N int bNewton = 5 * UnitsTools.N; + + // Kilonewton + @kN int aKilonewton = 5 * UnitsTools.kN; + @kN int bKilonewton = 5 * UnitsTools.kN; + // Units // Amperes // :: error: (assignment.type.incompatible) @@ -332,5 +410,17 @@ void bad() { // Second // :: error: (assignment.type.incompatible) @s int sSecond = aSecond + bSquareKilometer; + + // Newton + // :: error: (assignment.type.incompatible) + sKilogram = aNewton + bNewton; + + // Kilonewton + // :: error: (assignment.type.incompatible) + @kN int sKilonewton = aKilonewton + bNewton; + + // Metric Ton + // :: error: (assignment.type.incompatible) + @N int sNewton = aNewton + bMetricTon; } } diff --git a/checker/tests/units/BasicUnits.java b/checker/tests/units/BasicUnits.java index 692928a34418..68167f36ebb5 100644 --- a/checker/tests/units/BasicUnits.java +++ b/checker/tests/units/BasicUnits.java @@ -1,5 +1,22 @@ -import org.checkerframework.checker.units.*; -import org.checkerframework.checker.units.qual.*; +import org.checkerframework.checker.units.qual.Area; +import org.checkerframework.checker.units.qual.Prefix; +import org.checkerframework.checker.units.qual.Volume; +import org.checkerframework.checker.units.qual.degrees; +import org.checkerframework.checker.units.qual.h; +import org.checkerframework.checker.units.qual.kg; +import org.checkerframework.checker.units.qual.km; +import org.checkerframework.checker.units.qual.km2; +import org.checkerframework.checker.units.qual.km3; +import org.checkerframework.checker.units.qual.kmPERh; +import org.checkerframework.checker.units.qual.m; +import org.checkerframework.checker.units.qual.m2; +import org.checkerframework.checker.units.qual.m3; +import org.checkerframework.checker.units.qual.mPERs; +import org.checkerframework.checker.units.qual.mPERs2; +import org.checkerframework.checker.units.qual.radians; +import org.checkerframework.checker.units.qual.s; +import org.checkerframework.checker.units.qual.t; +import org.checkerframework.checker.units.util.UnitsTools; public class BasicUnits { @@ -48,6 +65,15 @@ void demo() { // :: error: (assignment.type.incompatible) @km2 int bae1 = m * m; + @Volume int vol = m * m * m; + @m3 int gvol = m * m * m; + + // :: error: (assignment.type.incompatible) + @Volume int bvol = m * m * m * m; + + // :: error: (assignment.type.incompatible) + @km3 int bvol1 = m * m * m; + @radians double rad = 20.0d * UnitsTools.rad; @degrees double deg = 30.0d * UnitsTools.deg; @@ -94,6 +120,10 @@ void demo() { speed /= 2; speed = (speed /= 2); + + @kg int kiloGrams = 1000 * UnitsTools.kg; + @t int metricTons = UnitsTools.fromKiloGramToMetricTon(kiloGrams); + kiloGrams = UnitsTools.fromMetricTonToKiloGram(metricTons); } void prefixOutputTest() { diff --git a/checker/tests/units/Consistency.java b/checker/tests/units/Consistency.java index 094127e798e7..3a42759a77bd 100644 --- a/checker/tests/units/Consistency.java +++ b/checker/tests/units/Consistency.java @@ -1,5 +1,10 @@ -import org.checkerframework.checker.units.*; -import org.checkerframework.checker.units.qual.*; +import org.checkerframework.checker.units.qual.Area; +import org.checkerframework.checker.units.qual.Length; +import org.checkerframework.checker.units.qual.km; +import org.checkerframework.checker.units.qual.km2; +import org.checkerframework.checker.units.qual.m; +import org.checkerframework.checker.units.qual.m2; +import org.checkerframework.checker.units.util.UnitsTools; /** * One possible future extension is adding method annotations to check for consistency of arguments. diff --git a/checker/tests/units/Division.java b/checker/tests/units/Division.java index c7fed1274933..bc0ff74bbf50 100644 --- a/checker/tests/units/Division.java +++ b/checker/tests/units/Division.java @@ -1,5 +1,22 @@ -import org.checkerframework.checker.units.*; -import org.checkerframework.checker.units.qual.*; +import org.checkerframework.checker.units.qual.N; +import org.checkerframework.checker.units.qual.h; +import org.checkerframework.checker.units.qual.kN; +import org.checkerframework.checker.units.qual.kg; +import org.checkerframework.checker.units.qual.km; +import org.checkerframework.checker.units.qual.km2; +import org.checkerframework.checker.units.qual.km3; +import org.checkerframework.checker.units.qual.kmPERh; +import org.checkerframework.checker.units.qual.m; +import org.checkerframework.checker.units.qual.m2; +import org.checkerframework.checker.units.qual.m3; +import org.checkerframework.checker.units.qual.mPERs; +import org.checkerframework.checker.units.qual.mPERs2; +import org.checkerframework.checker.units.qual.mm; +import org.checkerframework.checker.units.qual.mm2; +import org.checkerframework.checker.units.qual.mm3; +import org.checkerframework.checker.units.qual.s; +import org.checkerframework.checker.units.qual.t; +import org.checkerframework.checker.units.util.UnitsTools; public class Division { void d() { @@ -27,6 +44,13 @@ void d() { @mPERs int mPERs = 20 * UnitsTools.mPERs; @kmPERh int kmPERh = 2 * UnitsTools.kmPERh; @mPERs2 int mPERs2 = 30 * UnitsTools.mPERs2; + @m3 int m3 = 125 * UnitsTools.m3; + @km3 int km3 = 27 * UnitsTools.km3; + @mm3 int mm3 = 64 * UnitsTools.mm3; + @kg int kg = 11 * UnitsTools.kg; + @t int t = 19 * UnitsTools.t; + @N int N = 7 * UnitsTools.N; + @kN int kN = 13 * UnitsTools.kN; // m / s = mPERs @mPERs int velocitym = m / s; @@ -53,6 +77,21 @@ void d() { // :: error: (assignment.type.incompatible) distancemm = km2 / mm; + // m3 / m2 = m + distancem = m3 / m2; + // :: error: (assignment.type.incompatible) + distancem = m3 / km2; + + // km3 / km2 = km + distancekm = km3 / km2; + // :: error: (assignment.type.incompatible) + distancekm = km3 / m2; + + // mm3 / mm2 = mm + distancemm = mm3 / mm2; + // :: error: (assignment.type.incompatible) + distancemm = km3 / mm2; + // m / mPERs = s @s int times = m / mPERs; // :: error: (assignment.type.incompatible) @@ -72,6 +111,26 @@ void d() { @s int times2 = mPERs / mPERs2; // :: error: (assignment.type.incompatible) times2 = kmPERh / mPERs2; + + // mPERs2 = N / kg + @mPERs2 int accel2 = N / kg; + // :: error: (assignment.type.incompatible) + accel2 = N / km; + + // mPERs2 = kN / t + @mPERs2 int accel3 = kN / t; + // :: error: (assignment.type.incompatible) + accel3 = N / t; + + // kg = N / mPERs2 + @kg int mass = N / mPERs2; + // :: error: (assignment.type.incompatible) + mass = s / mPERs2; + + // t = kN / mPERs2 + @t int mass2 = kN / mPERs2; + // :: error: (assignment.type.incompatible) + mass2 = N / mPERs2; } void SpeedOfSoundTests() { diff --git a/checker/tests/units/Issue4549.java b/checker/tests/units/Issue4549.java new file mode 100644 index 000000000000..41c0fd9c0824 --- /dev/null +++ b/checker/tests/units/Issue4549.java @@ -0,0 +1,11 @@ +import java.util.HashMap; +import java.util.Map; + +class Issue4549 { + private Map map = new HashMap<>(); + + void testMethod(String s, int i) { + Long l = map.get(s); + l += i; + } +} diff --git a/checker/tests/units/Manual.java b/checker/tests/units/Manual.java index 28f6dfbdcb0c..d4df3842e114 100644 --- a/checker/tests/units/Manual.java +++ b/checker/tests/units/Manual.java @@ -1,5 +1,7 @@ -import org.checkerframework.checker.units.UnitsTools; -import org.checkerframework.checker.units.qual.*; +import org.checkerframework.checker.units.qual.m; +import org.checkerframework.checker.units.qual.mPERs; +import org.checkerframework.checker.units.qual.s; +import org.checkerframework.checker.units.util.UnitsTools; // Include all the examples from the manual here, // to ensure they work as expected. diff --git a/checker/tests/units/Multiples.java b/checker/tests/units/Multiples.java index 9cfc2765b45f..250e5b3f1ba1 100644 --- a/checker/tests/units/Multiples.java +++ b/checker/tests/units/Multiples.java @@ -1,5 +1,24 @@ -import org.checkerframework.checker.units.UnitsTools; -import org.checkerframework.checker.units.qual.*; +import org.checkerframework.checker.units.qual.N; +import org.checkerframework.checker.units.qual.Prefix; +import org.checkerframework.checker.units.qual.g; +import org.checkerframework.checker.units.qual.h; +import org.checkerframework.checker.units.qual.kN; +import org.checkerframework.checker.units.qual.kg; +import org.checkerframework.checker.units.qual.km; +import org.checkerframework.checker.units.qual.km2; +import org.checkerframework.checker.units.qual.km3; +import org.checkerframework.checker.units.qual.kmPERh; +import org.checkerframework.checker.units.qual.m; +import org.checkerframework.checker.units.qual.m2; +import org.checkerframework.checker.units.qual.m3; +import org.checkerframework.checker.units.qual.mPERs; +import org.checkerframework.checker.units.qual.mPERs2; +import org.checkerframework.checker.units.qual.mm; +import org.checkerframework.checker.units.qual.mm2; +import org.checkerframework.checker.units.qual.mm3; +import org.checkerframework.checker.units.qual.s; +import org.checkerframework.checker.units.qual.t; +import org.checkerframework.checker.units.util.UnitsTools; public class Multiples { void m() { @@ -49,6 +68,26 @@ void m() { mm = notmm; mm = alsomm; + // N + @N int N = 5 * UnitsTools.N; + @N(Prefix.one) int alsoN = N; + @N(Prefix.giga) + // :: error: (assignment.type.incompatible) + int notN = N; + // :: error: (assignment.type.incompatible) + N = notN; + N = alsoN; + + // kN + @kN int kN = 5 * UnitsTools.kN; + @N(Prefix.kilo) int alsokN = kN; + @N(Prefix.giga) + // :: error: (assignment.type.incompatible) + int notkN = kN; + // :: error: (assignment.type.incompatible) + kN = notkN; + kN = alsokN; + // s @s int s = 5 * UnitsTools.s; @@ -76,6 +115,48 @@ void m() { // :: error: (assignment.type.incompatible) @km2 int areammbad2 = mm * mm; + // m * m2 = m3 + @m3 int volume = m * area; + // :: error: (assignment.type.incompatible) + @km3 int volumembad1 = m * area; + // :: error: (assignment.type.incompatible) + @mm3 int volumembad2 = m * area; + + // km * km2 = km3 + @km3 int kvolume = km * karea; + // :: error: (assignment.type.incompatible) + @m3 int volumekmbad1 = km * karea; + // :: error: (assignment.type.incompatible) + @mm3 int volumekmbad2 = km * karea; + + // mm * mm2 = mm3 + @mm3 int mvolume = mm * marea; + // :: error: (assignment.type.incompatible) + @m3 int volumemmbad1 = mm * marea; + // :: error: (assignment.type.incompatible) + @km3 int volumemmbad2 = mm * marea; + + // m2 * m = m3 + volume = area * m; + // :: error: (assignment.type.incompatible) + volumembad1 = area * m; + // :: error: (assignment.type.incompatible) + volumembad2 = area * m; + + // km2 * km = km3 + kvolume = karea * km; + // :: error: (assignment.type.incompatible) + volumekmbad1 = karea * km; + // :: error: (assignment.type.incompatible) + volumekmbad2 = karea * km; + + // mm2 * mm = mm3 + mvolume = marea * mm; + // :: error: (assignment.type.incompatible) + volumemmbad1 = marea * mm; + // :: error: (assignment.type.incompatible) + volumemmbad2 = marea * mm; + // s * mPERs = m @mPERs int speedm = 10 * UnitsTools.mPERs; @m int lengthm = s * speedm; @@ -101,6 +182,18 @@ void m() { // :: error: (assignment.type.incompatible) @mm int lengthkmbad2 = h * speedkm; + // kg * mPERs2 = N + @kg int mass = 40 * UnitsTools.kg; + @mPERs2 int accel = 50 * UnitsTools.mPERs2; + @N int force = mass * accel; + + // mPERs2 * kg = N + @N int alsoforce = accel * mass; + + @t int massMetricTons = 50 * UnitsTools.t; + @kN int forceKiloNewtons = massMetricTons * accel; + forceKiloNewtons = accel * massMetricTons; + // s * s * mPERs2 = m // TODO: fix checker so it is insensitive to order of operations as long as final results' // unit makes sense. diff --git a/checker/tests/units/PolyUnitTest.java b/checker/tests/units/PolyUnitTest.java index 46a75cc94df7..df848df97ba1 100644 --- a/checker/tests/units/PolyUnitTest.java +++ b/checker/tests/units/PolyUnitTest.java @@ -1,5 +1,7 @@ -import org.checkerframework.checker.units.*; -import org.checkerframework.checker.units.qual.*; +import org.checkerframework.checker.units.qual.PolyUnit; +import org.checkerframework.checker.units.qual.m; +import org.checkerframework.checker.units.qual.s; +import org.checkerframework.checker.units.util.UnitsTools; public class PolyUnitTest { diff --git a/checker/tests/units/SubtractionUnits.java b/checker/tests/units/SubtractionUnits.java index 3da657c29f84..9201cd5b7455 100644 --- a/checker/tests/units/SubtractionUnits.java +++ b/checker/tests/units/SubtractionUnits.java @@ -1,5 +1,40 @@ -import org.checkerframework.checker.units.UnitsTools; -import org.checkerframework.checker.units.qual.*; +import org.checkerframework.checker.units.qual.A; +import org.checkerframework.checker.units.qual.Acceleration; +import org.checkerframework.checker.units.qual.Area; +import org.checkerframework.checker.units.qual.C; +import org.checkerframework.checker.units.qual.Current; +import org.checkerframework.checker.units.qual.Force; +import org.checkerframework.checker.units.qual.K; +import org.checkerframework.checker.units.qual.Length; +import org.checkerframework.checker.units.qual.Luminance; +import org.checkerframework.checker.units.qual.Mass; +import org.checkerframework.checker.units.qual.N; +import org.checkerframework.checker.units.qual.Substance; +import org.checkerframework.checker.units.qual.Temperature; +import org.checkerframework.checker.units.qual.Time; +import org.checkerframework.checker.units.qual.Volume; +import org.checkerframework.checker.units.qual.cd; +import org.checkerframework.checker.units.qual.g; +import org.checkerframework.checker.units.qual.h; +import org.checkerframework.checker.units.qual.kN; +import org.checkerframework.checker.units.qual.kg; +import org.checkerframework.checker.units.qual.km; +import org.checkerframework.checker.units.qual.km2; +import org.checkerframework.checker.units.qual.km3; +import org.checkerframework.checker.units.qual.kmPERh; +import org.checkerframework.checker.units.qual.m; +import org.checkerframework.checker.units.qual.m2; +import org.checkerframework.checker.units.qual.m3; +import org.checkerframework.checker.units.qual.mPERs; +import org.checkerframework.checker.units.qual.mPERs2; +import org.checkerframework.checker.units.qual.min; +import org.checkerframework.checker.units.qual.mm; +import org.checkerframework.checker.units.qual.mm2; +import org.checkerframework.checker.units.qual.mm3; +import org.checkerframework.checker.units.qual.mol; +import org.checkerframework.checker.units.qual.s; +import org.checkerframework.checker.units.qual.t; +import org.checkerframework.checker.units.util.UnitsTools; public class SubtractionUnits { // Subtraction is legal when the operands have the same units. @@ -35,6 +70,11 @@ void good() { @K int bKelvin = 5 * UnitsTools.K; @K int sKelvin = aKelvin - bKelvin; + // Kilonewton + @kN int aKilonewton = 5 * UnitsTools.kN; + @kN int bKilonewton = 5 * UnitsTools.kN; + @kN int sKiloewton = aKilonewton - bKilonewton; + // Kilogram @kg int aKilogram = 5 * UnitsTools.kg; @kg int bKilogram = 5 * UnitsTools.kg; @@ -50,6 +90,11 @@ void good() { @km2 int bSquareKilometer = 5 * UnitsTools.km2; @km2 int sSquareKilometer = aSquareKilometer - bSquareKilometer; + // Cubic kilometer + @km3 int aCubicKilometer = 5 * UnitsTools.km3; + @km3 int bCubicKilometer = 5 * UnitsTools.km3; + @km3 int sCubicKilometer = aCubicKilometer - bCubicKilometer; + // Kilometer per hour @kmPERh int aKilometerPerHour = 5 * UnitsTools.kmPERh; @kmPERh int bKilometerPerHour = 5 * UnitsTools.kmPERh; @@ -65,6 +110,11 @@ void good() { @m2 int bSquareMeter = 5 * UnitsTools.m2; @m2 int sSquareMeter = aSquareMeter - bSquareMeter; + // Cubic meter + @m3 int aCubicMeter = 5 * UnitsTools.m3; + @m3 int bCubicMeter = 5 * UnitsTools.m3; + @m3 int sCubicMeter = aCubicMeter - bCubicMeter; + // Meter per second @mPERs int aMeterPerSecond = 5 * UnitsTools.mPERs; @mPERs int bMeterPerSecond = 5 * UnitsTools.mPERs; @@ -90,20 +140,30 @@ void good() { @mm2 int bSquareMillimeter = 5 * UnitsTools.mm2; @mm2 int sSquareMillimeter = aSquareMillimeter - bSquareMillimeter; + // Cubic millimeter + @mm3 int aCubicMillimeter = 5 * UnitsTools.mm3; + @mm3 int bCubicMillimeter = 5 * UnitsTools.mm3; + @mm3 int sCubicMillimeter = aCubicMillimeter - bCubicMillimeter; + // Mole @mol int aMole = 5 * UnitsTools.mol; @mol int bMole = 5 * UnitsTools.mol; @mol int sMole = aMole - bMole; + // Newton + @N int aNewton = 5 * UnitsTools.N; + @N int bNewton = 5 * UnitsTools.N; + @N int sNewton = aNewton - bNewton; + // Second @s int aSecond = 5 * UnitsTools.s; @s int bSecond = 5 * UnitsTools.s; @s int sSecond = aSecond - bSecond; } - // Subtraction is illegal when the operands have different units or one - // is unqualified. In these tests, we cycle between the result and - // the first or second operand having an incorrect type. + // Subtraction is illegal when the operands have different units or one is unqualified. In + // these tests, we cycle between the result and the first or second operand having an incorrect + // type. void bad() { // Dimensions // Acceleration @@ -118,6 +178,10 @@ void bad() { @Current int aCurrent = 5 * UnitsTools.A; @Current int bCurrent = 5 * UnitsTools.A; + // Force + @Force int aForce = 5 * UnitsTools.N; + @Force int bForce = 5 * UnitsTools.N; + // Length @Length int aLength = 5 * UnitsTools.m; @Length int bLength = 5 * UnitsTools.mm; @@ -142,6 +206,10 @@ void bad() { @Time int aTime = 5 * UnitsTools.min; @Time int bTime = 5 * UnitsTools.h; + // Volume + @Volume int aVolume = 5 * UnitsTools.m3; + @Volume int bVolume = 5 * UnitsTools.km3; + // Dimensions // :: error: (assignment.type.incompatible) @Acceleration int sAcceleration = aAcceleration - bMass; @@ -178,6 +246,14 @@ void bad() { // :: error: (assignment.type.incompatible) @Time int sTime = aArea - bTime; + // Volume + // :: error: (assignment.type.incompatible) + @Volume int sVolume = aVolume - bArea; + + // Force + // :: error: (assignment.type.incompatible) + sMass = aForce - bForce; + // Units // Amperes @A int aAmpere = 5 * UnitsTools.A; @@ -203,6 +279,10 @@ void bad() { @K int aKelvin = 5 * UnitsTools.K; @K int bKelvin = 5 * UnitsTools.K; + // Kilonewton + @kN int aKilonewton = 5 * UnitsTools.kN; + @kN int bKilonewton = 5 * UnitsTools.kN; + // Kilogram @kg int aKilogram = 5 * UnitsTools.kg; @kg int bKilogram = 5 * UnitsTools.kg; @@ -215,6 +295,10 @@ void bad() { @km2 int aSquareKilometer = 5 * UnitsTools.km2; @km2 int bSquareKilometer = 5 * UnitsTools.km2; + // Cubic kilometer + @km3 int aCubicKilometer = 5 * UnitsTools.km3; + @km3 int bCubicKilometer = 5 * UnitsTools.km3; + // Kilometer per hour @kmPERh int aKilometerPerHour = 5 * UnitsTools.kmPERh; @kmPERh int bKilometerPerHour = 5 * UnitsTools.kmPERh; @@ -227,6 +311,10 @@ void bad() { @m2 int aSquareMeter = 5 * UnitsTools.m2; @m2 int bSquareMeter = 5 * UnitsTools.m2; + // Cubic meter + @m3 int aCubicMeter = 5 * UnitsTools.m3; + @m3 int bCubicMeter = 5 * UnitsTools.m3; + // Meter per second @mPERs int aMeterPerSecond = 5 * UnitsTools.mPERs; @mPERs int bMeterPerSecond = 5 * UnitsTools.mPERs; @@ -247,6 +335,10 @@ void bad() { @mm2 int aSquareMillimeter = 5 * UnitsTools.mm2; @mm2 int bSquareMillimeter = 5 * UnitsTools.mm2; + // Cubic millimeter + @mm3 int aCubicMillimeter = 5 * UnitsTools.mm3; + @mm3 int bCubicMillimeter = 5 * UnitsTools.mm3; + // Mole @mol int aMole = 5 * UnitsTools.mol; @mol int bMole = 5 * UnitsTools.mol; @@ -255,6 +347,14 @@ void bad() { @s int aSecond = 5 * UnitsTools.s; @s int bSecond = 5 * UnitsTools.s; + // Metric Tons + @t int aMetricTon = 5 * UnitsTools.t; + @t int bMetricTon = 5 * UnitsTools.t; + + // Newton + @N int aNewton = 5 * UnitsTools.N; + @N int bNewton = 5 * UnitsTools.N; + // Units // Amperes // :: error: (assignment.type.incompatible) @@ -292,6 +392,10 @@ void bad() { // :: error: (assignment.type.incompatible) @km2 int sSquareKilometer = aSquareKilometer - bAmpere; + // Cubic kilometer + // :: error: (assignment.type.incompatible) + @km3 int sCubicKilometer = aCubicKilometer - bAmpere; + // Kilometer per hour // :: error: (assignment.type.incompatible) @mPERs int sMeterPerSecond = aKilometerPerHour - bKilometerPerHour; @@ -304,6 +408,10 @@ void bad() { // :: error: (assignment.type.incompatible) @m2 int sSquareMeter = aSquareMeter - bGram; + // Cubic meter + // :: error: (assignment.type.incompatible) + @m3 int sCubicMeter = aCubicMeter - bGram; + // Meter per second // :: error: (assignment.type.incompatible) @mm2 int sSquareMillimeter = aMeterPerSecond - bMeterPerSecond; @@ -331,5 +439,17 @@ void bad() { // Second // :: error: (assignment.type.incompatible) @s int sSecond = aSecond - bSquareKilometer; + + // Newton + // :: error: (assignment.type.incompatible) + sKilogram = aNewton - bNewton; + + // Kilonewton + // :: error: (assignment.type.incompatible) + @kN int sKilonewton = aKilonewton - bNewton; + + // Metric Ton + // :: error: (assignment.type.incompatible) + @N int sNewton = aNewton - bMetricTon; } } diff --git a/checker/tests/units/Units.java b/checker/tests/units/Units.java index a4119abc50bd..3244e266138f 100644 --- a/checker/tests/units/Units.java +++ b/checker/tests/units/Units.java @@ -1,14 +1,14 @@ -import static org.checkerframework.checker.units.UnitsTools.s; +import static org.checkerframework.checker.units.util.UnitsTools.s; -import org.checkerframework.checker.units.*; -import org.checkerframework.checker.units.qual.*; +import org.checkerframework.checker.units.qual.m; +import org.checkerframework.checker.units.qual.s; +import org.checkerframework.checker.units.util.UnitsTools; public class Units { @m int m1 = 5 * UnitsTools.m; - // The advantage of using the multiplication with a unit is that - // also double, float, etc. are easily handled and we don't need - // to end a huge number of methods to UnitsTools. + // The advantage of using the multiplication with a unit is that also double, float, etc. are + // easily handled and we don't need to end a huge number of methods to UnitsTools. @m double dm = 9.34d * UnitsTools.m; // With a static import: diff --git a/checker/tests/units/UnqualTest.java b/checker/tests/units/UnqualTest.java index 2baf9bc7e742..3e7b0704ecd6 100644 --- a/checker/tests/units/UnqualTest.java +++ b/checker/tests/units/UnqualTest.java @@ -1,4 +1,4 @@ -import org.checkerframework.checker.units.qual.*; +import org.checkerframework.checker.units.qual.kg; public class UnqualTest { // :: error: (assignment.type.incompatible) diff --git a/checker/tests/value-index-interaction/MethodOverrides3.java b/checker/tests/value-index-interaction/MethodOverrides3.java index e45653984fb5..f0a8ecfd5bb6 100644 --- a/checker/tests/value-index-interaction/MethodOverrides3.java +++ b/checker/tests/value-index-interaction/MethodOverrides3.java @@ -1,11 +1,12 @@ // This class should not issues any errors, since these annotations are identical to the ones // on java.io.PrintWriter in the Index JDK. +import org.checkerframework.checker.index.qual.IndexFor; +import org.checkerframework.checker.index.qual.IndexOrHigh; + import java.io.File; import java.io.FileNotFoundException; import java.io.PrintWriter; -import org.checkerframework.checker.index.qual.IndexFor; -import org.checkerframework.checker.index.qual.IndexOrHigh; public class MethodOverrides3 extends PrintWriter { public MethodOverrides3(File file) throws FileNotFoundException { diff --git a/checker/tests/value-index-interaction/MinLenFromPositive.java b/checker/tests/value-index-interaction/MinLenFromPositive.java index 4921607d96a2..f14a9d556eb1 100644 --- a/checker/tests/value-index-interaction/MinLenFromPositive.java +++ b/checker/tests/value-index-interaction/MinLenFromPositive.java @@ -3,7 +3,7 @@ import org.checkerframework.common.value.qual.IntRange; import org.checkerframework.common.value.qual.MinLen; -class MinLenFromPositive { +public class MinLenFromPositive { public @Positive int x = 0; diff --git a/checker/tests/wpi-many/README.md b/checker/tests/wpi-many/README.md new file mode 100644 index 000000000000..9c2c9789b10d --- /dev/null +++ b/checker/tests/wpi-many/README.md @@ -0,0 +1,33 @@ +The file `testin.txt` in this directory contains a list of github repositories that +are used to test the `wpi-many.sh` script (stored in `checker/bin`). Each entry is a +git URL and commit hash, separated by whitespace. + +The projects listed in `testin.txt` are derived from plume-lib projects; each is a hard fork. +These forks have had their (inferrable) annotations removed, and their typical checker +build infrastructure disabled. The `./gradlew wpiManyTest` task defined in `checker/build.gradle` +runs the `wpi-many.sh` script on these projects, and then checks that they typecheck afterwards. + +The use of a hard fork means these tests may fail to compile under newer versions of the JDK. +When a new Java version is released, the projects may need to be updated and a new SHA commit +put in the `testin.txt` file. +TODO: List the required steps, such as using a newer version of Gradle. + +To add a new project (named `$PROJECT` below) to `testin.txt`, follow these steps: +1. Create a new GitHub repository under your own user name with the name "wpi-many-tests-$PROJECT". +Do not initialize it with any content. +2. Clone that repo and the project you intend to add locally. +3. Copy the contents of the project you are adding to your new repo and commit the result. +4. Run `java -cp $CHECKERFRAMEWORK/checker/dist/checker.jar org.checkerframework.framework.stub.RemoveAnnotationsForInference .` on your new repository. +5. Edit the build file of the new project and disable the Checker Framework. This is usually done +by commenting out the checkerframework-gradle-plugin and checkerFramework lines for Gradle users. +In addition, you probably will need to add `org.checkerframework:checker-qual:$LATEST-CF-VERSION` as an +`implementation` dependency. Ensure that the build still succeeds (correcting any errors) and then commit. +6. Run `wpi.sh` on the project yourself and locate any annotations that cannot be inferred by WPI and/or +any false positives from the relevant typecheckers (see `checker/build.gradle` for the list of +checkers that the `wpiManyTest` task uses). You must add annotations or warning suppressions to your +project until running `wpi.sh` on the project with those checkers produces no errors in its final iteration. +When that is the case, commit the result and note the commit hash. +7. Push the state of your new repo to GitHub. +8. Add the HTTPS version of the GitHub URL (without the `.git`!) and the commit hash you noted in step 6 +to `testin.txt`. +9. Run `./gradlew wpiManyTest` to ensure that the tests still pass, and correct any errors. diff --git a/checker/tests/wpi-many/testin.txt b/checker/tests/wpi-many/testin.txt new file mode 100644 index 000000000000..30011155d8dc --- /dev/null +++ b/checker/tests/wpi-many/testin.txt @@ -0,0 +1,7 @@ +https://github.com/kelloggm/wpi-many-tests-bcel-util b8f661c24014328e4cef6f55b2772cc5e62233df +https://github.com/kelloggm/wpi-many-tests-bibtex-clean 2d82118d66f118d89f2f69c847badd5179e34c0a +https://github.com/kelloggm/wpi-many-tests-ensures-called-methods 326bc8abfcf635c97b13f1e8d6df9f4b1c7d4bfe +https://github.com/kelloggm/wpi-many-tests-html-pretty-print f4a7268f8f6e7014db59d208e6c0e8fae72ec0fd +https://github.com/kelloggm/-wpi-many-tests-bibtex-clean 28fb7e20f011fc754112ead28d6a81eb0093b392 +# This comment line tests that the commenting feature works (if it doesn't, then this line will be read and fail, as it's not a URL). +https://github.com/Nargeshdb/wpi-many-tests-owning-field f98270f6de95061df5faa9eba7f6c2fc27bc0de6 diff --git a/dataflow/build.gradle b/dataflow/build.gradle index b8d6545b63ca..da91d39cc4a2 100644 --- a/dataflow/build.gradle +++ b/dataflow/build.gradle @@ -1,61 +1,256 @@ -dependencies { - implementation project(':javacutil') - implementation project(':checker-qual') +plugins { + id 'java-library' + id 'base' } -shadowJar { - archiveFileName = "dataflow-shaded.jar" - relocate 'org.checkerframework.dataflow', 'org.checkerframework.shaded.dataflow' - relocate 'org.checkerframework.javacutil', 'org.checkerframework.shaded.javacutil' +dependencies { + api project(':javacutil') + api project(':checker-qual') + + // Node implements org.plumelib.util.UniqueId, so this dependency must be "api". + api "org.plumelib:plume-util:${versions.plumeUtil}" + + // External dependencies: + // If you add an external dependency, you must shadow its packages both in the dataflow-shaded + // artifact (see shadowJar block below) and also in checker.jar (see the comment in + // ../build.gradle in the createDataflowShaded task). } -artifacts { - archives shadowJar +// Shadowing Test Sources and Dependencies +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +/** + * Creates a task with name dataflow${shadedPkgName} that creates a shaded dataflow jar. The packages will be shaded to + * "org.checkerframework.{@code shadedPkgName}" and the jar name is dataflow-${shadedPkgName}.jar. + * @param shadedPkgName + * @return + */ +def createDataflowShaded(shadedPkgName) { + tasks.create(name: "dataflow${shadedPkgName}Jar", type: ShadowJar, dependsOn: compileJava, group: 'Build') { + description "Builds dataflow-${shadedPkgName}.jar." + includeEmptyDirs = false + archiveBaseName.set("dataflow-${shadedPkgName}") + // Without this line, the Maven artifact will have the classifier "all". + archiveClassifier.set('') + + from shadowJar.source + configurations = shadowJar.configurations + + destinationDirectory = file("${buildDir}/shadow/dataflow${shadedPkgName}") + + + relocate('org.checkerframework', "org.checkerframework.${shadedPkgName}") { + // Shade all Checker Framework packages, except for the dataflow qualifiers. + exclude 'org.checkerframework.dataflow.qual.*' + } + + // Relocate external dependencies + relocate 'org.plumelib', "org.checkerframework.${shadedPkgName}.org.plumelib" + relocate 'com.google', "org.checkerframework.${shadedPkgName}.com.google" + } } -task deployArtifactsToLocalRepo(dependsOn: jar) { - description "Deploys ${jar.archiveFileName.get()} to the local Maven repository" - dependsOn ('jar', 'shadowJar') +// Creates a new shaded dataflow artifact. To add a new one, add a new method call below, and add +// the new jar to publishing and signing blocks. +createDataflowShaded('shaded') +createDataflowShaded('nullaway') +createDataflowShaded('errorprone') +task manual(group: 'Documentation') { + description 'Build the manual' doLast { - mvnDeployToLocalRepo(project, "${pomFiles}/dataflow-pom.xml") - mvnDeployToLocalRepo(shadowJar.archiveFile.get().toString(), "${pomFiles}/dataflow-shaded-pom.xml") + exec { + commandLine 'make', '-C', 'manual' + } } } -task deployArtifactsToSonatype { - description "Deploys ${jar.archiveFileName.get()} to the Sonatype repository" - dependsOn ('jar', 'sourcesJar', 'javadocJar', 'shadowJar') - doLast { - mvnSignAndDeployMultipleToSonatype(project, "${pomFiles}/dataflow-pom.xml") - mvnSignAndDeployMultipleToSonatype(shadowJar.archiveFile.get().toString(), sourcesJar.archiveFile.get().toString(), javadocJar.archiveFile.get().toString(), "${pomFiles}/dataflow-shaded-pom.xml") +tasks.withType(Test) { + dependsOn('allDataflowTests') +} + +tasks.create(name: 'allDataflowTests', group: 'Verification') { + description 'Run all dataflow analysis tests' + // 'allDataflowTests' is automatically populated by testDataflowAnalysis(). +} + +/** + * Create a task to run a dataflow analysis test case. + * To add a new dataflow analysis test, you need to provide the + * task name, the directory which contains the test files, and the fully-qualified class + * name of the test class. + * Optionally, when option {@code diff} is true, the output is compared against a + * {@code Expected.txt} file. + * See org.checkerframework.dataflow.cfg.visualize.CFGVisualizeLauncher for more details. + * @param taskName the task name for the new task + * @param dirName the directory name of the directory that contains the test files + * @param className the fully-qualified name of the test class + * @param diff whether to compare the generated output with a {@code Expected.txt} file + * @return + */ +def testDataflowAnalysis(taskName, dirName, className, diff) { + tasks.create(name: taskName, dependsOn: [assemble, compileTestJava], group: 'Verification') { + description "Run the ${dirName} dataflow framework test." + if (diff) { + inputs.file("tests/${dirName}/Expected.txt") + } + inputs.file("tests/${dirName}/Test.java") + + outputs.file("tests/${dirName}/Out.txt") + outputs.file("tests/${dirName}/Test.class") + + delete("tests/${dirName}/Out.txt") + delete("tests/${dirName}/Test.class") + doLast { + javaexec { + workingDir = "tests/${dirName}" + if (!JavaVersion.current().java9Compatible) { + jvmArgs += "-Xbootclasspath/p:${configurations.javacJar.asPath}".toString() + } + else if (JavaVersion.current() > JavaVersion.VERSION_11) { + jvmArgs += [ + '--add-opens', + 'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', + '--add-opens', + 'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', + '--add-opens', + 'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', + '--add-opens', + 'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', + '--add-opens', + 'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', + '--add-opens', + 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', + '--add-opens', + 'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED', + ] + } + + classpath = sourceSets.test.runtimeClasspath + classpath += sourceSets.test.output + mainClass = "${className}" + } + if (diff) { + exec { + workingDir = "tests/${dirName}" + executable 'diff' + args = [ + '-u', + 'Expected.txt', + 'Out.txt' + ] + } + } + } } + + allDataflowTests.dependsOn << taskName } -task liveVariableTest(dependsOn: compileTestJava, group: 'Verification') { - description 'Test the live variable analysis test for dataflow framework.' - inputs.file('tests/live-variable/Expected.txt') - inputs.file('tests/live-variable/Test.java') +// When adding a new test case, don't forget to add the temporary files to `../.gitignore`. +testDataflowAnalysis("busyExpressionTest", "busyexpr", "busyexpr.BusyExpression", true) +testDataflowAnalysis("cfgConstructionTest", "cfgconstruction", "cfgconstruction.CFGConstruction", false) +testDataflowAnalysis("constantPropagationTest", "constant-propagation", "constantpropagation.ConstantPropagation", true) +testDataflowAnalysis("issue3447Test", "issue3447", "livevar.LiveVariable", false) +testDataflowAnalysis("liveVariableTest", "live-variable", "livevar.LiveVariable", true) +testDataflowAnalysis("reachingDefinitionTest", "reachingdef", "reachingdef.ReachingDefinition", true) - outputs.file('tests/live-variable/Out.txt') - outputs.file('tests/live-variable/Test.class') +apply from: rootProject.file('gradle-mvn-push.gradle') - delete('tests/live-variable/Out.txt') - delete('tests/live-variable/Test.class') - doLast { - javaexec { - workingDir = 'tests/live-variable' - if (!JavaVersion.current().java9Compatible) { - jvmArgs += "-Xbootclasspath/p:${configurations.javacJar.asPath}" +/** Adds information to the publication for uploading the dataflow artifacts to Maven repositories. */ +final dataflowPom(publication) { + sharedPublicationConfiguration(publication) + publication.from components.java + // Information that is in all pom files is configured in checker-framework/gradle-mvn-push.gradle. + publication.pom { + name = 'Dataflow' + description = 'Dataflow is a dataflow framework based on the javac compiler.' + licenses { + license { + name = 'GNU General Public License, version 2 (GPL2), with the classpath exception' + url = 'http://www.gnu.org/software/classpath/license.html' + distribution = 'repo' } - classpath = sourceSets.test.compileClasspath - classpath += sourceSets.test.output - main = 'livevar.LiveVariable' } - exec { - workingDir = 'tests/live-variable' - executable 'diff' - args = ['-u', 'Expected.txt', 'Out.txt'] + } +} + +/** + * Adds information to the publication for uploading the dataflow-${shadedPkgName} artifacts to Maven repositories. + * @param shadedPkgName the name of the shaded package to use; also used as part of the artifact name: "dataflow-${shadePkgName}" + */ +final dataflowShadedPom(MavenPublication publication, String shadedPkgName) { + sharedPublicationConfiguration(publication) + + publication.artifactId = "dataflow-${shadedPkgName}" + publication.pom { + name = "Dataflow (${shadedPkgName})" + description = "dataflow-${shadedPkgName} is a dataflow framework based on the javac compiler.\n" + + '\n' + + 'It differs from the org.checkerframework:dataflow artifact in two ways.\n' + + "First, the packages in this artifact have been renamed to org.checkerframework.${shadedPkgName}.*.\n" + + 'Second, unlike the dataflow artifact, this artifact contains the dependencies it requires.' + licenses { + license { + name = 'GNU General Public License, version 2 (GPL2), with the classpath exception' + url = 'http://www.gnu.org/software/classpath/license.html' + distribution = 'repo' + } + } + } +} + + +publishing { + publications { + dataflow(MavenPublication) { + dataflowPom it + } + + dataflowShaded(MavenPublication) { + dataflowShadedPom(it, 'shaded') + artifact project.tasks.getByName('dataflowshadedJar').archiveFile + artifact sourcesJar + artifact javadocJar + } + + dataflowShadednullaway(MavenPublication) { + dataflowShadedPom(it, 'nullaway') + + artifact project.tasks.getByName('dataflownullawayJar').archiveFile + artifact sourcesJar + artifact javadocJar + } + + dataflowShadederrorprone(MavenPublication) { + dataflowShadedPom(it, 'errorprone') + + artifact project.tasks.getByName('dataflowerrorproneJar').archiveFile + artifact sourcesJar + artifact javadocJar } } } + +signing { + sign publishing.publications.dataflow + sign publishing.publications.dataflowShaded + sign publishing.publications.dataflowShadednullaway + sign publishing.publications.dataflowShadederrorprone +} + +publishDataflowPublicationToMavenRepository.dependsOn(signDataflowShadedPublication) +publishDataflowPublicationToMavenRepository.dependsOn(signDataflowShadederrorpronePublication) +publishDataflowPublicationToMavenRepository.dependsOn(signDataflowShadednullawayPublication) + +publishDataflowShadedPublicationToMavenRepository.dependsOn(signDataflowPublication) +publishDataflowShadedPublicationToMavenRepository.dependsOn(signDataflowShadederrorpronePublication) +publishDataflowShadedPublicationToMavenRepository.dependsOn(signDataflowShadednullawayPublication) + +publishDataflowShadederrorpronePublicationToMavenRepository.dependsOn(signDataflowShadedPublication) +publishDataflowShadederrorpronePublicationToMavenRepository.dependsOn(signDataflowPublication) +publishDataflowShadederrorpronePublicationToMavenRepository.dependsOn(signDataflowShadednullawayPublication) + +publishDataflowShadednullawayPublicationToMavenRepository.dependsOn(signDataflowShadederrorpronePublication) +publishDataflowShadednullawayPublicationToMavenRepository.dependsOn(signDataflowShadedPublication) +publishDataflowShadednullawayPublicationToMavenRepository.dependsOn(signDataflowPublication) diff --git a/dataflow/manual/Makefile b/dataflow/manual/Makefile index 5177f8006dc9..149fe75b6395 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 68fbb53b7320..0c3d9b04ddd8 100644 --- a/dataflow/manual/content.tex +++ b/dataflow/manual/content.tex @@ -1,59 +1,99 @@ +% -*- mode: latex; TeX-master: "dataflow"; TeX-command-default: "PDF"; -*- +%% Put local variables at beginning of file because Emacs function +%% `hack-local-variables` only looks backwards 3000 characters for the "Local +%% Variables" block, that number is hardcoded, and the "LocalWords" part of this +%% file is longer than that. + \section{Introduction} This document describes a \emph{Dataflow Framework} for the Java -Programming Language. The framework is used in the -\href{https://checkerframework.org/}{Checker Framework}, -\href{http://errorprone.info/}{Error Prone}, -\href{https://github.com/uber/NullAway}{NullAway}, and other contexts. +programming language. This is an industrial-strength framework. +The Dataflow Framework is used in the +\href{https://eisop.github.io/}{Checker Framework}, +Google's \href{http://errorprone.info/}{Error Prone}, +Uber's \href{https://github.com/uber/NullAway}{NullAway}, +Meta's \href{https://engineering.fb.com/2022/11/22/developer-tools/meta-java-nullsafe/}{Nullsafe}, +and in 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 properties for +each variable, that are true for every value the 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 -analysis over the control flow graph. The effect of a single node on +blocks made up of nodes representing single operations. An analysis +operates 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 -analysis reaches a fix point, the result can be accessed by client +analysis reaches a fixed point, the result can be accessed by client code. +The \emph{easiest way} to create a dataflow analysis for Java is to use the +Checker Framework. The Checker Framework performs type checking along with +flow-sensitive type inference, which is equivalent to a dataflow analysis or +an abstract interpretation. In the Checker Framework, the abstract values to be computed are -annotated types. An individual checkers can customize its analysis by +annotated types. You would define a type hierarchy corresponding to the +lattice for your dataflow analysis. +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. +This approach requires you to write very little code. The downside is +that it will run relatively slowly because of the Checker Framework's rich +functionality. If you do not need that functionality, you can write a +faster analysis directly on the Dataflow Framework. Some users first +create an analysis using the Checker Framework, and then if it is not fast +enough, they rewrite it for the Dataflow Framework. + +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 +% missing parts. Eventually, these paragraphs will be removed. +% \end{workinprogress} -\begin{workinprogress} - Paragraphs colored in gray with a gray bar on the left side (just - like this one) contain questions, additional comments or indicate - missing parts. Eventually, these paragraphs will be removed. -\end{workinprogress} +\subsection{Potential version conflicts if you export the Dataflow Framework} +If your tool uses the Dataflow Framework, then \textbf{please shade the + Dataflow Framework in your tool}. You can do this using the Maven Shade +Plugin, \url{https://maven.apache.org/plugins/maven-shade-plugin/}, or the +Gradle Shadow Plugin, \url{https://imperceptiblethoughts.com/shadow/}. +If you cannot shade the Checker Framework and Dataflow Framework classes, then +please ask the Checker Framework developers to create a shaded artifact +just for you, every time they release the Checker Framework. +See \code{dataflow/build.gradle} for instructions. +The reason to shade the Dataflow Framework is to permit users to run multiple tools. +Suppose that two tools both utilize the Dataflow Framework. +If the two tools use different versions of the Dataflow Framework, +then it may be impossible for users to run both tools due to a version conflict. +The Checker Framework uses an unshaded version the Dataflow Framework, +since the Dataflow Framework is part of the Checker Framework. +If you do not shade the Dataflow Framework in your tool, then users will +not be able to use both your tool and the Checker Framework (or any other +tool that does not shade the Checker Framework and Dataflow Framework classes). \section{Organization} @@ -62,9 +102,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 +125,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 +147,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 +168,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; @@ -172,7 +209,7 @@ \subsubsection{Blocks} \end{verbatim} A ConditionalBlock contains no operations at all. It represents a -control-flow split to either a `then' or an `else' successor based on +control-flow split to either a ``then'' or an ``else'' successor based on the immediately preceding boolean-valued Node. \begin{verbatim} @@ -227,22 +264,22 @@ \subsubsubsection{CFGVisualizeLauncher} class CFGVisualizeLauncher \end{verbatim} -\subsubsection{FlowExpressions} +\subsubsection{JavaExpressions} \label{sec:flow_expressions_class} The Dataflow Framework records the abstract values of certain -expressions, called FlowExpressions: local variables, field accesses, +expressions, called JavaExpressions: local variables, field accesses, array accesses, references to \code{this}, and pure method calls. -FlowExpressions are keys in the store of abstract values. +JavaExpressions are keys in the store of abstract values. \begin{verbatim} package org.checkerframework.dataflow.analysis; -class FlowExpressions +class JavaExpressions \end{verbatim} Java expressions that appear in method pre- and postconditions are -parsed into FlowExpressions using helper routines in -\code{org.checkerframework.framework.util.FlowExpressionParseUtil}. +parsed into JavaExpressions using helper routines in +\code{org.checkerframework.framework.util.JavaExpressionParseUtil}. \subsubsection{AbstractValue} @@ -273,9 +310,9 @@ \subsubsection{AbstractValue} For the Nullness Checker, abstract values additionally track the meaning of PolyNull, which may be either Nullable or NonNull. The meaning of PolyNull can change when a PolyNull value is compared to -the null literal, which is specific to the NullnessChecker. Other +the null literal, which is specific to the Nullness Checker. Other checkers often also support a Poly* qualifier, but only the -NullnessChecker tracks the meaning of its poly qualifier using the +Nullness Checker tracks the meaning of its poly qualifier using the dataflow analysis. \begin{verbatim} @@ -283,12 +320,32 @@ \subsubsection{AbstractValue} class NullnessValue extends CFAbstractValue \end{verbatim} +\subsubsection{UnusedAbstractValue} +\label{sec:unused_abstract_value_class} + +UnusedAbstractValue is an AbstractValue that is +not used during dataflow analysis. This class should +only be used as a type argument in transfer functions, for +analyses that do not need and do not have their own AbstractValue classes. + +For example, LiveVariable analysis (\autoref{sec:live_variable}), +needs to compute the least upper bound of successors' stores, and +it is meaningless to further calculate the least upper bound of two +individual live variables at a smaller granularity. +Since there is no computation between two AbstractValues, LiveVariable +analysis uses the UnusedAbstractValue rather than +implementing a specific AbstractValue for this analysis. + +\begin{verbatim} +package org.checkerframework.dataflow.analysis; +public final class UnusedAbstractValue implements AbstractValue +\end{verbatim} \subsubsection{Store} \label{sec:store_classes} A Store is a set of dataflow facts computed by an analysis, so it is a -mapping from FlowExpressions to AbstractValues. As with +mapping from JavaExpressions to AbstractValues. As with AbstractValues, one can take the least upper bound of two Stores. \begin{verbatim} @@ -307,7 +364,7 @@ \subsubsection{Store} class CFStore extends CFAbstractStore \end{verbatim} -An InitializationStore tracks which fields of the `self' reference +An InitializationStore tracks which fields of the ``self'' reference have been initialized. \begin{verbatim} @@ -338,7 +395,7 @@ \subsubsubsection{TransferInput} The TransferInput represents the set of dataflow facts known to be true immediately before the node to be analyzed. A TransferInput may -contain a single store, or a pair of `then' and `else' stores when +contain a single store, or a pair of ``then'' and ``else'' stores when following a boolean-valued expression. \begin{verbatim} @@ -353,7 +410,7 @@ \subsubsubsection{TransferResult} A TransferResult is the output of a transfer function. In other words, it is the set of dataflow facts known to be true immediately after a node. A Boolean-valued expression produces a -ConditionalTransferResult that contains both a `then' and an `else' +ConditionalTransferResult that contains both a ``then'' and an ``else'' store, while most other Nodes produce a RegularTransferResult with a single store. @@ -404,7 +461,7 @@ \subsubsubsection{TransferFunction} \end{verbatim} The Initialization Checker's transfer function tracks which fields of -the `self' reference have been initialized. +the ``self'' reference have been initialized. \begin{verbatim} package org.checkerframework.checker.initialization; @@ -415,7 +472,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 +590,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. @@ -599,7 +650,7 @@ \subsection{Formal Definition of the Control-Flow Graph} \item \textbf{Regular basic block.} A \emph{regular basic block} contains any non-empty sequence of nodes and has exactly one successor. None of the nodes in the block can - throw an exception at runtime. + throw an exception at run time. \item \textbf{Special basic blocks.} A \emph{special basic block} contains the empty sequence of nodes (i.e., is empty) @@ -610,29 +661,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 + exception at run time (e.g., a method call). There are zero + 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 +699,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 +748,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 @@ -732,11 +773,14 @@ \subsection{Formal Definition of the Control-Flow Graph} to see what expressions are summed up and because we don't create internal names for expression results. -\autoref{tab:nodes} lists all node types in the framework. We use the -Java class name of the implementation, but leave out the suffix -\code{Node}, which is present for every type. +\autoref{tab:nodes} lists all node types in the framework. All classes are in package \code{org.checkerframework.dataflow.cfg.node}. +% Defining a command for node types makes it easier to determine all the +% node types that the manual discusses. One can compare this list with the +% source code to ensure that every node type is listed. +\newcommand{\nodetype}[1]{\code{#1}} + \begin{longtable}{lp{0.4\linewidth}l} \midrule \multicolumn{3}{c}{\autoref{tab:nodes}: All node types in the Dataflow Framework.} \\ \\ @@ -746,194 +790,180 @@ \subsection{Formal Definition of the Control-Flow Graph} \hline \multicolumn{3}{|c|}{{Continued on next page}} \\ \hline \endfoot \endlastfoot - \code{Node} & The base class of all nodes. & \\ + \nodetype{Node} & The base class of all nodes & \\ \midrule - \code{ValueLiteral} & The base class of literal value nodes. & \\ - \code{BooleanLiteral} & & \code{true} \\ - \code{CharacterLiteral} & & \code{'c'} \\ - \code{DoubleLiteral} & & \code{3.14159} \\ - \code{FloatLiteral} & & \code{1.414f} \\ - \code{IntegerLiteral} & & \code{42} \\ - \code{LongLiteral} & & \code{1024L} \\ - \code{NullLiteral} & & \code{null} \\ - \code{ShortLiteral} & & \code{512} \\ - \code{StringLiteral} & & \code{"memo"} \\ + \nodetype{ValueLiteralNode} & The base class of literal value nodes & \\ + \nodetype{BooleanLiteralNode} & & \code{true} \\ + \nodetype{CharacterLiteralNode} & & \code{'c'} \\ + \nodetype{DoubleLiteralNode} & & \code{3.14159} \\ + \nodetype{FloatLiteralNode} & & \code{1.414f} \\ + \nodetype{IntegerLiteralNode} & & \code{42} \\ + \nodetype{LongLiteralNode} & & \code{1024L} \\ + \nodetype{NullLiteralNode} & & \code{null} \\ + \nodetype{ShortLiteralNode} & & \code{512} \\ + \nodetype{StringLiteralNode} & & \code{"memo"} \\ \midrule & Accessor expressions & \\ - \code{ArrayAccess} & & \code{args[i]} \\ - \code{FieldAccess} & & \code{f}, \code{obj.f} \\ - \code{MethodAccess} & & \code{obj.hashCode} \\ - \code{ThisLiteral} & Base class of references to \code{this} & \\ - \code{ExplicitThisLiteral} & Explicit use of \code{this} in an expression & \\ - \code{ImplicitThisLiteral} & Implicit use of \code{this} in an expression & \\ - \code{Super} & Explicit use of \code{super} in expression. & \code{super(x, y)} \\ - \code{LocalVariable} & Use of a local variable, either as l-value or r-value & \\ + \nodetype{ArrayAccessNode} & & \code{args[i]} \\ + \nodetype{FieldAccessNode} & & \code{f}, \code{obj.f} \\ + \nodetype{MethodAccessNode} & & \code{obj.hashCode} \\ + \nodetype{LocalVariableNode} & Use of a local variable, either as l-value or r-value & \\ + \nodetype{ThisNode} & Base class of references to \code{this} & \\ + \nodetype{ExplicitThisNode} & Explicit use of \code{this} in an expression & \\ + \nodetype{ImplicitThisNode} & Implicit use of \code{this} in an expression & \\ + \nodetype{SuperNode} & Explicit use of \code{super} in expression & \code{super(x, y)} \\ \midrule - \code{MethodInvocation} & Note that access and invocation are distinct. & \code{hashCode()} \\ + \nodetype{MethodInvocationNode} & Note that access and invocation are distinct & \code{hashCode()} \\ \midrule - & Arithmetic and logical operations. & \\ - \code{BitwiseAnd} & & a \& \code{b} \\ - \code{BitwiseComplement} & & \verb|~b| \\ - \code{BitwiseOr} & & \code{a | b} \\ - \code{BitwiseXor} & & \code{a ^ b} \\ - \code{ConditionalAnd} & Short-circuiting. & a \&\& \code{b} \\ - \code{ConditionalNot} & & \code{!a} \\ - \code{ConditionalOr} & Short-circuiting. & \code{a || b} \\ - \code{FloatingDivision} & & \code{1.0 / 2.0} \\ - \code{FloatingRemainder} & & \code{13.0 \% 4.0} \\ - \code{LeftShift} & & \code{x << 3} \\ - \code{IntegerDivision} & & \code{3 / 2} \\ - \code{IntegerRemainder} & & \code{13 \% 4} \\ - \code{NumericalAddition} & & \code{x + y} \\ - \code{NumericalMinus} & & \code{-x} \\ - \code{NumericalMultiplication} & & \code{x * y} \\ - \code{NumericalPlus} & & \code{+x} \\ - \code{NumericalSubtraction} & & \code{x - y} \\ - \code{SignedRightShift} & & \code{x >> 3} \\ - \code{StringConcatenate} & & \code{s + ".txt"} \\ - \code{TernaryExpression} & & \code{c ? t : f} \\ - \code{UnsignedRightShift} & & \code{x >>> 5} \\ + & Expression supertypes & \\ + \nodetype{BinaryOperationNode} & Base class of binary expression nodes & \\ + \nodetype{UnaryOperationNode} & Base class of unary expression nodes & \\ + + & Arithmetic and logical operations & \\ + \nodetype{BitwiseAndNode} & & a \& \code{b} \\ + \nodetype{BitwiseComplementNode} & & \verb|~b| \\ + \nodetype{BitwiseOrNode} & & \code{a | b} \\ + \nodetype{BitwiseXorNode} & & \code{a ^ b} \\ + \nodetype{ConditionalAndNode} & Short-circuiting & a \&\& \code{b} \\ + \nodetype{ConditionalNotNode} & & \code{!a} \\ + \nodetype{ConditionalOrNode} & Short-circuiting & \code{a || b} \\ + \nodetype{FloatingDivisionNode} & & \code{1.0 / 2.0} \\ + \nodetype{FloatingRemainderNode} & & \code{13.0 \% 4.0} \\ + \nodetype{LeftShiftNode} & & \code{x << 3} \\ + \nodetype{IntegerDivisionNode} & & \code{3 / 2} \\ + \nodetype{IntegerRemainderNode} & & \code{13 \% 4} \\ + \nodetype{NumericalAdditionNode} & & \code{x + y} \\ + \nodetype{NumericalMinusNode} & & \code{-x} \\ + \nodetype{NumericalMultiplicationNode} & & \code{x * y} \\ + \nodetype{NumericalPlusNode} & & \code{+x} \\ + \nodetype{NumericalSubtractionNode} & & \code{x - y} \\ + \nodetype{SignedRightShiftNode} & & \code{x >> 3} \\ + \nodetype{StringConcatenateNode} & & \code{s + ".txt"} \\ + \nodetype{SwitchExpressionNode} & & \\ + \nodetype{TernaryExpressionNode} & & \code{c ? t : f} \\ + \nodetype{UnsignedRightShiftNode} & & \code{x >>> 5} \\ \midrule & Relational operations & \\ - \code{EqualTo} & & \code{x == y} \\ - \code{NotEqual} & & \code{x != y} \\ - \code{GreaterThan} & & \code{x > y} \\ - \code{GreaterThanOrEqual} & & \code{x >= y} \\ - \code{LessThan} & & \code{x < y} \\ - \code{LessThanOrEqual} & & \code{x <= y} \\ - - \code{Case} & Case of a switch. Acts as an equality test. & \\ + \nodetype{EqualToNode} & & \code{x == y} \\ + \nodetype{NotEqualNode} & & \code{x != y} \\ + \nodetype{GreaterThanNode} & & \code{x > y} \\ + \nodetype{GreaterThanOrEqualNode} & & \code{x >= y} \\ + \nodetype{LessThanNode} & & \code{x < y} \\ + \nodetype{LessThanOrEqualNode} & & \code{x <= y} \\ + \midrule + + \code{SwitchExpression} & A switch expression (not statement!) & \\ + + \nodetype{CaseNode} & Case of a switch. Acts as an equality test. & \\ \midrule - \code{Assignment} & & \code{x = 1} \\ -% \midrule + \nodetype{AssignmentNode} & & \code{x = 1} \\ + \midrule - \code{StringConcatenateAssignment} & A compound assignment. & \code{s += ".txt"} \\ + \nodetype{ArrayCreationNode} & & \code{new double[]} \\ + \nodetype{ObjectCreationNode} & & \code{new Object()} \\ \midrule - \code{ArrayCreation} & & \code{new double[]} \\ - \code{ObjectCreation} & & \code{new Object()} \\ + \nodetype{FunctionalInterfaceNode} & A member reference or lambda & \\ \midrule - \code{TypeCast} & & \code{(float) 42} \\ - \code{InstanceOf} & & \code{x instanceof Float} \\ + \nodetype{TypeCastNode} & & \code{(float) 42} \\ + \nodetype{InstanceOfNode} & & \code{x instanceof Float} \\ \midrule - & Conversion nodes. & \\ - \code{NarrowingConversion} & Implicit conversion. & \\ - \code{StringConversion} & Might be implicit. & \code{obj.toString()} \\ - \code{WideningConversion} & Implicit conversion. & \\ + & Conversion nodes & \\ + \nodetype{NarrowingConversionNode} & Implicit conversion & \\ + \nodetype{StringConversionNode} & Might be implicit & \code{obj.toString()} \\ + \nodetype{WideningConversionNode} & Implicit conversion & \\ \midrule \midrule & \textbf{Non-value nodes} & \\ & Types appearing in expressions, such as \code{MyType.class} & \\ - \code{ArrayType} & & \\ - \code{ParameterizedType} & & \\ - \code{PrimitiveType} & & \\ + \nodetype{ArrayTypeNode} & & \\ + \nodetype{ParameterizedTypeNode} & & \\ + \nodetype{PrimitiveTypeNode} & & \\ \midrule - \code{ClassName} & Identifier referring to Java class or interface. & \code{java.util.HashMap} \\ - \code{PackageName} & Identifier referring to Java package. & \code{java.util} \\ + \nodetype{ClassNameNode} & Identifier referring to Java class or interface & \code{java.util.HashMap} \\ + \nodetype{PackageNameNode} & Identifier referring to Java package & \code{java.util} \\ \midrule - \code{Throw} & Throw an exception. & \\ - \code{Return} & Return from a method. & \\ + \nodetype{ThrowNode} & Throw an exception & \\ + \nodetype{ReturnNode} & Return from a method & \\ \midrule - \code{AssertionError} & & \code{assert x != null : "Hey"} \\ + \nodetype{AssertionErrorNode} & & \code{assert x != null : "Hey"} \\ \midrule - \code{Marker} & No-op nodes used to annotate a CFG with + \nodetype{ExpressionStatementNode} & An expression that is used as a statement & \code{m();} \\ + \midrule + + \nodetype{SynchronizedNode} & Start or end of a synchronized code block & \\ + \nodetype{MarkerNode} & No-op node used to annotate a CFG with information of the underlying Java source code. Mostly useful for debugging and visualization. An example is indicating the - start/end of switch statements. & \\ \midrule - - \code{NullChk} & Null checks inserted by javac & \\ + start/end of switch statements. & \\ \midrule - \code{VariableDeclaration} & Declaration of a local variable & \\ + \nodetype{NullChkNode} & Null checks inserted by javac & \\ \midrule - \caption{All node types in the Dataflow Framework.} - \label{tab:nodes} - \end{longtable} - -\autoref{tab:nodesWithException} shows all node types which 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}. - -Considering \code{Error} such as \code{OutOfMemoryError} or \code{NoSuchFieldError}, -an exceptional edge of \code{Error} is not created except for method or -constructor invocation, assertion statement or throw statement. Because -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} + \nodetype{VariableDeclarationNode} & Declaration of a local variable & \\ + \nodetype{ClassDeclarationNode} & Declaration of a class & \\ \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 - \code{ArrayAccess} & \code{NullPointerException}, \code{ArrayIndexOutOfBoundsException} \\ - \code{FieldAccess} & \code{NullPointerException} \\ - \code{MethodAccess} & \code{NullPointerException} \\ - \code{MethodInvocation} & \code{Throwable}, types in throws clause of the signature \\ - \code{IntegerDivision} & \code{ArithmeticException} \\ - \code{IntegerRemainder} & \code{ArithmeticException} \\ - \code{ObjectCreation} & \code{Throwable}, types in throws clause of the signature \\ - \code{TypeCast} & \code{ClassCastException} \\ - \code{Throw} & Type of \code{e} when \code{throw e} \\ - \code{AssertionError} & \code{AssertionError} \\ + \nodetype{LambdaResultExpressionNode} & Body of a single-expression lambda & \\ \midrule - \caption{All node types could throw Exception and types to be thrown.} - \label{tab:nodesWithException} + \caption{All node types in the Dataflow Framework.} + \label{tab:nodes} \end{longtable} -\begin{workinprogress} -ThisLiteral shouldn't be considered a literal value node, because a use of -\code{this} is not a literal expression. I would change the name and group -it with accessor expressions. -\end{workinprogress} - -\begin{workinprogress} -Can \code{StringConversion} be implicit? I think so, but in any event discuss. -\end{workinprogress} -\begin{workinprogress} -For each non-expression, explain its purpose, just like the explanation for -Marker that still needs to be fleshed out. -\end{workinprogress} +In theory, nearly any statement can throw an \code{Error} such as +\code{OutOfMemoryError} or \code{NoSuchFieldError}. The Dataflow Framework +does not represent all that possible flow. It only creates the exceptional +edges shown in \autoref{tab:nodesWithException}. + +\begin{table} + \begin{tabular}{ll} + \hline + \textbf{Node type} & \textbf{Exception type} \\ \hline + + \code{ArrayAccessNode} & \code{NullPointerException}, \code{ArrayIndexOutOfBoundsException} \\ + \code{FieldAccessNode} & \code{NullPointerException} \\ + \code{MethodAccessNode} & \code{NullPointerException} \\ + \code{MethodInvocationNode} & \code{Throwable}, types in throws clause of the signature \\ + \code{IntegerDivisionNode} & \code{ArithmeticException} \\ + \code{IntegerRemainderNode} & \code{ArithmeticException} \\ + \code{ObjectCreationNode} & \code{Throwable}, types in throws clause of the signature \\ + \code{ArrayCreationNode} & \code{NegativeArraySizeException}, \code{OutOfMemoryError} \\ + \code{TypeCastNode} & \code{ClassCastException} \\ + \code{ThrowNode} & Type of \code{e} when \code{throw e} \\ + \code{AssertionErrorNode} & \code{AssertionError} \\ + \code{ClassNameNode} & \code{ClassCircularityError}, \code{ClassFormatError}, \\ + & \code{NoClassDefFoundError}, \code{OutOfMemoryError} \\ + \hline + \end{tabular} + + \caption{All node types that could throw Exception, and the types + to be thrown. + All exception types are in package \code{java.lang}.} + \label{tab:nodesWithException} +\end{table} -\begin{workinprogress} -``Could be desugared'' on ``StringConcatenateAssignment'' and ``Conversion -nodes'' was confusing. What is the design rationale for -desugaring in the Dataflow Framework? Discuss that. Here, at least -forward-reference to \autoref{sec:desugaring}, if that's relevant. \\ -More generally, for any cases that will be discussed in the text, add a -forward reference to the section with the discussion. -\end{workinprogress} +Other AST constructs are desugared into the above nodes (see \autoref{sec:desugaring}). -\begin{workinprogress} -There is a \code{StringConcatenateAssignmentNode}. -What about other compound assignments? Are they desugared? -\end{workinprogress} - -\begin{workinprogress} -When we added Java~8 support, did we add additional nodes that are not -listed here? Cross-check with implementation. -\end{workinprogress} +% \begin{workinprogress} +% Can \code{StringConversionNode} be implicit? I think so, but in any event discuss. +% \end{workinprogress} \begin{workinprogress} Why is it \code{AssertionErrorNode} instead of \code{AssertNode}? @@ -951,16 +981,16 @@ \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 -`false' successor and executes the first statement after the loop. +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. \subsubsection{Assignment} @@ -970,35 +1000,36 @@ \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 and in conditional operator expressions. 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. @@ -1011,6 +1042,9 @@ \subsubsection{Conditional stores} just means taking the least upper bound of the two stores and it happens automatically by calling \code{TransferInput.getRegularStore}. +Assignments usually merge conditional stores. For the translation of +conditional operator expressions, a special non-merging assignment +node is used (see \autoref{sec:cond-exp} below). \subsubsection{Branches} @@ -1023,7 +1057,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 +1070,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 +1085,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,10 +1109,24 @@ \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.} +Conditional operator expressions (using the ternary conditional operator +\code{? :}, cf. \jlsref{15.25}) use an additional synthetic local +variable in the CFG. The second (then) and third (else) expressions are +assigned to this local variable. In contrast to other assignments, this +assignment does not merge the stores, to allow the propagation of +conditional stores from these sub-expressions. +The \code{TernaryExpressionNode} at the merge of the two branches can +then either be treated like a local variable read or return a +conditional store. + +\flow{CFGConditionalOpExpr}{.33}{.33}{Example of a conditional operator + expression (\code{? :}).} + + \subsubsection{Implicit \code{this} access} The Java compiler AST uses the same type (\code{IdentifierTree}) for @@ -1086,12 +1134,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 +1148,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,49 +1168,52 @@ \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} + In Java, varargs in method or constructor invocation is compiled as new array creation (cf.\ \jlsref{15.12.4.2}). For example, \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 -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 +Note that the expression of a switch statement must be executed only +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. A \code{THEN_TO_BOTH} +edge goes to nodes in the case and a \code{ELSE_TO_BOTH} edge goes to the next +\code{CaseNode}. When the next case is the 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} -is created and each of case nodes creates the branches. +to the first node in the next case is created after nodes in the case. +For example, see \autoref{fig:CFGSwitch}. The fake variable named \code{switch#num0} +is created and each case node creates the branches. + +\flow{CFGSwitch}{.21}{1.45}{Example of a switch statement with case, default and fall through.} -\flow{Switch}{Example of a switch statement with case, default and fall through.} +\subsubsection{Switch expressions} +\label{sec:switch-expr} + +A switch expression is translated similarly to a switch statement. +An additional fake variable \code{switchExpr} is used to store the value +from the cases. The \code{SwitchExpressionNode} itself can then be +handled like a read of that variable. + +% TODO: add an example \subsubsection{Handling \code{finally} blocks} \label{sec:try-finally} @@ -1196,15 +1240,10 @@ \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. + node might throw an exception at run time. \item \textbf{Unconditional jump.} An unconditional jump indicates that control flow proceeds non-sequentially to a location indicated by a target label. @@ -1232,15 +1271,6 @@ \subsection{AST to CFG Translation} nodes. It is used only temporarily during CFG construction. \end{definition} -% I agree, we should keep this in mind in the future. -%\begin{workinprogress} -%I find it useful to never name phases ``one'', ``two'', ``three'', but -%always to give them English names. This makes the meaning clearer to -%readers --- often because it forces the writer to think harder about the -%meaning of each. It can still be useful to number the phases even after -%they have more mnemonic names. -%\end{workinprogress} - The process of translating an AST to a CFG proceeds in three distinct phases. \begin{enumerate} @@ -1293,8 +1323,8 @@ \subsection{AST to CFG Translation} \begin{itemize} \item Regular basic blocks might be empty. \item Some conditional basic blocks might be unnecessary, in that - they have the same target for both the `then' as well as the - `else' branch. + they have the same target for both the ``then'' as well as the + ``else'' branch. \item Two consecutive, non-empty, regular basic blocks can exist, even if the second block has only exactly one predecessor and the two blocks could thus be merged. @@ -1344,6 +1374,13 @@ \subsubsection{Desugaring} all compound assignments greatly reduced the total number of node classes. + \item String concatenation assignments are also desugared. A string + concatenation assignment like + \begin{verbatim}String s; s += "str";\end{verbatim} is represented as + \begin{verbatim}String s; s = s + "str";\end{verbatim} in CFGs. + This avoids duplicating the same logic that is necessary in assignment + and concatenation nodes, which was error prone. + \end{itemize} In order to desugar code and still maintain the invariant that every @@ -1390,14 +1427,10 @@ \subsubsection{Conversions and node-tree mapping} apply to the conversions with no special work on the part of a checker developer. -Widening and narrowing conversions are still represented as special +Widening and narrowing conversions are represented as special node types, although it would be more consistent to change them into type casts. -\begin{workinprogress} -Is the last point a to-do item? -\end{workinprogress} - We maintain the invariant that a CFG node maps to zero or one AST tree and almost all of them map to a single tree. But we can't maintain a unique inverse mapping because some trees have both pre- and @@ -1417,11 +1450,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 @@ -1430,51 +1459,19 @@ \subsection{Overview} algorithm is used to compute a fix-point, by iteratively applying a set of transfer functions to the nodes in the CFG\@. These transfer 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. - - +approximate the run-time behavior of different statements and expressions. \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 +every node and flow expression\footnote{Certain dataflow analyses 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 +1480,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 +1523,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 +1557,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 +1581,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 +1659,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 +1671,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 +1689,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,15 +1728,16 @@ \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} +\label{sec:live_variable} A live variable analysis for local variables and fields was implemented to show the backward analysis works properly. The main class is @@ -1762,24 +1757,72 @@ \section{Example: Live Variable} \textbf{The transfer function.} The transfer function \code{LiveVarTransfer} initializes empty stores at normal and exceptional exit blocks (because this is a backward transfer function). The transfer function visits assignments to -update the information of live variables for each node in the stores. +update the live variable values 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{Example: Very Busy Expression Analysis} + +An expression is ``very busy'' if no matter what path is taken, +the expression is used before any variables occurring in it are redefined. +This analysis is a backward analysis that intersects abstract states from successors. +\\ +The main class is \code{org.checkerframework.dataflow.cfg.playground.BusyExpressionPlayground}. + +\textbf{Abstract values.} The \code{BusyExprValue} is a very busy expression, represented by a node. +The node can be any \code{BinaryOperationNode}, such as \code{NumericalAdditionNode} +or \code{LeftShiftNode}. + +\textbf{The store.} The busy expression store \code{BusyExprStore} has a field +\code{busyExprValueSet} which contains a set of \code{BusyExprValue}s. +If a node is a \code{BinaryOperationNode}, \code{addUseInExpression(Node)} +will recursively analyze the subexpressions of the node to determine +if they are nested \code{BinaryOperationNode}s. It then uses \code{putBusyExpr(BusyExprValue)} +to put the expression in the set. \code{killBusyExpr(Node)} is used when a variable is re-assigned. + +\textbf{The transfer function.} The transfer function \code{BusyExprTransfer} +initializes empty stores at normal and exceptional exit blocks (because this +is a backward transfer function). The transfer function visits assignment, method invocation, +and return statement nodes to update the busy expression values in the stores. + +\textbf{Example.} An example is shown in \autoref{fig:BusyExprSimple}. + +\flow{BusyExprSimple}{.33}{1}{Very Busy Expression Analysis illustration. Intermediate analysis results are shown.} + +\section{Example: Reaching Definition Analysis} + +The reaching definitions for a given program point are those assignments that +may have updated the current values of variables. +This reaching definition analysis is a standard example of a forward analysis. + +\textbf{Abstract values.} A class \code{ReachingDefinitionNode} is used as an +abstract value, which can only wrap \code{AssignmentNode}. The reaching definition analysis +processes such values in the store. + +\textbf{The store.} The reaching definition store \code{ReachingDefinitionStore} has a field +\code{reachingDefSet} which contains a set of \code{ReachingDefinitionNode}s. The store defines methods +\code{putDef(ReachingDefinitionValue)} and \code{killDef(Node)} to add +and kill reaching definitions. + +\textbf{The transfer function.} The transfer function visits \code{AssignmentNode} to +update the information of reaching definitions in the stores. A reaching definition will +be killed in the store when its left-hand side is same as that of the new generated value, and the new +generated value will be added into the store. + +\textbf{Example.} An example is shown in \autoref{fig:ReachSimple}. + +\flow{ReachSimple}{.33}{1}{Simple sequential program to illustrate reaching definitions. 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 @@ -1793,9 +1836,9 @@ \subsection{Overview} left or right-hand side to the true (resp. false) successor. It also applies type qualifiers from method postconditions after calls. -\begin{workinprogress} -Preconditions are not mentioned at all in this manual. How are they handled? -\end{workinprogress} +% \begin{workinprogress} +% Preconditions are not mentioned at all in this manual. How are they handled? +% \end{workinprogress} \subsection{Interaction of the Checker Framework and the Dataflow Analysis} @@ -1812,24 +1855,17 @@ \subsection{Interaction of the Checker Framework and the Dataflow Analysis} and set the constructor parameter \code{useFlow} to true will automatically run dataflow analysis and use the result of the analysis when creating an \code{AnnotatedTypeMirror}. The first time that a \code{GenericAnnotatedTypeFactory} -instance visits a \code{ClassTree}, the type factory runs the dataflow analysis on all the field initializers of the class first, then the bodies of methods in +instance visits a \code{ClassTree}, the type factory runs the dataflow analysis +on all the field initializers of the class first, then the bodies of methods in the class, and then finally the dataflow analysis is -ran recursively on the members of nested classes. The result of -dataflow analysis are stored in the \code{GenericAnnotatedTypeFactory} instance. +run recursively on the members of nested classes. The result of +dataflow analysis is stored in the \code{GenericAnnotatedTypeFactory} instance. When creating an \code{AnnotatedTypeMirror} for a tree node, the type factory queries the result of the dataflow analysis to determine if a more refined type for the tree node was inferred by the analysis. This is the first type of query described in \autoref{sec:answering-questions}. -\begin{workinprogress} -I found the below section confusing. I had a hard time putting my finger -on it, but perhaps you could re-read the section. As one minor issue, -``very similar'': similar to what? ``intermediary nodes'': what are -those? ``we'': is that the analysis writer or the framework implementor -(or the runtime system)? -\end{workinprogress} - Dataflow itself uses the type factory to get the initial \code{AnnotatedTypeMirror} for a tree node in the following way. @@ -1901,11 +1937,15 @@ \subsection{The Checker Framework Store and Dealing with Aliasing} Section~\ref{sec:field-access}) to abstract values, and the subsequent sections describe the operations that keep this mapping up-to-date. -\begin{workinprogress} - There hasn't been a good introduction of pure vs. non-pure methods. - Maybe this is a good location for a discussion? - Introduce the purity annotations somewhere. -\end{workinprogress} +A side-effect-free method has no visible side-effects, such +as setting a field of an object that existed before the method was called. +A deterministic method returns the same value (according to ==) every time it is called with the same arguments and in the same environment. +A pure method is both side-effect-free and deterministic. +The Dataflow Framework respects the +\href{https://eisop.github.io/cf/api/org/checkerframework/dataflow/qual/SideEffectFree.html}{\code{@SideEffectFree}}, +\href{https://eisop.github.io/cf/api/org/checkerframework/dataflow/qual/Deterministic.html}{\code{@Deterministic}},and +\href{https://eisop.github.io/cf/api/org/checkerframework/dataflow/qual/Pure.html}{\code{@Pure}} +annotations of the Checker Framework \subsubsection{Internal Representation of field access} @@ -1953,22 +1993,18 @@ \subsubsubsection{At Field Assignments} dataflow analysis first determines the abstract value $e_\text{val}$ for $e$. Then, it updates the store $S$ as follows. \begin{enumerate} - \item For every field access $o_2.f_2$ that is a key in $S$, remove its - information if $f_1 = f_2$ and + \item For every field access $o_2.f_1$ that is a key in $S$, remove its + information if $o_2$ lexically contains a \nonterminal{Receiver} that \emph{might} - alias $o_1.f$ as determined by the $\alias$ + alias $o_1.f_1$ as determined by the $\alias$ predicate. Note that the ``lexically contains'' notion for pure method calls includes both the receiver as well as the arguments. This includes the case where $o_1 = o_2$ (that is, they are - syntactically the same) and the case where $o_2$ and $o_1.f$ might be + syntactically the same) and the case where $o_2$ and $o_1.f_1$ might be aliases. - \begin{workinprogress} - Should the two occurrences of field $f$ in this paragraph be $f_1$? - \end{workinprogress} - \item $S[o_1.f_1] = e_\text{val}$ \end{enumerate} @@ -2021,53 +2057,29 @@ \subsubsubsection{Alias Information} and $<:$ denotes standard Java subtyping. -%%% Local Variables: -%%% mode: latex -%%% TeX-master: "dataflow" -%%% TeX-command-default: "PDF" -%%% End: - -% LocalWords: pre cfg javacutils InternalUtils DivideByZero BlockImpl se -% LocalWords: SingleSuccessorBlock RegularBlock SingleSuccessorBlockImpl -% LocalWords: ExceptionBlock SpecialBlock ConditionalBlock SpecialBlocks -% LocalWords: ControlFlowGraph ControlFlowGraphs CFGBuilder ast CFValue -% LocalWords: FlowExpressions AbstractValue AbstractValues PolyNull Poly -% LocalWords: AnnotatedTypeMirrors CFAbstractValue NullnessValue CFStore -% LocalWords: NullnessCheckers CFAbstractStore InitializationStore Regex -% LocalWords: NullnessStore TransferInput TransferResult TransferInputs -% LocalWords: ConditionalTransferResult RegularTransferResult CFTransfer -% LocalWords: TransferFunction NodeVisitor TransferResults isRegex lst -% LocalWords: CFAbstractTransfer AbstractNodeVisitor asRegex ClassTree -% LocalWords: InitializationTransfer visitMethodInvocation RegexTransfer -% LocalWords: CFAbstractAnalysis NullnessTransfer RegexChecker's lp Uber -% LocalWords: AnalysisResult AnnotatedTypeFactory AnnotatedTypeFactorys -% LocalWords: AnnotationProvider getAnnotatedType BaseTypeChecker -% LocalWords: GenericAnnotatedTypeFactory FlowAnalysis CFAnalysis -% LocalWords: BaseAnnotatedTypeFactory CFGSimple ValueLiteral txt -% LocalWords: BooleanLiteral CharacterLiteral DoubleLiteral FloatLiteral -% LocalWords: IntegerLiteral LongLiteral NullLiteral ShortLiteral args -% LocalWords: StringLiteral Accessor ArrayAccess FieldAccess ThisLiteral -% LocalWords: MethodAccess ExplicitThisLiteral ImplicitThisLiteral java -% LocalWords: LocalVariable MethodInvocation BitwiseAnd BitwiseOr MyType -% LocalWords: BitwiseComplement BitwiseXor ConditionalAnd ConditionalNot -% LocalWords: ConditionalOr FloatingDivision FloatingRemainder LeftShift -% LocalWords: IntegerDivision IntegerRemainder NumericalAddition EqualTo -% LocalWords: NumericalMinus NumericalMultiplication NumericalPlus util -% LocalWords: NumericalSubtraction SignedRightShift StringConcatenate -% LocalWords: TernaryExpression UnsignedRightShift NotEqual GreaterThan -% LocalWords: GreaterThanOrEqual LessThan LessThanOrEqual ArrayCreation -% LocalWords: StringConcatenateAssignment ObjectCreation TypeCast cond -% LocalWords: InstanceOf instanceof NarrowingConversion StringConversion -% LocalWords: WideningConversion ArrayType ParameterizedType ClassName -% LocalWords: PrimitiveType PackageName AssertionError NullChk accessor -% LocalWords: VariableDeclaration desugared desugaring FieldAssignment -% LocalWords: CFGFieldAssignment getRegularStore CFGConditionalOr fnc -% LocalWords: CFGConditionalOr2 ConditionalOrNode ConditionalOr2 valueOf -% LocalWords: IdentifierTree FieldAccessNode ImplicitThisNode unboxing -% LocalWords: TreePath pathHack MethodTree VarSymbol DetachedVarSymbols -% LocalWords: DeclarationFromElement BoxedClass treeLookup getValue -% LocalWords: nodeValues AdditionNode postconversion treelookup desugar -% LocalWords: LocalVariableNode AconcurrentSemantics MonotonicNonNull -% LocalWords: MonotonicQualifier IntegerLiteralNode EqualToNode -% LocalWords: ConstSimple SelfReference PureMethodCall javacutil -% LocalWords: stubparser TreeParser TreeBuilder DetachedVarSymbol LiveSimple +% LocalWords: pre cfg javacutils InternalUtils DivideByZero BlockImpl se SingleSuccessorBlock RegularBlock SingleSuccessorBlockImpl ExceptionBlock SpecialBlock ConditionalBlock SpecialBlocks +% LocalWords: ControlFlowGraph ControlFlowGraphs CFGBuilder ast CFValue JavaExpressions AbstractValue AbstractValues PolyNull Poly AnnotatedTypeMirrors CFAbstractValue NullnessValue CFStore +% LocalWords: NullnessCheckers CFAbstractStore InitializationStore Regex NullnessStore TransferInput TransferResult TransferInputs ConditionalTransferResult RegularTransferResult CFTransfer +% LocalWords: TransferFunction NodeVisitor TransferResults isRegex lst CFAbstractTransfer AbstractNodeVisitor asRegex ClassTree InitializationTransfer visitMethodInvocation RegexTransfer +% LocalWords: CFAbstractAnalysis NullnessTransfer RegexChecker's lp AnalysisResult AnnotatedTypeFactory AnnotatedTypeFactorys AnnotationProvider getAnnotatedType BaseTypeChecker +% LocalWords: GenericAnnotatedTypeFactory FlowAnalysis CFAnalysis BaseAnnotatedTypeFactory CFGSimple ValueLiteral txt BooleanLiteral CharacterLiteral DoubleLiteral FloatLiteral +% LocalWords: IntegerLiteral LongLiteral NullLiteral ShortLiteral args StringLiteral Accessor ArrayAccess FieldAccess This MethodAccess ExplicitThis ImplicitThis NullAway +% LocalWords: LocalVariable MethodInvocation BitwiseAnd BitwiseOr MyType BitwiseComplement BitwiseXor ConditionalAnd ConditionalNot ConditionalOr FloatingDivision FloatingRemainder LeftShift +% LocalWords: IntegerDivision IntegerRemainder NumericalAddition EqualTo NumericalMinus NumericalMultiplication NumericalPlus util NumericalSubtraction SignedRightShift StringConcatenate +% LocalWords: TernaryExpression UnsignedRightShift NotEqual GreaterThan GreaterThanOrEqual LessThan ArrayCreation ObjectCreation TypeCast cond +% LocalWords: InstanceOf NarrowingConversion StringConversion WideningConversion ArrayType ParameterizedType ClassName PrimitiveType PackageName AssertionError NullChk accessor +% LocalWords: VariableDeclaration FieldAssignment CFGFieldAssignment getRegularStore CFGConditionalOr fnc CFGConditionalOr2 ConditionalOrNode ConditionalOr2 valueOf +% LocalWords: IdentifierTree FieldAccessNode ImplicitThisNode unboxing TreePath pathHack MethodTree VarSymbol DetachedVarSymbols DeclarationFromElement BoxedClass treeLookup getValue +% LocalWords: nodeValues AdditionNode postconversion treelookup LocalVariableNode AconcurrentSemantics MonotonicNonNull MonotonicQualifier IntegerLiteralNode EqualToNode unshaded +% LocalWords: ConstSimple SelfReference PureMethodCall javacutil stubparser TreeParser TreeBuilder DetachedVarSymbol LiveSimple +% LocalWords: Uber's Nullsafe CFGVisualizeLauncher intra boolean subparts ValueLiteralNode c' lang BooleanLiteralNode CharacterLiteralNode DoubleLiteralNode FloatLiteralNode ThisNode +% LocalWords: LongLiteralNode NullLiteralNode ShortLiteralNode StringLiteralNode ArrayAccessNode MethodAccessNode ExplicitThisNode SuperNode MethodInvocationNode BinaryOperationNode +% LocalWords: UnaryOperationNode BitwiseAndNode BitwiseComplementNode BitwiseOrNode BitwiseXorNode ConditionalAndNode ConditionalNotNode FloatingDivisionNode FloatingRemainderNode b1 +% LocalWords: LeftShiftNode IntegerDivisionNode IntegerRemainderNode NumericalAdditionNode b2 b3 NumericalMinusNode NumericalMultiplicationNode NumericalPlusNode NotEqualNode num0 +% LocalWords: NumericalSubtractionNode SignedRightShiftNode StringConcatenateNode GreaterThanNode SwitchExpressionNode TernaryExpressionNode UnsignedRightShiftNode LessThanNode +% LocalWords: GreaterThanOrEqualNode LessThanOrEqualNode CaseNode AssignmentNode ArrayCreationNode StringConcatenateAssignmentNode ObjectCreationNode FunctionalInterfaceNode ThrowNode +% LocalWords: TypeCastNode InstanceOfNode instanceof NarrowingConversionNode StringConversionNode WideningConversionNode ArrayTypeNode ParameterizedTypeNode PrimitiveTypeNode Varargs +% LocalWords: ClassNameNode PackageNameNode ReturnNode AssertionErrorNode SynchronizedNode varargs MarkerNode NullChkNode VariableDeclarationNode ClassDeclarationNode NoSuchFieldError +% LocalWords: LambdaResultExpressionNode ArrayIndexOutOfBoundsException Throwable ClassFormatError ArithmeticException NegativeArraySizeException ClassCircularityError CFGIfStatement +% LocalWords: NoClassDefFoundError unaryAssignNodeLookup assertionsEnabled CFGAssert CFGSwitch +% LocalWords: intValue diff --git a/dataflow/manual/dataflow.tex b/dataflow/manual/dataflow.tex index eabb57691fe6..7c6def356b18 100644 --- a/dataflow/manual/dataflow.tex +++ b/dataflow/manual/dataflow.tex @@ -15,6 +15,8 @@ \usepackage{ucs} \usepackage{longtable} +\usepackage{relsize} + % At least 80% of every float page must be taken up by % floats; there will be no page with more than 20% white space. \def\topfraction{.95} @@ -50,13 +52,14 @@ numberstyle=\tiny, stepnumber=1, breaklines=true, + breakatwhitespace, frame=lines, showstringspaces=false, tabsize=2, commentstyle=\color{gray}, captionpos=b } -\newcommand{\code}[1]{\lstinline{#1}} +\newcommand{\code}[1]{{\smaller\lstinline{#1}}} \usepackage{fullpage} @@ -87,28 +90,29 @@ \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. +% TODO: turn into links, maybe by using section-subsection arguments. +% https://docs.oracle.com/javase/specs/jls/se17/html/index.html +% Maybe look in CF manual. \newcommand{\jlsref}[1]{JLS~\textsection{}#1} @@ -127,7 +131,7 @@ \setlength{\parskip}{.5em} \title{A Dataflow Framework for Java} -\author{\url{https://checkerframework.org/}} +\author{\url{https://eisop.github.io/}} \begin{document} diff --git a/dataflow/manual/examples/BusyExprSimple.java b/dataflow/manual/examples/BusyExprSimple.java new file mode 100644 index 000000000000..4d68e1d525af --- /dev/null +++ b/dataflow/manual/examples/BusyExprSimple.java @@ -0,0 +1,13 @@ +class Test { + public void test() { + int a = 2, b = 3; + if (a != b) { + int x = b - a; + int y = a - b; + } else { + int y = b - a; + a = 0; + int x = a - b; + } + } +} diff --git a/dataflow/manual/examples/CFGAssert.java b/dataflow/manual/examples/CFGAssert.java index 5f5050357a10..1bc3eb591357 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/CFGConditionalOpExpr.java b/dataflow/manual/examples/CFGConditionalOpExpr.java new file mode 100644 index 000000000000..64475e0bd09d --- /dev/null +++ b/dataflow/manual/examples/CFGConditionalOpExpr.java @@ -0,0 +1,6 @@ +class Test { + int test(boolean b) { + int x = b ? this.hashCode() : 5; + return x; + } +} diff --git a/dataflow/manual/examples/CFGConditionalOr.java b/dataflow/manual/examples/CFGConditionalOr.java index 896cf44bce72..03e263a416fa 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 93bcf908b407..dc9b2855074b 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 0bbecdddee1f..898e1882072d 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 db83f01ef58d..f4ffd99be409 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 387ba72ce615..2aedb61de84a 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 91f2d95141ca..ae8eea293ce9 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 144fe4cad35d..150dfd202815 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 891241c18fe4..74c1c2dfedbe 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; - } +class Test { + 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/README.txt b/dataflow/manual/examples/README.txt index b32d1de54d69..aa6dec429e78 100644 --- a/dataflow/manual/examples/README.txt +++ b/dataflow/manual/examples/README.txt @@ -1 +1,7 @@ -The graphs in ./graphs have been generated by running the Checker Framework on the Java files (using the -Aflowdotdir commmand-line option) and then running Graphviz on the output. +The graphs in './graphs' have been generated by running the Checker Framework on +the Java files (using the '-Aflowdotdir=dir' commmand-line option) and then +running Graphviz on the output (e.g. using 'dot -Tpdf -o file.pdf file.dot'). + +TODO: How to include only the CFG, without stores and values? +TODO: The macro that includes these PDFs uses a fixed scale. What is the +recommended PDF width? diff --git a/dataflow/manual/examples/ReachSimple.java b/dataflow/manual/examples/ReachSimple.java new file mode 100644 index 000000000000..cb349c355d90 --- /dev/null +++ b/dataflow/manual/examples/ReachSimple.java @@ -0,0 +1,12 @@ +class Test { + public void test() { + int a = 1, b = 2, c = 3; + if (a > 0) { + int d = a + c; + } else { + int e = a + b; + } + b = 0; + a = b; + } +} diff --git a/dataflow/manual/examples/graphs/BusyExprSimple.pdf b/dataflow/manual/examples/graphs/BusyExprSimple.pdf new file mode 100644 index 000000000000..5f30eb6eaabb Binary files /dev/null and b/dataflow/manual/examples/graphs/BusyExprSimple.pdf differ diff --git a/dataflow/manual/examples/graphs/CFGConditionalOpExpr.pdf b/dataflow/manual/examples/graphs/CFGConditionalOpExpr.pdf new file mode 100644 index 000000000000..98c0ca6187e6 Binary files /dev/null and b/dataflow/manual/examples/graphs/CFGConditionalOpExpr.pdf differ diff --git a/dataflow/manual/examples/graphs/LiveSimple.pdf b/dataflow/manual/examples/graphs/LiveSimple.pdf index 53f72a05274a..a1e44e54efc4 100644 Binary files a/dataflow/manual/examples/graphs/LiveSimple.pdf and b/dataflow/manual/examples/graphs/LiveSimple.pdf differ diff --git a/dataflow/manual/examples/graphs/ReachSimple.pdf b/dataflow/manual/examples/graphs/ReachSimple.pdf new file mode 100644 index 000000000000..d656a8e64d3d Binary files /dev/null and b/dataflow/manual/examples/graphs/ReachSimple.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 36ee3346a605..6dec6dc02e6e 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractAnalysis.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractAnalysis.java @@ -3,14 +3,9 @@ import com.sun.source.tree.ClassTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; -import java.util.Comparator; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.Objects; -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; @@ -22,9 +17,21 @@ import org.checkerframework.dataflow.cfg.node.AssignmentNode; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.ElementUtils; +import java.util.Comparator; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.PriorityQueue; +import java.util.Set; + +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; + /** * Implementation of common features for {@link BackwardAnalysisImpl} and {@link * ForwardAnalysisImpl}. @@ -46,7 +53,7 @@ public abstract class AbstractAnalysis< /** The transfer function for regular nodes. */ // TODO: make final. Currently, the transferFunction has a reference to the analysis, so it // can't be created until the Analysis is initialized. - protected @Nullable T transferFunction; + protected @MonotonicNonNull T transferFunction; /** The current control flow graph to perform the analysis on. */ protected @MonotonicNonNull ControlFlowGraph cfg; @@ -55,32 +62,34 @@ public abstract class AbstractAnalysis< * The transfer inputs of every basic block (assumed to be 'no information' if not present, * inputs before blocks in forward analysis, after blocks in backward analysis). */ - protected final IdentityHashMap> inputs; + protected final IdentityHashMap> inputs = new IdentityHashMap<>(); /** The worklist used for the fix-point iteration. */ protected final Worklist worklist; /** Abstract values of nodes. */ - protected final IdentityHashMap nodeValues; + protected final IdentityHashMap nodeValues = new IdentityHashMap<>(); /** Map from (effectively final) local variable elements to their abstract value. */ - protected final HashMap finalLocalValues; + protected final HashMap finalLocalValues = new HashMap<>(); /** * The node that is currently handled in the analysis (if it is running). The following * invariant holds: * *
    -     *   !isRunning ==> (currentNode == null)
    +     *   !isRunning ⇒ (currentNode == null)
          * 
    */ - protected @Nullable Node currentNode; + // currentNode == null when isRunning is true. + // See https://github.com/typetools/checker-framework/issues/4115 + 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 +109,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}. @@ -112,10 +130,7 @@ public void setCurrentTree(Tree currentTree) { */ protected AbstractAnalysis(Direction direction) { this.direction = direction; - this.inputs = new IdentityHashMap<>(); this.worklist = new Worklist(this.direction); - this.nodeValues = new IdentityHashMap<>(); - this.finalLocalValues = new HashMap<>(); } /** Initialize the transfer inputs of every basic block before performing the analysis. */ @@ -150,18 +165,19 @@ 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) { throw new BugInCF( - "AbstractAnalysis::getResult() shouldn't be called when the analysis is running."); + "AbstractAnalysis::getResult() shouldn't be called when the analysis is" + + " running."); } return new AnalysisResult<>( nodeValues, inputs, cfg.getTreeLookup(), - cfg.getUnaryAssignNodeLookup(), + cfg.getPostfixNodeLookup(), finalLocalValues); } @@ -179,12 +195,11 @@ public AnalysisResult getResult() { || (currentTree != null && currentTree == n.getTree())) { return null; } - // check that 'n' is a subnode of 'node'. Check immediate operands + // check that 'n' is a subnode of 'currentNode'. Check immediate operands // first for efficiency. assert !n.isLValue() : "Did not expect an lvalue, but got " + n; - if (currentNode == n - || (!currentNode.getOperands().contains(n) - && !currentNode.getTransitiveOperands().contains(n))) { + if (!currentNode.getOperands().contains(n) + && !currentNode.getTransitiveOperands().contains(n)) { return null; } // fall through when the current node is not 'n', and 'n' is not a subnode. @@ -213,7 +228,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 +240,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,25 +266,32 @@ 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) { + // Dataflow is analyzing the tree, so no value is available. + if (t == currentTree || cfg == null) { return null; } - Set nodesCorrespondingToTree = getNodesForTree(t); - if (nodesCorrespondingToTree == null) { + V result = getValue(getNodesForTree(t)); + if (result == null) { + result = getValue(cfg.getTreeLookup().get(t)); + } + return result; + } + + /** + * Returns the least upper bound of the values of {@code nodes}. + * + * @param nodes a set of nodes + * @return the least upper bound of the values of {@code nodes} + */ + private @Nullable V getValue(@Nullable Set nodes) { + if (nodes == null) { return null; } + V merged = null; - for (Node aNode : nodesCorrespondingToTree) { + for (Node aNode : nodes) { if (aNode.isLValue()) { return null; } @@ -280,6 +302,7 @@ public IdentityHashMap getNodeValues() { merged = merged.leastUpperBound(v); } } + return merged; } @@ -324,20 +347,21 @@ protected TransferResult callTransferFunction( assert transferFunction != null : "@AssumeAssertion(nullness): invariant"; if (node.isLValue()) { // TODO: should the default behavior return a regular transfer result, a conditional - // transfer result (depending on store.hasTwoStores()), or is the following correct? + // transfer result (depending on store.containsTwoStores()), or is the following + // correct? return new RegularTransferResult<>(null, transferInput.getRegularStore()); } transferInput.node = node; - currentNode = node; + setCurrentNode(node); 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; Node lhst = assignment.getTarget(); if (lhst instanceof LocalVariableNode) { LocalVariableNode lhs = (LocalVariableNode) lhst; - Element elem = lhs.getElement(); + VariableElement elem = lhs.getElement(); if (ElementUtils.isEffectivelyFinal(elem)) { V resval = transferResult.getResultValue(); if (resval != null) { @@ -360,8 +384,22 @@ protected final void init(ControlFlowGraph cfg) { } /** - * Initialize class fields based on a given control flow graph. Sub-class may override this - * method to initialize customized fields. + * Should exceptional control flow for a particular exception type be ignored? + * + *

    The default implementation always returns {@code false}. Subclasses should override the + * method to implement a different policy. + * + * @param exceptionType the exception type + * @return {@code true} if exceptional control flow due to {@code exceptionType} should be + * ignored, {@code false} otherwise + */ + protected boolean isIgnoredExceptionType(TypeMirror exceptionType) { + return false; + } + + /** + * Initialize fields of this object based on a given control flow graph. Sub-class may override + * this method to initialize customized fields. * * @param cfg a given control flow graph */ @@ -425,14 +463,14 @@ protected void addToWorklist(Block b) { protected static class Worklist { /** Map all blocks in the CFG to their depth-first order. */ - protected final IdentityHashMap depthFirstOrder; + protected final IdentityHashMap depthFirstOrder = new IdentityHashMap<>(); /** * Comparators to allow priority queue to order blocks by their depth-first order, using by * 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 +482,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); @@ -460,12 +498,10 @@ public int compare(Block b1, Block b2) { * @param direction the direction (forward or backward) */ public Worklist(Direction direction) { - depthFirstOrder = new IdentityHashMap<>(); - if (direction == Direction.FORWARD) { - queue = new PriorityQueue<>(11, new ForwardDFOComparator()); + queue = new PriorityQueue<>(new ForwardDFOComparator()); } else if (direction == Direction.BACKWARD) { - queue = new PriorityQueue<>(11, new BackwardDFOComparator()); + queue = new PriorityQueue<>(new BackwardDFOComparator()); } else { throw new BugInCF("Unexpected Direction meet: " + direction.name()); } @@ -492,6 +528,7 @@ public void process(ControlFlowGraph cfg) { * @see PriorityQueue#isEmpty * @return true if {@link #queue} is empty else false */ + @Pure @EnsuresNonNullIf(result = false, expression = "poll()") @SuppressWarnings("nullness:contracts.conditional.postcondition.not.satisfied") // forwarded public boolean isEmpty() { @@ -509,7 +546,8 @@ public boolean contains(Block block) { } /** - * Add the given block to {@link #queue}. + * Add the given block to {@link #queue}. Adds unconditionally: does not check containment + * first. * * @param block the block to add to {@link #queue} */ @@ -523,6 +561,7 @@ public void add(Block block) { * @see PriorityQueue#poll * @return the head of {@link #queue} */ + @Pure public @Nullable Block poll() { return queue.poll(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractValue.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractValue.java index 12f647e2b8c8..f226a8feb6ea 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractValue.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AbstractValue.java @@ -4,7 +4,7 @@ public interface AbstractValue> { /** - * Compute the least upper bound of two stores. + * Compute the least upper bound of two values. * *

    Important: This method must fulfill the following contract: * @@ -16,6 +16,9 @@ public interface AbstractValue> { * more permissive. *

  • Is commutative. * + * + * @param other the other value + * @return the least upper bound of the two values */ V leastUpperBound(V other); } 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 476c61a75cd8..9ec9af400ddc 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/Analysis.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/Analysis.java @@ -1,12 +1,15 @@ package org.checkerframework.dataflow.analysis; -import java.util.IdentityHashMap; -import java.util.Map; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.cfg.ControlFlowGraph; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.node.Node; +import java.util.IdentityHashMap; +import java.util.Map; + /** * This interface defines a dataflow analysis, given a control flow graph and a transfer function. A * dataflow analysis has a direction, either forward or backward. The direction of corresponding @@ -28,6 +31,17 @@ enum Direction { BACKWARD } + /** + * In calls to {@code Analysis#runAnalysisFor}, whether to return the store before or after the + * given node. + */ + enum BeforeOrAfter { + /** Return the pre-store. */ + BEFORE, + /** Return the post-store. */ + AFTER + } + /** * Get the direction of this analysis. * @@ -67,10 +81,9 @@ enum Direction { * the nodes. * * @param node the node to analyze - * @param before the boolean value to indicate which store to return (if it is true, return the - * store immediately before {@code node}; otherwise, the store after {@code node} is - * returned) - * @param transferInput the transfer input of the block of this node + * @param preOrPost which store to return: the store immediately before {@code node} or the + * store after {@code node} + * @param blockTransferInput the transfer input of the block of this node * @param nodeValues abstract values of nodes * @param analysisCaches caches of analysis results * @return the store before or after {@code node} (depends on the value of {@code before}) after @@ -78,10 +91,11 @@ enum Direction { */ S runAnalysisFor( Node node, - boolean before, - TransferInput transferInput, + Analysis.BeforeOrAfter preOrPost, + TransferInput blockTransferInput, IdentityHashMap nodeValues, - Map, IdentityHashMap>> analysisCaches); + @Nullable Map, IdentityHashMap>> + analysisCaches); /** * The result of running the analysis. This is only available once the analysis finished @@ -116,6 +130,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/AnalysisResult.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AnalysisResult.java index e809776792bd..c66379b2b013 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AnalysisResult.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/AnalysisResult.java @@ -1,20 +1,27 @@ package org.checkerframework.dataflow.analysis; +import com.sun.source.tree.BinaryTree; import com.sun.source.tree.Tree; import com.sun.source.tree.UnaryTree; + +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.TreeUtils; +import org.plumelib.util.UniqueId; +import org.plumelib.util.UnmodifiableIdentityHashMap; + import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; -import javax.lang.model.element.Element; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.cfg.block.Block; -import org.checkerframework.dataflow.cfg.block.ExceptionBlock; -import org.checkerframework.dataflow.cfg.block.RegularBlock; -import org.checkerframework.dataflow.cfg.node.AssignmentNode; -import org.checkerframework.dataflow.cfg.node.Node; -import org.checkerframework.javacutil.BugInCF; +import java.util.StringJoiner; +import java.util.concurrent.atomic.AtomicLong; + +import javax.lang.model.element.VariableElement; /** * An {@link AnalysisResult} represents the result of a org.checkerframework.dataflow analysis by @@ -24,19 +31,33 @@ * @param type of the abstract value that is tracked * @param the store type used in the analysis */ -public class AnalysisResult, S extends Store> { +public class AnalysisResult, S extends Store> implements UniqueId { + + /** + * For efficiency, certain maps stored in the result are only copied lazily, when they need to + * be mutated. This flag tracks if the copying has occurred. + */ + private boolean mapsCopied = false; /** Abstract values of nodes. */ - protected final IdentityHashMap nodeValues; + protected IdentityHashMap nodeValues; - /** Map from AST {@link Tree}s to sets of {@link Node}s. */ - protected final IdentityHashMap> treeLookup; + /** + * Map from AST {@link Tree}s to sets of {@link Node}s. + * + *

    Some of those Nodes might not be keys in {@link #nodeValues}. One reason is that the Node + * is unreachable in the control flow graph, so dataflow never gave it a value. + */ + protected IdentityHashMap> treeLookup; - /** Map from AST {@link UnaryTree}s to corresponding {@link AssignmentNode}s. */ - protected final IdentityHashMap unaryAssignNodeLookup; + /** + * Map from postfix increment or decrement trees that are AST {@link UnaryTree}s to the + * synthetic tree that is {@code v + 1} or {@code v - 1}. + */ + protected IdentityHashMap postfixLookup; /** Map from (effectively final) local variable elements to their abstract value. */ - protected final HashMap finalLocalValues; + protected final Map finalLocalValues; /** The stores before every method call. */ protected final IdentityHashMap> stores; @@ -44,31 +65,42 @@ public class AnalysisResult, S extends Store> { /** * Caches of the analysis results for each input for the block of the node and each node. * - * @see #runAnalysisFor(Node, boolean, TransferInput, IdentityHashMap, Map) + * @see #runAnalysisFor(Node, Analysis.BeforeOrAfter, TransferInput, IdentityHashMap, Map) */ protected final Map, IdentityHashMap>> analysisCaches; + /** The unique ID for the next-created object. */ + private static final AtomicLong nextUid = new AtomicLong(0); + + /** The unique ID of this object. */ + private final transient long uid = nextUid.getAndIncrement(); + + @Override + public long getUid(@UnknownInitialization AnalysisResult this) { + return uid; + } + /** * Initialize with given mappings. * * @param nodeValues {@link #nodeValues} * @param stores {@link #stores} * @param treeLookup {@link #treeLookup} - * @param unaryAssignNodeLookup {@link #unaryAssignNodeLookup} + * @param postfixLookup {@link #postfixLookup} * @param finalLocalValues {@link #finalLocalValues} * @param analysisCaches {@link #analysisCaches} */ protected AnalysisResult( - Map nodeValues, + IdentityHashMap nodeValues, IdentityHashMap> stores, IdentityHashMap> treeLookup, - IdentityHashMap unaryAssignNodeLookup, - HashMap finalLocalValues, + IdentityHashMap postfixLookup, + Map finalLocalValues, Map, IdentityHashMap>> analysisCaches) { - this.nodeValues = new IdentityHashMap<>(nodeValues); - this.treeLookup = new IdentityHashMap<>(treeLookup); - this.unaryAssignNodeLookup = new IdentityHashMap<>(unaryAssignNodeLookup); + this.nodeValues = UnmodifiableIdentityHashMap.wrap(nodeValues); + this.treeLookup = UnmodifiableIdentityHashMap.wrap(treeLookup); + this.postfixLookup = UnmodifiableIdentityHashMap.wrap(postfixLookup); // TODO: why are stores and finalLocalValues captured? this.stores = stores; this.finalLocalValues = finalLocalValues; @@ -81,20 +113,20 @@ protected AnalysisResult( * @param nodeValues {@link #nodeValues} * @param stores {@link #stores} * @param treeLookup {@link #treeLookup} - * @param unaryAssignNodeLookup {@link #unaryAssignNodeLookup} + * @param postfixLookup {@link #postfixLookup} * @param finalLocalValues {@link #finalLocalValues} */ public AnalysisResult( - Map nodeValues, + IdentityHashMap nodeValues, IdentityHashMap> stores, IdentityHashMap> treeLookup, - IdentityHashMap unaryAssignNodeLookup, - HashMap finalLocalValues) { + IdentityHashMap postfixLookup, + Map finalLocalValues) { this( nodeValues, stores, treeLookup, - unaryAssignNodeLookup, + postfixLookup, finalLocalValues, new IdentityHashMap<>()); } @@ -121,13 +153,24 @@ public AnalysisResult( * @param other an analysis result to combine with this */ public void combine(AnalysisResult other) { + copyMapsIfNeeded(); nodeValues.putAll(other.nodeValues); mergeTreeLookup(treeLookup, other.treeLookup); - unaryAssignNodeLookup.putAll(other.unaryAssignNodeLookup); + postfixLookup.putAll(other.postfixLookup); stores.putAll(other.stores); finalLocalValues.putAll(other.finalLocalValues); } + /** Make copies of certain internal IdentityHashMaps, if they have not been copied already. */ + private void copyMapsIfNeeded() { + if (!mapsCopied) { + nodeValues = new IdentityHashMap<>(nodeValues); + treeLookup = new IdentityHashMap<>(treeLookup); + postfixLookup = new IdentityHashMap<>(postfixLookup); + mapsCopied = true; + } + } + /** * Merge all entries from otherTreeLookup into treeLookup. Merge sets if already present. * @@ -152,7 +195,7 @@ private static void mergeTreeLookup( * * @return the value of effectively final local variables */ - public HashMap getFinalLocalValues() { + public Map getFinalLocalValues() { return finalLocalValues; } @@ -203,8 +246,8 @@ public HashMap getFinalLocalValues() { *

      *
    1. In a lambda expression such as {@code () -> 5} the {@code 5} is both an {@code * IntegerLiteralNode} and a {@code LambdaResultExpressionNode}. - *
    2. Narrowing and widening primitive conversions can result in {@code - * NarrowingConversionNode} and {@code WideningConversionNode}. + *
    3. Widening and narrowing primitive conversions can result in {@code + * WideningConversionNode} and {@code NarrowingConversionNode}. *
    4. Automatic String conversion can result in a {@code StringConversionNode}. *
    5. Trees for {@code finally} blocks are cloned to achieve a precise CFG. Any {@code Tree} * within a finally block can have multiple corresponding {@code Node}s attached to them. @@ -221,16 +264,18 @@ public HashMap getFinalLocalValues() { } /** - * Returns the corresponding {@link AssignmentNode} for a given {@link UnaryTree}. + * Returns the synthetic {@code v + 1} or {@code v - 1} corresponding to the postfix increment + * or decrement tree. * - * @param tree a unary tree - * @return the corresponding assignment node + * @param postfixTree a postfix increment or decrement tree + * @return the synthetic {@code v + 1} or {@code v - 1} corresponding to the postfix increment + * or decrement tree */ - public AssignmentNode getAssignForUnaryTree(UnaryTree tree) { - if (!unaryAssignNodeLookup.containsKey(tree)) { - throw new BugInCF(tree + " is not in unaryAssignNodeLookup"); + public BinaryTree getPostfixBinaryTree(UnaryTree postfixTree) { + if (!postfixLookup.containsKey(postfixTree)) { + throw new BugInCF(postfixTree + " is not in postfixLookup"); } - return unaryAssignNodeLookup.get(tree); + return postfixLookup.get(postfixTree); } /** @@ -263,7 +308,7 @@ public AssignmentNode getAssignForUnaryTree(UnaryTree tree) { * @return the store immediately before a given {@link Node} */ public @Nullable S getStoreBefore(Node node) { - return runAnalysisFor(node, true); + return runAnalysisFor(node, Analysis.BeforeOrAfter.BEFORE); } /** @@ -281,23 +326,19 @@ public S getStoreBefore(Block block) { case FORWARD: return transferInput.getRegularStore(); case BACKWARD: - Node firstNode; - switch (block.getType()) { - case REGULAR_BLOCK: - firstNode = ((RegularBlock) block).getContents().get(0); - break; - case EXCEPTION_BLOCK: - firstNode = ((ExceptionBlock) block).getNode(); - break; - default: - firstNode = null; - } - if (firstNode == null) { - // This block doesn't contains any node, return the store in the transfer input + List nodes = block.getNodes(); + if (nodes.isEmpty()) { + // This block doesn't contain any node, return the store in the transfer input. return transferInput.getRegularStore(); + } else { + Node firstNode = nodes.get(0); + return analysis.runAnalysisFor( + firstNode, + Analysis.BeforeOrAfter.BEFORE, + transferInput, + nodeValues, + analysisCaches); } - return analysis.runAnalysisFor( - firstNode, true, transferInput, nodeValues, analysisCaches); default: throw new BugInCF("Unknown direction: " + analysis.getDirection()); } @@ -316,13 +357,18 @@ public S getStoreAfter(Block block) { Analysis analysis = transferInput.analysis; switch (analysis.getDirection()) { case FORWARD: - Node lastNode = getLastNode(block); + Node lastNode = block.getLastNode(); if (lastNode == null) { - // This block doesn't contains any node, return the store in the transfer input + // This block doesn't contain any node, return the store in the transfer input. return transferInput.getRegularStore(); + } else { + return analysis.runAnalysisFor( + lastNode, + Analysis.BeforeOrAfter.AFTER, + transferInput, + nodeValues, + analysisCaches); } - return analysis.runAnalysisFor( - lastNode, false, transferInput, nodeValues, analysisCaches); case BACKWARD: return transferInput.getRegularStore(); default: @@ -330,27 +376,6 @@ public S getStoreAfter(Block block) { } } - /** - * Returns the last node of the given block, or {@code null} if none. - * - * @param block the block - * @return the last node of this block or {@code null} - */ - protected @Nullable Node getLastNode(Block block) { - switch (block.getType()) { - case REGULAR_BLOCK: - List blockContents = ((RegularBlock) block).getContents(); - return blockContents.get(blockContents.size() - 1); - case CONDITIONAL_BLOCK: - case SPECIAL_BLOCK: - return null; - case EXCEPTION_BLOCK: - return ((ExceptionBlock) block).getNode(); - default: - throw new BugInCF("Unrecognized block type: " + block.getType()); - } - } - /** * Returns the store immediately after a given {@link Tree}. * @@ -381,7 +406,7 @@ public S getStoreAfter(Block block) { * @return the store immediately after a given {@link Node} */ public @Nullable S getStoreAfter(Node node) { - return runAnalysisFor(node, false); + return runAnalysisFor(node, Analysis.BeforeOrAfter.AFTER); } /** @@ -393,20 +418,26 @@ public S getStoreAfter(Block block) { * is returned. * * @param node the node to analyze - * @param before the boolean value to indicate which store to return (if it is true, return the - * store immediately before {@code node}; otherwise, the store after {@code node} is - * returned) + * @param preOrPost which store to return: the store immediately before {@code node} or the + * store after {@code node} * @return the store before or after {@code node} (depends on the value of {@code before}) after * running the analysis */ - protected @Nullable S runAnalysisFor(Node node, boolean before) { + protected @Nullable S runAnalysisFor(Node node, Analysis.BeforeOrAfter preOrPost) { + // block is null if node is a formal parameter of a method, or is a field access thereof Block block = node.getBlock(); - assert block != null : "@AssumeAssertion(nullness): invariant"; + assert block != null : "@AssumeAssertion(nullness): null block for node " + node; TransferInput transferInput = stores.get(block); if (transferInput == null) { return null; } - return runAnalysisFor(node, before, transferInput, nodeValues, analysisCaches); + // Calling Analysis.runAnalysisFor() may mutate the internal nodeValues map inside an + // AbstractAnalysis object, and by default the AnalysisResult constructor just wraps this + // map without copying it. So here the AnalysisResult maps must be copied, to preserve + // them. + // TODO: Wouldn't it be safer to do at the beginning of the called method? + copyMapsIfNeeded(); + return runAnalysisFor(node, preOrPost, transferInput, nodeValues, analysisCaches); } /** @@ -422,9 +453,8 @@ public S getStoreAfter(Block block) { * @param the abstract value type to be tracked by the analysis * @param the store type used in the analysis * @param node the node to analyze - * @param before the boolean value to indicate which store to return (if it is true, return the - * store immediately before {@code node}; otherwise, the store after {@code node} is - * returned) + * @param preOrPost which store to return: the store immediately before {@code node} or the + * store after {@code node} * @param transferInput a transfer input * @param nodeValues {@link #nodeValues} * @param analysisCaches {@link #analysisCaches} @@ -433,14 +463,81 @@ public S getStoreAfter(Block block) { */ public static , S extends Store> S runAnalysisFor( Node node, - boolean before, + Analysis.BeforeOrAfter preOrPost, TransferInput transferInput, IdentityHashMap nodeValues, - Map, IdentityHashMap>> analysisCaches) { + @Nullable Map, IdentityHashMap>> + analysisCaches) { if (transferInput.analysis == null) { throw new BugInCF("Analysis in transferInput cannot be null."); } return transferInput.analysis.runAnalysisFor( - node, before, transferInput, nodeValues, analysisCaches); + node, preOrPost, transferInput, nodeValues, analysisCaches); + } + + /** + * Returns a verbose string representation of this, useful for debugging. + * + * @return a string representation of this + */ + public String toStringDebug() { + StringJoiner result = + new StringJoiner( + String.format("%n "), + String.format("AnalysisResult{%n "), + String.format("%n}")); + result.add("nodeValues = " + nodeValuesToString(nodeValues)); + result.add("treeLookup = " + treeLookupToString(treeLookup)); + result.add("postfixLookup = " + postfixLookup); + result.add("finalLocalValues = " + finalLocalValues); + result.add("stores = " + stores); + result.add("analysisCaches = " + analysisCaches); + return result.toString(); + } + + /** + * Returns a verbose string representation, useful for debugging. The map has the same type as + * the {@code nodeValues} field. + * + * @param the type of values in the map + * @param nodeValues a map to format + * @return a printed representation of the given map + */ + public static String nodeValuesToString(Map nodeValues) { + if (nodeValues.isEmpty()) { + return "{}"; + } + StringJoiner result = new StringJoiner(String.format("%n ")); + result.add("{"); + for (Map.Entry entry : nodeValues.entrySet()) { + Node key = entry.getKey(); + result.add(String.format("%s => %s", key.toStringDebug(), entry.getValue())); + } + result.add("}"); + return result.toString(); + } + + /** + * Returns a verbose string representation of a map, useful for debugging. The map has the same + * type as the {@code treeLookup} field. + * + * @param treeLookup a map to format + * @return a printed representation of the given map + */ + public static String treeLookupToString(Map> treeLookup) { + if (treeLookup.isEmpty()) { + return "{}"; + } + StringJoiner result = new StringJoiner(String.format("%n ")); + result.add("{"); + for (Map.Entry> entry : treeLookup.entrySet()) { + Tree key = entry.getKey(); + result.add( + TreeUtils.toStringTruncated(key, 65) + + " => " + + Node.nodeCollectionToString(entry.getValue())); + } + result.add("}"); + return result.toString(); } } 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 888b0a801d73..f6e632629775 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/BackwardAnalysisImpl.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/BackwardAnalysisImpl.java @@ -1,16 +1,12 @@ package org.checkerframework.dataflow.analysis; -import java.util.IdentityHashMap; -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; @@ -20,6 +16,14 @@ import org.checkerframework.dataflow.cfg.node.ReturnNode; import org.checkerframework.javacutil.BugInCF; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; + +import javax.lang.model.type.TypeMirror; + /** * An implementation of a backward analysis to solve a org.checkerframework.dataflow problem given a * control flow graph and a backward transfer function. @@ -37,39 +41,36 @@ public class BackwardAnalysisImpl< // TODO: Add widening support like what the forward analysis does. /** Out stores after every basic block (assumed to be 'no information' if not present). */ - protected final IdentityHashMap outStores; + protected final IdentityHashMap outStores = new IdentityHashMap<>(); /** * Exception store of an exception block, propagated by exceptional successors of its exception * block, and merged with the normal {@link TransferResult}. */ - protected final IdentityHashMap exceptionStores; + protected final IdentityHashMap exceptionStores = new IdentityHashMap<>(); /** The store right before the entry block. */ - protected @Nullable S storeAtEntry; + protected @Nullable S storeAtEntry = null; // `@code`, not `@link`, because dataflow module doesn't depend on framework module. /** * Construct an object that can perform a org.checkerframework.dataflow backward analysis over a - * control flow graph. The transfer function is set by the subclass, e.g., {@code - * org.checkerframework.framework.flow.CFAbstractAnalysis}, later. + * control flow graph. When using this constructor, the transfer function is set later by the + * subclass, e.g., {@code org.checkerframework.framework.flow.CFAbstractAnalysis}. */ public BackwardAnalysisImpl() { super(Direction.BACKWARD); - this.outStores = new IdentityHashMap<>(); - this.exceptionStores = new IdentityHashMap<>(); - this.storeAtEntry = null; } /** * Construct an object that can perform a org.checkerframework.dataflow backward analysis over a * control flow graph given a transfer function. * - * @param transfer the transfer function + * @param transferFunction the transfer function */ - public BackwardAnalysisImpl(@Nullable T transfer) { + public BackwardAnalysisImpl(T transferFunction) { this(); - this.transferFunction = transfer; + this.transferFunction = transferFunction; } @Override @@ -103,7 +104,7 @@ public void performAnalysisBlock(Block b) { currentInput = inputAfter.copy(); Node firstNode = null; boolean addToWorklistAgain = false; - List nodeList = rb.getContents(); + List nodeList = rb.getNodes(); ListIterator reverseIter = nodeList.listIterator(nodeList.size()); while (reverseIter.hasPrevious()) { Node node = reverseIter.previous(); @@ -115,7 +116,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 +144,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 +155,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; @@ -164,7 +165,7 @@ public void performAnalysisBlock(Block b) { // Special basic blocks are empty and cannot throw exceptions, // thus there is no need to perform any analysis. SpecialBlock sb = (SpecialBlock) b; - final SpecialBlockType sType = sb.getSpecialType(); + SpecialBlockType sType = sb.getSpecialType(); if (sType == SpecialBlockType.ENTRY) { // storage the store at entry storeAtEntry = outStores.get(sb); @@ -173,7 +174,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); } } @@ -212,7 +213,8 @@ protected void initInitialInputs() { if (worklist.depthFirstOrder.get(regularExitBlock) == null && worklist.depthFirstOrder.get(exceptionExitBlock) == null) { throw new BugInCF( - "regularExitBlock and exceptionExitBlock should never both be null at the same time."); + "regularExitBlock and exceptionExitBlock should never both be null at the same" + + " time."); } UnderlyingAST underlyingAST = cfg.getUnderlyingAST(); List returnNodes = cfg.getReturnNodes(); @@ -249,7 +251,8 @@ protected void propagateStoresTo( boolean addToWorklistAgain) { if (flowRule != FlowRule.EACH_TO_EACH) { throw new BugInCF( - "Backward analysis always propagates EACH to EACH, because there is no control flow."); + "Backward analysis always propagates EACH to EACH, because there is no control" + + " flow."); } addStoreAfter(pred, node, currentInput.getRegularStore(), addToWorklistAgain); @@ -268,25 +271,23 @@ protected void propagateStoresTo( protected void addStoreAfter(Block pred, @Nullable Node node, S s, boolean addBlockToWorklist) { // If the block pred is an exception block, decide whether the block of passing node is an // exceptional successor of the block pred - if (pred instanceof ExceptionBlock - && ((ExceptionBlock) pred).getSuccessor() != null - && node != null) { - @Nullable Block succBlock = ((ExceptionBlock) pred).getSuccessor(); - @Nullable Block block = node.getBlock(); - if (succBlock != null && block != null && succBlock.getId() == block.getId()) { - // If the block of passing node is an exceptional successor of Block pred, propagate - // store to the exceptionStores. Currently it doesn't track the label of an - // exceptional edge from exception block to its exceptional successors in backward - // direction. Instead, all exception stores of exceptional successors of an - // exception block will merge to one exception store at the exception block - ExceptionBlock ebPred = (ExceptionBlock) pred; - S exceptionStore = exceptionStores.get(ebPred); - S newExceptionStore = - (exceptionStore != null) ? exceptionStore.leastUpperBound(s) : s; - if (!newExceptionStore.equals(exceptionStore)) { - exceptionStores.put(ebPred, newExceptionStore); - addBlockToWorklist = true; - } + TypeMirror excSuccType = getSuccExceptionType(pred, node); + if (excSuccType != null) { + if (isIgnoredExceptionType(excSuccType)) { + return; + } + // If the block of passing node is an exceptional successor of Block pred, propagate + // store to the exceptionStores. Currently it doesn't track the label of an + // exceptional edge from exception block to its exceptional successors in backward + // direction. Instead, all exception stores of exceptional successors of an + // exception block will merge to one exception store at the exception block + ExceptionBlock ebPred = (ExceptionBlock) pred; + S exceptionStore = exceptionStores.get(ebPred); + S newExceptionStore = (exceptionStore != null) ? exceptionStore.leastUpperBound(s) : s; + if (!newExceptionStore.equals(exceptionStore)) { + exceptionStores.put(ebPred, newExceptionStore); + inputs.put(ebPred, new TransferInput(node, this, newExceptionStore)); + addBlockToWorklist = true; } } else { S predOutStore = getStoreAfter(pred); @@ -302,6 +303,36 @@ protected void addStoreAfter(Block pred, @Nullable Node node, S s, boolean addBl } } + /** + * Checks if the block for a node is an exceptional successor of a predecessor block, and if so, + * returns the exception type for the control-flow edge. + * + * @param pred the predecessor block + * @param node the successor node + * @return the exception type leading to a control flow edge from {@code pred} to the block for + * {@code node}, if it exists; {@code null} otherwise + */ + @SuppressWarnings("interning:not.interned") // Block equality + private @Nullable TypeMirror getSuccExceptionType(Block pred, @Nullable Node node) { + if (!(pred instanceof ExceptionBlock) || node == null) { + return null; + } + Block block = node.getBlock(); + if (block == null) { + return null; + } + Map> exceptionalSuccessors = + ((ExceptionBlock) pred).getExceptionalSuccessors(); + for (Map.Entry> excTypeEntry : exceptionalSuccessors.entrySet()) { + for (Block excSuccBlock : excTypeEntry.getValue()) { + if (excSuccBlock == block) { + return excTypeEntry.getKey(); + } + } + } + return null; + } + /** * Returns the store corresponding to the location right after the basic block {@code b}. * @@ -314,11 +345,12 @@ protected void addStoreAfter(Block pred, @Nullable Node node, S s, boolean addBl @Override public S runAnalysisFor( - Node node, - boolean before, - TransferInput transferInput, + @FindDistinct Node node, + Analysis.BeforeOrAfter preOrPost, + TransferInput blockTransferInput, IdentityHashMap nodeValues, - Map, IdentityHashMap>> analysisCaches) { + @Nullable Map, IdentityHashMap>> + analysisCaches) { Block block = node.getBlock(); assert block != null : "@AssumeAssertion(nullness): invariant"; Node oldCurrentNode = currentNode; @@ -334,16 +366,17 @@ public S runAnalysisFor( RegularBlock rBlock = (RegularBlock) block; // Apply transfer function to contents until we found the node we are // looking for. - TransferInput store = transferInput; - List nodeList = rBlock.getContents(); + TransferInput store = blockTransferInput; + List nodeList = rBlock.getNodes(); ListIterator reverseIter = nodeList.listIterator(nodeList.size()); while (reverseIter.hasPrevious()) { Node n = reverseIter.previous(); - currentNode = n; - if (n == node && !before) { + setCurrentNode(n); + if (n == node && preOrPost == Analysis.BeforeOrAfter.AFTER) { 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) { @@ -351,9 +384,7 @@ public S runAnalysisFor( } store = new TransferInput<>(n, this, transferResult); } - // This point should never be reached. If the block of 'node' is - // 'block', then 'node' must be part of the contents of 'block'. - throw new BugInCF("This point should never be reached."); + throw new BugInCF("node %s is not in node.getBlock()=%s", node, block); } case EXCEPTION_BLOCK: { @@ -365,12 +396,14 @@ public S runAnalysisFor( + "\teb.getNode(): " + eb.getNode()); } - if (!before) { - return transferInput.getRegularStore(); + if (preOrPost == Analysis.BeforeOrAfter.AFTER) { + return blockTransferInput.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, blockTransferInput.copy()); // Merge transfer result with the exception store of this exceptional block S exceptionStore = exceptionStores.get(eb); return exceptionStore == null @@ -383,7 +416,7 @@ public S runAnalysisFor( } } finally { - currentNode = oldCurrentNode; + setCurrentNode(oldCurrentNode); isRunning = false; } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/BackwardTransferFunction.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/BackwardTransferFunction.java index 118fcc016066..e1c3e0701725 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/BackwardTransferFunction.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/BackwardTransferFunction.java @@ -1,9 +1,10 @@ package org.checkerframework.dataflow.analysis; -import java.util.List; -import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.node.ReturnNode; +import org.checkerframework.dataflow.qual.SideEffectFree; + +import java.util.List; /** * Interface of a backward transfer function for the abstract interpretation used for the backward @@ -23,11 +24,12 @@ public interface BackwardTransferFunction, S extends * Returns the initial store that should be used at the normal exit block. * * @param underlyingAST the underlying AST of the given control flow graph - * @param returnNodes the return nodes of the given control flow graph if the underlying AST of - * this graph is a method. Otherwise will be set to {@code null} + * @param returnNodes the return nodes of the given control flow graph (an empty list if the + * underlying AST is not a method) * @return the initial store that should be used at the normal exit block */ - S initialNormalExitStore(UnderlyingAST underlyingAST, @Nullable List returnNodes); + @SideEffectFree + S initialNormalExitStore(UnderlyingAST underlyingAST, List returnNodes); /** * Returns the initial store that should be used at the exceptional exit block or given the @@ -36,5 +38,6 @@ public interface BackwardTransferFunction, S extends * @param underlyingAST the underlying AST of the given control flow graph * @return the initial store that should be used at the exceptional exit block */ + @SideEffectFree S initialExceptionalExitStore(UnderlyingAST underlyingAST); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ConditionalTransferResult.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ConditionalTransferResult.java index ffc60bfc769b..2cda1f9b1d85 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ConditionalTransferResult.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ConditionalTransferResult.java @@ -1,14 +1,19 @@ package org.checkerframework.dataflow.analysis; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.plumelib.util.StringsPlume; + import java.util.Map; import java.util.StringJoiner; + import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.Nullable; /** - * Implementation of a {@link TransferResult} with two non-exceptional store; one for the 'then' - * edge and one for 'else'. The result of {@code getRegularStore} will be the least upper bound of - * the two underlying stores. + * Implementation of a {@link TransferResult} with two non-exceptional stores. The 'then' store + * contains information valid when the previous boolean-valued expression was true, and the 'else' + * store contains information valid when the expression was false. + * + *

      {@link #getRegularStore} returns the least upper bound of the two underlying stores. * * @param type of the abstract value that is tracked * @param the store type used in the analysis @@ -65,7 +70,7 @@ public ConditionalTransferResult(@Nullable V value, S thenStore, S elseStore) { /** * Create a new {@link #ConditionalTransferResult(AbstractValue, Store, Store, Map, boolean)}, - * using {@code false} for {@link #storeChanged}. + * using {@code false} for the {@code storeChanged} formal parameter. * * @param value the abstract value produced by the transfer function * @param thenStore {@link #thenStore} @@ -74,7 +79,7 @@ public ConditionalTransferResult(@Nullable V value, S thenStore, S elseStore) { * @see #ConditionalTransferResult(AbstractValue, Store, Store, Map, boolean) */ public ConditionalTransferResult( - V value, S thenStore, S elseStore, Map exceptionalStores) { + V value, S thenStore, S elseStore, @Nullable Map exceptionalStores) { this(value, thenStore, elseStore, exceptionalStores, false); } @@ -83,9 +88,6 @@ public ConditionalTransferResult( * the corresponding {@link org.checkerframework.dataflow.cfg.node.Node} evaluates to {@code * true} and {@code elseStore} otherwise. * - *

      For the meaning of {@code storeChanged}, see {@link - * org.checkerframework.dataflow.analysis.TransferResult#storeChanged}. - * *

      Exceptions: If the corresponding {@link * org.checkerframework.dataflow.cfg.node.Node} throws an exception, then the corresponding * store in {@code exceptionalStores} is used. If no exception is found in {@code @@ -101,7 +103,8 @@ public ConditionalTransferResult( * @param thenStore {@link #thenStore} * @param elseStore {@link #elseStore} * @param exceptionalStores {@link #exceptionalStores} - * @param storeChanged {@link #storeChanged} + * @param storeChanged whether the store changed; see {@link + * org.checkerframework.dataflow.analysis.TransferResult#storeChanged}. */ public ConditionalTransferResult( @Nullable V value, @@ -140,18 +143,13 @@ public boolean containsTwoStores() { public String toString() { StringJoiner result = new StringJoiner(System.lineSeparator()); result.add("RegularTransferResult("); - result.add(" resultValue = " + resultValue); - result.add(" thenStore = " + thenStore); - result.add(" elseStore = " + elseStore); + result.add(" resultValue = " + StringsPlume.indentLinesExceptFirst(2, resultValue)); + result.add(" thenStore = " + StringsPlume.indentLinesExceptFirst(2, thenStore)); + result.add(" elseStore = " + StringsPlume.indentLinesExceptFirst(2, elseStore)); result.add(")"); return result.toString(); } - /** - * See {@link org.checkerframework.dataflow.analysis.TransferResult#storeChanged()}. - * - * @see org.checkerframework.dataflow.analysis.TransferResult#storeChanged() - */ @Override public boolean storeChanged() { return storeChanged; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/FlowExpressions.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/FlowExpressions.java deleted file mode 100644 index 6fcfc44abc2c..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/FlowExpressions.java +++ /dev/null @@ -1,1478 +0,0 @@ -package org.checkerframework.dataflow.analysis; - -import com.sun.source.tree.ArrayAccessTree; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.IdentifierTree; -import com.sun.source.tree.LiteralTree; -import com.sun.source.tree.MemberSelectTree; -import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.MethodTree; -import com.sun.source.tree.NewArrayTree; -import com.sun.source.tree.Tree.Kind; -import com.sun.source.tree.UnaryTree; -import com.sun.source.tree.VariableTree; -import com.sun.source.util.TreePath; -import com.sun.tools.javac.code.Symbol.VarSymbol; -import com.sun.tools.javac.tree.JCTree; -import com.sun.tools.javac.tree.Pretty; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.cfg.node.ArrayAccessNode; -import org.checkerframework.dataflow.cfg.node.ArrayCreationNode; -import org.checkerframework.dataflow.cfg.node.BinaryOperationNode; -import org.checkerframework.dataflow.cfg.node.ClassNameNode; -import org.checkerframework.dataflow.cfg.node.ExplicitThisLiteralNode; -import org.checkerframework.dataflow.cfg.node.FieldAccessNode; -import org.checkerframework.dataflow.cfg.node.LocalVariableNode; -import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; -import org.checkerframework.dataflow.cfg.node.NarrowingConversionNode; -import org.checkerframework.dataflow.cfg.node.Node; -import org.checkerframework.dataflow.cfg.node.StringConversionNode; -import org.checkerframework.dataflow.cfg.node.SuperNode; -import org.checkerframework.dataflow.cfg.node.ThisLiteralNode; -import org.checkerframework.dataflow.cfg.node.ValueLiteralNode; -import org.checkerframework.dataflow.cfg.node.WideningConversionNode; -import org.checkerframework.dataflow.util.PurityUtils; -import org.checkerframework.javacutil.AnnotationProvider; -import org.checkerframework.javacutil.BugInCF; -import org.checkerframework.javacutil.ElementUtils; -import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypeAnnotationUtils; -import org.checkerframework.javacutil.TypesUtils; - -/** - * Collection of classes and helper functions to represent Java expressions about which the - * org.checkerframework.dataflow analysis can possibly infer facts. Expressions include: - * - *

        - *
      • Field accesses (e.g., o.f) - *
      • Local variables (e.g., l) - *
      • This reference (e.g., this) - *
      • Pure method calls (e.g., o.m()) - *
      • Unknown other expressions to mark that something else was present. - *
      - */ -public class FlowExpressions { - - /** - * Returns the internal representation (as {@link FieldAccess}) of a {@link FieldAccessNode}. - * Can contain {@link Unknown} as receiver. - * - * @return the internal representation (as {@link FieldAccess}) of a {@link FieldAccessNode}. - * Can contain {@link Unknown} as receiver. - */ - public static FieldAccess internalReprOfFieldAccess( - AnnotationProvider provider, FieldAccessNode node) { - Receiver receiver; - Node receiverNode = node.getReceiver(); - if (node.isStatic()) { - receiver = new ClassName(receiverNode.getType()); - } else { - receiver = internalReprOf(provider, receiverNode); - } - return new FieldAccess(receiver, node); - } - - /** - * Returns the internal representation (as {@link FieldAccess}) of a {@link FieldAccessNode}. - * Can contain {@link Unknown} as receiver. - * - * @return the internal representation (as {@link FieldAccess}) of a {@link FieldAccessNode}. - * Can contain {@link Unknown} as receiver. - */ - public static ArrayAccess internalReprOfArrayAccess( - AnnotationProvider provider, ArrayAccessNode node) { - Receiver receiver = internalReprOf(provider, node.getArray()); - Receiver index = internalReprOf(provider, node.getIndex()); - return new ArrayAccess(node.getType(), receiver, index); - } - - /** - * We ignore operations such as widening and narrowing when computing the internal - * representation. - * - * @return the internal representation (as {@link Receiver}) of any {@link Node}. Might contain - * {@link Unknown}. - */ - public static Receiver internalReprOf(AnnotationProvider provider, Node receiverNode) { - return internalReprOf(provider, receiverNode, false); - } - - /** - * We ignore operations such as widening and narrowing when computing the internal - * representation. - * - * @return the internal representation (as {@link Receiver}) of any {@link Node}. Might contain - * {@link Unknown}. - */ - public static Receiver internalReprOf( - AnnotationProvider provider, Node receiverNode, boolean allowNonDeterministic) { - Receiver receiver = null; - if (receiverNode instanceof FieldAccessNode) { - FieldAccessNode fan = (FieldAccessNode) receiverNode; - - if (fan.getFieldName().equals("this")) { - // For some reason, "className.this" is considered a field access. - // We right this wrong here. - receiver = new ThisReference(fan.getReceiver().getType()); - } else if (fan.getFieldName().equals("class")) { - // "className.class" is considered a field access. This makes sense, - // since .class is similar to a field access which is the equivalent - // of a call to getClass(). However for the purposes of dataflow - // analysis, and value stores, this is the equivalent of a ClassNameNode. - receiver = new ClassName(fan.getReceiver().getType()); - } else { - receiver = internalReprOfFieldAccess(provider, fan); - } - } else if (receiverNode instanceof ExplicitThisLiteralNode) { - receiver = new ThisReference(receiverNode.getType()); - } else if (receiverNode instanceof ThisLiteralNode) { - receiver = new ThisReference(receiverNode.getType()); - } else if (receiverNode instanceof SuperNode) { - receiver = new ThisReference(receiverNode.getType()); - } else if (receiverNode instanceof LocalVariableNode) { - LocalVariableNode lv = (LocalVariableNode) receiverNode; - receiver = new LocalVariable(lv); - } else if (receiverNode instanceof ArrayAccessNode) { - ArrayAccessNode a = (ArrayAccessNode) receiverNode; - receiver = internalReprOfArrayAccess(provider, a); - } else if (receiverNode instanceof StringConversionNode) { - // ignore string conversion - return internalReprOf(provider, ((StringConversionNode) receiverNode).getOperand()); - } else if (receiverNode instanceof WideningConversionNode) { - // ignore widening - return internalReprOf(provider, ((WideningConversionNode) receiverNode).getOperand()); - } else if (receiverNode instanceof NarrowingConversionNode) { - // ignore narrowing - return internalReprOf(provider, ((NarrowingConversionNode) receiverNode).getOperand()); - } else if (receiverNode instanceof BinaryOperationNode) { - BinaryOperationNode bopn = (BinaryOperationNode) receiverNode; - return new BinaryOperation( - bopn, - internalReprOf(provider, bopn.getLeftOperand(), allowNonDeterministic), - internalReprOf(provider, bopn.getRightOperand(), allowNonDeterministic)); - } else if (receiverNode instanceof ClassNameNode) { - ClassNameNode cn = (ClassNameNode) receiverNode; - receiver = new ClassName(cn.getType()); - } else if (receiverNode instanceof ValueLiteralNode) { - ValueLiteralNode vn = (ValueLiteralNode) receiverNode; - receiver = new ValueLiteral(vn.getType(), vn); - } else if (receiverNode instanceof ArrayCreationNode) { - ArrayCreationNode an = (ArrayCreationNode) receiverNode; - List dimensions = new ArrayList<>(); - for (Node dimension : an.getDimensions()) { - dimensions.add(internalReprOf(provider, dimension, allowNonDeterministic)); - } - List initializers = new ArrayList<>(); - for (Node initializer : an.getInitializers()) { - initializers.add(internalReprOf(provider, initializer, allowNonDeterministic)); - } - receiver = new ArrayCreation(an.getType(), dimensions, initializers); - } else if (receiverNode instanceof MethodInvocationNode) { - MethodInvocationNode mn = (MethodInvocationNode) receiverNode; - MethodInvocationTree t = mn.getTree(); - if (t == null) { - throw new BugInCF("Unexpected null tree for node: " + mn); - } - assert TreeUtils.isUseOfElement(t) : "@AssumeAssertion(nullness): tree kind"; - ExecutableElement invokedMethod = TreeUtils.elementFromUse(t); - - if (allowNonDeterministic || PurityUtils.isDeterministic(provider, invokedMethod)) { - List parameters = new ArrayList<>(); - for (Node p : mn.getArguments()) { - parameters.add(internalReprOf(provider, p)); - } - Receiver methodReceiver; - if (ElementUtils.isStatic(invokedMethod)) { - methodReceiver = new ClassName(mn.getTarget().getReceiver().getType()); - } else { - methodReceiver = internalReprOf(provider, mn.getTarget().getReceiver()); - } - receiver = new MethodCall(mn.getType(), invokedMethod, methodReceiver, parameters); - } - } - - if (receiver == null) { - receiver = new Unknown(receiverNode.getType()); - } - return receiver; - } - - /** - * Returns the internal representation (as {@link Receiver}) of any {@link ExpressionTree}. - * Might contain {@link Unknown}. - * - * @return the internal representation (as {@link Receiver}) of any {@link ExpressionTree}. - * Might contain {@link Unknown}. - */ - public static Receiver internalReprOf( - AnnotationProvider provider, ExpressionTree receiverTree) { - return internalReprOf(provider, receiverTree, true); - } - /** - * We ignore operations such as widening and narrowing when computing the internal - * representation. - * - * @return the internal representation (as {@link Receiver}) of any {@link ExpressionTree}. - * Might contain {@link Unknown}. - */ - public static Receiver internalReprOf( - AnnotationProvider provider, - ExpressionTree receiverTree, - boolean allowNonDeterministic) { - Receiver receiver; - switch (receiverTree.getKind()) { - case ARRAY_ACCESS: - ArrayAccessTree a = (ArrayAccessTree) receiverTree; - Receiver arrayAccessExpression = internalReprOf(provider, a.getExpression()); - Receiver index = internalReprOf(provider, a.getIndex()); - receiver = new ArrayAccess(TreeUtils.typeOf(a), arrayAccessExpression, index); - break; - case BOOLEAN_LITERAL: - case CHAR_LITERAL: - case DOUBLE_LITERAL: - case FLOAT_LITERAL: - case INT_LITERAL: - case LONG_LITERAL: - case NULL_LITERAL: - case STRING_LITERAL: - LiteralTree vn = (LiteralTree) receiverTree; - receiver = new ValueLiteral(TreeUtils.typeOf(receiverTree), vn.getValue()); - break; - case NEW_ARRAY: - NewArrayTree newArrayTree = (NewArrayTree) receiverTree; - List dimensions = new ArrayList<>(); - if (newArrayTree.getDimensions() != null) { - for (ExpressionTree dimension : newArrayTree.getDimensions()) { - dimensions.add(internalReprOf(provider, dimension, allowNonDeterministic)); - } - } - List initializers = new ArrayList<>(); - if (newArrayTree.getInitializers() != null) { - for (ExpressionTree initializer : newArrayTree.getInitializers()) { - initializers.add( - internalReprOf(provider, initializer, allowNonDeterministic)); - } - } - - receiver = - new ArrayCreation(TreeUtils.typeOf(receiverTree), dimensions, initializers); - break; - case METHOD_INVOCATION: - MethodInvocationTree mn = (MethodInvocationTree) receiverTree; - assert TreeUtils.isUseOfElement(mn) : "@AssumeAssertion(nullness): tree kind"; - ExecutableElement invokedMethod = TreeUtils.elementFromUse(mn); - if (PurityUtils.isDeterministic(provider, invokedMethod) || allowNonDeterministic) { - List parameters = new ArrayList<>(); - for (ExpressionTree p : mn.getArguments()) { - parameters.add(internalReprOf(provider, p)); - } - Receiver methodReceiver; - if (ElementUtils.isStatic(invokedMethod)) { - methodReceiver = new ClassName(TreeUtils.typeOf(mn.getMethodSelect())); - } else { - ExpressionTree methodReceiverTree = TreeUtils.getReceiverTree(mn); - if (methodReceiverTree != null) { - methodReceiver = internalReprOf(provider, methodReceiverTree); - } else { - methodReceiver = internalReprOfImplicitReceiver(invokedMethod); - } - } - TypeMirror type = TreeUtils.typeOf(mn); - receiver = new MethodCall(type, invokedMethod, methodReceiver, parameters); - } else { - receiver = null; - } - break; - case MEMBER_SELECT: - receiver = internalReprOfMemberSelect(provider, (MemberSelectTree) receiverTree); - break; - case IDENTIFIER: - IdentifierTree identifierTree = (IdentifierTree) receiverTree; - TypeMirror typeOfId = TreeUtils.typeOf(identifierTree); - if (identifierTree.getName().contentEquals("this") - || identifierTree.getName().contentEquals("super")) { - receiver = new ThisReference(typeOfId); - break; - } - assert TreeUtils.isUseOfElement(identifierTree) - : "@AssumeAssertion(nullness): tree kind"; - Element ele = TreeUtils.elementFromUse(identifierTree); - if (ElementUtils.isClassElement(ele)) { - receiver = new ClassName(ele.asType()); - break; - } - switch (ele.getKind()) { - case LOCAL_VARIABLE: - case RESOURCE_VARIABLE: - case EXCEPTION_PARAMETER: - case PARAMETER: - receiver = new LocalVariable(ele); - break; - case FIELD: - // Implicit access expression, such as "this" or a class name - Receiver fieldAccessExpression; - @SuppressWarnings( - "nullness:dereference.of.nullable") // a field has enclosing class - TypeMirror enclosingType = ElementUtils.enclosingClass(ele).asType(); - if (ElementUtils.isStatic(ele)) { - fieldAccessExpression = new ClassName(enclosingType); - } else { - fieldAccessExpression = new ThisReference(enclosingType); - } - receiver = - new FieldAccess( - fieldAccessExpression, typeOfId, (VariableElement) ele); - break; - default: - receiver = null; - } - break; - case UNARY_PLUS: - return internalReprOf( - provider, - ((UnaryTree) receiverTree).getExpression(), - allowNonDeterministic); - default: - receiver = null; - } - - if (receiver == null) { - receiver = new Unknown(TreeUtils.typeOf(receiverTree)); - } - return receiver; - } - - /** - * Returns the implicit receiver of ele. - * - *

      Returns either a new ClassName or a new ThisReference depending on whether ele is static - * or not. The passed element must be a field, method, or class. - * - * @param ele field, method, or class - * @return either a new ClassName or a new ThisReference depending on whether ele is static or - * not - */ - public static Receiver internalReprOfImplicitReceiver(Element ele) { - TypeElement enclosingClass = ElementUtils.enclosingClass(ele); - if (enclosingClass == null) { - throw new BugInCF( - "internalReprOfImplicitReceiver's arg has no enclosing class: " + ele); - } - TypeMirror enclosingType = enclosingClass.asType(); - if (ElementUtils.isStatic(ele)) { - return new ClassName(enclosingType); - } else { - return new ThisReference(enclosingType); - } - } - - /** - * Returns either a new ClassName or ThisReference Receiver object for the enclosingType. - * - *

      The Tree should be an expression or a statement that does not have a receiver or an - * implicit receiver. For example, a local variable declaration. - * - * @param path TreePath to tree - * @param enclosingType type of the enclosing type - * @return a new ClassName or ThisReference that is a Receiver object for the enclosingType - */ - public static Receiver internalReprOfPseudoReceiver(TreePath path, TypeMirror enclosingType) { - if (TreeUtils.isTreeInStaticScope(path)) { - return new ClassName(enclosingType); - } else { - return new ThisReference(enclosingType); - } - } - - private static Receiver internalReprOfMemberSelect( - AnnotationProvider provider, MemberSelectTree memberSelectTree) { - TypeMirror expressionType = TreeUtils.typeOf(memberSelectTree.getExpression()); - if (TreeUtils.isClassLiteral(memberSelectTree)) { - return new ClassName(expressionType); - } - assert TreeUtils.isUseOfElement(memberSelectTree) : "@AssumeAssertion(nullness): tree kind"; - Element ele = TreeUtils.elementFromUse(memberSelectTree); - if (ElementUtils.isClassElement(ele)) { - // o instanceof MyClass.InnerClass - // o instanceof MyClass.InnerInterface - TypeMirror selectType = TreeUtils.typeOf(memberSelectTree); - return new ClassName(selectType); - } - switch (ele.getKind()) { - case METHOD: - case CONSTRUCTOR: - return internalReprOf(provider, memberSelectTree.getExpression()); - case ENUM_CONSTANT: - case FIELD: - TypeMirror fieldType = TreeUtils.typeOf(memberSelectTree); - Receiver r = internalReprOf(provider, memberSelectTree.getExpression()); - return new FieldAccess(r, fieldType, (VariableElement) ele); - default: - throw new BugInCF("Unexpected element kind: %s element: %s", ele.getKind(), ele); - } - } - - /** - * Returns Receiver objects for the formal parameters of the method in which path is enclosed. - * - * @param annotationProvider annotationProvider - * @param path TreePath that is enclosed by the method - * @return list of Receiver objects for the formal parameters of the method in which path is - * enclosed, {@code null} otherwise - */ - public static @Nullable List getParametersOfEnclosingMethod( - AnnotationProvider annotationProvider, TreePath path) { - MethodTree methodTree = TreeUtils.enclosingMethod(path); - if (methodTree == null) { - return null; - } - List internalArguments = new ArrayList<>(); - for (VariableTree arg : methodTree.getParameters()) { - internalArguments.add(internalReprOf(annotationProvider, new LocalVariableNode(arg))); - } - return internalArguments; - } - - /** - * The poorly-named Receiver class is actually a Java AST. Each subclass represents a different - * type of expression, such as MethodCall, ArrayAccess, LocalVariable, etc. - */ - public abstract static class Receiver { - /** The type of this expression. */ - protected final TypeMirror type; - - /** - * Create a Receiver (a Java AST node representing an expression). - * - * @param type the type of the expression - */ - protected Receiver(TypeMirror type) { - assert type != null; - this.type = type; - } - - public TypeMirror getType() { - return type; - } - - public abstract boolean containsOfClass(Class clazz); - - public boolean containsUnknown() { - return containsOfClass(Unknown.class); - } - - /** - * Returns true if and only if the value this expression stands for cannot be changed (with - * respect to ==) by a method call. This is the case for local variables, the self - * reference, final field accesses whose receiver is {@link #isUnassignableByOtherCode}, and - * binary operations whose left and right operands are both {@link - * #isUnmodifiableByOtherCode}. - * - * @see #isUnmodifiableByOtherCode - */ - public abstract boolean isUnassignableByOtherCode(); - - /** - * Returns true if and only if the value this expression stands for cannot be changed by a - * method call, including changes to any of its fields. - * - *

      Approximately, this returns true if the expression is {@link - * #isUnassignableByOtherCode} and its type is immutable. - * - * @see #isUnassignableByOtherCode - */ - public abstract boolean isUnmodifiableByOtherCode(); - - /** - * Returns true if and only if the two receiver are syntactically identical. - * - * @return true if and only if the two receiver are syntactically identical - */ - public boolean syntacticEquals(Receiver other) { - return other == this; - } - - /** - * Returns true if and only if this receiver contains a receiver that is syntactically equal - * to {@code other}. - * - * @return true if and only if this receiver contains a receiver that is syntactically equal - * to {@code other} - */ - public boolean containsSyntacticEqualReceiver(Receiver other) { - return syntacticEquals(other); - } - - /** - * Returns true if and only if {@code other} appears anywhere in this receiver or an - * expression appears in this receiver such that {@code other} might alias this expression, - * and that expression is modifiable. - * - *

      This is always true, except for cases where the Java type information prevents - * aliasing and none of the subexpressions can alias 'other'. - */ - public boolean containsModifiableAliasOf(Store store, Receiver other) { - return this.equals(other) || store.canAlias(this, other); - } - - /** - * Print this verbosely, for debugging. - * - * @return a verbose printed representation of this - */ - public String debugToString() { - return String.format( - "Receiver (%s) %s type=%s", getClass().getSimpleName(), toString(), type); - } - } - - public static class FieldAccess extends Receiver { - protected final Receiver receiver; - protected final VariableElement field; - - public Receiver getReceiver() { - return receiver; - } - - public VariableElement getField() { - return field; - } - - public FieldAccess(Receiver receiver, FieldAccessNode node) { - super(node.getType()); - this.receiver = receiver; - this.field = node.getElement(); - } - - public FieldAccess(Receiver receiver, TypeMirror type, VariableElement fieldElement) { - super(type); - this.receiver = receiver; - this.field = fieldElement; - } - - public boolean isFinal() { - return ElementUtils.isFinal(field); - } - - public boolean isStatic() { - return ElementUtils.isStatic(field); - } - - @Override - public boolean equals(@Nullable Object obj) { - if (!(obj instanceof FieldAccess)) { - return false; - } - FieldAccess fa = (FieldAccess) obj; - return fa.getField().equals(getField()) && fa.getReceiver().equals(getReceiver()); - } - - @Override - public int hashCode() { - return Objects.hash(getField(), getReceiver()); - } - - @Override - public boolean containsModifiableAliasOf(Store store, Receiver other) { - return super.containsModifiableAliasOf(store, other) - || receiver.containsModifiableAliasOf(store, other); - } - - @Override - public boolean containsSyntacticEqualReceiver(Receiver other) { - return syntacticEquals(other) || receiver.containsSyntacticEqualReceiver(other); - } - - @Override - public boolean syntacticEquals(Receiver other) { - if (!(other instanceof FieldAccess)) { - return false; - } - FieldAccess fa = (FieldAccess) other; - return super.syntacticEquals(other) - || (fa.getField().equals(getField()) - && fa.getReceiver().syntacticEquals(getReceiver())); - } - - @Override - public String toString() { - if (receiver instanceof ClassName) { - return receiver.getType() + "." + field; - } else { - return receiver + "." + field; - } - } - - @Override - public boolean containsOfClass(Class clazz) { - return getClass() == clazz || receiver.containsOfClass(clazz); - } - - @Override - public boolean isUnassignableByOtherCode() { - return isFinal() && getReceiver().isUnassignableByOtherCode(); - } - - @Override - public boolean isUnmodifiableByOtherCode() { - return isUnassignableByOtherCode() - && TypesUtils.isImmutableTypeInJdk(getReceiver().type); - } - } - - public static class ThisReference extends Receiver { - public ThisReference(TypeMirror type) { - super(type); - } - - @Override - public boolean equals(@Nullable Object obj) { - return obj instanceof ThisReference; - } - - @Override - public int hashCode() { - return 0; - } - - @Override - public String toString() { - return "this"; - } - - @Override - public boolean containsOfClass(Class clazz) { - return getClass() == clazz; - } - - @Override - public boolean syntacticEquals(Receiver other) { - return other instanceof ThisReference; - } - - @Override - public boolean isUnassignableByOtherCode() { - return true; - } - - @Override - public boolean isUnmodifiableByOtherCode() { - return TypesUtils.isImmutableTypeInJdk(type); - } - - @Override - public boolean containsModifiableAliasOf(Store store, Receiver other) { - return false; // 'this' is not modifiable - } - } - - /** - * A ClassName represents the occurrence of a class as part of a static field access or method - * invocation. - */ - public static class ClassName extends Receiver { - private final String typeString; - - public ClassName(TypeMirror type) { - super(type); - typeString = type.toString(); - } - - @Override - public boolean equals(@Nullable Object obj) { - if (!(obj instanceof ClassName)) { - return false; - } - ClassName other = (ClassName) obj; - return typeString.equals(other.typeString); - } - - @Override - public int hashCode() { - return Objects.hash(typeString); - } - - @Override - public String toString() { - return typeString + ".class"; - } - - @Override - public boolean containsOfClass(Class clazz) { - return getClass() == clazz; - } - - @Override - public boolean syntacticEquals(Receiver other) { - return this.equals(other); - } - - @Override - public boolean isUnassignableByOtherCode() { - return true; - } - - @Override - public boolean isUnmodifiableByOtherCode() { - return true; - } - - @Override - public boolean containsModifiableAliasOf(Store store, Receiver other) { - return false; // not modifiable - } - } - - public static class Unknown extends Receiver { - public Unknown(TypeMirror type) { - super(type); - } - - @Override - public boolean equals(@Nullable Object obj) { - return obj == this; - } - - @Override - public int hashCode() { - return System.identityHashCode(this); - } - - @Override - public String toString() { - return "?"; - } - - @Override - public boolean containsModifiableAliasOf(Store store, Receiver other) { - return true; - } - - @Override - public boolean containsOfClass(Class clazz) { - return getClass() == clazz; - } - - @Override - public boolean isUnassignableByOtherCode() { - return false; - } - - @Override - public boolean isUnmodifiableByOtherCode() { - return false; - } - } - - public static class LocalVariable extends Receiver { - protected final Element element; - - public LocalVariable(LocalVariableNode localVar) { - super(localVar.getType()); - this.element = localVar.getElement(); - } - - public LocalVariable(Element elem) { - super(ElementUtils.getType(elem)); - this.element = elem; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (!(obj instanceof LocalVariable)) { - return false; - } - - LocalVariable other = (LocalVariable) obj; - VarSymbol vs = (VarSymbol) element; - VarSymbol vsother = (VarSymbol) other.element; - // The code below isn't just return vs.equals(vsother) because an element might be - // different between subcheckers. The owner of a lambda parameter is the enclosing - // method, so a local variable and a lambda parameter might have the same name and the - // same owner. pos is used to differentiate this case. - return vs.pos == vsother.pos - && vsother.name.contentEquals(vs.name) - && vsother.owner.toString().equals(vs.owner.toString()); - } - - public Element getElement() { - return element; - } - - @Override - public int hashCode() { - VarSymbol vs = (VarSymbol) element; - return Objects.hash( - vs.name.toString(), - TypeAnnotationUtils.unannotatedType(vs.type).toString(), - vs.owner.toString()); - } - - @Override - public String toString() { - return element.toString(); - } - - @Override - public boolean containsOfClass(Class clazz) { - return getClass() == clazz; - } - - @Override - public boolean syntacticEquals(Receiver other) { - if (!(other instanceof LocalVariable)) { - return false; - } - LocalVariable l = (LocalVariable) other; - return l.equals(this); - } - - @Override - public boolean containsSyntacticEqualReceiver(Receiver other) { - return syntacticEquals(other); - } - - @Override - public boolean isUnassignableByOtherCode() { - return true; - } - - @Override - public boolean isUnmodifiableByOtherCode() { - return TypesUtils.isImmutableTypeInJdk(((VarSymbol) element).type); - } - } - - /** FlowExpression.Receiver for literals. */ - public static class ValueLiteral extends Receiver { - - /** The value of the literal. */ - protected final @Nullable Object value; - - /** - * Creates a ValueLiteral from the node with the given type. - * - * @param type type of the literal - * @param node the literal represents by this {@link ValueLiteral} - */ - public ValueLiteral(TypeMirror type, ValueLiteralNode node) { - super(type); - value = node.getValue(); - } - - /** - * Creates a ValueLiteral where the value is {@code value} that has the given type. - * - * @param type type of the literal - * @param value the literal value - */ - public ValueLiteral(TypeMirror type, Object value) { - super(type); - this.value = value; - } - - @Override - public boolean containsOfClass(Class clazz) { - return getClass() == clazz; - } - - @Override - public boolean isUnassignableByOtherCode() { - return true; - } - - @Override - public boolean isUnmodifiableByOtherCode() { - return true; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (!(obj instanceof ValueLiteral)) { - return false; - } - ValueLiteral other = (ValueLiteral) obj; - // TODO: Can this string comparison be cleaned up? - // Cannot use Types.isSameType(type, other.type) because we don't have a Types object. - return type.toString().equals(other.type.toString()) - && Objects.equals(value, other.value); - } - - @Override - public String toString() { - if (TypesUtils.isString(type)) { - return "\"" + value + "\""; - } else if (type.getKind() == TypeKind.LONG) { - assert value != null : "@AssumeAssertion(nullness): invariant"; - return value.toString() + "L"; - } else if (type.getKind() == TypeKind.CHAR) { - return "\'" + value + "\'"; - } - return value == null ? "null" : value.toString(); - } - - @Override - public int hashCode() { - return Objects.hash(value, type.toString()); - } - - @Override - public boolean syntacticEquals(Receiver other) { - return this.equals(other); - } - - @Override - public boolean containsModifiableAliasOf(Store store, Receiver other) { - return false; // not modifiable - } - - /** - * Returns the value of this literal. - * - * @return the value of this literal - */ - public @Nullable Object getValue() { - return value; - } - } - - /** A call to a @Deterministic method. */ - public static class MethodCall extends Receiver { - - protected final Receiver receiver; - protected final List parameters; - protected final ExecutableElement method; - - public MethodCall( - TypeMirror type, - ExecutableElement method, - Receiver receiver, - List parameters) { - super(type); - this.receiver = receiver; - this.parameters = parameters; - this.method = method; - } - - @Override - public boolean containsOfClass(Class clazz) { - if (getClass() == clazz) { - return true; - } - if (receiver.containsOfClass(clazz)) { - return true; - } - for (Receiver p : parameters) { - if (p.containsOfClass(clazz)) { - return true; - } - } - return false; - } - - /** - * Returns the method call receiver (for inspection only - do not modify). - * - * @return the method call receiver (for inspection only - do not modify) - */ - public Receiver getReceiver() { - return receiver; - } - - /** - * Returns the method call parameters (for inspection only - do not modify any of the - * parameters). - * - * @return the method call parameters (for inspection only - do not modify any of the - * parameters) - */ - public List getParameters() { - return Collections.unmodifiableList(parameters); - } - - /** - * Returns the ExecutableElement for the method call. - * - * @return the ExecutableElement for the method call - */ - public ExecutableElement getElement() { - return method; - } - - @Override - public boolean isUnassignableByOtherCode() { - // There is no need to check that the method is deterministic, because a MethodCall is - // only created for deterministic methods. - return receiver.isUnmodifiableByOtherCode() - && parameters.stream().allMatch(Receiver::isUnmodifiableByOtherCode); - } - - @Override - public boolean isUnmodifiableByOtherCode() { - return isUnassignableByOtherCode(); - } - - @Override - public boolean containsSyntacticEqualReceiver(Receiver other) { - return syntacticEquals(other) || receiver.syntacticEquals(other); - } - - @Override - public boolean syntacticEquals(Receiver other) { - if (!(other instanceof MethodCall)) { - return false; - } - MethodCall otherMethod = (MethodCall) other; - if (!receiver.syntacticEquals(otherMethod.receiver)) { - return false; - } - if (parameters.size() != otherMethod.parameters.size()) { - return false; - } - int i = 0; - for (Receiver p : parameters) { - if (!p.syntacticEquals(otherMethod.parameters.get(i))) { - return false; - } - i++; - } - return method.equals(otherMethod.method); - } - - public boolean containsSyntacticEqualParameter(LocalVariable var) { - for (Receiver p : parameters) { - if (p.containsSyntacticEqualReceiver(var)) { - return true; - } - } - return false; - } - - @Override - public boolean containsModifiableAliasOf(Store store, Receiver other) { - if (receiver.containsModifiableAliasOf(store, other)) { - return true; - } - for (Receiver p : parameters) { - if (p.containsModifiableAliasOf(store, other)) { - return true; - } - } - return false; // the method call itself is not modifiable - } - - @Override - public boolean equals(@Nullable Object obj) { - if (!(obj instanceof MethodCall)) { - return false; - } - if (method.getKind() == ElementKind.CONSTRUCTOR) { - return this == obj; - } - MethodCall other = (MethodCall) obj; - return parameters.equals(other.parameters) - && receiver.equals(other.receiver) - && method.equals(other.method); - } - - @Override - public int hashCode() { - if (method.getKind() == ElementKind.CONSTRUCTOR) { - return super.hashCode(); - } - return Objects.hash(method, receiver, parameters); - } - - @Override - public String toString() { - StringBuilder result = new StringBuilder(); - if (receiver instanceof ClassName) { - result.append(receiver.getType()); - } else { - result.append(receiver); - } - result.append("."); - String methodName = method.getSimpleName().toString(); - result.append(methodName); - result.append("("); - boolean first = true; - for (Receiver p : parameters) { - if (!first) { - result.append(", "); - } - result.append(p.toString()); - first = false; - } - result.append(")"); - return result.toString(); - } - } - - /** FlowExpression.Receiver for binary operations. */ - public static class BinaryOperation extends Receiver { - - /** The binary operation kind. */ - protected final Kind operationKind; - /** The binary operation kind for pretty printing. */ - protected final JCTree.Tag tag; - /** The left operand. */ - protected final Receiver left; - /** The right operand. */ - protected final Receiver right; - - /** - * Create a binary operation. - * - * @param node the binary operation node - * @param left the left operand - * @param right the right operand - */ - public BinaryOperation(BinaryOperationNode node, Receiver left, Receiver right) { - super(node.getType()); - this.operationKind = node.getTree().getKind(); - this.tag = ((JCTree) node.getTree()).getTag(); - this.left = left; - this.right = right; - } - - /** - * Returns the operator of this binary operation. - * - * @return the binary operation kind - */ - public Kind getOperationKind() { - return operationKind; - } - - /** - * Returns the left operand of this binary operation. - * - * @return the left operand - */ - public Receiver getLeft() { - return left; - } - - /** - * Returns the right operand of this binary operation. - * - * @return the right operand - */ - public Receiver getRight() { - return right; - } - - @Override - public boolean containsOfClass(Class clazz) { - if (getClass() == clazz) { - return true; - } - return left.containsOfClass(clazz) || right.containsOfClass(clazz); - } - - @Override - public boolean isUnassignableByOtherCode() { - return left.isUnassignableByOtherCode() && right.isUnassignableByOtherCode(); - } - - @Override - public boolean isUnmodifiableByOtherCode() { - return left.isUnmodifiableByOtherCode() && right.isUnmodifiableByOtherCode(); - } - - @Override - public boolean syntacticEquals(Receiver other) { - if (!(other instanceof BinaryOperation)) { - return false; - } - BinaryOperation biOp = (BinaryOperation) other; - if (!(operationKind == biOp.getOperationKind())) { - return false; - } - return left.equals(biOp.left) && right.equals(biOp.right); - } - - @Override - public boolean containsModifiableAliasOf(Store store, Receiver other) { - return left.containsModifiableAliasOf(store, other) - || right.containsModifiableAliasOf(store, other); - } - - @Override - public int hashCode() { - return Objects.hash(operationKind, left, right); - } - - @Override - public boolean equals(@Nullable Object other) { - if (!(other instanceof BinaryOperation)) { - return false; - } - BinaryOperation biOp = (BinaryOperation) other; - if (!(operationKind == biOp.getOperationKind())) { - return false; - } - if (isCommutative()) { - return (left.equals(biOp.left) && right.equals(biOp.right)) - || (left.equals(biOp.right) && right.equals(biOp.left)); - } - return left.equals(biOp.left) && right.equals(biOp.right); - } - - /** - * Returns true if the binary operation is commutative, e.g., x + y == y + x. - * - * @return true if the binary operation is commutative - */ - private boolean isCommutative() { - switch (operationKind) { - case PLUS: - case MULTIPLY: - case AND: - case OR: - case XOR: - case EQUAL_TO: - case NOT_EQUAL_TO: - case CONDITIONAL_AND: - case CONDITIONAL_OR: - return true; - default: - return false; - } - } - - @Override - public String toString() { - final Pretty pretty = new Pretty(null, true); - StringBuilder result = new StringBuilder(); - result.append(left.toString()); - result.append(pretty.operatorName(tag)); - result.append(right.toString()); - return result.toString(); - } - } - - /** An array access. */ - public static class ArrayAccess extends Receiver { - - protected final Receiver receiver; - protected final Receiver index; - - public ArrayAccess(TypeMirror type, Receiver receiver, Receiver index) { - super(type); - this.receiver = receiver; - this.index = index; - } - - @Override - public boolean containsOfClass(Class clazz) { - if (getClass() == clazz) { - return true; - } - if (receiver.containsOfClass(clazz)) { - return true; - } - return index.containsOfClass(clazz); - } - - public Receiver getReceiver() { - return receiver; - } - - public Receiver getIndex() { - return index; - } - - @Override - public boolean isUnassignableByOtherCode() { - return false; - } - - @Override - public boolean isUnmodifiableByOtherCode() { - return false; - } - - @Override - public boolean containsSyntacticEqualReceiver(Receiver other) { - return syntacticEquals(other) - || receiver.syntacticEquals(other) - || index.syntacticEquals(other); - } - - @Override - public boolean syntacticEquals(Receiver other) { - if (!(other instanceof ArrayAccess)) { - return false; - } - ArrayAccess otherArrayAccess = (ArrayAccess) other; - if (!receiver.syntacticEquals(otherArrayAccess.receiver)) { - return false; - } - return index.syntacticEquals(otherArrayAccess.index); - } - - @Override - public boolean containsModifiableAliasOf(Store store, Receiver other) { - if (receiver.containsModifiableAliasOf(store, other)) { - return true; - } - return index.containsModifiableAliasOf(store, other); - } - - @Override - public boolean equals(@Nullable Object obj) { - if (!(obj instanceof ArrayAccess)) { - return false; - } - ArrayAccess other = (ArrayAccess) obj; - return receiver.equals(other.receiver) && index.equals(other.index); - } - - @Override - public int hashCode() { - return Objects.hash(receiver, index); - } - - @Override - public String toString() { - StringBuilder result = new StringBuilder(); - result.append(receiver.toString()); - result.append("["); - result.append(index.toString()); - result.append("]"); - return result.toString(); - } - } - - /** FlowExpression for array creations. {@code new String[]()}. */ - public static class ArrayCreation extends Receiver { - - /** - * List of dimensions expressions. {code null} means that there is no dimension expression. - */ - protected final List dimensions; - /** List of initializers. */ - protected final List initializers; - - /** - * Creates an ArrayCreation object. - * - * @param type array type - * @param dimensions list of dimension expressions; {code null} means that there is no - * dimension expression - * @param initializers list of initializer expressions - */ - public ArrayCreation( - TypeMirror type, - List dimensions, - List initializers) { - super(type); - this.dimensions = dimensions; - this.initializers = initializers; - } - - /** - * Returns a list of receivers representing the dimension of this array creation. - * - * @return a list of receivers representing the dimension of this array creation - */ - public List getDimensions() { - return dimensions; - } - - public List getInitializers() { - return initializers; - } - - @Override - public boolean containsOfClass(Class clazz) { - for (Receiver n : dimensions) { - if (n != null && n.getClass() == clazz) { - return true; - } - } - for (Receiver n : initializers) { - if (n.getClass() == clazz) { - return true; - } - } - return false; - } - - @Override - public boolean isUnassignableByOtherCode() { - return false; - } - - @Override - public boolean isUnmodifiableByOtherCode() { - return false; - } - - @Override - public int hashCode() { - return Objects.hash(dimensions, initializers, getType().toString()); - } - - @Override - public boolean equals(@Nullable Object obj) { - if (!(obj instanceof ArrayCreation)) { - return false; - } - ArrayCreation other = (ArrayCreation) obj; - return this.dimensions.equals(other.getDimensions()) - && this.initializers.equals(other.getInitializers()) - // It might be better to use Types.isSameType(getType(), other.getType()), but I - // don't have a Types object. - && getType().toString().equals(other.getType().toString()); - } - - @Override - public boolean syntacticEquals(Receiver other) { - return this.equals(other); - } - - @Override - public boolean containsSyntacticEqualReceiver(Receiver other) { - return syntacticEquals(other); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("new " + type); - if (!dimensions.isEmpty()) { - for (Receiver dim : dimensions) { - sb.append("["); - sb.append(dim == null ? "" : dim); - sb.append("]"); - } - } - if (!initializers.isEmpty()) { - boolean needComma = false; - sb.append(" {"); - for (Receiver init : initializers) { - if (needComma) { - sb.append(", "); - } - sb.append(init); - needComma = true; - } - sb.append("}"); - } - return sb.toString(); - } - } -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardAnalysis.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardAnalysis.java index 362e3e37e15f..9e60cc62565a 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardAnalysis.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardAnalysis.java @@ -1,9 +1,10 @@ package org.checkerframework.dataflow.analysis; -import java.util.List; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.cfg.node.ReturnNode; -import org.checkerframework.javacutil.Pair; +import org.plumelib.util.IPair; + +import java.util.List; /** * This interface defines a forward analysis, given a control flow graph and a forward transfer @@ -26,5 +27,5 @@ public interface ForwardAnalysis< * * @return the transfer results for each return node in the CFG */ - List>> getReturnStatementStores(); + List>> getReturnStatementStores(); } 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 d075f562f685..a3fba8c58649 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardAnalysisImpl.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardAnalysisImpl.java @@ -2,20 +2,14 @@ import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MethodTree; -import com.sun.source.tree.VariableTree; -import java.util.ArrayList; -import java.util.IdentityHashMap; -import java.util.List; -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; import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGLambda; import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGMethod; -import org.checkerframework.dataflow.cfg.UnderlyingAST.Kind; import org.checkerframework.dataflow.cfg.block.Block; import org.checkerframework.dataflow.cfg.block.ConditionalBlock; import org.checkerframework.dataflow.cfg.block.ExceptionBlock; @@ -24,8 +18,18 @@ import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.cfg.node.ReturnNode; +import org.checkerframework.dataflow.qual.SideEffectFree; import org.checkerframework.javacutil.BugInCF; -import org.checkerframework.javacutil.Pair; +import org.plumelib.util.CollectionsPlume; +import org.plumelib.util.IPair; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.lang.model.type.TypeMirror; /** * An implementation of a forward analysis to solve a org.checkerframework.dataflow problem given a @@ -65,8 +69,8 @@ public class ForwardAnalysisImpl< // `@code`, not `@link`, because dataflow module doesn't depend on framework module. /** * Construct an object that can perform a org.checkerframework.dataflow forward analysis over a - * control flow graph. The transfer function is set by the subclass, e.g., {@code - * org.checkerframework.framework.flow.CFAbstractAnalysis}, later. + * control flow graph. When using this constructor, the transfer function is set later by the + * subclass, e.g., {@code org.checkerframework.framework.flow.CFAbstractAnalysis}. * * @param maxCountBeforeWidening number of times a block can be analyzed before widening */ @@ -83,18 +87,19 @@ public ForwardAnalysisImpl(int maxCountBeforeWidening) { * Construct an object that can perform a org.checkerframework.dataflow forward analysis over a * control flow graph given a transfer function. * - * @param transfer the transfer function + * @param transferFunction the transfer function */ - public ForwardAnalysisImpl(@Nullable T transfer) { + public ForwardAnalysisImpl(T transferFunction) { this(-1); - this.transferFunction = transfer; + this.transferFunction = transferFunction; } @Override public void performAnalysis(ControlFlowGraph cfg) { if (isRunning) { throw new BugInCF( - "ForwardAnalysisImpl::performAnalysis() shouldn't be called when the analysis is running."); + "ForwardAnalysisImpl::performAnalysis() shouldn't be called when the analysis" + + " is running."); } isRunning = true; @@ -123,7 +128,7 @@ public void performAnalysisBlock(Block b) { currentInput = inputBefore.copy(); Node lastNode = null; boolean addToWorklistAgain = false; - for (Node n : rb.getContents()) { + for (Node n : rb.getNodes()) { assert currentInput != null : "@AssumeAssertion(nullness): invariant"; TransferResult transferResult = callTransferFunction(n, currentInput); addToWorklistAgain |= updateNodeValues(n, transferResult); @@ -135,7 +140,8 @@ public void performAnalysisBlock(Block b) { // Propagate store to successors Block succ = rb.getSuccessor(); assert succ != null - : "@AssumeAssertion(nullness): regular basic block without non-exceptional successor unexpected"; + : "@AssumeAssertion(nullness): regular basic block without" + + " non-exceptional successor unexpected"; propagateStoresTo( succ, lastNode, currentInput, rb.getFlowRule(), addToWorklistAgain); break; @@ -154,8 +160,6 @@ public void performAnalysisBlock(Block b) { Block succ = eb.getSuccessor(); if (succ != null) { currentInput = new TransferInput<>(node, this, transferResult); - // TODO: Variable wasn't used. - // Store.FlowRule storeFlow = eb.getFlowRule(); propagateStoresTo( succ, node, currentInput, eb.getFlowRule(), addToWorklistAgain); } @@ -163,6 +167,9 @@ public void performAnalysisBlock(Block b) { for (Map.Entry> e : eb.getExceptionalSuccessors().entrySet()) { TypeMirror cause = e.getKey(); + if (isIgnoredExceptionType(cause)) { + continue; + } S exceptionalStore = transferResult.getExceptionalStore(cause); if (exceptionalStore != null) { for (Block exceptionSucc : e.getValue()) { @@ -224,24 +231,24 @@ 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<>(); - for (ReturnNode returnNode : cfg.getReturnNodes()) { - TransferResult store = storesAtReturnStatements.get(returnNode); - result.add(Pair.of(returnNode, store)); - } - return result; + public List>> getReturnStatementStores() { + return CollectionsPlume + .>>mapList( + returnNode -> + IPair.of(returnNode, storesAtReturnStatements.get(returnNode)), + cfg.getReturnNodes()); } @Override public S runAnalysisFor( - Node node, - boolean before, - TransferInput transferInput, + @FindDistinct Node node, + Analysis.BeforeOrAfter preOrPost, + TransferInput blockTransferInput, IdentityHashMap nodeValues, - Map, IdentityHashMap>> analysisCaches) { + @Nullable Map, IdentityHashMap>> + analysisCaches) { Block block = node.getBlock(); assert block != null : "@AssumeAssertion(nullness): invariant"; Node oldCurrentNode = currentNode; @@ -249,11 +256,9 @@ public S runAnalysisFor( // Prepare cache IdentityHashMap> cache; if (analysisCaches != null) { - cache = analysisCaches.get(transferInput); - if (cache == null) { - cache = new IdentityHashMap<>(); - analysisCaches.put(transferInput, cache); - } + cache = + analysisCaches.computeIfAbsent( + blockTransferInput, __ -> new IdentityHashMap<>()); } else { cache = null; } @@ -271,17 +276,18 @@ public S runAnalysisFor( RegularBlock rb = (RegularBlock) block; // Apply transfer function to contents until we found the node we are // looking for. - TransferInput store = transferInput; + TransferInput store = blockTransferInput; TransferResult transferResult; - for (Node n : rb.getContents()) { - currentNode = n; - if (n == node && before) { + for (Node n : rb.getNodes()) { + setCurrentNode(n); + if (n == node && preOrPost == Analysis.BeforeOrAfter.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); @@ -292,9 +298,7 @@ public S runAnalysisFor( } store = new TransferInput<>(n, this, transferResult); } - // This point should never be reached. If the block of 'node' is - // 'block', then 'node' must be part of the contents of 'block'. - throw new BugInCF("This point should never be reached."); + throw new BugInCF("node %s is not in node.getBlock()=%s", node, block); } case EXCEPTION_BLOCK: { @@ -307,12 +311,23 @@ public S runAnalysisFor( + "\teb.getNode(): " + eb.getNode()); } - if (before) { - return transferInput.getRegularStore(); + if (preOrPost == Analysis.BeforeOrAfter.BEFORE) { + return blockTransferInput.getRegularStore(); + } + setCurrentNode(node); + // Copy the store to avoid changing other blocks' transfer inputs in {@link + // #inputs} + TransferResult transferResult; + if (cache != null && cache.containsKey(node)) { + transferResult = cache.get(node); + } else { + // Copy the store to avoid changing other blocks' transfer inputs in + // {@link #inputs} + transferResult = callTransferFunction(node, blockTransferInput.copy()); + if (cache != null) { + cache.put(node, transferResult); + } } - currentNode = node; - TransferResult transferResult = - callTransferFunction(node, transferInput); return transferResult.getRegularStore(); } default: @@ -320,7 +335,7 @@ public S runAnalysisFor( throw new BugInCF("Unexpected block type: " + block.getType()); } } finally { - currentNode = oldCurrentNode; + setCurrentNode(oldCurrentNode); isRunning = false; } } @@ -342,25 +357,8 @@ protected void initInitialInputs() { worklist.process(cfg); Block entry = cfg.getEntryBlock(); worklist.add(entry); - List parameters = null; UnderlyingAST underlyingAST = cfg.getUnderlyingAST(); - if (underlyingAST.getKind() == Kind.METHOD) { - MethodTree tree = ((CFGMethod) underlyingAST).getMethod(); - parameters = new ArrayList<>(); - for (VariableTree p : tree.getParameters()) { - LocalVariableNode var = new LocalVariableNode(p); - parameters.add(var); - // TODO: document that LocalVariableNode has no block that it belongs to - } - } else if (underlyingAST.getKind() == Kind.LAMBDA) { - LambdaExpressionTree lambda = ((CFGLambda) underlyingAST).getLambdaTree(); - parameters = new ArrayList<>(); - for (VariableTree p : lambda.getParameters()) { - LocalVariableNode var = new LocalVariableNode(p); - parameters.add(var); - // TODO: document that LocalVariableNode has no block that it belongs to - } - } + List parameters = getParameters(underlyingAST); assert transferFunction != null : "@AssumeAssertion(nullness): invariant"; S initialStore = transferFunction.initialStore(underlyingAST, parameters); thenStores.put(entry, initialStore); @@ -368,13 +366,35 @@ protected void initInitialInputs() { inputs.put(entry, new TransferInput<>(null, this, initialStore)); } + /** + * Returns the formal parameters for a method. + * + * @param underlyingAST the AST for the method + * @return the formal parameters for the method + */ + @SideEffectFree + private List getParameters(UnderlyingAST underlyingAST) { + switch (underlyingAST.getKind()) { + case METHOD: + MethodTree tree = ((CFGMethod) underlyingAST).getMethod(); + // TODO: document that LocalVariableNode has no block that it belongs to + return CollectionsPlume.mapList(LocalVariableNode::new, tree.getParameters()); + case LAMBDA: + LambdaExpressionTree lambda = ((CFGLambda) underlyingAST).getLambdaTree(); + // TODO: document that LocalVariableNode has no block that it belongs to + return CollectionsPlume.mapList(LocalVariableNode::new, lambda.getParameters()); + default: + return Collections.emptyList(); + } + } + @Override protected TransferResult callTransferFunction(Node node, TransferInput input) { TransferResult transferResult = super.callTransferFunction(node, input); if (node instanceof ReturnNode) { // Save a copy of the store to later check if some property holds at a given return - // statement + // statement. storesAtReturnStatements.put((ReturnNode) node, transferResult); } return transferResult; @@ -463,10 +483,7 @@ protected void addStoreBefore( S elseStore = getStoreBefore(b, Store.Kind.ELSE); boolean shouldWiden = false; if (blockCount != null) { - Integer count = blockCount.get(b); - if (count == null) { - count = 0; - } + Integer count = blockCount.getOrDefault(b, 0); shouldWiden = count >= maxCountBeforeWidening; if (shouldWiden) { blockCount.put(b, 0); @@ -502,7 +519,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)) { @@ -566,7 +585,7 @@ private S mergeStores(S newStore, @Nullable S previousStore, boolean shouldWiden case ELSE: return readFromStore(elseStores, b); default: - throw new BugInCF("Unexpected Store Kind: " + kind); + throw new BugInCF("Unexpected Store.Kind: " + kind); } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardTransferFunction.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardTransferFunction.java index da48b0b22429..8856e8bc4162 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardTransferFunction.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/ForwardTransferFunction.java @@ -1,10 +1,10 @@ package org.checkerframework.dataflow.analysis; -import java.util.List; -import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import java.util.List; + /** * Interface of a forward transfer function for the abstract interpretation used for the forward * flow analysis. @@ -20,12 +20,11 @@ public interface ForwardTransferFunction, S extends S extends TransferFunction { /** - * Returns the initial store to be used by the org.checkerframework.dataflow analysis. {@code - * parameters} is non-null if the underlying AST is a method. + * Returns the initial store to be used by the org.checkerframework.dataflow analysis. * * @param underlyingAST an abstract syntax tree - * @param parameters a list of local variable nodes + * @param parameters a list of local variable nodes representing formal parameters (if any) * @return the initial store */ - S initialStore(UnderlyingAST underlyingAST, @Nullable List parameters); + S initialStore(UnderlyingAST underlyingAST, List parameters); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/RegularTransferResult.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/RegularTransferResult.java index b281b4b31a52..1885f0d94510 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/RegularTransferResult.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/RegularTransferResult.java @@ -1,9 +1,12 @@ package org.checkerframework.dataflow.analysis; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.plumelib.util.StringsPlume; + import java.util.Map; import java.util.StringJoiner; + import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.Nullable; /** * Implementation of a {@link TransferResult} with just one non-exceptional store. The result of @@ -18,12 +21,15 @@ public class RegularTransferResult, S extends StoreExceptions: If the corresponding {@link * org.checkerframework.dataflow.cfg.node.Node} throws an exception, then it is assumed that no @@ -36,7 +42,8 @@ public class RegularTransferResult, S extends Store exceptionalStores) { + @Nullable V value, S resultStore, @Nullable Map exceptionalStores) { this(value, resultStore, exceptionalStores, false); } @@ -74,9 +83,6 @@ public RegularTransferResult( * corresponding {@link org.checkerframework.dataflow.cfg.node.Node} is a boolean node, then * {@code resultStore} is used for both the 'then' and 'else' edge. * - *

      For the meaning of {@code storeChanged}, see {@link - * org.checkerframework.dataflow.analysis.TransferResult#storeChanged}. - * *

      Exceptions: If the corresponding {@link * org.checkerframework.dataflow.cfg.node.Node} throws an exception, then the corresponding * store in {@code exceptionalStores} is used. If no exception is found in {@code @@ -89,9 +95,11 @@ public RegularTransferResult( * control over the objects is transferred to this class. * * @param value the abstract value produced by the transfer function - * @param resultStore {@link #store} - * @param exceptionalStores {@link #exceptionalStores} - * @param storeChanged {@link #storeChanged} + * @param resultStore the regular result store + * @param exceptionalStores the stores in case the basic block throws an exception, or null if + * the basic block does not throw any exceptions + * @param storeChanged see {@link + * org.checkerframework.dataflow.analysis.TransferResult#storeChanged} */ public RegularTransferResult( @Nullable V value, @@ -130,9 +138,12 @@ public boolean containsTwoStores() { public String toString() { StringJoiner result = new StringJoiner(System.lineSeparator()); result.add("RegularTransferResult("); - result.add(" resultValue = " + resultValue); - result.add(" store = " + store); - result.add(")"); + result.add(" resultValue = " + StringsPlume.indentLinesExceptFirst(2, resultValue)); + // "toString().trim()" works around bug where toString ends with a newline. + result.add( + " store = " + + StringsPlume.indentLinesExceptFirst(2, store.toString().trim()) + + ")"); return result.toString(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/Store.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/Store.java index 4c9dbad162bb..cbf403ece59d 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/Store.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/Store.java @@ -1,6 +1,8 @@ package org.checkerframework.dataflow.analysis; -import org.checkerframework.dataflow.cfg.CFGVisualizer; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizer; +import org.checkerframework.dataflow.expression.JavaExpression; /** * A store is used to keep track of the information that the org.checkerframework.dataflow analysis @@ -17,20 +19,27 @@ public interface Store> { // as a regular unconditional store. // Once we have some information for both the then and else store, we // create a TransferInput for the block and allow it to be analyzed. - public static enum Kind { + public enum Kind { THEN, ELSE, BOTH } /** A flow rule describes how stores flow along one edge between basic blocks. */ - public static enum FlowRule { - EACH_TO_EACH, // The normal case, then store flows to the then store - // and else store flows to the else store. - THEN_TO_BOTH, // Then store flows to both then and else of successor. - ELSE_TO_BOTH, // Else store flows to both then and else of successor. - THEN_TO_THEN, // Then store flows to the then of successor. Else store is ignored. - ELSE_TO_ELSE, // Else store flows to the else of successor. Then store is ignored. + public enum FlowRule { + /** + * The normal case: then store flows to the then store, and else store flows to the else + * store. + */ + EACH_TO_EACH, + /** Then store flows to both then and else of successor. */ + THEN_TO_BOTH, + /** Else store flows to both then and else of successor. */ + ELSE_TO_BOTH, + /** Then store flows to the then of successor. Else store is ignored. */ + THEN_TO_THEN, + /** Else store flows to the else of successor. Then store is ignored. */ + ELSE_TO_ELSE, } /** @@ -83,7 +92,7 @@ public static enum FlowRule { * Can the objects {@code a} and {@code b} be aliases? Returns a conservative answer (i.e., * returns {@code true} if not enough information is available to determine aliasing). */ - boolean canAlias(FlowExpressions.Receiver a, FlowExpressions.Receiver b); + boolean canAlias(JavaExpression a, JavaExpression b); /** * Delegate visualization responsibility to a visualizer. @@ -92,4 +101,15 @@ public static enum FlowRule { * @return the String representation of this store */ String visualize(CFGVisualizer viz); + + // I'm not sure why this is necessary, but without it `Store.equals()` is treated as requiring a + // @NonNull argument. TODO: fix. + /** + * Returns true if this is equal to the given argument. + * + * @param o the object to compare against this + * @return true if this is equal to the given argument + */ + @Override + boolean equals(@Nullable Object o); } 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 6682873967de..0dfe3f22cc99 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferInput.java @@ -1,8 +1,13 @@ package org.checkerframework.dataflow.analysis; -import java.util.Objects; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.cfg.node.Node; +import org.plumelib.util.StringsPlume; +import org.plumelib.util.UniqueId; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; /** * {@code TransferInput} is used as the input type of the individual transfer functions of a {@link @@ -15,15 +20,15 @@ * @param type of the abstract value that is tracked * @param the store type used in the analysis */ -public class TransferInput, S extends Store> { +public class TransferInput, S extends Store> implements UniqueId { /** The corresponding node. */ // TODO: explain when the node is changed. 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,28 +37,58 @@ 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; /** The corresponding analysis class to get intermediate flow results. */ protected final Analysis analysis; + /** The unique ID for the next-created object. */ + private static final AtomicLong nextUid = new AtomicLong(0); + + /** The unique ID of this object. */ + private final transient long uid = nextUid.getAndIncrement(); + + @Override + public long getUid(@UnknownInitialization TransferInput this) { + return uid; + } + + /** + * Private helper constructor; all TransferInput construction bottoms out here. + * + * @param node the corresponding node + * @param store the regular result store, or {@code null} if none is present + * @param thenStore the 'then' result store, or {@code null} if none is present + * @param elseStore the 'else' result store, or {@code null} if none is present + * @param analysis analysis the corresponding analysis class to get intermediate flow results + */ + private TransferInput( + @Nullable Node node, + @Nullable S store, + @Nullable S thenStore, + @Nullable S elseStore, + Analysis analysis) { + if (store == null) { + assert thenStore != null && elseStore != null; + } else { + assert thenStore == null && elseStore == null; + } + this.node = node; + this.store = store; + this.thenStore = thenStore; + this.elseStore = elseStore; + this.analysis = analysis; + } + /** * Create a {@link TransferInput}, given a {@link TransferResult} and a node-value mapping. * @@ -69,16 +104,12 @@ public class TransferInput, S extends Store> { * @param to a transfer result */ public TransferInput(Node n, Analysis analysis, TransferResult to) { - node = n; - this.analysis = analysis; - if (to.containsTwoStores()) { - thenStore = to.getThenStore(); - elseStore = to.getElseStore(); - store = null; - } else { - store = to.getRegularStore(); - thenStore = elseStore = null; - } + this( + n, + to.containsTwoStores() ? null : to.getRegularStore(), + to.containsTwoStores() ? to.getThenStore() : null, + to.containsTwoStores() ? to.getElseStore() : null, + analysis); } /** @@ -95,10 +126,7 @@ public TransferInput(Node n, Analysis analysis, TransferResult to * @param s {@link #store} */ public TransferInput(@Nullable Node n, Analysis analysis, S s) { - node = n; - this.analysis = analysis; - store = s; - thenStore = elseStore = null; + this(n, s, null, null, analysis); } /** @@ -113,11 +141,7 @@ public TransferInput(@Nullable Node n, Analysis analysis, S s) { * @param s2 {@link #elseStore} */ public TransferInput(@Nullable Node n, Analysis analysis, S s1, S s2) { - node = n; - this.analysis = analysis; - thenStore = s1; - elseStore = s2; - store = null; + this(n, null, s1, s2, analysis); } /** @@ -125,19 +149,14 @@ public TransferInput(@Nullable Node n, Analysis analysis, S s1, S s2) { * * @param from a {@link TransferInput} to copy */ + @SuppressWarnings("nullness:dereference.of.nullable") // object invariant: store vs thenStore protected TransferInput(TransferInput from) { - this.node = from.node; - this.analysis = from.analysis; - if (from.store == null) { - assert from.thenStore != null && from.elseStore != null - : "@AssumeAssertion(nullness): invariant"; - thenStore = from.thenStore.copy(); - elseStore = from.elseStore.copy(); - store = null; - } else { - store = from.store.copy(); - thenStore = elseStore = null; - } + this( + from.node, + from.store == null ? null : from.store.copy(), + from.store == null ? from.thenStore.copy() : null, + from.store == null ? from.elseStore.copy() : null, + from.analysis); } /** @@ -221,7 +240,7 @@ public S getElseStore() { * potentially not equal */ public boolean containsTwoStores() { - return (thenStore != null && elseStore != null); + return store == null; } /** @@ -285,7 +304,13 @@ public int hashCode() { @Override public String toString() { if (store == null) { - return "[then=" + thenStore + ", else=" + elseStore + "]"; + return "[then=" + + StringsPlume.indentLinesExceptFirst(2, thenStore) + + "," + + System.lineSeparator() + + " else=" + + StringsPlume.indentLinesExceptFirst(2, elseStore) + + "]"; } else { return "[" + store + "]"; } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferResult.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferResult.java index ba88e4aac228..938a01a43adc 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferResult.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/TransferResult.java @@ -1,17 +1,19 @@ package org.checkerframework.dataflow.analysis; +import org.checkerframework.checker.nullness.qual.Nullable; + import java.util.Map; + import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.Nullable; /** * {@code TransferResult} is used as the result type of the individual transfer functions of a * {@link TransferFunction}. It always belongs to the result of the individual transfer function for * a particular {@link org.checkerframework.dataflow.cfg.node.Node}, even though that {@code - * org.checkerframework.dataflow.cfg.node.Node} is not explicitly store in {@code TransferResult}. + * org.checkerframework.dataflow.cfg.node.Node} is not explicitly stored in {@code TransferResult}. * - *

      A {@code TransferResult} contains one or two stores (for 'then' and 'else'), and zero or more - * stores with a cause ({@link TypeMirror}). + *

      A {@code TransferResult} consists of a result value, plus one or more stores. It contains one + * or two stores (for 'then' and 'else'), and zero or more stores with a cause ({@link TypeMirror}). * * @param type of the abstract value that is tracked * @param the store type used in the analysis @@ -21,6 +23,8 @@ public abstract class TransferResult, S extends Store /** * The abstract value of the {@link org.checkerframework.dataflow.cfg.node.Node} associated with * this {@link TransferResult}, or {@code null} if no value has been produced. + * + *

      Is set by {@link #setResultValue}. */ protected @Nullable V resultValue; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/analysis/UnusedAbstractValue.java b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/UnusedAbstractValue.java new file mode 100644 index 000000000000..ff6088f5500a --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/analysis/UnusedAbstractValue.java @@ -0,0 +1,23 @@ +package org.checkerframework.dataflow.analysis; + +import org.checkerframework.javacutil.BugInCF; + +/** + * UnusedAbstractValue is an AbstractValue that is not involved in any lub computation during + * dataflow analysis. For those analyses which handle lub computation at a higher level (e.g., store + * level), it is sufficient to use UnusedAbstractValue and unnecessary to implement another specific + * AbstractValue. Example analysis using UnusedAbstractValue is LiveVariable analysis. This is a + * workaround for issue https://github.com/eisop/checker-framework/issues/200 + */ +public final class UnusedAbstractValue implements AbstractValue { + + /** This class cannot be instantiated */ + private UnusedAbstractValue() { + throw new AssertionError("Class UnusedAbstractValue cannot be instantiated."); + } + + @Override + public UnusedAbstractValue leastUpperBound(UnusedAbstractValue other) { + throw new BugInCF("UnusedAbstractValue.leastUpperBound was called!"); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java new file mode 100644 index 000000000000..d66c72fb29b9 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprStore.java @@ -0,0 +1,143 @@ +package org.checkerframework.dataflow.busyexpr; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.cfg.node.BinaryOperationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizer; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.javacutil.BugInCF; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.StringJoiner; + +/** A busy expression store contains a set of busy expressions represented by nodes. */ +public class BusyExprStore implements Store { + + /** A set of busy expression abstract values. */ + private final Set busyExprValueSet; + + /** + * Create a new BusyExprStore. + * + * @param busyExprValueSet a set of busy expression abstract values. The parameter is captured + * and the caller should not retain an alias. + */ + public BusyExprStore(Set busyExprValueSet) { + this.busyExprValueSet = busyExprValueSet; + } + + /** Create a new BusyExprStore. */ + public BusyExprStore() { + busyExprValueSet = new LinkedHashSet<>(); + } + + /** + * Kill expressions if they contain variable var. + * + * @param var a variable + */ + public void killBusyExpr(Node var) { + busyExprValueSet.removeIf( + busyExprValue -> exprContainsVariable(busyExprValue.busyExpression, var)); + } + + /** + * Return true if the expression contains variable var. Note that {@code .equals} is used in the + * return statement to verify value equality, as the statement decides whether the two nodes + * have the same value, not represent the same CFG node. + * + * @param expr the expression checked + * @param var the variable + * @return true if the expression contains the variable + */ + public boolean exprContainsVariable(Node expr, Node var) { + if (expr instanceof BinaryOperationNode) { + BinaryOperationNode binaryNode = (BinaryOperationNode) expr; + return exprContainsVariable(binaryNode.getLeftOperand(), var) + || exprContainsVariable(binaryNode.getRightOperand(), var); + } + + return expr.equals(var); + } + + /** + * Add busy expression e to busy expression value set. + * + * @param e the busy expression to be added + */ + public void putBusyExpr(BusyExprValue e) { + busyExprValueSet.add(e); + } + + /** + * Add expressions to the store, add sub-expressions to the store recursively + * + * @param e the expression to be added + */ + public void addUseInExpression(Node e) { + if (e instanceof BinaryOperationNode) { + BinaryOperationNode binaryNode = (BinaryOperationNode) e; + putBusyExpr(new BusyExprValue(binaryNode)); + // recursively add expressions + addUseInExpression(binaryNode.getLeftOperand()); + addUseInExpression(binaryNode.getRightOperand()); + } + } + + @Override + public BusyExprStore copy() { + return new BusyExprStore(new LinkedHashSet<>(busyExprValueSet)); + } + + @Override + public BusyExprStore leastUpperBound(BusyExprStore other) { + Set busyExprValueSetLub = new LinkedHashSet<>(this.busyExprValueSet); + busyExprValueSetLub.retainAll(other.busyExprValueSet); + + return new BusyExprStore(busyExprValueSetLub); + } + + @Override + public BusyExprStore widenedUpperBound(BusyExprStore previous) { + throw new BugInCF("BusyExprStore.widenedUpperBound was called!"); + } + + @Override + public boolean canAlias(JavaExpression a, JavaExpression b) { + return true; + } + + @Override + public String visualize(CFGVisualizer viz) { + String key = "busy expressions"; + if (busyExprValueSet.isEmpty()) { + return viz.visualizeStoreKeyVal(key, "none"); + } + StringJoiner sjStoreVal = new StringJoiner(", "); + for (BusyExprValue busyExprValue : busyExprValueSet) { + sjStoreVal.add(busyExprValue.toString()); + } + return viz.visualizeStoreKeyVal(key, sjStoreVal.toString()); + } + + @Override + public String toString() { + return busyExprValueSet.toString(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof BusyExprStore)) { + return false; + } + BusyExprStore other = (BusyExprStore) obj; + return other.busyExprValueSet.equals(this.busyExprValueSet); + } + + @Override + public int hashCode() { + return this.busyExprValueSet.hashCode(); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprTransfer.java b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprTransfer.java new file mode 100644 index 000000000000..ab9259c61848 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprTransfer.java @@ -0,0 +1,92 @@ +package org.checkerframework.dataflow.busyexpr; + +import org.checkerframework.dataflow.analysis.BackwardTransferFunction; +import org.checkerframework.dataflow.analysis.RegularTransferResult; +import org.checkerframework.dataflow.analysis.TransferInput; +import org.checkerframework.dataflow.analysis.TransferResult; +import org.checkerframework.dataflow.analysis.UnusedAbstractValue; +import org.checkerframework.dataflow.cfg.UnderlyingAST; +import org.checkerframework.dataflow.cfg.node.AbstractNodeVisitor; +import org.checkerframework.dataflow.cfg.node.AssignmentNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; +import org.checkerframework.dataflow.cfg.node.ReturnNode; + +import java.util.List; + +/** A busy expression transfer function */ +public class BusyExprTransfer + extends AbstractNodeVisitor< + TransferResult, + TransferInput> + implements BackwardTransferFunction { + + @Override + public BusyExprStore initialNormalExitStore( + UnderlyingAST underlyingAST, List returnNodes) { + return new BusyExprStore(); + } + + @Override + public BusyExprStore initialExceptionalExitStore(UnderlyingAST underlyingAST) { + return new BusyExprStore(); + } + + @Override + public RegularTransferResult visitNode( + Node n, TransferInput p) { + return new RegularTransferResult<>(null, p.getRegularStore()); + } + + @Override + public RegularTransferResult visitAssignment( + AssignmentNode n, TransferInput p) { + RegularTransferResult transferResult = + (RegularTransferResult) + super.visitAssignment(n, p); + BusyExprStore store = transferResult.getRegularStore(); + store.killBusyExpr(n.getTarget()); + store.addUseInExpression(n.getExpression()); + return transferResult; + } + + @Override + public RegularTransferResult visitMethodInvocation( + MethodInvocationNode n, TransferInput p) { + RegularTransferResult transferResult = + (RegularTransferResult) + super.visitMethodInvocation(n, p); + BusyExprStore store = transferResult.getRegularStore(); + for (Node arg : n.getArguments()) { + store.addUseInExpression(arg); + } + return transferResult; + } + + @Override + public RegularTransferResult visitObjectCreation( + ObjectCreationNode n, TransferInput p) { + RegularTransferResult transferResult = + (RegularTransferResult) + super.visitObjectCreation(n, p); + BusyExprStore store = transferResult.getRegularStore(); + for (Node arg : n.getArguments()) { + store.addUseInExpression(arg); + } + return transferResult; + } + + @Override + public RegularTransferResult visitReturn( + ReturnNode n, TransferInput p) { + RegularTransferResult transferResult = + (RegularTransferResult) super.visitReturn(n, p); + Node result = n.getResult(); + if (result != null) { + BusyExprStore store = transferResult.getRegularStore(); + store.addUseInExpression(result); + } + return transferResult; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprValue.java b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprValue.java new file mode 100644 index 000000000000..206e7ba0b8b7 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/busyexpr/BusyExprValue.java @@ -0,0 +1,49 @@ +package org.checkerframework.dataflow.busyexpr; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.node.BinaryOperationNode; + +/** + * BusyExprValue class contains a BinaryOperationNode. So we only consider expressions that are in + * form of BinaryOperationNode: lefOperandNode operator rightOperandNode. + * We override {@code .equals} in this class to compare nodes by value equality rather than + * reference equality. We want two different nodes with the same value (that is, two nodes refer to + * the same busy expression in the program) to be regarded as the same. + */ +public class BusyExprValue { + + /** + * A busy expression is represented by a node, which can be a {@link + * org.checkerframework.dataflow.cfg.node.BinaryOperationNode} + */ + protected final BinaryOperationNode busyExpression; + + /** + * Create a new busy expression. + * + * @param n a node + */ + public BusyExprValue(BinaryOperationNode n) { + this.busyExpression = n; + } + + @Override + public String toString() { + return this.busyExpression.toString(); + } + + @Override + public int hashCode() { + return this.busyExpression.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof BusyExprValue)) { + return false; + } + BusyExprValue other = (BusyExprValue) obj; + // Use `equals` to check equality rather than using `==`. + return this.busyExpression.equals(other.busyExpression); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/AbstractCFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/AbstractCFGVisualizer.java deleted file mode 100644 index e10175c36004..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/AbstractCFGVisualizer.java +++ /dev/null @@ -1,500 +0,0 @@ -package org.checkerframework.dataflow.cfg; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Set; -import java.util.StringJoiner; -import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.analysis.AbstractValue; -import org.checkerframework.dataflow.analysis.Analysis; -import org.checkerframework.dataflow.analysis.Analysis.Direction; -import org.checkerframework.dataflow.analysis.Store; -import org.checkerframework.dataflow.analysis.TransferFunction; -import org.checkerframework.dataflow.analysis.TransferInput; -import org.checkerframework.dataflow.cfg.block.Block; -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.node.Node; -import org.checkerframework.javacutil.BugInCF; - -/** - * This abstract class makes implementing a {@link CFGVisualizer} easier. Some of the methods in - * {@link CFGVisualizer} are already implemented in this abstract class, but can be overridden if - * necessary. - * - * @param the abstract value type to be tracked by the analysis - * @param the store type used in the analysis - * @param the transfer function type that is used to approximate runtime behavior - * @see DOTCFGVisualizer - * @see StringCFGVisualizer - */ -public abstract class AbstractCFGVisualizer< - V extends AbstractValue, S extends Store, T extends TransferFunction> - implements CFGVisualizer { - - /** - * Initialized in {@link #init(Map)}. If its value is {@code true}, {@link CFGVisualizer} - * returns more detailed information. - */ - protected boolean verbose; - - /** The line separator. */ - protected final String lineSeparator = System.lineSeparator(); - - /** The indentation for elements of the store. */ - protected final String storeEntryIndent = " "; - - @Override - public void init(Map args) { - Object verb = args.get("verbose"); - this.verbose = - verb != null - && (verb instanceof String - ? Boolean.parseBoolean((String) verb) - : (boolean) verb); - } - - /** - * Visualize a control flow graph. - * - * @param cfg the current control flow graph - * @param entry the entry block of the control flow graph - * @param analysis the current analysis - * @return the representation of the control flow graph - */ - protected String visualizeGraph( - ControlFlowGraph cfg, Block entry, @Nullable Analysis analysis) { - return visualizeGraphHeader() - + visualizeGraphWithoutHeaderAndFooter(cfg, entry, analysis) - + visualizeGraphFooter(); - } - - /** - * Helper method to visualize a control flow graph, without outputting a header or footer. - * - * @param cfg the control flow graph - * @param entry the entry block of the control flow graph - * @param analysis the current analysis - * @return the String representation of the control flow graph - */ - protected String visualizeGraphWithoutHeaderAndFooter( - ControlFlowGraph cfg, Block entry, @Nullable Analysis analysis) { - Set visited = new LinkedHashSet<>(); - StringBuilder sbGraph = new StringBuilder(); - Queue workList = new ArrayDeque<>(); - Block cur = entry; - visited.add(entry); - while (cur != null) { - handleSuccessorsHelper(cur, visited, workList, sbGraph); - cur = workList.poll(); - } - sbGraph.append(visualizeNodes(visited, cfg, analysis)); - return sbGraph.toString(); - } - - /** - * Adds the successors of the current 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 - */ - protected void handleSuccessorsHelper( - Block cur, Set visited, Queue workList, StringBuilder sbGraph) { - if (cur.getType() == Block.BlockType.CONDITIONAL_BLOCK) { - ConditionalBlock ccur = ((ConditionalBlock) cur); - Block thenSuccessor = ccur.getThenSuccessor(); - sbGraph.append( - addEdge( - ccur.getId(), - thenSuccessor.getId(), - ccur.getThenFlowRule().toString())); - addBlock(thenSuccessor, visited, workList); - Block elseSuccessor = ccur.getElseSuccessor(); - sbGraph.append( - addEdge( - ccur.getId(), - elseSuccessor.getId(), - ccur.getElseFlowRule().toString())); - addBlock(elseSuccessor, visited, workList); - } else { - SingleSuccessorBlock sscur = (SingleSuccessorBlock) cur; - Block succ = sscur.getSuccessor(); - if (succ != null) { - sbGraph.append(addEdge(cur.getId(), succ.getId(), sscur.getFlowRule().name())); - addBlock(succ, visited, workList); - } - } - if (cur.getType() == Block.BlockType.EXCEPTION_BLOCK) { - ExceptionBlock ecur = (ExceptionBlock) cur; - for (Map.Entry> e : ecur.getExceptionalSuccessors().entrySet()) { - TypeMirror cause = e.getKey(); - String exception = cause.toString(); - if (exception.startsWith("java.lang.")) { - exception = exception.replace("java.lang.", ""); - } - for (Block b : e.getValue()) { - sbGraph.append(addEdge(cur.getId(), b.getId(), exception)); - addBlock(b, visited, workList); - } - } - } - } - - /** - * Checks whether a block exists in the visited blocks list, and, if not, adds it to the visited - * blocks list and the work list. - * - * @param b the block to check - * @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 - */ - protected void addBlock(Block b, Set visited, Queue workList) { - if (!visited.contains(b)) { - visited.add(b); - workList.add(b); - } - } - - /** - * Helper method to visualize a block. - * - * @param bb the block - * @param analysis the current analysis - * @param escapeString the escape String for the special need of visualization, e.g., "\\l" for - * {@link DOTCFGVisualizer} to keep line left-justification, "\n" for {@link - * StringCFGVisualizer} to simply add a new line - * @return the String representation of the block - */ - protected String visualizeBlockHelper( - Block bb, @Nullable Analysis analysis, String escapeString) { - StringBuilder sbBlock = new StringBuilder(); - sbBlock.append(loopOverBlockContents(bb, analysis, escapeString)); - - if (sbBlock.length() == 0) { - if (bb.getType() == Block.BlockType.SPECIAL_BLOCK) { - sbBlock.append(visualizeSpecialBlock((SpecialBlock) bb)); - } else if (bb.getType() == Block.BlockType.CONDITIONAL_BLOCK) { - sbBlock.append(visualizeConditionalBlock((ConditionalBlock) bb)); - } else { - sbBlock.append(""); - } - } - - // Visualize transfer input if necessary. - if (analysis != null) { - sbBlock.insert(0, visualizeBlockTransferInputBefore(bb, analysis)); - if (verbose) { - Node lastNode = getLastNode(bb); - if (lastNode != null) { - sbBlock.append(visualizeBlockTransferInputAfter(bb, analysis)); - } - } - } - return sbBlock.toString(); - } - - /** - * Iterates over the block content and visualizes all the nodes in it. - * - * @param bb the block - * @param analysis the current analysis - * @param separator the separator between the nodes of the block - * @return the String representation of the contents of the block - */ - protected String loopOverBlockContents( - Block bb, @Nullable Analysis analysis, String separator) { - - List contents = addBlockContent(bb); - StringJoiner sjBlockContents = new StringJoiner(separator, "", separator); - sjBlockContents.setEmptyValue(""); - for (Node t : contents) { - sjBlockContents.add(visualizeBlockNode(t, analysis)); - } - return sjBlockContents.toString(); - } - - /** - * Returns the contents of the block. - * - * @param bb the block - * @return the contents of the block, as a list of nodes - */ - protected List addBlockContent(Block bb) { - switch (bb.getType()) { - case REGULAR_BLOCK: - return ((RegularBlock) bb).getContents(); - case EXCEPTION_BLOCK: - return Collections.singletonList(((ExceptionBlock) bb).getNode()); - case CONDITIONAL_BLOCK: - case SPECIAL_BLOCK: - return Collections.emptyList(); - default: - throw new BugInCF("Unrecognized basic block type: " + bb.getType()); - } - } - - /** - * Format the given object as a String suitable for the output format, i.e. with format-specific - * characters escaped. - * - * @param obj an object - * @return the formatted String from the given object - */ - protected abstract String format(Object obj); - - @Override - public String visualizeBlockNode(Node t, @Nullable Analysis analysis) { - StringBuilder sbBlockNode = new StringBuilder(); - sbBlockNode.append(format(t)).append(" [ ").append(getNodeSimpleName(t)).append(" ]"); - if (analysis != null) { - V value = analysis.getValue(t); - if (value != null) { - sbBlockNode.append(" > ").append(format(value)); - } - } - return sbBlockNode.toString(); - } - - /** - * Visualize the transfer input before the given block. - * - * @param bb the block - * @param analysis the current analysis - * @param escapeString the escape String for the special need of visualization, e.g., "\\l" for - * {@link DOTCFGVisualizer} to keep line left-justification, "\n" for {@link - * StringCFGVisualizer} to simply add a new line - * @return the visualization of the transfer input before the given block - */ - protected String visualizeBlockTransferInputBeforeHelper( - Block bb, Analysis analysis, String escapeString) { - if (analysis == null) { - throw new BugInCF( - "analysis must be non-null when visualizing the transfer input of a block."); - } - - S regularStore; - S thenStore = null; - S elseStore = null; - boolean isTwoStores = false; - - StringBuilder sbStore = new StringBuilder(); - sbStore.append("Before: "); - - Direction analysisDirection = analysis.getDirection(); - - if (analysisDirection == Direction.FORWARD) { - TransferInput input = analysis.getInput(bb); - assert input != null : "@AssumeAssertion(nullness): invariant"; - isTwoStores = input.containsTwoStores(); - regularStore = input.getRegularStore(); - thenStore = input.getThenStore(); - elseStore = input.getElseStore(); - } else { - regularStore = analysis.getResult().getStoreBefore(bb); - } - - if (!isTwoStores) { - sbStore.append(visualizeStore(regularStore)); - } else { - assert thenStore != null : "@AssumeAssertion(nullness): invariant"; - assert elseStore != null : "@AssumeAssertion(nullness): invariant"; - sbStore.append("then="); - sbStore.append(visualizeStore(thenStore)); - sbStore.append(", else="); - sbStore.append(visualizeStore(elseStore)); - } - sbStore.append("~~~~~~~~~").append(escapeString); - return sbStore.toString(); - } - - /** - * Visualize the transfer input after the given block. - * - * @param bb the given block - * @param analysis the current analysis - * @param escapeString the escape String for the special need of visualization, e.g., "\\l" for - * {@link DOTCFGVisualizer} to keep line left-justification, "\n" for {@link - * StringCFGVisualizer} to simply add a new line - * @return the visualization of the transfer input after the given block - */ - protected String visualizeBlockTransferInputAfterHelper( - Block bb, Analysis analysis, String escapeString) { - if (analysis == null) { - throw new BugInCF( - "analysis should be non-null when visualizing the transfer input of a block."); - } - - S regularStore; - S thenStore = null; - S elseStore = null; - boolean isTwoStores = false; - - StringBuilder sbStore = new StringBuilder(); - sbStore.append("After: "); - - Direction analysisDirection = analysis.getDirection(); - - if (analysisDirection == Direction.FORWARD) { - regularStore = analysis.getResult().getStoreAfter(bb); - } else { - TransferInput input = analysis.getInput(bb); - assert input != null : "@AssumeAssertion(nullness): invariant"; - isTwoStores = input.containsTwoStores(); - regularStore = input.getRegularStore(); - thenStore = input.getThenStore(); - elseStore = input.getElseStore(); - } - - if (!isTwoStores) { - sbStore.append(visualizeStore(regularStore)); - } else { - assert thenStore != null : "@AssumeAssertion(nullness): invariant"; - assert elseStore != null : "@AssumeAssertion(nullness): invariant"; - sbStore.append("then="); - sbStore.append(visualizeStore(thenStore)); - sbStore.append(", else="); - sbStore.append(visualizeStore(elseStore)); - } - sbStore.insert(0, "~~~~~~~~~" + escapeString); - return sbStore.toString(); - } - - /** - * Visualize a special block. - * - * @param sbb the special block - * @param separator the separator String to put at the end of the result - * @return the String representation of the special block, followed by the separator - */ - protected String visualizeSpecialBlockHelper(SpecialBlock sbb, String separator) { - switch (sbb.getSpecialType()) { - case ENTRY: - return "" + separator; - case EXIT: - return "" + separator; - case EXCEPTIONAL_EXIT: - return "" + separator; - default: - throw new BugInCF("Unrecognized special block type: " + sbb.getType()); - } - } - - /** - * Returns the last node of a block, or null if none. - * - * @param bb the block - * @return the last node of this block or {@code null} - */ - protected @Nullable Node getLastNode(Block bb) { - switch (bb.getType()) { - case REGULAR_BLOCK: - List blockContents = ((RegularBlock) bb).getContents(); - return blockContents.get(blockContents.size() - 1); - case CONDITIONAL_BLOCK: - case SPECIAL_BLOCK: - return null; - case EXCEPTION_BLOCK: - return ((ExceptionBlock) bb).getNode(); - default: - throw new Error("Unrecognized block type: " + bb.getType()); - } - } - - /** - * Generate the order of processing blocks. Because a block may appears more than once in {@link - * ControlFlowGraph#getDepthFirstOrderedBlocks()}, the orders of each block are stored in a - * separate array list. - * - * @param cfg the current control flow graph - * @return an IdentityHashMap that maps from blocks to their orders - */ - protected IdentityHashMap> getProcessOrder(ControlFlowGraph cfg) { - IdentityHashMap> depthFirstOrder = new IdentityHashMap<>(); - int count = 1; - for (Block b : cfg.getDepthFirstOrderedBlocks()) { - depthFirstOrder.computeIfAbsent(b, k -> new ArrayList<>()); - @SuppressWarnings( - "nullness:assignment.type.incompatible") // computeIfAbsent's function doesn't - // return null - @NonNull List blockIds = depthFirstOrder.get(b); - blockIds.add(count++); - } - return depthFirstOrder; - } - - @Override - public String visualizeStore(S store) { - return store.visualize(this); - } - - /** - * Generate the String representation of the nodes of a control flow graph. - * - * @param blocks the set of all the blocks in a control flow graph - * @param cfg the control flow graph - * @param analysis the current analysis - * @return the String representation of the nodes - */ - protected abstract String visualizeNodes( - Set blocks, ControlFlowGraph cfg, @Nullable Analysis analysis); - - /** - * Generate the String representation of an edge. - * - * @param sId the ID of current block - * @param eId the ID of successor block - * @param flowRule the content of the edge - * @return the String representation of the edge - */ - protected abstract String addEdge(long sId, long eId, String flowRule); - - /** - * Return the header of the generated graph. - * - * @return the String representation of the header of the control flow graph - */ - protected abstract String visualizeGraphHeader(); - - /** - * Return the footer of the generated graph. - * - * @return the String representation of the footer of the control flow graph - */ - 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". - * - * @param order the list of the process order to be processed - * @return the String representation of the process order of the node - */ - protected String getProcessOrderSimpleString(List order) { - return "Process order: " + order.toString().replaceAll("[\\[\\]]", ""); - } - - /** - * Get the simple name of a node. - * - * @param t a node - * @return the node's simple name, without "Node" - */ - protected String getNodeSimpleName(Node t) { - String name = t.getClass().getSimpleName(); - return name.replace("Node", ""); - } -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/CFGBuilder.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/CFGBuilder.java deleted file mode 100644 index f42d5a1d4633..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/CFGBuilder.java +++ /dev/null @@ -1,5050 +0,0 @@ -package org.checkerframework.dataflow.cfg; - -import com.sun.source.tree.AnnotatedTypeTree; -import com.sun.source.tree.AnnotationTree; -import com.sun.source.tree.ArrayAccessTree; -import com.sun.source.tree.ArrayTypeTree; -import com.sun.source.tree.AssertTree; -import com.sun.source.tree.AssignmentTree; -import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.BlockTree; -import com.sun.source.tree.BreakTree; -import com.sun.source.tree.CaseTree; -import com.sun.source.tree.CatchTree; -import com.sun.source.tree.ClassTree; -import com.sun.source.tree.CompilationUnitTree; -import com.sun.source.tree.CompoundAssignmentTree; -import com.sun.source.tree.ConditionalExpressionTree; -import com.sun.source.tree.ContinueTree; -import com.sun.source.tree.DoWhileLoopTree; -import com.sun.source.tree.EmptyStatementTree; -import com.sun.source.tree.EnhancedForLoopTree; -import com.sun.source.tree.ErroneousTree; -import com.sun.source.tree.ExpressionStatementTree; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.ForLoopTree; -import com.sun.source.tree.IdentifierTree; -import com.sun.source.tree.IfTree; -import com.sun.source.tree.ImportTree; -import com.sun.source.tree.InstanceOfTree; -import com.sun.source.tree.LabeledStatementTree; -import com.sun.source.tree.LambdaExpressionTree; -import com.sun.source.tree.LiteralTree; -import com.sun.source.tree.MemberReferenceTree; -import com.sun.source.tree.MemberSelectTree; -import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.MethodTree; -import com.sun.source.tree.ModifiersTree; -import com.sun.source.tree.NewArrayTree; -import com.sun.source.tree.NewClassTree; -import com.sun.source.tree.ParameterizedTypeTree; -import com.sun.source.tree.ParenthesizedTree; -import com.sun.source.tree.PrimitiveTypeTree; -import com.sun.source.tree.ReturnTree; -import com.sun.source.tree.StatementTree; -import com.sun.source.tree.SwitchTree; -import com.sun.source.tree.SynchronizedTree; -import com.sun.source.tree.ThrowTree; -import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; -import com.sun.source.tree.TryTree; -import com.sun.source.tree.TypeCastTree; -import com.sun.source.tree.TypeParameterTree; -import com.sun.source.tree.UnaryTree; -import com.sun.source.tree.UnionTypeTree; -import com.sun.source.tree.VariableTree; -import com.sun.source.tree.WhileLoopTree; -import com.sun.source.tree.WildcardTree; -import com.sun.source.util.TreePath; -import com.sun.source.util.TreePathScanner; -import com.sun.source.util.Trees; -import com.sun.tools.javac.code.Symbol.MethodSymbol; -import com.sun.tools.javac.code.Type; -import com.sun.tools.javac.processing.JavacProcessingEnvironment; -import com.sun.tools.javac.tree.TreeInfo; -import com.sun.tools.javac.util.Context; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -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.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.element.VariableElement; -import javax.lang.model.type.ArrayType; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.PrimitiveType; -import javax.lang.model.type.ReferenceType; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.type.TypeVariable; -import javax.lang.model.type.UnionType; -import javax.lang.model.util.Elements; -import javax.lang.model.util.Types; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.analysis.Store; -import org.checkerframework.dataflow.cfg.CFGBuilder.ExtendedNode.ExtendedNodeType; -import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGMethod; -import org.checkerframework.dataflow.cfg.block.Block; -import org.checkerframework.dataflow.cfg.block.Block.BlockType; -import org.checkerframework.dataflow.cfg.block.BlockImpl; -import org.checkerframework.dataflow.cfg.block.ConditionalBlockImpl; -import org.checkerframework.dataflow.cfg.block.ExceptionBlockImpl; -import org.checkerframework.dataflow.cfg.block.RegularBlockImpl; -import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlockImpl; -import org.checkerframework.dataflow.cfg.block.SpecialBlock.SpecialBlockType; -import org.checkerframework.dataflow.cfg.block.SpecialBlockImpl; -import org.checkerframework.dataflow.cfg.node.ArrayAccessNode; -import org.checkerframework.dataflow.cfg.node.ArrayCreationNode; -import org.checkerframework.dataflow.cfg.node.ArrayTypeNode; -import org.checkerframework.dataflow.cfg.node.AssertionErrorNode; -import org.checkerframework.dataflow.cfg.node.AssignmentNode; -import org.checkerframework.dataflow.cfg.node.BitwiseAndNode; -import org.checkerframework.dataflow.cfg.node.BitwiseComplementNode; -import org.checkerframework.dataflow.cfg.node.BitwiseOrNode; -import org.checkerframework.dataflow.cfg.node.BitwiseXorNode; -import org.checkerframework.dataflow.cfg.node.BooleanLiteralNode; -import org.checkerframework.dataflow.cfg.node.CaseNode; -import org.checkerframework.dataflow.cfg.node.CharacterLiteralNode; -import org.checkerframework.dataflow.cfg.node.ClassDeclarationNode; -import org.checkerframework.dataflow.cfg.node.ClassNameNode; -import org.checkerframework.dataflow.cfg.node.ConditionalAndNode; -import org.checkerframework.dataflow.cfg.node.ConditionalNotNode; -import org.checkerframework.dataflow.cfg.node.ConditionalOrNode; -import org.checkerframework.dataflow.cfg.node.DoubleLiteralNode; -import org.checkerframework.dataflow.cfg.node.EqualToNode; -import org.checkerframework.dataflow.cfg.node.ExplicitThisLiteralNode; -import org.checkerframework.dataflow.cfg.node.FieldAccessNode; -import org.checkerframework.dataflow.cfg.node.FloatLiteralNode; -import org.checkerframework.dataflow.cfg.node.FloatingDivisionNode; -import org.checkerframework.dataflow.cfg.node.FloatingRemainderNode; -import org.checkerframework.dataflow.cfg.node.FunctionalInterfaceNode; -import org.checkerframework.dataflow.cfg.node.GreaterThanNode; -import org.checkerframework.dataflow.cfg.node.GreaterThanOrEqualNode; -import org.checkerframework.dataflow.cfg.node.ImplicitThisLiteralNode; -import org.checkerframework.dataflow.cfg.node.InstanceOfNode; -import org.checkerframework.dataflow.cfg.node.IntegerDivisionNode; -import org.checkerframework.dataflow.cfg.node.IntegerLiteralNode; -import org.checkerframework.dataflow.cfg.node.IntegerRemainderNode; -import org.checkerframework.dataflow.cfg.node.LambdaResultExpressionNode; -import org.checkerframework.dataflow.cfg.node.LeftShiftNode; -import org.checkerframework.dataflow.cfg.node.LessThanNode; -import org.checkerframework.dataflow.cfg.node.LessThanOrEqualNode; -import org.checkerframework.dataflow.cfg.node.LocalVariableNode; -import org.checkerframework.dataflow.cfg.node.LongLiteralNode; -import org.checkerframework.dataflow.cfg.node.MarkerNode; -import org.checkerframework.dataflow.cfg.node.MethodAccessNode; -import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; -import org.checkerframework.dataflow.cfg.node.NarrowingConversionNode; -import org.checkerframework.dataflow.cfg.node.Node; -import org.checkerframework.dataflow.cfg.node.NotEqualNode; -import org.checkerframework.dataflow.cfg.node.NullChkNode; -import org.checkerframework.dataflow.cfg.node.NullLiteralNode; -import org.checkerframework.dataflow.cfg.node.NumericalAdditionNode; -import org.checkerframework.dataflow.cfg.node.NumericalMinusNode; -import org.checkerframework.dataflow.cfg.node.NumericalMultiplicationNode; -import org.checkerframework.dataflow.cfg.node.NumericalPlusNode; -import org.checkerframework.dataflow.cfg.node.NumericalSubtractionNode; -import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; -import org.checkerframework.dataflow.cfg.node.PackageNameNode; -import org.checkerframework.dataflow.cfg.node.ParameterizedTypeNode; -import org.checkerframework.dataflow.cfg.node.PrimitiveTypeNode; -import org.checkerframework.dataflow.cfg.node.ReturnNode; -import org.checkerframework.dataflow.cfg.node.SignedRightShiftNode; -import org.checkerframework.dataflow.cfg.node.StringConcatenateAssignmentNode; -import org.checkerframework.dataflow.cfg.node.StringConcatenateNode; -import org.checkerframework.dataflow.cfg.node.StringConversionNode; -import org.checkerframework.dataflow.cfg.node.StringLiteralNode; -import org.checkerframework.dataflow.cfg.node.SuperNode; -import org.checkerframework.dataflow.cfg.node.SynchronizedNode; -import org.checkerframework.dataflow.cfg.node.TernaryExpressionNode; -import org.checkerframework.dataflow.cfg.node.ThisLiteralNode; -import org.checkerframework.dataflow.cfg.node.ThrowNode; -import org.checkerframework.dataflow.cfg.node.TypeCastNode; -import org.checkerframework.dataflow.cfg.node.UnsignedRightShiftNode; -import org.checkerframework.dataflow.cfg.node.ValueLiteralNode; -import org.checkerframework.dataflow.cfg.node.VariableDeclarationNode; -import org.checkerframework.dataflow.cfg.node.WideningConversionNode; -import org.checkerframework.dataflow.qual.TerminatesExecution; -import org.checkerframework.dataflow.util.IdentityMostlySingleton; -import org.checkerframework.dataflow.util.MostlySingleton; -import org.checkerframework.javacutil.AnnotationProvider; -import org.checkerframework.javacutil.BasicAnnotationProvider; -import org.checkerframework.javacutil.BugInCF; -import org.checkerframework.javacutil.ElementUtils; -import org.checkerframework.javacutil.Pair; -import org.checkerframework.javacutil.TreeUtils; -import org.checkerframework.javacutil.TypesUtils; -import org.checkerframework.javacutil.trees.TreeBuilder; - -/** - * Builds the control flow graph of some Java code (either a method, or an arbitrary statement). - * - *

      The translation of the AST to the CFG is split into three phases: - * - *

        - *
      1. Phase one. In the first phase, the AST is translated into a sequence of {@link - * org.checkerframework.dataflow.cfg.CFGBuilder.ExtendedNode}s. An extended node can either be - * a {@link Node}, or one of several meta elements such as a conditional or unconditional jump - * or a node with additional information about exceptions. Some of the extended nodes contain - * labels (e.g., for the jump target), and phase one additionally creates a mapping from - * labels to extended nodes. Finally, the list of leaders is computed: A leader is an extended - * node which will give rise to a basic block in phase two. - *
      2. Phase two. In this phase, the sequence of extended nodes is translated to a graph - * of control flow blocks that contain nodes. The meta elements from phase one are translated - * into the correct edges. - *
      3. Phase three. The control flow graph generated in phase two can contain degenerate - * basic blocks such as empty regular basic blocks or conditional basic blocks that have the - * same block as both 'then' and 'else' successor. This phase removes these cases while - * preserving the control flow structure. - *
      - */ -public class CFGBuilder { - - /** This class should never be instantiated. Protected to still allow subclasses. */ - protected CFGBuilder() {} - - /** Build the control flow graph of some code. */ - public static ControlFlowGraph build( - CompilationUnitTree root, - UnderlyingAST underlyingAST, - boolean assumeAssertionsEnabled, - boolean assumeAssertionsDisabled, - ProcessingEnvironment env) { - TreeBuilder builder = new TreeBuilder(env); - AnnotationProvider annotationProvider = new BasicAnnotationProvider(); - PhaseOneResult phase1result = - new CFGTranslationPhaseOne( - builder, - annotationProvider, - assumeAssertionsEnabled, - assumeAssertionsDisabled, - env) - .process(root, underlyingAST); - ControlFlowGraph phase2result = CFGTranslationPhaseTwo.process(phase1result); - ControlFlowGraph phase3result = CFGTranslationPhaseThree.process(phase2result); - return phase3result; - } - - /** - * Build the control flow graph of some code (method, initializer block, ...). bodyPath is the - * TreePath to the body of that code. - */ - public static ControlFlowGraph build( - TreePath bodyPath, - UnderlyingAST underlyingAST, - boolean assumeAssertionsEnabled, - boolean assumeAssertionsDisabled, - ProcessingEnvironment env) { - TreeBuilder builder = new TreeBuilder(env); - AnnotationProvider annotationProvider = new BasicAnnotationProvider(); - PhaseOneResult phase1result = - new CFGTranslationPhaseOne( - builder, - annotationProvider, - assumeAssertionsEnabled, - assumeAssertionsDisabled, - env) - .process(bodyPath, underlyingAST); - ControlFlowGraph phase2result = CFGTranslationPhaseTwo.process(phase1result); - ControlFlowGraph phase3result = CFGTranslationPhaseThree.process(phase2result); - return phase3result; - } - - /** Build the control flow graph of some code. */ - public static ControlFlowGraph build( - CompilationUnitTree root, UnderlyingAST underlyingAST, ProcessingEnvironment env) { - return build(root, underlyingAST, false, false, env); - } - - /** Build the control flow graph of a method. */ - public static ControlFlowGraph build( - CompilationUnitTree root, - MethodTree tree, - @Nullable ClassTree classTree, - @Nullable ProcessingEnvironment env) { - UnderlyingAST underlyingAST = new CFGMethod(tree, classTree); - return build(root, underlyingAST, false, false, env); - } - - /** - * 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. - *
      - */ - protected abstract static class ExtendedNode { - - /** The basic block this extended node belongs to (as determined in phase two). */ - protected BlockImpl block; - - /** Type of this node. */ - protected final ExtendedNodeType type; - - /** Does this node terminate the execution? (e.g., "System.exit()") */ - protected boolean terminatesExecution = false; - - /** - * Create a new ExtendedNode. - * - * @param type the type of this node - */ - protected ExtendedNode(ExtendedNodeType type) { - this.type = type; - } - - /** Extended node types (description see above). */ - public enum ExtendedNodeType { - NODE, - EXCEPTION_NODE, - UNCONDITIONAL_JUMP, - CONDITIONAL_JUMP - } - - public ExtendedNodeType getType() { - return type; - } - - public boolean getTerminatesExecution() { - return terminatesExecution; - } - - public void setTerminatesExecution(boolean terminatesExecution) { - this.terminatesExecution = terminatesExecution; - } - - /** - * Returns the node contained in this extended node (only applicable if the type is {@code - * NODE} or {@code EXCEPTION_NODE}). - * - * @return the node contained in this extended node (only applicable if the type is {@code - * NODE} or {@code EXCEPTION_NODE}) - */ - public Node getNode() { - assert false; - return null; - } - - /** - * Returns the label associated with this extended node (only applicable if type is {@link - * ExtendedNodeType#CONDITIONAL_JUMP} or {@link ExtendedNodeType#UNCONDITIONAL_JUMP}). - * - * @return the label associated with this extended node (only applicable if type is {@link - * ExtendedNodeType#CONDITIONAL_JUMP} or {@link ExtendedNodeType#UNCONDITIONAL_JUMP}) - */ - public Label getLabel() { - assert false; - return null; - } - - public BlockImpl getBlock() { - return block; - } - - public void setBlock(BlockImpl b) { - this.block = b; - } - - @Override - public String toString() { - throw new BugInCF("DO NOT CALL ExtendedNode.toString(). Write your own."); - } - } - - /** An extended node of type {@code NODE}. */ - protected static class NodeHolder extends ExtendedNode { - - /** The node to hold. */ - protected final Node node; - - /** - * Construct a NodeHolder for the given Node. - * - * @param node the node to hold - */ - public NodeHolder(Node node) { - super(ExtendedNodeType.NODE); - this.node = node; - } - - @Override - public Node getNode() { - return node; - } - - @Override - public String toString() { - return "NodeHolder(" + node + ")"; - } - } - - /** An extended node of type {@code EXCEPTION_NODE}. */ - protected static class NodeWithExceptionsHolder extends ExtendedNode { - - /** The node to hold. */ - protected final Node node; - - /** - * Map from exception type to labels of successors that may be reached as a result of that - * exception. - */ - protected final Map> exceptions; - - /** - * Construct a NodeWithExceptionsHolder for the given node and exceptions. - * - * @param node the node to hold - * @param exceptions the exceptions to hold - */ - public NodeWithExceptionsHolder(Node node, Map> exceptions) { - super(ExtendedNodeType.EXCEPTION_NODE); - this.node = node; - this.exceptions = exceptions; - } - - /** - * Get the exceptions for the node. - * - * @return exceptions for the node - */ - public Map> getExceptions() { - return exceptions; - } - - @Override - public Node getNode() { - return node; - } - - @Override - public String toString() { - return "NodeWithExceptionsHolder(" + node + ")"; - } - } - - /** - * An extended node of type {@link ExtendedNodeType#CONDITIONAL_JUMP}. - * - *

      Important: In the list of extended nodes, there should not be any labels that - * point to a conditional jump. Furthermore, the node directly ahead of any conditional jump has - * to be a {@link NodeWithExceptionsHolder} or {@link NodeHolder}, and the node held by that - * extended node is required to be of boolean type. - */ - protected static class ConditionalJump extends ExtendedNode { - - /** The true successor label. */ - protected final Label trueSucc; - /** The false successor label. */ - protected final Label falseSucc; - - /** The true branch flow rule. */ - protected Store.FlowRule trueFlowRule; - /** The false branch flow rule. */ - protected Store.FlowRule falseFlowRule; - - /** - * Construct a ConditionalJump. - * - * @param trueSucc true successor label - * @param falseSucc false successor label - */ - public ConditionalJump(Label trueSucc, Label falseSucc) { - super(ExtendedNodeType.CONDITIONAL_JUMP); - assert trueSucc != null; - this.trueSucc = trueSucc; - assert falseSucc != null; - this.falseSucc = falseSucc; - } - - public Label getThenLabel() { - return trueSucc; - } - - public Label getElseLabel() { - return falseSucc; - } - - public Store.FlowRule getTrueFlowRule() { - return trueFlowRule; - } - - public Store.FlowRule getFalseFlowRule() { - return falseFlowRule; - } - - public void setTrueFlowRule(Store.FlowRule rule) { - trueFlowRule = rule; - } - - public void setFalseFlowRule(Store.FlowRule rule) { - falseFlowRule = rule; - } - - /** - * Produce a string representation. - * - * @return a string representation - * @see org.checkerframework.dataflow.cfg.CFGBuilder.PhaseOneResult#nodeToString - */ - @Override - public String toString() { - return "TwoTargetConditionalJump(" + getThenLabel() + ", " + getElseLabel() + ")"; - } - } - - /** An extended node of type {@link ExtendedNodeType#UNCONDITIONAL_JUMP}. */ - protected static class UnconditionalJump extends ExtendedNode { - - /** The jump target label. */ - protected final Label jumpTarget; - - /** - * Construct an UnconditionalJump. - * - * @param jumpTarget the jump target label - */ - public UnconditionalJump(Label jumpTarget) { - super(ExtendedNodeType.UNCONDITIONAL_JUMP); - assert jumpTarget != null; - this.jumpTarget = jumpTarget; - } - - @Override - public Label getLabel() { - return jumpTarget; - } - - /** - * Produce a string representation. - * - * @return a string representation - * @see org.checkerframework.dataflow.cfg.CFGBuilder.PhaseOneResult#nodeToString - */ - @Override - public String toString() { - return "JumpMarker(" + getLabel() + ")"; - } - } - - /** - * A label is used to refer to other extended nodes using a mapping from labels to extended - * nodes. Labels get their names either from labeled statements in the source code or from - * internally generated unique names. - */ - protected static class Label { - private static int uid = 0; - - protected final String name; - - public Label(String name) { - this.name = name; - } - - public Label() { - this.name = uniqueName(); - } - - @Override - public String toString() { - return name; - } - - /** - * Return a new unique label name that cannot be confused with a Java source code label. - * - * @return a new unique label name - */ - private static String uniqueName() { - return "%L" + uid++; - } - } - - /** - * A TryFrame takes a thrown exception type and maps it to a set of possible control-flow - * successors. - */ - protected static interface TryFrame { - /** - * Given a type of thrown exception, add the set of possible control flow successor {@link - * Label}s to the argument set. Return true if the exception is known to be caught by one of - * those labels and false if it may propagate still further. - */ - public boolean possibleLabels(TypeMirror thrown, Set

        - *
      1. Empty regular basic blocks: These blocks will be removed and their predecessors linked - * directly to the successor. - *
      2. Conditional basic blocks that have the same basic block as the 'then' and 'else' - * successor: The conditional basic block will be removed in this case. - *
      3. Two consecutive, non-empty, regular basic blocks where the second block has exactly one - * predecessor (namely the other of the two blocks): In this case, the two blocks are - * merged. - *
      4. Some basic blocks might not be reachable from the entryBlock. These basic blocks are - * removed, and the list of predecessors (in the doubly-linked structure of basic blocks) - * are adapted correctly. - *
      - * - * Eliminating the second type of degenerate cases might introduce cases of the third problem. - * These are also removed. - */ - protected static class CFGTranslationPhaseThree { - - /** - * A simple wrapper object that holds a basic block and allows to set one of its successors. - */ - protected interface PredecessorHolder { - void setSuccessor(BlockImpl b); - - BlockImpl getBlock(); - } - - /** - * Perform phase three on the control flow graph {@code cfg}. - * - * @param cfg the control flow graph. Ownership is transfered to this method and the caller - * is not allowed to read or modify {@code cfg} after the call to {@code process} any - * more. - * @return the resulting control flow graph - */ - public static ControlFlowGraph process(ControlFlowGraph cfg) { - Set worklist = cfg.getAllBlocks(); - Set dontVisit = new HashSet<>(); - - // note: this method has to be careful when relinking basic blocks - // to not forget to adjust the predecessors, too - - // fix predecessor lists by removing any unreachable predecessors - for (Block c : worklist) { - BlockImpl cur = (BlockImpl) c; - for (BlockImpl pred : new HashSet<>(cur.getPredecessors())) { - if (!worklist.contains(pred)) { - cur.removePredecessor(pred); - } - } - } - - // remove empty blocks - for (Block cur : worklist) { - if (dontVisit.contains(cur)) { - continue; - } - - if (cur.getType() == BlockType.REGULAR_BLOCK) { - RegularBlockImpl b = (RegularBlockImpl) cur; - if (b.isEmpty()) { - Set empty = new HashSet<>(); - Set predecessors = new HashSet<>(); - BlockImpl succ = computeNeighborhoodOfEmptyBlock(b, empty, predecessors); - for (RegularBlockImpl e : empty) { - succ.removePredecessor(e); - dontVisit.add(e); - } - for (PredecessorHolder p : predecessors) { - BlockImpl block = p.getBlock(); - dontVisit.add(block); - succ.removePredecessor(block); - p.setSuccessor(succ); - } - } - } - } - - // remove useless conditional blocks - /* Issue 3267 revealed that this is a dangerous optimization: - it merges a block that evaluates one condition onto an unrelated following block, - which can also be a condition. The then/else stores from the first block are still - set, leading to incorrect results for the then/else stores in the following block. - The correct result would be to merge the then/else stores from the previous block. - However, as this is late in the CFG construction, I didn't see how to add e.g. a - dummy variable declaration node in a dummy regular block, which would cause a merge. - So for now, let's not perform this optimization. - It would be interesting to know how large the impact of this optimization is. - - worklist = cfg.getAllBlocks(); - for (Block c : worklist) { - BlockImpl cur = (BlockImpl) c; - - if (cur.getType() == BlockType.CONDITIONAL_BLOCK) { - ConditionalBlockImpl cb = (ConditionalBlockImpl) cur; - assert cb.getPredecessors().size() == 1; - if (cb.getThenSuccessor() == cb.getElseSuccessor()) { - BlockImpl pred = cb.getPredecessors().iterator().next(); - PredecessorHolder predecessorHolder = getPredecessorHolder(pred, cb); - BlockImpl succ = (BlockImpl) cb.getThenSuccessor(); - succ.removePredecessor(cb); - predecessorHolder.setSuccessor(succ); - } - } - } - */ - - // merge consecutive basic blocks if possible - worklist = cfg.getAllBlocks(); - for (Block cur : worklist) { - if (cur.getType() == BlockType.REGULAR_BLOCK) { - RegularBlockImpl b = (RegularBlockImpl) cur; - Block succ = b.getRegularSuccessor(); - if (succ.getType() == BlockType.REGULAR_BLOCK) { - RegularBlockImpl rs = (RegularBlockImpl) succ; - if (rs.getPredecessors().size() == 1) { - b.setSuccessor(rs.getRegularSuccessor()); - b.addNodes(rs.getContents()); - rs.getRegularSuccessor().removePredecessor(rs); - } - } - } - } - return cfg; - } - - /** - * Compute the set of empty regular basic blocks {@code empty}, starting at {@code start} - * and going both forward and backwards. Furthermore, compute the predecessors of these - * empty blocks ({@code predecessors} ), and their single successor (return value). - * - * @param start the starting point of the search (an empty, regular basic block) - * @param empty an empty set to be filled by this method with all empty basic blocks found - * (including {@code start}). - * @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 - */ - protected static BlockImpl computeNeighborhoodOfEmptyBlock( - RegularBlockImpl start, - Set empty, - Set predecessors) { - - // get empty neighborhood that come before 'start' - computeNeighborhoodOfEmptyBlockBackwards(start, empty, predecessors); - - // go forward - BlockImpl succ = (BlockImpl) start.getSuccessor(); - while (succ.getType() == BlockType.REGULAR_BLOCK) { - RegularBlockImpl cur = (RegularBlockImpl) succ; - if (cur.isEmpty()) { - computeNeighborhoodOfEmptyBlockBackwards(cur, empty, predecessors); - assert empty.contains(cur) : "cur ought to be in empty"; - succ = (BlockImpl) cur.getSuccessor(); - if (succ == cur) { - // An infinite loop, making exit block unreachable - break; - } - } else { - break; - } - } - return succ; - } - - /** - * Compute the set of empty regular basic blocks {@code empty}, starting at {@code start} - * and looking only backwards in the control flow graph. Furthermore, compute the - * predecessors of these empty blocks ( {@code predecessors}). - * - * @param start the starting point of the search (an empty, regular basic block) - * @param empty a set to be filled by this method with all empty basic blocks found - * (including {@code start}). - * @param predecessors a set to be filled by this method with all predecessors - */ - protected static void computeNeighborhoodOfEmptyBlockBackwards( - RegularBlockImpl start, - Set empty, - Set predecessors) { - - RegularBlockImpl cur = start; - empty.add(cur); - for (final BlockImpl pred : cur.getPredecessors()) { - switch (pred.getType()) { - case SPECIAL_BLOCK: - // add pred correctly to predecessor list - predecessors.add(getPredecessorHolder(pred, cur)); - break; - case CONDITIONAL_BLOCK: - // add pred correctly to predecessor list - predecessors.add(getPredecessorHolder(pred, cur)); - break; - case EXCEPTION_BLOCK: - // add pred correctly to predecessor list - predecessors.add(getPredecessorHolder(pred, cur)); - break; - case REGULAR_BLOCK: - RegularBlockImpl r = (RegularBlockImpl) pred; - if (r.isEmpty()) { - // recursively look backwards - if (!empty.contains(r)) { - computeNeighborhoodOfEmptyBlockBackwards(r, empty, predecessors); - } - } else { - // add pred correctly to predecessor list - predecessors.add(getPredecessorHolder(pred, cur)); - } - break; - } - } - } - - /** - * Return a predecessor holder that can be used to set the successor of {@code pred} in the - * 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). - */ - protected static PredecessorHolder getPredecessorHolder( - final BlockImpl pred, final BlockImpl cur) { - switch (pred.getType()) { - case SPECIAL_BLOCK: - SingleSuccessorBlockImpl s = (SingleSuccessorBlockImpl) pred; - return singleSuccessorHolder(s, cur); - case CONDITIONAL_BLOCK: - // add pred correctly to predecessor list - final ConditionalBlockImpl c = (ConditionalBlockImpl) pred; - if (c.getThenSuccessor() == cur) { - return new PredecessorHolder() { - @Override - public void setSuccessor(BlockImpl b) { - c.setThenSuccessor(b); - cur.removePredecessor(pred); - } - - @Override - public BlockImpl getBlock() { - return c; - } - }; - } else { - assert c.getElseSuccessor() == cur; - return new PredecessorHolder() { - @Override - public void setSuccessor(BlockImpl b) { - c.setElseSuccessor(b); - cur.removePredecessor(pred); - } - - @Override - public BlockImpl getBlock() { - return c; - } - }; - } - case EXCEPTION_BLOCK: - // add pred correctly to predecessor list - final ExceptionBlockImpl e = (ExceptionBlockImpl) pred; - if (e.getSuccessor() == cur) { - return singleSuccessorHolder(e, cur); - } else { - @SuppressWarnings( - "keyfor:assignment.type.incompatible") // ignore keyfor type - Set>> entrySet = - e.getExceptionalSuccessors().entrySet(); - for (final Map.Entry> entry : entrySet) { - if (entry.getValue().contains(cur)) { - return new PredecessorHolder() { - @Override - public void setSuccessor(BlockImpl b) { - e.addExceptionalSuccessor(b, entry.getKey()); - cur.removePredecessor(pred); - } - - @Override - public BlockImpl getBlock() { - return e; - } - }; - } - } - } - assert false; - break; - case REGULAR_BLOCK: - RegularBlockImpl r = (RegularBlockImpl) pred; - return singleSuccessorHolder(r, cur); - } - return null; - } - - /** - * Returns a {@link PredecessorHolder} that sets the successor of a single successor block - * {@code s}. - * - * @return a {@link PredecessorHolder} that sets the successor of a single successor block - * {@code s} - */ - protected static PredecessorHolder singleSuccessorHolder( - final SingleSuccessorBlockImpl s, final BlockImpl old) { - return new PredecessorHolder() { - @Override - public void setSuccessor(BlockImpl b) { - s.setSuccessor(b); - old.removePredecessor(s); - } - - @Override - public BlockImpl getBlock() { - return s; - } - }; - } - } - - /* --------------------------------------------------------- */ - /* Phase Two */ - /* --------------------------------------------------------- */ - - /** Tuple class with up to three members. */ - protected static class Tuple { - public final A a; - public final B b; - public final C c; - - public Tuple(A a, B b) { - this(a, b, null); - } - - public Tuple(A a, B b, C c) { - this.a = a; - this.b = b; - this.c = c; - } - - @Override - public String toString() { - return "Tuple<" + a + ", " + b + ", " + c + ">"; - } - } - - /** Class that performs phase two of the translation process. */ - protected static class CFGTranslationPhaseTwo { - - private CFGTranslationPhaseTwo() {} - - /** - * Perform phase two of the translation. - * - * @param in the result of phase one - * @return a control flow graph that might still contain degenerate basic block (such as - * empty regular basic blocks or conditional blocks with the same block as 'then' and - * 'else' successor) - */ - public static ControlFlowGraph process(PhaseOneResult in) { - - Map bindings = in.bindings; - ArrayList nodeList = in.nodeList; - Set leaders = in.leaders; - - assert !in.nodeList.isEmpty(); - - // exit blocks - SpecialBlockImpl regularExitBlock = new SpecialBlockImpl(SpecialBlockType.EXIT); - SpecialBlockImpl exceptionalExitBlock = - new SpecialBlockImpl(SpecialBlockType.EXCEPTIONAL_EXIT); - - // record missing edges that will be added later - Set> missingEdges = - new MostlySingleton<>(); - - // missing exceptional edges - Set> missingExceptionalEdges = - new HashSet<>(); - - // create start block - SpecialBlockImpl startBlock = new SpecialBlockImpl(SpecialBlockType.ENTRY); - missingEdges.add(new Tuple<>(startBlock, 0)); - - // loop through all 'leaders' (while dynamically detecting the - // leaders) - RegularBlockImpl block = new RegularBlockImpl(); - int i = 0; - for (ExtendedNode node : nodeList) { - switch (node.getType()) { - case NODE: - if (leaders.contains(i)) { - RegularBlockImpl b = new RegularBlockImpl(); - block.setSuccessor(b); - block = b; - } - block.addNode(node.getNode()); - node.setBlock(block); - - // does this node end the execution (modeled as an edge to - // the exceptional exit block) - boolean terminatesExecution = node.getTerminatesExecution(); - if (terminatesExecution) { - block.setSuccessor(exceptionalExitBlock); - block = new RegularBlockImpl(); - } - break; - case CONDITIONAL_JUMP: - { - ConditionalJump cj = (ConditionalJump) node; - // Exception nodes may fall through to conditional jumps, - // so we set the block which is required for the insertion - // of missing edges. - node.setBlock(block); - assert block != null; - final ConditionalBlockImpl cb = new ConditionalBlockImpl(); - if (cj.getTrueFlowRule() != null) { - cb.setThenFlowRule(cj.getTrueFlowRule()); - } - if (cj.getFalseFlowRule() != null) { - cb.setElseFlowRule(cj.getFalseFlowRule()); - } - block.setSuccessor(cb); - block = new RegularBlockImpl(); - // use two anonymous SingleSuccessorBlockImpl that set the - // 'then' and 'else' successor of the conditional block - final Label thenLabel = cj.getThenLabel(); - final Label elseLabel = cj.getElseLabel(); - Integer target = bindings.get(thenLabel); - assert target != null; - missingEdges.add( - new Tuple<>( - new SingleSuccessorBlockImpl(BlockType.REGULAR_BLOCK) { - @Override - public void setSuccessor(BlockImpl successor) { - cb.setThenSuccessor(successor); - } - }, - target)); - target = bindings.get(elseLabel); - assert target != null; - missingEdges.add( - new Tuple<>( - new SingleSuccessorBlockImpl(BlockType.REGULAR_BLOCK) { - @Override - public void setSuccessor(BlockImpl successor) { - cb.setElseSuccessor(successor); - } - }, - target)); - break; - } - case UNCONDITIONAL_JUMP: - if (leaders.contains(i)) { - RegularBlockImpl b = new RegularBlockImpl(); - block.setSuccessor(b); - block = b; - } - node.setBlock(block); - if (node.getLabel() == in.regularExitLabel) { - block.setSuccessor(regularExitBlock); - } else if (node.getLabel() == in.exceptionalExitLabel) { - block.setSuccessor(exceptionalExitBlock); - } else { - Integer target = bindings.get(node.getLabel()); - assert target != null; - missingEdges.add(new Tuple<>(block, target)); - } - block = new RegularBlockImpl(); - break; - case EXCEPTION_NODE: - NodeWithExceptionsHolder en = (NodeWithExceptionsHolder) node; - // create new exception block and link with previous block - ExceptionBlockImpl e = new ExceptionBlockImpl(); - Node nn = en.getNode(); - e.setNode(nn); - node.setBlock(e); - block.setSuccessor(e); - block = new RegularBlockImpl(); - - // ensure linking between e and next block (normal edge) - // Note: do not link to the next block for throw statements - // (these throw exceptions for sure) - if (!node.getTerminatesExecution()) { - missingEdges.add(new Tuple<>(e, i + 1)); - } - - // exceptional edges - for (Map.Entry> entry : - en.getExceptions().entrySet()) { - TypeMirror cause = entry.getKey(); - for (Label label : entry.getValue()) { - Integer target = bindings.get(label); - // TODO: This is sometimes null; is this a problem? - // assert target != null; - missingExceptionalEdges.add(new Tuple<>(e, target, cause)); - } - } - break; - } - i++; - } - - // add missing edges - for (Tuple p : missingEdges) { - Integer index = p.b; - assert index != null : "CFGBuilder: problem in CFG construction " + p.a; - ExtendedNode extendedNode = nodeList.get(index); - BlockImpl target = extendedNode.getBlock(); - SingleSuccessorBlockImpl source = p.a; - source.setSuccessor(target); - } - - // add missing exceptional edges - for (Tuple p : missingExceptionalEdges) { - Integer index = p.b; - TypeMirror cause = (TypeMirror) p.c; - ExceptionBlockImpl source = p.a; - if (index == null) { - // edge to exceptional exit - source.addExceptionalSuccessor(exceptionalExitBlock, cause); - } else { - // edge to specific target - ExtendedNode extendedNode = nodeList.get(index); - BlockImpl target = extendedNode.getBlock(); - source.addExceptionalSuccessor(target, cause); - } - } - - return new ControlFlowGraph( - startBlock, - regularExitBlock, - exceptionalExitBlock, - in.underlyingAST, - in.treeLookupMap, - in.convertedTreeLookupMap, - in.unaryAssignNodeLookupMap, - in.returnNodes, - in.declaredClasses, - in.declaredLambdas); - } - } - - /* --------------------------------------------------------- */ - /* Phase One */ - /* --------------------------------------------------------- */ - - /** - * A wrapper object to pass around the result of phase one. For a documentation of the fields - * see {@link CFGTranslationPhaseOne}. - */ - protected static class PhaseOneResult { - - private final IdentityHashMap> treeLookupMap; - private final IdentityHashMap> convertedTreeLookupMap; - private final IdentityHashMap unaryAssignNodeLookupMap; - private final UnderlyingAST underlyingAST; - private final Map bindings; - private final ArrayList nodeList; - private final Set leaders; - private final List returnNodes; - private final Label regularExitLabel; - private final Label exceptionalExitLabel; - private final List declaredClasses; - private final List declaredLambdas; - - public PhaseOneResult( - UnderlyingAST underlyingAST, - IdentityHashMap> treeLookupMap, - IdentityHashMap> convertedTreeLookupMap, - IdentityHashMap unaryAssignNodeLookupMap, - ArrayList nodeList, - Map bindings, - Set leaders, - List returnNodes, - Label regularExitLabel, - Label exceptionalExitLabel, - List declaredClasses, - List declaredLambdas) { - this.underlyingAST = underlyingAST; - this.treeLookupMap = treeLookupMap; - this.convertedTreeLookupMap = convertedTreeLookupMap; - this.unaryAssignNodeLookupMap = unaryAssignNodeLookupMap; - this.nodeList = nodeList; - this.bindings = bindings; - this.leaders = leaders; - this.returnNodes = returnNodes; - this.regularExitLabel = regularExitLabel; - this.exceptionalExitLabel = exceptionalExitLabel; - this.declaredClasses = declaredClasses; - this.declaredLambdas = declaredLambdas; - } - - @Override - public String toString() { - StringJoiner sj = new StringJoiner(System.lineSeparator()); - for (ExtendedNode n : nodeList) { - sj.add(nodeToString(n)); - } - return sj.toString(); - } - - protected String nodeToString(ExtendedNode n) { - if (n.getType() == ExtendedNodeType.CONDITIONAL_JUMP) { - ConditionalJump t = (ConditionalJump) n; - return "TwoTargetConditionalJump(" - + resolveLabel(t.getThenLabel()) - + ", " - + resolveLabel(t.getElseLabel()) - + ")"; - } else if (n.getType() == ExtendedNodeType.UNCONDITIONAL_JUMP) { - return "UnconditionalJump(" + resolveLabel(n.getLabel()) + ")"; - } else { - return n.toString(); - } - } - - private String resolveLabel(Label label) { - Integer index = bindings.get(label); - if (index == null) { - return "unbound label: " + label; - } - return nodeToString(nodeList.get(index)); - } - } - - /** - * Class that performs phase one of the translation process. It generates the following - * information: - * - *
        - *
      • A sequence of extended nodes. - *
      • A set of bindings from {@link Label}s to positions in the node sequence. - *
      • A set of leader nodes that give rise to basic blocks in phase two. - *
      • A lookup map that gives the mapping from AST tree nodes to {@link Node}s. - *
      - * - *

      The return type of this scanner is {@link Node}. For expressions, the corresponding node - * is returned to allow linking between different nodes. - * - *

      However, for statements there is usually no single {@link Node} that is created, and thus - * no node is returned (rather, null is returned). - * - *

      Every {@code visit*} method is assumed to add at least one extended node to the list of - * nodes (which might only be a jump). - */ - protected static class CFGTranslationPhaseOne extends TreePathScanner { - - /** Annotation processing environment and its associated type and tree utilities. */ - protected final ProcessingEnvironment env; - - protected final Elements elements; - protected final Types types; - protected final Trees trees; - protected final TreeBuilder treeBuilder; - protected final AnnotationProvider annotationProvider; - - /** Can assertions be assumed to be disabled? */ - protected final boolean assumeAssertionsDisabled; - - /** Can assertions be assumed to be enabled? */ - protected final boolean assumeAssertionsEnabled; - - /* --------------------------------------------------------- */ - /* Extended Node Types and Labels */ - /* --------------------------------------------------------- */ - - /** Special label to identify the regular exit. */ - protected final Label regularExitLabel; - - /** Special label to identify the exceptional exit. */ - protected final Label exceptionalExitLabel; - - /** - * Current {@link TryFinallyScopeCell} to which a return statement should jump, or null if - * there is no valid destination. - */ - protected @Nullable TryFinallyScopeCell returnTargetL; - - /** - * Current {@link TryFinallyScopeCell} to which a break statement with no label should jump, - * or null if there is no valid destination. - */ - protected @Nullable TryFinallyScopeCell breakTargetL; - - /** - * Map from AST label Names to CFG {@link Label}s for breaks. Each labeled statement creates - * two CFG {@link Label}s, one for break and one for continue. - */ - protected Map breakLabels; - - /** - * Current {@link TryFinallyScopeCell} to which a continue statement with no label should - * jump, or null if there is no valid destination. - */ - protected @Nullable TryFinallyScopeCell continueTargetL; - - /** - * Map from AST label Names to CFG {@link Label}s for continues. Each labeled statement - * creates two CFG {@link Label}s, one for break and one for continue. - */ - protected Map continueLabels; - - /** Nested scopes of try-catch blocks in force at the current program point. */ - private final TryStack tryStack; - - /** - * Maps from AST {@link Tree}s to sets of {@link Node}s. Every Tree that produces a value - * will have at least one corresponding Node. Trees that undergo conversions, such as boxing - * or unboxing, can map to two distinct Nodes. The Node for the pre-conversion value is - * stored in the treeLookupMap, while the Node for the post-conversion value is stored in - * the convertedTreeLookupMap. - */ - protected final IdentityHashMap> treeLookupMap; - - /** Map from AST {@link Tree}s to post-conversion sets of {@link Node}s. */ - protected final IdentityHashMap> convertedTreeLookupMap; - - /** Map from AST {@link UnaryTree}s to compound {@link AssignmentNode}s. */ - protected final IdentityHashMap unaryAssignNodeLookupMap; - - /** The list of extended nodes. */ - protected final ArrayList nodeList; - - /** The bindings of labels to positions (i.e., indices) in the {@code nodeList}. */ - protected final Map bindings; - - /** The set of leaders (represented as indices into {@code nodeList}). */ - protected final Set leaders; - - /** - * All return nodes (if any) encountered. Only includes return statements that actually - * return something - */ - private final List returnNodes; - - /** - * Class declarations that have been encountered when building the control-flow graph for a - * method. - */ - protected final List declaredClasses; - - /** - * Lambdas encountered when building the control-flow graph for a method, variable - * initializer, or initializer. - */ - protected final List declaredLambdas; - - /** - * @param treeBuilder builder for new AST nodes - * @param annotationProvider extracts annotations from AST nodes - * @param assumeAssertionsDisabled can assertions be assumed to be disabled? - * @param assumeAssertionsEnabled can assertions be assumed to be enabled? - * @param env annotation processing environment containing type utilities - */ - public CFGTranslationPhaseOne( - TreeBuilder treeBuilder, - AnnotationProvider annotationProvider, - boolean assumeAssertionsEnabled, - boolean assumeAssertionsDisabled, - ProcessingEnvironment env) { - this.env = env; - this.treeBuilder = treeBuilder; - this.annotationProvider = annotationProvider; - - assert !(assumeAssertionsDisabled && assumeAssertionsEnabled); - this.assumeAssertionsEnabled = assumeAssertionsEnabled; - this.assumeAssertionsDisabled = assumeAssertionsDisabled; - - elements = env.getElementUtils(); - types = env.getTypeUtils(); - trees = Trees.instance(env); - - // initialize lists and maps - treeLookupMap = new IdentityHashMap<>(); - convertedTreeLookupMap = new IdentityHashMap<>(); - unaryAssignNodeLookupMap = new IdentityHashMap<>(); - nodeList = new ArrayList<>(); - bindings = new HashMap<>(); - leaders = new HashSet<>(); - - regularExitLabel = new Label(); - exceptionalExitLabel = new Label(); - tryStack = new TryStack(exceptionalExitLabel); - returnTargetL = new TryFinallyScopeCell(regularExitLabel); - breakLabels = new HashMap<>(); - continueLabels = new HashMap<>(); - returnNodes = new ArrayList<>(); - declaredClasses = new ArrayList<>(); - declaredLambdas = new ArrayList<>(); - } - - /** - * Performs the actual work of phase one. - * - * @param bodyPath path to the body of the underlying AST's method - * @param underlyingAST the AST for which the CFG is to be built - * @return the result of phase one - */ - public PhaseOneResult process(TreePath bodyPath, UnderlyingAST underlyingAST) { - // traverse AST of the method body - Node finalNode = scan(bodyPath, null); - - // If we are building the CFG for a lambda with a single expression as the body, then - // add an extra node for the result of that lambda - if (underlyingAST.getKind() == UnderlyingAST.Kind.LAMBDA) { - LambdaExpressionTree lambdaTree = - ((UnderlyingAST.CFGLambda) underlyingAST).getLambdaTree(); - if (lambdaTree.getBodyKind() == LambdaExpressionTree.BodyKind.EXPRESSION) { - Node resultNode = - new LambdaResultExpressionNode( - (ExpressionTree) lambdaTree.getBody(), - finalNode, - env.getTypeUtils()); - extendWithNode(resultNode); - } - } - - // add marker to indicate that the next block will be the exit block - // Note: if there is a return statement earlier in the method (which - // is always the case for non-void methods), then this is not - // strictly necessary. However, it is also not a problem, as it will - // just generate a degenerated control graph case that will be - // removed in a later phase. - nodeList.add(new UnconditionalJump(regularExitLabel)); - - return new PhaseOneResult( - underlyingAST, - treeLookupMap, - convertedTreeLookupMap, - unaryAssignNodeLookupMap, - nodeList, - bindings, - leaders, - returnNodes, - regularExitLabel, - exceptionalExitLabel, - declaredClasses, - declaredLambdas); - } - - public PhaseOneResult process(CompilationUnitTree root, UnderlyingAST underlyingAST) { - // TODO: Isn't this costly? Is there no cache we can reuse? - TreePath bodyPath = trees.getPath(root, underlyingAST.getCode()); - assert bodyPath != null; - return process(bodyPath, underlyingAST); - } - - /** - * Perform any actions required when CFG translation creates a new Tree that is not part of - * the original AST. - * - * @param tree the newly created Tree - */ - public void handleArtificialTree(Tree tree) {} - - /* --------------------------------------------------------- */ - /* Nodes and Labels Management */ - /* --------------------------------------------------------- */ - - /** - * Add a node to the lookup map if it not already present. - * - * @param node the node to add to the lookup map - */ - protected void addToLookupMap(Node node) { - Tree tree = node.getTree(); - if (tree == null) { - return; - } - Set existing = treeLookupMap.get(tree); - if (existing == null) { - treeLookupMap.put(tree, new IdentityMostlySingleton<>(node)); - } else if (!existing.contains(node)) { - existing.add(node); - } else { - // Nothing to do if existing already contains the Node. - } - - Tree enclosingParens = parenMapping.get(tree); - while (enclosingParens != null) { - Set exp = treeLookupMap.get(enclosingParens); - if (exp == null) { - treeLookupMap.put(enclosingParens, new IdentityMostlySingleton<>(node)); - } else if (!existing.contains(node)) { - exp.add(node); - } - enclosingParens = parenMapping.get(enclosingParens); - } - } - - /** - * Add a node in the post-conversion lookup map. The node should refer to a Tree and that - * Tree should already be in the pre-conversion lookup map. This method is used to update - * the Tree-Node mapping with conversion nodes. - * - * @param node the node to add to the lookup map - */ - protected void addToConvertedLookupMap(Node node) { - Tree tree = node.getTree(); - addToConvertedLookupMap(tree, node); - } - - /** - * Add a node in the post-conversion lookup map. The tree argument should already be in the - * pre-conversion lookup map. This method is used to update the Tree-Node mapping with - * conversion nodes. - * - * @param tree the tree used as a key in the map - * @param node the node to add to the lookup map - */ - protected void addToConvertedLookupMap(Tree tree, Node node) { - assert tree != null; - assert treeLookupMap.containsKey(tree); - Set existing = convertedTreeLookupMap.get(tree); - if (existing == null) { - convertedTreeLookupMap.put(tree, new IdentityMostlySingleton<>(node)); - } else if (!existing.contains(node)) { - existing.add(node); - } else { - // Nothing to do if existing already contains the Node. - } - } - - /** - * Add a unary tree in the compound assign lookup map. This method is used to update the - * UnaryTree-AssignmentNode mapping with compound assign nodes. - * - * @param tree the tree used as a key in the map - * @param unaryAssignNode the node to add to the lookup map - */ - protected void addToUnaryAssignLookupMap(UnaryTree tree, AssignmentNode unaryAssignNode) { - unaryAssignNodeLookupMap.put(tree, unaryAssignNode); - } - - /** - * Extend the list of extended nodes with a node. - * - * @param node the node to add - * @return the same node (for convenience) - */ - protected T extendWithNode(T node) { - addToLookupMap(node); - extendWithExtendedNode(new NodeHolder(node)); - return node; - } - - /** - * Extend the list of extended nodes with a node, where {@code node} might throw the - * exception {@code cause}. - * - * @param node the node to add - * @param cause an exception that the node might throw - * @return the node holder - */ - protected NodeWithExceptionsHolder extendWithNodeWithException( - Node node, TypeMirror cause) { - addToLookupMap(node); - return extendWithNodeWithExceptions(node, Collections.singleton(cause)); - } - - /** - * Extend the list of extended nodes with a node, where {@code node} might throw any of the - * exception in {@code causes}. - * - * @param node the node to add - * @param causes set of exceptions that the node might throw - * @return the node holder - */ - protected NodeWithExceptionsHolder extendWithNodeWithExceptions( - Node node, Set causes) { - addToLookupMap(node); - Map> exceptions = new HashMap<>(); - for (TypeMirror cause : causes) { - exceptions.put(cause, tryStack.possibleLabels(cause)); - } - NodeWithExceptionsHolder exNode = new NodeWithExceptionsHolder(node, exceptions); - extendWithExtendedNode(exNode); - return exNode; - } - - /** - * Insert {@code node} after {@code pred} in the list of extended nodes, or append to the - * list if {@code pred} is not present. - * - * @param node the node to add - * @param pred the desired predecessor of node - * @return the node holder - */ - protected T insertNodeAfter(T node, Node pred) { - addToLookupMap(node); - insertExtendedNodeAfter(new NodeHolder(node), pred); - return node; - } - - /** - * Insert a {@code node} that might throw the exceptions in {@code causes} after {@code - * pred} in the list of extended nodes, or append to the list if {@code pred} is not - * present. - * - * @param node the node to add - * @param causes set of exceptions that the node might throw - * @param pred the desired predecessor of node - * @return the node holder - */ - protected NodeWithExceptionsHolder insertNodeWithExceptionsAfter( - Node node, Set causes, Node pred) { - addToLookupMap(node); - Map> exceptions = new HashMap<>(); - for (TypeMirror cause : causes) { - exceptions.put(cause, tryStack.possibleLabels(cause)); - } - NodeWithExceptionsHolder exNode = new NodeWithExceptionsHolder(node, exceptions); - insertExtendedNodeAfter(exNode, pred); - return exNode; - } - - /** - * Extend the list of extended nodes with an extended node. - * - * @param n the extended node - */ - protected void extendWithExtendedNode(ExtendedNode n) { - nodeList.add(n); - } - - /** - * Insert {@code n} after the node {@code pred} in the list of extended nodes, or append - * {@code n} if {@code pred} is not present. - * - * @param n the extended node - * @param pred the desired predecessor - */ - @SuppressWarnings("ModifyCollectionInEnhancedForLoop") - protected void insertExtendedNodeAfter(ExtendedNode n, Node pred) { - int index = -1; - for (int i = 0; i < nodeList.size(); i++) { - ExtendedNode inList = nodeList.get(i); - if (inList instanceof NodeHolder || inList instanceof NodeWithExceptionsHolder) { - if (inList.getNode() == pred) { - index = i; - break; - } - } - } - if (index != -1) { - nodeList.add(index + 1, n); - // update bindings - for (Map.Entry e : bindings.entrySet()) { - if (e.getValue() >= index + 1) { - bindings.put(e.getKey(), e.getValue() + 1); - } - } - // update leaders - Set oldLeaders = new HashSet<>(leaders); - leaders.clear(); - for (Integer l : oldLeaders) { - if (l >= index + 1) { - leaders.add(l + 1); - } else { - leaders.add(l); - } - } - } else { - nodeList.add(n); - } - } - - /** - * Add the label {@code l} to the extended node that will be placed next in the sequence. - */ - protected void addLabelForNextNode(Label l) { - assert !bindings.containsKey(l); - leaders.add(nodeList.size()); - bindings.put(l, nodeList.size()); - } - - /* --------------------------------------------------------- */ - /* Utility Methods */ - /* --------------------------------------------------------- */ - - protected long uid = 0; - - protected String uniqueName(String prefix) { - return prefix + "#num" + uid++; - } - - /** - * If the input node is an unboxed primitive type, insert a call to the appropriate valueOf - * method, otherwise leave it alone. - * - * @param node in input node - * @return a Node representing the boxed version of the input, which may simply be the input - * node - */ - protected Node box(Node node) { - // For boxing conversion, see JLS 5.1.7 - if (TypesUtils.isPrimitive(node.getType())) { - PrimitiveType primitive = types.getPrimitiveType(node.getType().getKind()); - TypeMirror boxedType = types.getDeclaredType(types.boxedClass(primitive)); - - TypeElement boxedElement = (TypeElement) ((DeclaredType) boxedType).asElement(); - IdentifierTree classTree = treeBuilder.buildClassUse(boxedElement); - handleArtificialTree(classTree); - ClassNameNode className = new ClassNameNode(classTree); - className.setInSource(false); - insertNodeAfter(className, node); - - MemberSelectTree valueOfSelect = treeBuilder.buildValueOfMethodAccess(classTree); - handleArtificialTree(valueOfSelect); - MethodAccessNode valueOfAccess = new MethodAccessNode(valueOfSelect, className); - valueOfAccess.setInSource(false); - insertNodeAfter(valueOfAccess, className); - - MethodInvocationTree valueOfCall = - treeBuilder.buildMethodInvocation( - valueOfSelect, (ExpressionTree) node.getTree()); - handleArtificialTree(valueOfCall); - Node boxed = - new MethodInvocationNode( - valueOfCall, - valueOfAccess, - Collections.singletonList(node), - getCurrentPath()); - boxed.setInSource(false); - // Add Throwable to account for unchecked exceptions - TypeElement throwableElement = elements.getTypeElement("java.lang.Throwable"); - addToConvertedLookupMap(node.getTree(), boxed); - insertNodeWithExceptionsAfter( - boxed, Collections.singleton(throwableElement.asType()), valueOfAccess); - return boxed; - } else { - return node; - } - } - - /** - * If the input node is a boxed type, unbox it, otherwise leave it alone. - * - * @param node in input node - * @return a Node representing the unboxed version of the input, which may simply be the - * input node - */ - protected Node unbox(Node node) { - if (TypesUtils.isBoxedPrimitive(node.getType())) { - - MemberSelectTree primValueSelect = - treeBuilder.buildPrimValueMethodAccess(node.getTree()); - handleArtificialTree(primValueSelect); - MethodAccessNode primValueAccess = new MethodAccessNode(primValueSelect, node); - primValueAccess.setInSource(false); - // Method access may throw NullPointerException - TypeElement npeElement = elements.getTypeElement("java.lang.NullPointerException"); - insertNodeWithExceptionsAfter( - primValueAccess, Collections.singleton(npeElement.asType()), node); - - MethodInvocationTree primValueCall = - treeBuilder.buildMethodInvocation(primValueSelect); - handleArtificialTree(primValueCall); - Node unboxed = - new MethodInvocationNode( - primValueCall, - primValueAccess, - Collections.emptyList(), - getCurrentPath()); - unboxed.setInSource(false); - - // Add Throwable to account for unchecked exceptions - TypeElement throwableElement = elements.getTypeElement("java.lang.Throwable"); - addToConvertedLookupMap(node.getTree(), unboxed); - insertNodeWithExceptionsAfter( - unboxed, Collections.singleton(throwableElement.asType()), primValueAccess); - return unboxed; - } else { - return node; - } - } - - private TreeInfo getTreeInfo(Tree tree) { - final TypeMirror type = TreeUtils.typeOf(tree); - final boolean boxed = TypesUtils.isBoxedPrimitive(type); - final TypeMirror unboxedType = boxed ? types.unboxedType(type) : type; - - final boolean bool = TypesUtils.isBooleanType(type); - final boolean numeric = TypesUtils.isNumeric(unboxedType); - - return new TreeInfo() { - @Override - public boolean isNumeric() { - return numeric; - } - - @Override - public boolean isBoxed() { - return boxed; - } - - @Override - public boolean isBoolean() { - return bool; - } - - @Override - public TypeMirror unboxedType() { - return unboxedType; - } - }; - } - - /** - * Returns the unboxed tree if necessary, as described in JLS 5.1.8. - * - * @return the unboxed tree if necessary, as described in JLS 5.1.8 - */ - private Node unboxAsNeeded(Node node, boolean boxed) { - return boxed ? unbox(node) : node; - } - - /** - * Convert the input node to String type, if it isn't already. - * - * @param node an input node - * @return a Node with the value promoted to String, which may be the input node - */ - protected Node stringConversion(Node node) { - // For string conversion, see JLS 5.1.11 - TypeElement stringElement = elements.getTypeElement("java.lang.String"); - if (!TypesUtils.isString(node.getType())) { - Node converted = - new StringConversionNode(node.getTree(), node, stringElement.asType()); - addToConvertedLookupMap(converted); - insertNodeAfter(converted, node); - return converted; - } else { - return node; - } - } - - /** - * Perform unary numeric promotion on the input node. - * - * @param node a node producing a value of numeric primitive or boxed type - * @return a Node with the value promoted to the int, long float or double, which may be the - * input node - */ - protected Node unaryNumericPromotion(Node node) { - // For unary numeric promotion, see JLS 5.6.1 - node = unbox(node); - - switch (node.getType().getKind()) { - case BYTE: - case CHAR: - case SHORT: - { - TypeMirror intType = types.getPrimitiveType(TypeKind.INT); - Node widened = new WideningConversionNode(node.getTree(), node, intType); - addToConvertedLookupMap(widened); - insertNodeAfter(widened, node); - return widened; - } - default: - // Nothing to do. - break; - } - - return node; - } - - /** - * Returns true if the argument type is a numeric primitive or a boxed numeric primitive and - * false otherwise. - */ - protected boolean isNumericOrBoxed(TypeMirror type) { - if (TypesUtils.isBoxedPrimitive(type)) { - type = types.unboxedType(type); - } - return TypesUtils.isNumeric(type); - } - - /** - * Compute the type to which two numeric types must be promoted before performing a binary - * numeric operation on them. The input types must both be numeric and the output type is - * primitive. - * - * @param left the type of the left operand - * @param right the type of the right operand - * @return a TypeMirror representing the binary numeric promoted type - */ - protected TypeMirror binaryPromotedType(TypeMirror left, TypeMirror right) { - if (TypesUtils.isBoxedPrimitive(left)) { - left = types.unboxedType(left); - } - if (TypesUtils.isBoxedPrimitive(right)) { - right = types.unboxedType(right); - } - TypeKind promotedTypeKind = TypesUtils.widenedNumericType(left, right); - return types.getPrimitiveType(promotedTypeKind); - } - - /** - * Perform binary numeric promotion on the input node to make it match the expression type. - * - * @param node a node producing a value of numeric primitive or boxed type - * @param exprType the type to promote the value to - * @return a Node with the value promoted to the exprType, which may be the input node - */ - protected Node binaryNumericPromotion(Node node, TypeMirror exprType) { - // For binary numeric promotion, see JLS 5.6.2 - node = unbox(node); - - if (!types.isSameType(node.getType(), exprType)) { - Node widened = new WideningConversionNode(node.getTree(), node, exprType); - addToConvertedLookupMap(widened); - insertNodeAfter(widened, node); - return widened; - } else { - return node; - } - } - - /** - * Perform widening primitive conversion on the input node to make it match the destination - * type. - * - * @param node a node producing a value of numeric primitive type - * @param destType the type to widen the value to - * @return a Node with the value widened to the exprType, which may be the input node - */ - protected Node widen(Node node, TypeMirror destType) { - // For widening conversion, see JLS 5.1.2 - assert TypesUtils.isPrimitive(node.getType()) && TypesUtils.isPrimitive(destType) - : "widening must be applied to primitive types"; - if (types.isSubtype(node.getType(), destType) - && !types.isSameType(node.getType(), destType)) { - Node widened = new WideningConversionNode(node.getTree(), node, destType); - addToConvertedLookupMap(widened); - insertNodeAfter(widened, node); - return widened; - } else { - return node; - } - } - - /** - * Perform narrowing conversion on the input node to make it match the destination type. - * - * @param node a node producing a value of numeric primitive type - * @param destType the type to narrow the value to - * @return a Node with the value narrowed to the exprType, which may be the input node - */ - protected Node narrow(Node node, TypeMirror destType) { - // For narrowing conversion, see JLS 5.1.3 - assert TypesUtils.isPrimitive(node.getType()) && TypesUtils.isPrimitive(destType) - : "narrowing must be applied to primitive types"; - if (types.isSubtype(destType, node.getType()) - && !types.isSameType(destType, node.getType())) { - Node narrowed = new NarrowingConversionNode(node.getTree(), node, destType); - addToConvertedLookupMap(narrowed); - insertNodeAfter(narrowed, node); - return narrowed; - } else { - return node; - } - } - - /** - * Perform narrowing conversion and optionally boxing conversion on the input node to make - * it match the destination type. - * - * @param node a node producing a value of numeric primitive type - * @param destType the type to narrow the value to (possibly boxed) - * @return a Node with the value narrowed and boxed to the destType, which may be the input - * node - */ - protected Node narrowAndBox(Node node, TypeMirror destType) { - if (TypesUtils.isBoxedPrimitive(destType)) { - return box(narrow(node, types.unboxedType(destType))); - } else { - return narrow(node, destType); - } - } - - /** - * Return whether a conversion from the type of the node to varType requires narrowing. - * - * @param varType the type of a variable (or general LHS) to be converted to - * @param node a node whose value is being converted - * @return whether this conversion requires narrowing to succeed - */ - protected boolean conversionRequiresNarrowing(TypeMirror varType, Node node) { - // Narrowing is restricted to cases where the left hand side - // is byte, char, short or Byte, Char, Short and the right - // hand side is a constant. - TypeMirror unboxedVarType = - TypesUtils.isBoxedPrimitive(varType) ? types.unboxedType(varType) : varType; - TypeKind unboxedVarKind = unboxedVarType.getKind(); - boolean isLeftNarrowableTo = - unboxedVarKind == TypeKind.BYTE - || unboxedVarKind == TypeKind.SHORT - || unboxedVarKind == TypeKind.CHAR; - boolean isRightConstant = node instanceof ValueLiteralNode; - return isLeftNarrowableTo && isRightConstant; - } - - /** - * Assignment conversion and method invocation conversion are almost identical, except that - * assignment conversion allows narrowing. We factor out the common logic here. - * - * @param node a Node producing a value - * @param varType the type of a variable - * @param contextAllowsNarrowing whether to allow narrowing (for assignment conversion) or - * not (for method invocation conversion) - * @return a Node with the value converted to the type of the variable, which may be the - * input node itself - */ - protected Node commonConvert( - Node node, TypeMirror varType, boolean contextAllowsNarrowing) { - // For assignment conversion, see JLS 5.2 - // For method invocation conversion, see JLS 5.3 - - // Check for identical types or "identity conversion" - TypeMirror nodeType = node.getType(); - boolean isSameType = types.isSameType(nodeType, varType); - if (isSameType) { - return node; - } - - boolean isRightNumeric = TypesUtils.isNumeric(nodeType); - boolean isRightPrimitive = TypesUtils.isPrimitive(nodeType); - boolean isRightBoxed = TypesUtils.isBoxedPrimitive(nodeType); - boolean isRightReference = nodeType instanceof ReferenceType; - boolean isLeftNumeric = TypesUtils.isNumeric(varType); - boolean isLeftPrimitive = TypesUtils.isPrimitive(varType); - // boolean isLeftBoxed = TypesUtils.isBoxedPrimitive(varType); - boolean isLeftReference = varType instanceof ReferenceType; - boolean isSubtype = types.isSubtype(nodeType, varType); - - if (isRightNumeric && isLeftNumeric && isSubtype) { - node = widen(node, varType); - nodeType = node.getType(); - } else if (isRightReference && isLeftReference && isSubtype) { - // widening reference conversion is a no-op, but if it - // applies, then later conversions do not. - } else if (isRightPrimitive && isLeftReference) { - if (contextAllowsNarrowing && conversionRequiresNarrowing(varType, node)) { - node = narrowAndBox(node, varType); - nodeType = node.getType(); - } else { - node = box(node); - nodeType = node.getType(); - } - } else if (isRightBoxed && isLeftPrimitive) { - node = unbox(node); - nodeType = node.getType(); - - if (types.isSubtype(nodeType, varType) && !types.isSameType(nodeType, varType)) { - node = widen(node, varType); - nodeType = node.getType(); - } - } else if (isRightPrimitive && isLeftPrimitive) { - if (contextAllowsNarrowing && conversionRequiresNarrowing(varType, node)) { - node = narrow(node, varType); - nodeType = node.getType(); - } - } - - // TODO: if checkers need to know about null references of - // a particular type, add logic for them here. - - return node; - } - - /** - * Perform assignment conversion so that it can be assigned to a variable of the given type. - * - * @param node a Node producing a value - * @param varType the type of a variable - * @return a Node with the value converted to the type of the variable, which may be the - * input node itself - */ - protected Node assignConvert(Node node, TypeMirror varType) { - return commonConvert(node, varType, true); - } - - /** - * Perform method invocation conversion so that the node can be passed as a formal parameter - * of the given type. - * - * @param node a Node producing a value - * @param formalType the type of a formal parameter - * @return a Node with the value converted to the type of the formal, which may be the input - * node itself - */ - protected Node methodInvocationConvert(Node node, TypeMirror formalType) { - return commonConvert(node, formalType, false); - } - - /** - * Given a method element and as list of argument expressions, return a list of {@link - * Node}s representing the arguments converted for a call of the method. This method applies - * to both method invocations and constructor calls. - * - * @param method an ExecutableElement representing a method to be called - * @param actualExprs a List of argument expressions to a call - * @return a List of {@link Node}s representing arguments after conversions required by a - * call to this method - */ - protected List convertCallArguments( - ExecutableElement method, List actualExprs) { - // NOTE: It is important to convert one method argument before - // generating CFG nodes for the next argument, since label binding - // expects nodes to be generated in execution order. Therefore, - // this method first determines which conversions need to be applied - // and then iterates over the actual arguments. - List formals = method.getParameters(); - - ArrayList convertedNodes = new ArrayList<>(); - - int numFormals = formals.size(); - int numActuals = actualExprs.size(); - if (method.isVarArgs()) { - // Create a new array argument if the actuals outnumber - // the formals, or if the last actual is not assignable - // to the last formal. - int lastArgIndex = numFormals - 1; - TypeMirror lastParamType = formals.get(lastArgIndex).asType(); - List dimensions = new ArrayList<>(); - List initializers = new ArrayList<>(); - if (numActuals == numFormals - && types.isAssignable( - TreeUtils.typeOf(actualExprs.get(numActuals - 1)), lastParamType)) { - // Normal call with no array creation, apply method - // invocation conversion to all arguments. - for (int i = 0; i < numActuals; i++) { - Node actualVal = scan(actualExprs.get(i), null); - convertedNodes.add( - methodInvocationConvert(actualVal, formals.get(i).asType())); - } - } else { - assert lastParamType instanceof ArrayType - : "variable argument formal must be an array"; - // Apply method invocation conversion to lastArgIndex - // arguments and use the remaining ones to initialize - // an array. - for (int i = 0; i < lastArgIndex; i++) { - Node actualVal = scan(actualExprs.get(i), null); - convertedNodes.add( - methodInvocationConvert(actualVal, formals.get(i).asType())); - } - - List inits = new ArrayList<>(); - TypeMirror elemType = ((ArrayType) lastParamType).getComponentType(); - for (int i = lastArgIndex; i < numActuals; i++) { - inits.add(actualExprs.get(i)); - Node actualVal = scan(actualExprs.get(i), null); - initializers.add(assignConvert(actualVal, elemType)); - } - - NewArrayTree wrappedVarargs = treeBuilder.buildNewArray(elemType, inits); - handleArtificialTree(wrappedVarargs); - - Node lastArgument = - new ArrayCreationNode( - wrappedVarargs, lastParamType, dimensions, initializers); - extendWithNode(lastArgument); - - convertedNodes.add(lastArgument); - } - } else { - for (int i = 0; i < numActuals; i++) { - Node actualVal = scan(actualExprs.get(i), null); - convertedNodes.add(methodInvocationConvert(actualVal, formals.get(i).asType())); - } - } - - return convertedNodes; - } - - /** - * Convert an operand of a conditional expression to the type of the whole expression. - * - * @param node a node occurring as the second or third operand of a conditional expression - * @param destType the type to promote the value to - * @return a Node with the value promoted to the destType, which may be the input node - */ - protected Node conditionalExprPromotion(Node node, TypeMirror destType) { - // For rules on converting operands of conditional expressions, - // JLS 15.25 - TypeMirror nodeType = node.getType(); - - // If the operand is already the same type as the whole - // expression, then do nothing. - if (types.isSameType(nodeType, destType)) { - return node; - } - - // If the operand is a primitive and the whole expression is - // boxed, then apply boxing. - if (TypesUtils.isPrimitive(nodeType) && TypesUtils.isBoxedPrimitive(destType)) { - return box(node); - } - - // If the operand is byte or Byte and the whole expression is - // short, then convert to short. - boolean isBoxedPrimitive = TypesUtils.isBoxedPrimitive(nodeType); - TypeMirror unboxedNodeType = isBoxedPrimitive ? types.unboxedType(nodeType) : nodeType; - TypeMirror unboxedDestType = - TypesUtils.isBoxedPrimitive(destType) ? types.unboxedType(destType) : destType; - if (TypesUtils.isNumeric(unboxedNodeType) && TypesUtils.isNumeric(unboxedDestType)) { - if (unboxedNodeType.getKind() == TypeKind.BYTE - && destType.getKind() == TypeKind.SHORT) { - if (isBoxedPrimitive) { - node = unbox(node); - } - return widen(node, destType); - } - - // If the operand is Byte, Short or Character and the whole expression - // is the unboxed version of it, then apply unboxing. - TypeKind destKind = destType.getKind(); - if (destKind == TypeKind.BYTE - || destKind == TypeKind.CHAR - || destKind == TypeKind.SHORT) { - if (isBoxedPrimitive) { - return unbox(node); - } else if (nodeType.getKind() == TypeKind.INT) { - return narrow(node, destType); - } - } - - return binaryNumericPromotion(node, destType); - } - - // For the final case in JLS 15.25, apply boxing but not lub. - if (TypesUtils.isPrimitive(nodeType) - && (destType.getKind() == TypeKind.DECLARED - || destType.getKind() == TypeKind.UNION - || destType.getKind() == TypeKind.INTERSECTION)) { - return box(node); - } - - return node; - } - - /** - * Returns the label {@link Name} of the leaf in the argument path, or null if the leaf is - * not a labeled statement. - */ - protected @Nullable Name getLabel(TreePath path) { - if (path.getParentPath() != null) { - Tree parent = path.getParentPath().getLeaf(); - if (parent.getKind() == Tree.Kind.LABELED_STATEMENT) { - return ((LabeledStatementTree) parent).getLabel(); - } - } - return null; - } - - /* --------------------------------------------------------- */ - /* Visitor Methods */ - /* --------------------------------------------------------- */ - - @Override - public Node visitAnnotatedType(AnnotatedTypeTree tree, Void p) { - return scan(tree.getUnderlyingType(), p); - } - - @Override - public Node visitAnnotation(AnnotationTree tree, Void p) { - assert false : "AnnotationTree is unexpected in AST to CFG translation"; - return null; - } - - @Override - public MethodInvocationNode visitMethodInvocation(MethodInvocationTree tree, Void p) { - - // see JLS 15.12.4 - - // First, compute the receiver, if any (15.12.4.1) - // Second, evaluate the actual arguments, left to right and - // possibly some arguments are stored into an array for variable - // arguments calls (15.12.4.2) - // Third, test the receiver, if any, for nullness (15.12.4.4) - // Fourth, convert the arguments to the type of the formal - // parameters (15.12.4.5) - // Fifth, if the method is synchronized, lock the receiving - // object or class (15.12.4.5) - ExecutableElement method = TreeUtils.elementFromUse(tree); - if (method == null) { - // The method wasn't found, e.g. because of a compilation error. - return null; - } - - // TODO? Variable wasn't used. - // boolean isBooleanMethod = TypesUtils.isBooleanType(method.getReturnType()); - - ExpressionTree methodSelect = tree.getMethodSelect(); - assert TreeUtils.isMethodAccess(methodSelect) - : "Expected a method access, but got: " + methodSelect; - - List actualExprs = tree.getArguments(); - - // Look up method to invoke and possibly throw NullPointerException - Node receiver = getReceiver(methodSelect); - - MethodAccessNode target = new MethodAccessNode(methodSelect, receiver); - - ExecutableElement element = TreeUtils.elementFromUse(tree); - if (ElementUtils.isStatic(element) || receiver instanceof ThisLiteralNode) { - // No NullPointerException can be thrown, use normal node - extendWithNode(target); - } else { - TypeElement npeElement = elements.getTypeElement("java.lang.NullPointerException"); - extendWithNodeWithException(target, npeElement.asType()); - } - - List arguments = new ArrayList<>(); - - // Don't convert arguments for enum super calls. The AST contains - // no actual arguments, while the method element expects two arguments, - // leading to an exception in convertCallArguments. Since no actual - // arguments are present in the AST that is being checked, it shouldn't - // cause any harm to omit the conversions. - // See also BaseTypeVisitor.visitMethodInvocation and - // QualifierPolymorphism.annotate - if (!TreeUtils.isEnumSuper(tree)) { - arguments = convertCallArguments(method, actualExprs); - } - - // TODO: lock the receiver for synchronized methods - - MethodInvocationNode node = - new MethodInvocationNode(tree, target, arguments, getCurrentPath()); - - Set thrownSet = new HashSet<>(); - // Add exceptions explicitly mentioned in the throws clause. - List thrownTypes = element.getThrownTypes(); - thrownSet.addAll(thrownTypes); - // Add Throwable to account for unchecked exceptions - TypeElement throwableElement = elements.getTypeElement("java.lang.Throwable"); - thrownSet.add(throwableElement.asType()); - - ExtendedNode extendedNode = extendWithNodeWithExceptions(node, thrownSet); - - /* Check for the TerminatesExecution annotation. */ - Element methodElement = TreeUtils.elementFromTree(tree); - boolean terminatesExecution = - annotationProvider.getDeclAnnotation(methodElement, TerminatesExecution.class) - != null; - if (terminatesExecution) { - extendedNode.setTerminatesExecution(true); - } - - return node; - } - - @Override - public Node visitAssert(AssertTree tree, Void p) { - - // see JLS 14.10 - - // If assertions are enabled, then we can just translate the - // assertion. - if (assumeAssertionsEnabled || assumeAssertionsEnabledFor(tree)) { - translateAssertWithAssertionsEnabled(tree); - return null; - } - - // If assertions are disabled, then nothing is executed. - if (assumeAssertionsDisabled) { - return null; - } - - // Otherwise, we don't know if assertions are enabled, so we use a - // variable "ea" and case-split on it. One branch does execute the - // assertion, while the other assumes assertions are disabled. - VariableTree ea = getAssertionsEnabledVariable(); - - // all necessary labels - Label assertionEnabled = new Label(); - Label assertionDisabled = new Label(); - - extendWithNode(new LocalVariableNode(ea)); - extendWithExtendedNode(new ConditionalJump(assertionEnabled, assertionDisabled)); - - // 'then' branch (i.e. check the assertion) - addLabelForNextNode(assertionEnabled); - - translateAssertWithAssertionsEnabled(tree); - - // 'else' branch - addLabelForNextNode(assertionDisabled); - - return null; - } - - /** - * Should assertions be assumed to be executed for a given {@link AssertTree}? False by - * default. - */ - protected boolean assumeAssertionsEnabledFor(AssertTree tree) { - return false; - } - - /** The {@link VariableTree} that indicates whether assertions are enabled or not. */ - protected VariableTree ea = null; - - /** - * Get a synthetic {@link VariableTree} that indicates whether assertions are enabled or - * not. - */ - protected VariableTree getAssertionsEnabledVariable() { - if (ea == null) { - String name = uniqueName("assertionsEnabled"); - Element owner = findOwner(); - ExpressionTree initializer = null; - ea = - treeBuilder.buildVariableDecl( - types.getPrimitiveType(TypeKind.BOOLEAN), name, owner, initializer); - } - return ea; - } - - /** - * Find nearest owner element(Method or Class) which holds current tree. - * - * @return nearest owner element of current tree - */ - private Element findOwner() { - MethodTree enclosingMethod = TreeUtils.enclosingMethod(getCurrentPath()); - if (enclosingMethod != null) { - return TreeUtils.elementFromDeclaration(enclosingMethod); - } else { - ClassTree enclosingClass = TreeUtils.enclosingClass(getCurrentPath()); - return TreeUtils.elementFromDeclaration(enclosingClass); - } - } - - /** - * Translates an assertion statement to the correct CFG nodes. The translation assumes that - * assertions are enabled. - */ - protected void translateAssertWithAssertionsEnabled(AssertTree tree) { - - // all necessary labels - Label assertEnd = new Label(); - Label elseEntry = new Label(); - - // basic block for the condition - Node condition = unbox(scan(tree.getCondition(), null)); - ConditionalJump cjump = new ConditionalJump(assertEnd, elseEntry); - extendWithExtendedNode(cjump); - - // else branch - Node detail = null; - addLabelForNextNode(elseEntry); - if (tree.getDetail() != null) { - detail = scan(tree.getDetail(), null); - } - TypeElement assertException = elements.getTypeElement("java.lang.AssertionError"); - AssertionErrorNode assertNode = - new AssertionErrorNode(tree, condition, detail, assertException.asType()); - extendWithNode(assertNode); - NodeWithExceptionsHolder exNode = - extendWithNodeWithException( - new ThrowNode(null, assertNode, env.getTypeUtils()), - assertException.asType()); - exNode.setTerminatesExecution(true); - - // then branch (nothing happens) - addLabelForNextNode(assertEnd); - } - - @Override - public Node visitAssignment(AssignmentTree tree, Void p) { - - // see JLS 15.26.1 - - AssignmentNode assignmentNode; - ExpressionTree variable = tree.getVariable(); - TypeMirror varType = TreeUtils.typeOf(variable); - - // case 1: field access - if (TreeUtils.isFieldAccess(variable)) { - // visit receiver - Node receiver = getReceiver(variable); - - // visit expression - Node expression = scan(tree.getExpression(), p); - expression = assignConvert(expression, varType); - - // visit field access (throws null-pointer exception) - FieldAccessNode target = new FieldAccessNode(variable, receiver); - target.setLValue(); - - Element element = TreeUtils.elementFromUse(variable); - if (ElementUtils.isStatic(element) || receiver instanceof ThisLiteralNode) { - // No NullPointerException can be thrown, use normal node - extendWithNode(target); - } else { - TypeElement npeElement = - elements.getTypeElement("java.lang.NullPointerException"); - extendWithNodeWithException(target, npeElement.asType()); - } - - // add assignment node - assignmentNode = new AssignmentNode(tree, target, expression); - extendWithNode(assignmentNode); - } - - // case 2: other cases - else { - Node target = scan(variable, p); - target.setLValue(); - - assignmentNode = translateAssignment(tree, target, tree.getExpression()); - } - - return assignmentNode; - } - - /** Translate an assignment. */ - protected AssignmentNode translateAssignment(Tree tree, Node target, ExpressionTree rhs) { - Node expression = scan(rhs, null); - return translateAssignment(tree, target, expression); - } - - /** Translate an assignment where the RHS has already been scanned. */ - protected AssignmentNode translateAssignment(Tree tree, Node target, Node expression) { - assert tree instanceof AssignmentTree || tree instanceof VariableTree; - target.setLValue(); - expression = assignConvert(expression, target.getType()); - AssignmentNode assignmentNode = new AssignmentNode(tree, target, expression); - extendWithNode(assignmentNode); - return assignmentNode; - } - - /** - * Note 1: Requires {@code tree} to be a field or method access tree. - * - *

      Note 2: Visits the receiver and adds all necessary blocks to the CFG. - * - * @param tree the field access tree containing the receiver - * @return the receiver of the field access - */ - private Node getReceiver(ExpressionTree tree) { - assert TreeUtils.isFieldAccess(tree) || TreeUtils.isMethodAccess(tree); - if (tree.getKind() == Tree.Kind.MEMBER_SELECT) { - MemberSelectTree mtree = (MemberSelectTree) tree; - return scan(mtree.getExpression(), null); - } else { - Element ele = TreeUtils.elementFromUse(tree); - TypeElement declaringClass = ElementUtils.enclosingClass(ele); - TypeMirror type = ElementUtils.getType(declaringClass); - if (ElementUtils.isStatic(ele)) { - Node node = new ClassNameNode(type, declaringClass); - extendWithNode(node); - return node; - } else { - Node node = new ImplicitThisLiteralNode(type); - extendWithNode(node); - return node; - } - } - } - - /** - * Map an operation with assignment to the corresponding operation without assignment. - * - * @param kind a Tree.Kind representing an operation with assignment - * @return the Tree.Kind for the same operation without assignment - */ - protected Tree.Kind withoutAssignment(Tree.Kind kind) { - switch (kind) { - case DIVIDE_ASSIGNMENT: - return Tree.Kind.DIVIDE; - case MULTIPLY_ASSIGNMENT: - return Tree.Kind.MULTIPLY; - case REMAINDER_ASSIGNMENT: - return Tree.Kind.REMAINDER; - case MINUS_ASSIGNMENT: - return Tree.Kind.MINUS; - case PLUS_ASSIGNMENT: - return Tree.Kind.PLUS; - case LEFT_SHIFT_ASSIGNMENT: - return Tree.Kind.LEFT_SHIFT; - case RIGHT_SHIFT_ASSIGNMENT: - return Tree.Kind.RIGHT_SHIFT; - case UNSIGNED_RIGHT_SHIFT_ASSIGNMENT: - return Tree.Kind.UNSIGNED_RIGHT_SHIFT; - case AND_ASSIGNMENT: - return Tree.Kind.AND; - case OR_ASSIGNMENT: - return Tree.Kind.OR; - case XOR_ASSIGNMENT: - return Tree.Kind.XOR; - default: - return Tree.Kind.ERRONEOUS; - } - } - - @Override - public Node visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { - // According the JLS 15.26.2, E1 op= E2 is equivalent to - // E1 = (T) ((E1) op (E2)), where T is the type of E1, - // except that E1 is evaluated only once. - // - - Tree.Kind kind = tree.getKind(); - switch (kind) { - case DIVIDE_ASSIGNMENT: - case MULTIPLY_ASSIGNMENT: - case REMAINDER_ASSIGNMENT: - { - // see JLS 15.17 and 15.26.2 - Node targetLHS = scan(tree.getVariable(), p); - Node value = scan(tree.getExpression(), p); - - TypeMirror exprType = TreeUtils.typeOf(tree); - TypeMirror leftType = TreeUtils.typeOf(tree.getVariable()); - TypeMirror rightType = TreeUtils.typeOf(tree.getExpression()); - TypeMirror promotedType = binaryPromotedType(leftType, rightType); - Node targetRHS = binaryNumericPromotion(targetLHS, promotedType); - value = binaryNumericPromotion(value, promotedType); - - BinaryTree operTree = - treeBuilder.buildBinary( - promotedType, - withoutAssignment(kind), - tree.getVariable(), - tree.getExpression()); - handleArtificialTree(operTree); - Node operNode; - if (kind == Tree.Kind.MULTIPLY_ASSIGNMENT) { - operNode = new NumericalMultiplicationNode(operTree, targetRHS, value); - } else if (kind == Tree.Kind.DIVIDE_ASSIGNMENT) { - if (TypesUtils.isIntegral(exprType)) { - operNode = new IntegerDivisionNode(operTree, targetRHS, value); - - TypeElement throwableElement = - elements.getTypeElement("java.lang.ArithmeticException"); - extendWithNodeWithException(operNode, throwableElement.asType()); - } else { - operNode = new FloatingDivisionNode(operTree, targetRHS, value); - } - } else { - assert kind == Kind.REMAINDER_ASSIGNMENT; - if (TypesUtils.isIntegral(exprType)) { - operNode = new IntegerRemainderNode(operTree, targetRHS, value); - - TypeElement throwableElement = - elements.getTypeElement("java.lang.ArithmeticException"); - extendWithNodeWithException(operNode, throwableElement.asType()); - } else { - operNode = new FloatingRemainderNode(operTree, targetRHS, value); - } - } - extendWithNode(operNode); - - TypeCastTree castTree = treeBuilder.buildTypeCast(leftType, operTree); - handleArtificialTree(castTree); - TypeCastNode castNode = - new TypeCastNode(castTree, operNode, leftType, types); - castNode.setInSource(false); - extendWithNode(castNode); - - AssignmentNode assignNode = new AssignmentNode(tree, targetLHS, castNode); - extendWithNode(assignNode); - return assignNode; - } - - case MINUS_ASSIGNMENT: - case PLUS_ASSIGNMENT: - { - // see JLS 15.18 and 15.26.2 - - Node targetLHS = scan(tree.getVariable(), p); - Node value = scan(tree.getExpression(), p); - - TypeMirror leftType = TreeUtils.typeOf(tree.getVariable()); - TypeMirror rightType = TreeUtils.typeOf(tree.getExpression()); - - if (TypesUtils.isString(leftType) || TypesUtils.isString(rightType)) { - assert (kind == Tree.Kind.PLUS_ASSIGNMENT); - Node targetRHS = stringConversion(targetLHS); - value = stringConversion(value); - Node r = new StringConcatenateAssignmentNode(tree, targetRHS, value); - extendWithNode(r); - return r; - } else { - TypeMirror promotedType = binaryPromotedType(leftType, rightType); - Node targetRHS = binaryNumericPromotion(targetLHS, promotedType); - value = binaryNumericPromotion(value, promotedType); - - BinaryTree operTree = - treeBuilder.buildBinary( - promotedType, - withoutAssignment(kind), - tree.getVariable(), - tree.getExpression()); - handleArtificialTree(operTree); - Node operNode; - if (kind == Tree.Kind.PLUS_ASSIGNMENT) { - operNode = new NumericalAdditionNode(operTree, targetRHS, value); - } else { - assert kind == Kind.MINUS_ASSIGNMENT; - operNode = new NumericalSubtractionNode(operTree, targetRHS, value); - } - extendWithNode(operNode); - - TypeCastTree castTree = treeBuilder.buildTypeCast(leftType, operTree); - handleArtificialTree(castTree); - TypeCastNode castNode = - new TypeCastNode(castTree, operNode, leftType, types); - castNode.setInSource(false); - extendWithNode(castNode); - - // Map the compound assignment tree to an assignment node, which - // will have the correct type. - AssignmentNode assignNode = - new AssignmentNode(tree, targetLHS, castNode); - extendWithNode(assignNode); - return assignNode; - } - } - - case LEFT_SHIFT_ASSIGNMENT: - case RIGHT_SHIFT_ASSIGNMENT: - case UNSIGNED_RIGHT_SHIFT_ASSIGNMENT: - { - // see JLS 15.19 and 15.26.2 - Node targetLHS = scan(tree.getVariable(), p); - Node value = scan(tree.getExpression(), p); - - TypeMirror leftType = TreeUtils.typeOf(tree.getVariable()); - - Node targetRHS = unaryNumericPromotion(targetLHS); - value = unaryNumericPromotion(value); - - BinaryTree operTree = - treeBuilder.buildBinary( - leftType, - withoutAssignment(kind), - tree.getVariable(), - tree.getExpression()); - handleArtificialTree(operTree); - Node operNode; - if (kind == Tree.Kind.LEFT_SHIFT_ASSIGNMENT) { - operNode = new LeftShiftNode(operTree, targetRHS, value); - } else if (kind == Tree.Kind.RIGHT_SHIFT_ASSIGNMENT) { - operNode = new SignedRightShiftNode(operTree, targetRHS, value); - } else { - assert kind == Kind.UNSIGNED_RIGHT_SHIFT_ASSIGNMENT; - operNode = new UnsignedRightShiftNode(operTree, targetRHS, value); - } - extendWithNode(operNode); - - TypeCastTree castTree = treeBuilder.buildTypeCast(leftType, operTree); - handleArtificialTree(castTree); - TypeCastNode castNode = - new TypeCastNode(castTree, operNode, leftType, types); - castNode.setInSource(false); - extendWithNode(castNode); - - AssignmentNode assignNode = new AssignmentNode(tree, targetLHS, castNode); - extendWithNode(assignNode); - return assignNode; - } - - case AND_ASSIGNMENT: - case OR_ASSIGNMENT: - case XOR_ASSIGNMENT: - // see JLS 15.22 - Node targetLHS = scan(tree.getVariable(), p); - Node value = scan(tree.getExpression(), p); - - TypeMirror leftType = TreeUtils.typeOf(tree.getVariable()); - TypeMirror rightType = TreeUtils.typeOf(tree.getExpression()); - - Node targetRHS = null; - if (isNumericOrBoxed(leftType) && isNumericOrBoxed(rightType)) { - TypeMirror promotedType = binaryPromotedType(leftType, rightType); - targetRHS = binaryNumericPromotion(targetLHS, promotedType); - value = binaryNumericPromotion(value, promotedType); - } else if (TypesUtils.isBooleanType(leftType) - && TypesUtils.isBooleanType(rightType)) { - targetRHS = unbox(targetLHS); - value = unbox(value); - } else { - assert false - : "Both argument to logical operation must be numeric or boolean"; - } - - BinaryTree operTree = - treeBuilder.buildBinary( - leftType, - withoutAssignment(kind), - tree.getVariable(), - tree.getExpression()); - handleArtificialTree(operTree); - Node operNode; - if (kind == Tree.Kind.AND_ASSIGNMENT) { - operNode = new BitwiseAndNode(operTree, targetRHS, value); - } else if (kind == Tree.Kind.OR_ASSIGNMENT) { - operNode = new BitwiseOrNode(operTree, targetRHS, value); - } else { - assert kind == Kind.XOR_ASSIGNMENT; - operNode = new BitwiseXorNode(operTree, targetRHS, value); - } - extendWithNode(operNode); - - TypeCastTree castTree = treeBuilder.buildTypeCast(leftType, operTree); - handleArtificialTree(castTree); - TypeCastNode castNode = new TypeCastNode(castTree, operNode, leftType, types); - castNode.setInSource(false); - extendWithNode(castNode); - - AssignmentNode assignNode = new AssignmentNode(tree, targetLHS, castNode); - extendWithNode(assignNode); - return assignNode; - default: - assert false : "unexpected compound assignment type"; - break; - } - assert false : "unexpected compound assignment type"; - return null; - } - - @Override - public Node visitBinary(BinaryTree tree, Void p) { - // Note that for binary operations it is important to perform any required - // promotion on the left operand before generating any Nodes for the right - // operand, because labels must be inserted AFTER ALL preceding Nodes and - // BEFORE ALL following Nodes. - Node r = null; - Tree leftTree = tree.getLeftOperand(); - Tree rightTree = tree.getRightOperand(); - - Tree.Kind kind = tree.getKind(); - switch (kind) { - case DIVIDE: - case MULTIPLY: - case REMAINDER: - { - // see JLS 15.17 - - TypeMirror exprType = TreeUtils.typeOf(tree); - TypeMirror leftType = TreeUtils.typeOf(leftTree); - TypeMirror rightType = TreeUtils.typeOf(rightTree); - TypeMirror promotedType = binaryPromotedType(leftType, rightType); - - Node left = binaryNumericPromotion(scan(leftTree, p), promotedType); - Node right = binaryNumericPromotion(scan(rightTree, p), promotedType); - - if (kind == Tree.Kind.MULTIPLY) { - r = new NumericalMultiplicationNode(tree, left, right); - } else if (kind == Tree.Kind.DIVIDE) { - if (TypesUtils.isIntegral(exprType)) { - r = new IntegerDivisionNode(tree, left, right); - - TypeElement throwableElement = - elements.getTypeElement("java.lang.ArithmeticException"); - extendWithNodeWithException(r, throwableElement.asType()); - } else { - r = new FloatingDivisionNode(tree, left, right); - } - } else { - assert kind == Kind.REMAINDER; - if (TypesUtils.isIntegral(exprType)) { - r = new IntegerRemainderNode(tree, left, right); - - TypeElement throwableElement = - elements.getTypeElement("java.lang.ArithmeticException"); - extendWithNodeWithException(r, throwableElement.asType()); - } else { - r = new FloatingRemainderNode(tree, left, right); - } - } - break; - } - - case MINUS: - case PLUS: - { - // see JLS 15.18 - - // TypeMirror exprType = InternalUtils.typeOf(tree); - TypeMirror leftType = TreeUtils.typeOf(leftTree); - TypeMirror rightType = TreeUtils.typeOf(rightTree); - - if (TypesUtils.isString(leftType) || TypesUtils.isString(rightType)) { - assert (kind == Tree.Kind.PLUS); - Node left = stringConversion(scan(leftTree, p)); - Node right = stringConversion(scan(rightTree, p)); - r = new StringConcatenateNode(tree, left, right); - } else { - TypeMirror promotedType = binaryPromotedType(leftType, rightType); - Node left = binaryNumericPromotion(scan(leftTree, p), promotedType); - Node right = binaryNumericPromotion(scan(rightTree, p), promotedType); - - // TODO: Decide whether to deal with floating-point value - // set conversion. - if (kind == Tree.Kind.PLUS) { - r = new NumericalAdditionNode(tree, left, right); - } else { - assert kind == Kind.MINUS; - r = new NumericalSubtractionNode(tree, left, right); - } - } - break; - } - - case LEFT_SHIFT: - case RIGHT_SHIFT: - case UNSIGNED_RIGHT_SHIFT: - { - // see JLS 15.19 - - Node left = unaryNumericPromotion(scan(leftTree, p)); - Node right = unaryNumericPromotion(scan(rightTree, p)); - - if (kind == Tree.Kind.LEFT_SHIFT) { - r = new LeftShiftNode(tree, left, right); - } else if (kind == Tree.Kind.RIGHT_SHIFT) { - r = new SignedRightShiftNode(tree, left, right); - } else { - assert kind == Kind.UNSIGNED_RIGHT_SHIFT; - r = new UnsignedRightShiftNode(tree, left, right); - } - break; - } - - case GREATER_THAN: - case GREATER_THAN_EQUAL: - case LESS_THAN: - case LESS_THAN_EQUAL: - { - // see JLS 15.20.1 - TypeMirror leftType = TreeUtils.typeOf(leftTree); - if (TypesUtils.isBoxedPrimitive(leftType)) { - leftType = types.unboxedType(leftType); - } - - TypeMirror rightType = TreeUtils.typeOf(rightTree); - if (TypesUtils.isBoxedPrimitive(rightType)) { - rightType = types.unboxedType(rightType); - } - - TypeMirror promotedType = binaryPromotedType(leftType, rightType); - Node left = binaryNumericPromotion(scan(leftTree, p), promotedType); - Node right = binaryNumericPromotion(scan(rightTree, p), promotedType); - - Node node; - if (kind == Tree.Kind.GREATER_THAN) { - node = new GreaterThanNode(tree, left, right); - } else if (kind == Tree.Kind.GREATER_THAN_EQUAL) { - node = new GreaterThanOrEqualNode(tree, left, right); - } else if (kind == Tree.Kind.LESS_THAN) { - node = new LessThanNode(tree, left, right); - } else { - assert kind == Tree.Kind.LESS_THAN_EQUAL; - node = new LessThanOrEqualNode(tree, left, right); - } - - extendWithNode(node); - - return node; - } - - case EQUAL_TO: - case NOT_EQUAL_TO: - { - // see JLS 15.21 - TreeInfo leftInfo = getTreeInfo(leftTree); - TreeInfo rightInfo = getTreeInfo(rightTree); - Node left = scan(leftTree, p); - Node right = scan(rightTree, p); - - if (leftInfo.isNumeric() - && rightInfo.isNumeric() - && !(leftInfo.isBoxed() && rightInfo.isBoxed())) { - // JLS 15.21.1 numerical equality - TypeMirror promotedType = - binaryPromotedType( - leftInfo.unboxedType(), rightInfo.unboxedType()); - left = binaryNumericPromotion(left, promotedType); - right = binaryNumericPromotion(right, promotedType); - } else if (leftInfo.isBoolean() - && rightInfo.isBoolean() - && !(leftInfo.isBoxed() && rightInfo.isBoxed())) { - // JSL 15.21.2 boolean equality - left = unboxAsNeeded(left, leftInfo.isBoxed()); - right = unboxAsNeeded(right, rightInfo.isBoxed()); - } - - Node node; - if (kind == Tree.Kind.EQUAL_TO) { - node = new EqualToNode(tree, left, right); - } else { - assert kind == Kind.NOT_EQUAL_TO; - node = new NotEqualNode(tree, left, right); - } - extendWithNode(node); - - return node; - } - - case AND: - case OR: - case XOR: - { - // see JLS 15.22 - TypeMirror leftType = TreeUtils.typeOf(leftTree); - TypeMirror rightType = TreeUtils.typeOf(rightTree); - boolean isBooleanOp = - TypesUtils.isBooleanType(leftType) - && TypesUtils.isBooleanType(rightType); - - Node left; - Node right; - - if (isBooleanOp) { - left = unbox(scan(leftTree, p)); - right = unbox(scan(rightTree, p)); - } else if (isNumericOrBoxed(leftType) && isNumericOrBoxed(rightType)) { - TypeMirror promotedType = binaryPromotedType(leftType, rightType); - left = binaryNumericPromotion(scan(leftTree, p), promotedType); - right = binaryNumericPromotion(scan(rightTree, p), promotedType); - } else { - left = unbox(scan(leftTree, p)); - right = unbox(scan(rightTree, p)); - } - - Node node; - if (kind == Tree.Kind.AND) { - node = new BitwiseAndNode(tree, left, right); - } else if (kind == Tree.Kind.OR) { - node = new BitwiseOrNode(tree, left, right); - } else { - assert kind == Kind.XOR; - node = new BitwiseXorNode(tree, left, right); - } - - extendWithNode(node); - - return node; - } - - case CONDITIONAL_AND: - case CONDITIONAL_OR: - { - // see JLS 15.23 and 15.24 - - // all necessary labels - Label rightStartL = new Label(); - Label shortCircuitL = new Label(); - - // left-hand side - Node left = scan(leftTree, p); - - ConditionalJump cjump; - if (kind == Tree.Kind.CONDITIONAL_AND) { - cjump = new ConditionalJump(rightStartL, shortCircuitL); - cjump.setFalseFlowRule(Store.FlowRule.ELSE_TO_ELSE); - } else { - cjump = new ConditionalJump(shortCircuitL, rightStartL); - cjump.setTrueFlowRule(Store.FlowRule.THEN_TO_THEN); - } - extendWithExtendedNode(cjump); - - // right-hand side - addLabelForNextNode(rightStartL); - Node right = scan(rightTree, p); - - // conditional expression itself - addLabelForNextNode(shortCircuitL); - Node node; - if (kind == Tree.Kind.CONDITIONAL_AND) { - node = new ConditionalAndNode(tree, left, right); - } else { - node = new ConditionalOrNode(tree, left, right); - } - extendWithNode(node); - return node; - } - default: - assert false : "unexpected binary tree: " + kind; - break; - } - assert r != null : "unexpected binary tree"; - return extendWithNode(r); - } - - @Override - public Node visitBlock(BlockTree tree, Void p) { - for (StatementTree n : tree.getStatements()) { - scan(n, null); - } - return null; - } - - @Override - public Node visitBreak(BreakTree tree, Void p) { - Name label = tree.getLabel(); - if (label == null) { - assert breakTargetL != null : "no target for break statement"; - - extendWithExtendedNode(new UnconditionalJump(breakTargetL.accessLabel())); - } else { - assert breakLabels.containsKey(label); - - extendWithExtendedNode(new UnconditionalJump(breakLabels.get(label))); - } - - return null; - } - - @Override - public Node visitSwitch(SwitchTree tree, Void p) { - SwitchBuilder builder = new SwitchBuilder(tree); - builder.build(); - return null; - } - - /** Helper class for handling switch statements. */ - private class SwitchBuilder { - /** The switch tree. */ - private final SwitchTree switchTree; - /** The labels for the case bodies. */ - private final Label[] caseBodyLabels; - /** The Node for the switch expression. */ - private Node switchExpr; - - /** - * Construct a SwitchBuilder. - * - * @param tree switch tree - */ - private SwitchBuilder(SwitchTree tree) { - this.switchTree = tree; - this.caseBodyLabels = new Label[switchTree.getCases().size() + 1]; - } - - /** Build up the CFG for the switchTree. */ - public void build() { - TryFinallyScopeCell oldBreakTargetL = breakTargetL; - breakTargetL = new TryFinallyScopeCell(new Label()); - int cases = caseBodyLabels.length - 1; - for (int i = 0; i < cases; ++i) { - caseBodyLabels[i] = new Label(); - } - caseBodyLabels[cases] = breakTargetL.peekLabel(); - - TypeMirror switchExprType = TreeUtils.typeOf(switchTree.getExpression()); - VariableTree variable = - treeBuilder.buildVariableDecl( - switchExprType, uniqueName("switch"), findOwner(), null); - handleArtificialTree(variable); - - VariableDeclarationNode variableNode = new VariableDeclarationNode(variable); - variableNode.setInSource(false); - extendWithNode(variableNode); - - ExpressionTree variableUse = treeBuilder.buildVariableUse(variable); - handleArtificialTree(variableUse); - - LocalVariableNode variableUseNode = new LocalVariableNode(variableUse); - variableUseNode.setInSource(false); - extendWithNode(variableUseNode); - - Node switchExprNode = unbox(scan(switchTree.getExpression(), null)); - - AssignmentTree assign = - treeBuilder.buildAssignment(variableUse, switchTree.getExpression()); - handleArtificialTree(assign); - - switchExpr = new AssignmentNode(assign, variableUseNode, switchExprNode); - switchExpr.setInSource(false); - extendWithNode(switchExpr); - - extendWithNode( - new MarkerNode( - switchTree, - "start of switch statement #" + switchTree.hashCode(), - env.getTypeUtils())); - - Integer defaultIndex = null; - for (int i = 0; i < cases; ++i) { - CaseTree caseTree = switchTree.getCases().get(i); - if (caseTree.getExpression() == null) { - defaultIndex = i; - } else { - buildCase(caseTree, i); - } - } - if (defaultIndex != null) { - // the checks of all cases must happen before the default case, - // therefore we build the default case last. - // fallthrough is still handled correctly with the caseBodyLabels. - buildCase(switchTree.getCases().get(defaultIndex), defaultIndex); - } - - addLabelForNextNode(breakTargetL.peekLabel()); - breakTargetL = oldBreakTargetL; - - extendWithNode( - new MarkerNode( - switchTree, - "end of switch statement #" + switchTree.hashCode(), - env.getTypeUtils())); - } - - private void buildCase(CaseTree tree, int index) { - final Label thisBodyL = caseBodyLabels[index]; - final Label nextBodyL = caseBodyLabels[index + 1]; - final Label nextCaseL = new Label(); - - ExpressionTree exprTree = tree.getExpression(); - if (exprTree != null) { - // non-default cases - Node expr = scan(exprTree, null); - CaseNode test = new CaseNode(tree, switchExpr, expr, env.getTypeUtils()); - extendWithNode(test); - extendWithExtendedNode(new ConditionalJump(thisBodyL, nextCaseL)); - } - addLabelForNextNode(thisBodyL); - for (StatementTree stmt : tree.getStatements()) { - scan(stmt, null); - } - extendWithExtendedNode(new UnconditionalJump(nextBodyL)); - addLabelForNextNode(nextCaseL); - } - } - - @Override - public Node visitCase(CaseTree tree, Void p) { - throw new AssertionError("case visitor is implemented in SwitchBuilder"); - } - - @Override - public Node visitCatch(CatchTree tree, Void p) { - scan(tree.getParameter(), p); - scan(tree.getBlock(), p); - return null; - } - - @Override - public Node visitClass(ClassTree tree, Void p) { - declaredClasses.add(tree); - Node classbody = new ClassDeclarationNode(tree); - extendWithNode(classbody); - return classbody; - } - - @Override - public Node visitConditionalExpression(ConditionalExpressionTree tree, Void p) { - // see JLS 15.25 - TypeMirror exprType = TreeUtils.typeOf(tree); - - Label trueStart = new Label(); - Label falseStart = new Label(); - Label merge = new Label(); - - Node condition = unbox(scan(tree.getCondition(), p)); - ConditionalJump cjump = new ConditionalJump(trueStart, falseStart); - extendWithExtendedNode(cjump); - - addLabelForNextNode(trueStart); - Node trueExpr = scan(tree.getTrueExpression(), p); - trueExpr = conditionalExprPromotion(trueExpr, exprType); - extendWithExtendedNode(new UnconditionalJump(merge)); - - addLabelForNextNode(falseStart); - Node falseExpr = scan(tree.getFalseExpression(), p); - falseExpr = conditionalExprPromotion(falseExpr, exprType); - - addLabelForNextNode(merge); - Node node = new TernaryExpressionNode(tree, condition, trueExpr, falseExpr); - extendWithNode(node); - - return node; - } - - @Override - public Node visitContinue(ContinueTree tree, Void p) { - Name label = tree.getLabel(); - if (label == null) { - assert continueTargetL != null : "no target for continue statement"; - - extendWithExtendedNode(new UnconditionalJump(continueTargetL.accessLabel())); - } else { - assert continueLabels.containsKey(label); - - extendWithExtendedNode(new UnconditionalJump(continueLabels.get(label))); - } - - return null; - } - - @Override - public Node visitDoWhileLoop(DoWhileLoopTree tree, Void p) { - Name parentLabel = getLabel(getCurrentPath()); - - Label loopEntry = new Label(); - Label loopExit = new Label(); - - // If the loop is a labeled statement, then its continue - // target is identical for continues with no label and - // continues with the loop's label. - Label conditionStart; - if (parentLabel != null) { - conditionStart = continueLabels.get(parentLabel); - } else { - conditionStart = new Label(); - } - - TryFinallyScopeCell oldBreakTargetL = breakTargetL; - breakTargetL = new TryFinallyScopeCell(loopExit); - - TryFinallyScopeCell oldContinueTargetL = continueTargetL; - continueTargetL = new TryFinallyScopeCell(conditionStart); - - // Loop body - addLabelForNextNode(loopEntry); - assert tree.getStatement() != null; - scan(tree.getStatement(), p); - - // Condition - addLabelForNextNode(conditionStart); - assert tree.getCondition() != null; - unbox(scan(tree.getCondition(), p)); - ConditionalJump cjump = new ConditionalJump(loopEntry, loopExit); - extendWithExtendedNode(cjump); - - // Loop exit - addLabelForNextNode(loopExit); - - breakTargetL = oldBreakTargetL; - continueTargetL = oldContinueTargetL; - - return null; - } - - @Override - public Node visitErroneous(ErroneousTree tree, Void p) { - assert false : "ErroneousTree is unexpected in AST to CFG translation"; - return null; - } - - @Override - public Node visitExpressionStatement(ExpressionStatementTree tree, Void p) { - return scan(tree.getExpression(), p); - } - - @Override - public Node visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) { - // see JLS 14.14.2 - Name parentLabel = getLabel(getCurrentPath()); - - Label conditionStart = new Label(); - Label loopEntry = new Label(); - Label loopExit = new Label(); - - // If the loop is a labeled statement, then its continue - // target is identical for continues with no label and - // continues with the loop's label. - Label updateStart; - if (parentLabel != null) { - updateStart = continueLabels.get(parentLabel); - } else { - updateStart = new Label(); - } - - TryFinallyScopeCell oldBreakTargetL = breakTargetL; - breakTargetL = new TryFinallyScopeCell(loopExit); - - TryFinallyScopeCell oldContinueTargetL = continueTargetL; - continueTargetL = new TryFinallyScopeCell(updateStart); - - // Distinguish loops over Iterables from loops over arrays. - - TypeElement iterableElement = elements.getTypeElement("java.lang.Iterable"); - TypeMirror iterableType = types.erasure(iterableElement.asType()); - - VariableTree variable = tree.getVariable(); - VariableElement variableElement = TreeUtils.elementFromDeclaration(variable); - ExpressionTree expression = tree.getExpression(); - StatementTree statement = tree.getStatement(); - - TypeMirror exprType = TreeUtils.typeOf(expression); - - if (types.isSubtype(exprType, iterableType)) { - // Take the upper bound of a type variable or wildcard - exprType = TypesUtils.upperBound(exprType); - - assert (exprType instanceof DeclaredType) : "an Iterable must be a DeclaredType"; - DeclaredType declaredExprType = (DeclaredType) exprType; - declaredExprType.getTypeArguments(); - - MemberSelectTree iteratorSelect = treeBuilder.buildIteratorMethodAccess(expression); - handleArtificialTree(iteratorSelect); - - MethodInvocationTree iteratorCall = - treeBuilder.buildMethodInvocation(iteratorSelect); - handleArtificialTree(iteratorCall); - - VariableTree iteratorVariable = - createEnhancedForLoopIteratorVariable(iteratorCall, variableElement); - handleArtificialTree(iteratorVariable); - - VariableDeclarationNode iteratorVariableDecl = - new VariableDeclarationNode(iteratorVariable); - iteratorVariableDecl.setInSource(false); - - extendWithNode(iteratorVariableDecl); - - Node expressionNode = scan(expression, p); - - MethodAccessNode iteratorAccessNode = - new MethodAccessNode(iteratorSelect, expressionNode); - iteratorAccessNode.setInSource(false); - extendWithNode(iteratorAccessNode); - MethodInvocationNode iteratorCallNode = - new MethodInvocationNode( - iteratorCall, - iteratorAccessNode, - Collections.emptyList(), - getCurrentPath()); - iteratorCallNode.setInSource(false); - extendWithNode(iteratorCallNode); - - translateAssignment( - iteratorVariable, - new LocalVariableNode(iteratorVariable), - iteratorCallNode); - - // Test the loop ending condition - addLabelForNextNode(conditionStart); - IdentifierTree iteratorUse1 = treeBuilder.buildVariableUse(iteratorVariable); - handleArtificialTree(iteratorUse1); - - LocalVariableNode iteratorReceiverNode = new LocalVariableNode(iteratorUse1); - iteratorReceiverNode.setInSource(false); - extendWithNode(iteratorReceiverNode); - - MemberSelectTree hasNextSelect = treeBuilder.buildHasNextMethodAccess(iteratorUse1); - handleArtificialTree(hasNextSelect); - - MethodAccessNode hasNextAccessNode = - new MethodAccessNode(hasNextSelect, iteratorReceiverNode); - hasNextAccessNode.setInSource(false); - extendWithNode(hasNextAccessNode); - - MethodInvocationTree hasNextCall = treeBuilder.buildMethodInvocation(hasNextSelect); - handleArtificialTree(hasNextCall); - - MethodInvocationNode hasNextCallNode = - new MethodInvocationNode( - hasNextCall, - hasNextAccessNode, - Collections.emptyList(), - getCurrentPath()); - hasNextCallNode.setInSource(false); - extendWithNode(hasNextCallNode); - extendWithExtendedNode(new ConditionalJump(loopEntry, loopExit)); - - // Loop body, starting with declaration of the loop iteration variable - addLabelForNextNode(loopEntry); - extendWithNode(new VariableDeclarationNode(variable)); - - IdentifierTree iteratorUse2 = treeBuilder.buildVariableUse(iteratorVariable); - handleArtificialTree(iteratorUse2); - - LocalVariableNode iteratorReceiverNode2 = new LocalVariableNode(iteratorUse2); - iteratorReceiverNode2.setInSource(false); - extendWithNode(iteratorReceiverNode2); - - MemberSelectTree nextSelect = treeBuilder.buildNextMethodAccess(iteratorUse2); - handleArtificialTree(nextSelect); - - MethodAccessNode nextAccessNode = - new MethodAccessNode(nextSelect, iteratorReceiverNode2); - nextAccessNode.setInSource(false); - extendWithNode(nextAccessNode); - - MethodInvocationTree nextCall = treeBuilder.buildMethodInvocation(nextSelect); - handleArtificialTree(nextCall); - - MethodInvocationNode nextCallNode = - new MethodInvocationNode( - nextCall, - nextAccessNode, - Collections.emptyList(), - getCurrentPath()); - nextCallNode.setInSource(false); - extendWithNode(nextCallNode); - - translateAssignment(variable, new LocalVariableNode(variable), nextCall); - - assert statement != null; - scan(statement, p); - - // Loop back edge - addLabelForNextNode(updateStart); - extendWithExtendedNode(new UnconditionalJump(conditionStart)); - - } else { - // TODO: Shift any labels after the initialization of the - // temporary array variable. - - VariableTree arrayVariable = - createEnhancedForLoopArrayVariable(expression, variableElement); - handleArtificialTree(arrayVariable); - - VariableDeclarationNode arrayVariableNode = - new VariableDeclarationNode(arrayVariable); - arrayVariableNode.setInSource(false); - extendWithNode(arrayVariableNode); - Node expressionNode = scan(expression, p); - - translateAssignment( - arrayVariable, new LocalVariableNode(arrayVariable), expressionNode); - - // Declare and initialize the loop index variable - TypeMirror intType = types.getPrimitiveType(TypeKind.INT); - - LiteralTree zero = treeBuilder.buildLiteral(Integer.valueOf(0)); - handleArtificialTree(zero); - - VariableTree indexVariable = - treeBuilder.buildVariableDecl( - intType, - uniqueName("index"), - variableElement.getEnclosingElement(), - zero); - handleArtificialTree(indexVariable); - VariableDeclarationNode indexVariableNode = - new VariableDeclarationNode(indexVariable); - indexVariableNode.setInSource(false); - extendWithNode(indexVariableNode); - IntegerLiteralNode zeroNode = extendWithNode(new IntegerLiteralNode(zero)); - - translateAssignment(indexVariable, new LocalVariableNode(indexVariable), zeroNode); - - // Compare index to array length - addLabelForNextNode(conditionStart); - IdentifierTree indexUse1 = treeBuilder.buildVariableUse(indexVariable); - handleArtificialTree(indexUse1); - LocalVariableNode indexNode1 = new LocalVariableNode(indexUse1); - indexNode1.setInSource(false); - extendWithNode(indexNode1); - - IdentifierTree arrayUse1 = treeBuilder.buildVariableUse(arrayVariable); - handleArtificialTree(arrayUse1); - LocalVariableNode arrayNode1 = extendWithNode(new LocalVariableNode(arrayUse1)); - - MemberSelectTree lengthSelect = treeBuilder.buildArrayLengthAccess(arrayUse1); - handleArtificialTree(lengthSelect); - FieldAccessNode lengthAccessNode = new FieldAccessNode(lengthSelect, arrayNode1); - lengthAccessNode.setInSource(false); - extendWithNode(lengthAccessNode); - - BinaryTree lessThan = treeBuilder.buildLessThan(indexUse1, lengthSelect); - handleArtificialTree(lessThan); - - LessThanNode lessThanNode = - new LessThanNode(lessThan, indexNode1, lengthAccessNode); - lessThanNode.setInSource(false); - extendWithNode(lessThanNode); - extendWithExtendedNode(new ConditionalJump(loopEntry, loopExit)); - - // Loop body, starting with declaration of the loop iteration variable - addLabelForNextNode(loopEntry); - extendWithNode(new VariableDeclarationNode(variable)); - - IdentifierTree arrayUse2 = treeBuilder.buildVariableUse(arrayVariable); - handleArtificialTree(arrayUse2); - LocalVariableNode arrayNode2 = new LocalVariableNode(arrayUse2); - arrayNode2.setInSource(false); - extendWithNode(arrayNode2); - - IdentifierTree indexUse2 = treeBuilder.buildVariableUse(indexVariable); - handleArtificialTree(indexUse2); - LocalVariableNode indexNode2 = new LocalVariableNode(indexUse2); - indexNode2.setInSource(false); - extendWithNode(indexNode2); - - ArrayAccessTree arrayAccess = treeBuilder.buildArrayAccess(arrayUse2, indexUse2); - handleArtificialTree(arrayAccess); - ArrayAccessNode arrayAccessNode = - new ArrayAccessNode(arrayAccess, arrayNode2, indexNode2); - arrayAccessNode.setInSource(false); - extendWithNode(arrayAccessNode); - translateAssignment(variable, new LocalVariableNode(variable), arrayAccessNode); - Element npeElement = elements.getTypeElement("java.lang.NullPointerException"); - extendWithNodeWithException(arrayAccessNode, npeElement.asType()); - - assert statement != null; - scan(statement, p); - - // Loop back edge - addLabelForNextNode(updateStart); - - IdentifierTree indexUse3 = treeBuilder.buildVariableUse(indexVariable); - handleArtificialTree(indexUse3); - LocalVariableNode indexNode3 = new LocalVariableNode(indexUse3); - indexNode3.setInSource(false); - extendWithNode(indexNode3); - - LiteralTree oneTree = treeBuilder.buildLiteral(Integer.valueOf(1)); - handleArtificialTree(oneTree); - Node one = new IntegerLiteralNode(oneTree); - one.setInSource(false); - extendWithNode(one); - - BinaryTree addOneTree = - treeBuilder.buildBinary(intType, Tree.Kind.PLUS, indexUse3, oneTree); - handleArtificialTree(addOneTree); - Node addOneNode = new NumericalAdditionNode(addOneTree, indexNode3, one); - addOneNode.setInSource(false); - extendWithNode(addOneNode); - - AssignmentTree assignTree = treeBuilder.buildAssignment(indexUse3, addOneTree); - handleArtificialTree(assignTree); - Node assignNode = new AssignmentNode(assignTree, indexNode3, addOneNode); - assignNode.setInSource(false); - extendWithNode(assignNode); - - extendWithExtendedNode(new UnconditionalJump(conditionStart)); - } - - // Loop exit - addLabelForNextNode(loopExit); - - breakTargetL = oldBreakTargetL; - continueTargetL = oldContinueTargetL; - - return null; - } - - protected VariableTree createEnhancedForLoopIteratorVariable( - MethodInvocationTree iteratorCall, VariableElement variableElement) { - TypeMirror iteratorType = TreeUtils.typeOf(iteratorCall); - - // Declare and initialize a new, unique iterator variable - VariableTree iteratorVariable = - treeBuilder.buildVariableDecl( - iteratorType, // annotatedIteratorTypeTree, - uniqueName("iter"), - variableElement.getEnclosingElement(), - iteratorCall); - return iteratorVariable; - } - - protected VariableTree createEnhancedForLoopArrayVariable( - ExpressionTree expression, VariableElement variableElement) { - TypeMirror arrayType = TreeUtils.typeOf(expression); - - // Declare and initialize a temporary array variable - VariableTree arrayVariable = - treeBuilder.buildVariableDecl( - arrayType, - uniqueName("array"), - variableElement.getEnclosingElement(), - expression); - return arrayVariable; - } - - @Override - public Node visitForLoop(ForLoopTree tree, Void p) { - Name parentLabel = getLabel(getCurrentPath()); - - Label conditionStart = new Label(); - Label loopEntry = new Label(); - Label loopExit = new Label(); - - // If the loop is a labeled statement, then its continue - // target is identical for continues with no label and - // continues with the loop's label. - Label updateStart; - if (parentLabel != null) { - updateStart = continueLabels.get(parentLabel); - } else { - updateStart = new Label(); - } - - TryFinallyScopeCell oldBreakTargetL = breakTargetL; - breakTargetL = new TryFinallyScopeCell(loopExit); - - TryFinallyScopeCell oldContinueTargetL = continueTargetL; - continueTargetL = new TryFinallyScopeCell(updateStart); - - // Initializer - for (StatementTree init : tree.getInitializer()) { - scan(init, p); - } - - // Condition - addLabelForNextNode(conditionStart); - if (tree.getCondition() != null) { - unbox(scan(tree.getCondition(), p)); - ConditionalJump cjump = new ConditionalJump(loopEntry, loopExit); - extendWithExtendedNode(cjump); - } - - // Loop body - addLabelForNextNode(loopEntry); - assert tree.getStatement() != null; - scan(tree.getStatement(), p); - - // Update - addLabelForNextNode(updateStart); - for (ExpressionStatementTree update : tree.getUpdate()) { - scan(update, p); - } - - extendWithExtendedNode(new UnconditionalJump(conditionStart)); - - // Loop exit - addLabelForNextNode(loopExit); - - breakTargetL = oldBreakTargetL; - continueTargetL = oldContinueTargetL; - - return null; - } - - @Override - public Node visitIdentifier(IdentifierTree tree, Void p) { - Node node; - if (TreeUtils.isFieldAccess(tree)) { - Node receiver = getReceiver(tree); - node = new FieldAccessNode(tree, receiver); - } else { - Element element = TreeUtils.elementFromUse(tree); - switch (element.getKind()) { - case FIELD: - // Note that "this"/"super" is a field, but not a field access. - if (element.getSimpleName().contentEquals("this")) { - node = new ExplicitThisLiteralNode(tree); - } else { - node = new SuperNode(tree); - } - break; - case EXCEPTION_PARAMETER: - case LOCAL_VARIABLE: - case RESOURCE_VARIABLE: - case PARAMETER: - node = new LocalVariableNode(tree); - break; - case PACKAGE: - node = new PackageNameNode(tree); - break; - default: - if (ElementUtils.isTypeDeclaration(element)) { - node = new ClassNameNode(tree); - break; - } - throw new BugInCF("bad element kind " + element.getKind()); - } - } - extendWithNode(node); - return node; - } - - @Override - public Node visitIf(IfTree tree, Void p) { - // all necessary labels - Label thenEntry = new Label(); - Label elseEntry = new Label(); - Label endIf = new Label(); - - // basic block for the condition - unbox(scan(tree.getCondition(), p)); - - ConditionalJump cjump = new ConditionalJump(thenEntry, elseEntry); - extendWithExtendedNode(cjump); - - // then branch - addLabelForNextNode(thenEntry); - StatementTree thenStatement = tree.getThenStatement(); - scan(thenStatement, p); - extendWithExtendedNode(new UnconditionalJump(endIf)); - - // else branch - addLabelForNextNode(elseEntry); - StatementTree elseStatement = tree.getElseStatement(); - if (elseStatement != null) { - scan(elseStatement, p); - } - - // label the end of the if statement - addLabelForNextNode(endIf); - - return null; - } - - @Override - public Node visitImport(ImportTree tree, Void p) { - assert false : "ImportTree is unexpected in AST to CFG translation"; - return null; - } - - @Override - public Node visitArrayAccess(ArrayAccessTree tree, Void p) { - Node array = scan(tree.getExpression(), p); - Node index = unaryNumericPromotion(scan(tree.getIndex(), p)); - Node arrayAccess = extendWithNode(new ArrayAccessNode(tree, array, index)); - Element aioobeElement = - elements.getTypeElement("java.lang.ArrayIndexOutOfBoundsException"); - extendWithNodeWithException(arrayAccess, aioobeElement.asType()); - Element npeElement = elements.getTypeElement("java.lang.NullPointerException"); - extendWithNodeWithException(arrayAccess, npeElement.asType()); - return arrayAccess; - } - - @Override - public Node visitLabeledStatement(LabeledStatementTree tree, Void p) { - // This method can set the break target after generating all Nodes - // in the contained statement, but it can't set the continue target, - // which may be in the middle of a sequence of nodes. Labeled loops - // must look up and use the continue Labels. - Name labelName = tree.getLabel(); - - Label breakL = new Label(labelName + "_break"); - Label continueL = new Label(labelName + "_continue"); - - breakLabels.put(labelName, breakL); - continueLabels.put(labelName, continueL); - - scan(tree.getStatement(), p); - - addLabelForNextNode(breakL); - - breakLabels.remove(labelName); - continueLabels.remove(labelName); - - return null; - } - - @Override - public Node visitLiteral(LiteralTree tree, Void p) { - Node r = null; - switch (tree.getKind()) { - case BOOLEAN_LITERAL: - r = new BooleanLiteralNode(tree); - break; - case CHAR_LITERAL: - r = new CharacterLiteralNode(tree); - break; - case DOUBLE_LITERAL: - r = new DoubleLiteralNode(tree); - break; - case FLOAT_LITERAL: - r = new FloatLiteralNode(tree); - break; - case INT_LITERAL: - r = new IntegerLiteralNode(tree); - break; - case LONG_LITERAL: - r = new LongLiteralNode(tree); - break; - case NULL_LITERAL: - r = new NullLiteralNode(tree); - break; - case STRING_LITERAL: - r = new StringLiteralNode(tree); - break; - default: - assert false : "unexpected literal tree"; - break; - } - assert r != null : "unexpected literal tree"; - Node result = extendWithNode(r); - return result; - } - - @Override - public Node visitMethod(MethodTree tree, Void p) { - assert false : "MethodTree is unexpected in AST to CFG translation"; - return null; - } - - @Override - public Node visitModifiers(ModifiersTree tree, Void p) { - assert false : "ModifiersTree is unexpected in AST to CFG translation"; - return null; - } - - @Override - public Node visitNewArray(NewArrayTree tree, Void p) { - // see JLS 15.10 - - ArrayType type = (ArrayType) TreeUtils.typeOf(tree); - TypeMirror elemType = type.getComponentType(); - - List dimensions = tree.getDimensions(); - List initializers = tree.getInitializers(); - - List dimensionNodes = new ArrayList<>(); - assert dimensions != null; - for (ExpressionTree dim : dimensions) { - dimensionNodes.add(unaryNumericPromotion(scan(dim, p))); - } - - List initializerNodes = new ArrayList<>(); - if (initializers != null) { - for (ExpressionTree init : initializers) { - initializerNodes.add(assignConvert(scan(init, p), elemType)); - } - } - - Node node = new ArrayCreationNode(tree, type, dimensionNodes, initializerNodes); - return extendWithNode(node); - } - - @Override - public Node visitNewClass(NewClassTree tree, Void p) { - // see JLS 15.9 - - Tree enclosingExpr = tree.getEnclosingExpression(); - if (enclosingExpr != null) { - scan(enclosingExpr, p); - } - - // Convert constructor arguments - ExecutableElement constructor = TreeUtils.elementFromUse(tree); - - List actualExprs = tree.getArguments(); - - List arguments = convertCallArguments(constructor, actualExprs); - - // TODO: for anonymous classes, don't use the identifier alone. - // See Issue 890. - Node constructorNode = scan(tree.getIdentifier(), p); - - // Handle anonymous classes in visitClass. - // Note that getClassBody() and therefore classbody can be null. - ClassDeclarationNode classbody = (ClassDeclarationNode) scan(tree.getClassBody(), p); - - Node node = new ObjectCreationNode(tree, constructorNode, arguments, classbody); - - Set thrownSet = new HashSet<>(); - // Add exceptions explicitly mentioned in the throws clause. - List thrownTypes = constructor.getThrownTypes(); - thrownSet.addAll(thrownTypes); - // Add Throwable to account for unchecked exceptions - TypeElement throwableElement = elements.getTypeElement("java.lang.Throwable"); - thrownSet.add(throwableElement.asType()); - - extendWithNodeWithExceptions(node, thrownSet); - - return node; - } - - /** - * Maps a {@code Tree} to its directly enclosing {@code ParenthesizedTree} if one exists. - * - *

      This map is used by {@link CFGTranslationPhaseOne#addToLookupMap(Node)} to associate a - * {@code ParenthesizedTree} with the dataflow {@code Node} that was used during inference. - * This map is necessary because dataflow does not create a {@code Node} for a {@code - * ParenthesizedTree}. - */ - private final Map parenMapping = new HashMap<>(); - - @Override - public Node visitParenthesized(ParenthesizedTree tree, Void p) { - parenMapping.put(tree.getExpression(), tree); - return scan(tree.getExpression(), p); - } - - @Override - public Node visitReturn(ReturnTree tree, Void p) { - ExpressionTree ret = tree.getExpression(); - // TODO: also have a return-node if nothing is returned - ReturnNode result = null; - if (ret != null) { - Node node = scan(ret, p); - Tree enclosing = - TreeUtils.enclosingOfKind( - getCurrentPath(), - new HashSet<>(Arrays.asList(Kind.METHOD, Kind.LAMBDA_EXPRESSION))); - if (enclosing.getKind() == Kind.LAMBDA_EXPRESSION) { - LambdaExpressionTree lambdaTree = (LambdaExpressionTree) enclosing; - TreePath lambdaTreePath = - TreePath.getPath(getCurrentPath().getCompilationUnit(), lambdaTree); - Context ctx = ((JavacProcessingEnvironment) env).getContext(); - Element overriddenElement = - com.sun.tools.javac.code.Types.instance(ctx) - .findDescriptorSymbol( - ((Type) trees.getTypeMirror(lambdaTreePath)).tsym); - - result = - new ReturnNode( - tree, - node, - env.getTypeUtils(), - lambdaTree, - (MethodSymbol) overriddenElement); - } else { - result = new ReturnNode(tree, node, env.getTypeUtils(), (MethodTree) enclosing); - } - returnNodes.add(result); - extendWithNode(result); - } - - extendWithExtendedNode(new UnconditionalJump(this.returnTargetL.accessLabel())); - - return result; - } - - @Override - public Node visitMemberSelect(MemberSelectTree tree, Void p) { - Node expr = scan(tree.getExpression(), p); - if (!TreeUtils.isFieldAccess(tree)) { - // Could be a selector of a class or package - Element element = TreeUtils.elementFromUse(tree); - if (ElementUtils.isClassElement(element)) { - return extendWithNode(new ClassNameNode(tree, expr)); - } else if (element.getKind() == ElementKind.PACKAGE) { - return extendWithNode(new PackageNameNode(tree, (PackageNameNode) expr)); - } else { - assert false : "Unexpected element kind: " + element.getKind(); - return null; - } - } - - Node node = new FieldAccessNode(tree, expr); - - Element element = TreeUtils.elementFromUse(tree); - if (ElementUtils.isStatic(element) - || expr instanceof ImplicitThisLiteralNode - || expr instanceof ExplicitThisLiteralNode) { - // No NullPointerException can be thrown, use normal node - extendWithNode(node); - } else { - TypeElement npeElement = elements.getTypeElement("java.lang.NullPointerException"); - extendWithNodeWithException(node, npeElement.asType()); - } - - return node; - } - - @Override - public Node visitEmptyStatement(EmptyStatementTree tree, Void p) { - return null; - } - - @Override - public Node visitSynchronized(SynchronizedTree tree, Void p) { - // see JLS 14.19 - - Node synchronizedExpr = scan(tree.getExpression(), p); - SynchronizedNode synchronizedStartNode = - new SynchronizedNode(tree, synchronizedExpr, true, env.getTypeUtils()); - extendWithNode(synchronizedStartNode); - scan(tree.getBlock(), p); - SynchronizedNode synchronizedEndNode = - new SynchronizedNode(tree, synchronizedExpr, false, env.getTypeUtils()); - extendWithNode(synchronizedEndNode); - - return null; - } - - @Override - public Node visitThrow(ThrowTree tree, Void p) { - Node expression = scan(tree.getExpression(), p); - TypeMirror exception = expression.getType(); - ThrowNode throwsNode = new ThrowNode(tree, expression, env.getTypeUtils()); - NodeWithExceptionsHolder exNode = extendWithNodeWithException(throwsNode, exception); - exNode.setTerminatesExecution(true); - return throwsNode; - } - - @Override - public Node visitCompilationUnit(CompilationUnitTree tree, Void p) { - assert false : "CompilationUnitTree is unexpected in AST to CFG translation"; - return null; - } - - @Override - public Node visitTry(TryTree tree, Void p) { - List catches = tree.getCatches(); - BlockTree finallyBlock = tree.getFinallyBlock(); - - extendWithNode( - new MarkerNode( - tree, - "start of try statement #" + tree.hashCode(), - env.getTypeUtils())); - - // TODO: Should we handle try-with-resources blocks by also generating code - // for automatically closing the resources? - List resources = tree.getResources(); - for (Tree resource : resources) { - scan(resource, p); - } - - List> catchLabels = new ArrayList<>(); - for (CatchTree c : catches) { - TypeMirror type = TreeUtils.typeOf(c.getParameter().getType()); - assert type != null : "exception parameters must have a type"; - catchLabels.add(Pair.of(type, new Label())); - } - - // Store return/break/continue labels, just in case we need them for a finally block. - TryFinallyScopeCell oldReturnTargetL = returnTargetL; - TryFinallyScopeCell oldBreakTargetL = breakTargetL; - Map oldBreakLabels = breakLabels; - TryFinallyScopeCell oldContinueTargetL = continueTargetL; - Map oldContinueLabels = continueLabels; - - Label finallyLabel = null; - Label exceptionalFinallyLabel = null; - - if (finallyBlock != null) { - finallyLabel = new Label(); - - exceptionalFinallyLabel = new Label(); - tryStack.pushFrame(new TryFinallyFrame(exceptionalFinallyLabel)); - - returnTargetL = new TryFinallyScopeCell(); - - breakTargetL = new TryFinallyScopeCell(); - breakLabels = new TryFinallyScopeMap(); - - continueTargetL = new TryFinallyScopeCell(); - continueLabels = new TryFinallyScopeMap(); - } - - Label doneLabel = new Label(); - - tryStack.pushFrame(new TryCatchFrame(types, catchLabels)); - - extendWithNode( - new MarkerNode( - tree, "start of try block #" + tree.hashCode(), env.getTypeUtils())); - scan(tree.getBlock(), p); - extendWithNode( - new MarkerNode( - tree, "end of try block #" + tree.hashCode(), env.getTypeUtils())); - - extendWithExtendedNode(new UnconditionalJump(firstNonNull(finallyLabel, doneLabel))); - - tryStack.popFrame(); - - int catchIndex = 0; - for (CatchTree c : catches) { - addLabelForNextNode(catchLabels.get(catchIndex).second); - extendWithNode( - new MarkerNode( - tree, - "start of catch block for " - + c.getParameter().getType() - + " #" - + tree.hashCode(), - env.getTypeUtils())); - scan(c, p); - extendWithNode( - new MarkerNode( - tree, - "end of catch block for " - + c.getParameter().getType() - + " #" - + tree.hashCode(), - env.getTypeUtils())); - - catchIndex++; - extendWithExtendedNode( - new UnconditionalJump(firstNonNull(finallyLabel, doneLabel))); - } - - if (finallyLabel != null) { - // Reset values before analyzing the finally block! - - tryStack.popFrame(); - - { // Scan 'finallyBlock' for only 'finallyLabel' (a successful path) - addLabelForNextNode(finallyLabel); - extendWithNode( - new MarkerNode( - tree, - "start of finally block #" + tree.hashCode(), - env.getTypeUtils())); - scan(finallyBlock, p); - extendWithNode( - new MarkerNode( - tree, - "end of finally block #" + tree.hashCode(), - env.getTypeUtils())); - extendWithExtendedNode(new UnconditionalJump(doneLabel)); - } - - if (hasExceptionalPath(exceptionalFinallyLabel)) { - // If an exceptional path exists, scan 'finallyBlock' for - // 'exceptionalFinallyLabel', and scan copied 'finallyBlock' for 'finallyLabel' - // (a successful path). If there is no successful path, it will be removed in - // later phase. TODO: Don't we need a separate finally block for each kind of - // exception? - addLabelForNextNode(exceptionalFinallyLabel); - extendWithNode( - new MarkerNode( - tree, - "start of finally block for Throwable #" + tree.hashCode(), - env.getTypeUtils())); - - scan(finallyBlock, p); - - TypeMirror throwableType = - elements.getTypeElement("java.lang.Throwable").asType(); - NodeWithExceptionsHolder throwing = - extendWithNodeWithException( - new MarkerNode( - tree, - "end of finally block for Throwable #" - + tree.hashCode(), - env.getTypeUtils()), - throwableType); - - throwing.setTerminatesExecution(true); - } - - if (returnTargetL.wasAccessed()) { - addLabelForNextNode(returnTargetL.peekLabel()); - returnTargetL = oldReturnTargetL; - - extendWithNode( - new MarkerNode( - tree, - "start of finally block for return #" + tree.hashCode(), - env.getTypeUtils())); - scan(finallyBlock, p); - extendWithNode( - new MarkerNode( - tree, - "end of finally block for return #" + tree.hashCode(), - env.getTypeUtils())); - extendWithExtendedNode(new UnconditionalJump(returnTargetL.accessLabel())); - } else { - returnTargetL = oldReturnTargetL; - } - - if (breakTargetL.wasAccessed()) { - addLabelForNextNode(breakTargetL.peekLabel()); - breakTargetL = oldBreakTargetL; - - extendWithNode( - new MarkerNode( - tree, - "start of finally block for break #" + tree.hashCode(), - env.getTypeUtils())); - scan(finallyBlock, p); - extendWithNode( - new MarkerNode( - tree, - "end of finally block for break #" + tree.hashCode(), - env.getTypeUtils())); - extendWithExtendedNode(new UnconditionalJump(breakTargetL.accessLabel())); - } else { - breakTargetL = oldBreakTargetL; - } - - Map accessedBreakLabels = - ((TryFinallyScopeMap) breakLabels).getAccessedNames(); - if (!accessedBreakLabels.isEmpty()) { - breakLabels = oldBreakLabels; - - for (Map.Entry access : accessedBreakLabels.entrySet()) { - addLabelForNextNode(access.getValue()); - extendWithNode( - new MarkerNode( - tree, - "start of finally block for break label " - + access.getKey() - + " #" - + tree.hashCode(), - env.getTypeUtils())); - scan(finallyBlock, p); - extendWithNode( - new MarkerNode( - tree, - "end of finally block for break label " - + access.getKey() - + " #" - + tree.hashCode(), - env.getTypeUtils())); - extendWithExtendedNode( - new UnconditionalJump(breakLabels.get(access.getKey()))); - } - } else { - breakLabels = oldBreakLabels; - } - - if (continueTargetL.wasAccessed()) { - addLabelForNextNode(continueTargetL.peekLabel()); - continueTargetL = oldContinueTargetL; - - extendWithNode( - new MarkerNode( - tree, - "start of finally block for continue #" + tree.hashCode(), - env.getTypeUtils())); - scan(finallyBlock, p); - extendWithNode( - new MarkerNode( - tree, - "end of finally block for continue #" + tree.hashCode(), - env.getTypeUtils())); - extendWithExtendedNode(new UnconditionalJump(continueTargetL.accessLabel())); - } else { - continueTargetL = oldContinueTargetL; - } - - Map accessedContinueLabels = - ((TryFinallyScopeMap) continueLabels).getAccessedNames(); - if (!accessedContinueLabels.isEmpty()) { - continueLabels = oldContinueLabels; - - for (Map.Entry access : accessedContinueLabels.entrySet()) { - addLabelForNextNode(access.getValue()); - extendWithNode( - new MarkerNode( - tree, - "start of finally block for continue label " - + access.getKey() - + " #" - + tree.hashCode(), - env.getTypeUtils())); - scan(finallyBlock, p); - extendWithNode( - new MarkerNode( - tree, - "end of finally block for continue label " - + access.getKey() - + " #" - + tree.hashCode(), - env.getTypeUtils())); - extendWithExtendedNode( - new UnconditionalJump(continueLabels.get(access.getKey()))); - } - } else { - continueLabels = oldContinueLabels; - } - } - - addLabelForNextNode(doneLabel); - - return null; - } - - /** - * Returns whether an exceptional node for {@code target} exists in {@link #nodeList} or - * not. - * - * @param target label for exception - * @return true when an exceptional node for {@code target} exists in {@link #nodeList} - */ - private boolean hasExceptionalPath(Label target) { - for (ExtendedNode node : nodeList) { - if (node instanceof NodeWithExceptionsHolder) { - NodeWithExceptionsHolder exceptionalNode = (NodeWithExceptionsHolder) node; - for (Set

      Usage: Directly run it as the main class to generate the DOT representation of the control - * flow graph of a given method in a given class. See {@link - * org.checkerframework.dataflow.cfg.playground.ConstantPropagationPlayground} for another way to - * use it. - */ -public class CFGVisualizeLauncher { - - /** - * The main entry point of CFGVisualizeLauncher. - * - * @param args the passed arguments, see {@link #printUsage()} for the usage - */ - public static void main(String[] args) { - CFGVisualizeLauncher cfgVisualizeLauncher = new CFGVisualizeLauncher(); - if (args.length < 2) { - 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()); - cfgVisualizeLauncher.printUsage(); - System.exit(1); - } - - String method = "test"; - String clas = "Test"; - boolean pdf = false; - boolean error = false; - boolean verbose = false; - - for (int i = 2; i < args.length; i++) { - switch (args[i]) { - case "-pdf": - pdf = true; - break; - case "-method": - if (i >= args.length - 1) { - cfgVisualizeLauncher.printError("Did not find after -method."); - continue; - } - i++; - method = args[i]; - break; - case "-class": - if (i >= args.length - 1) { - cfgVisualizeLauncher.printError("Did not find after -class."); - continue; - } - i++; - clas = args[i]; - break; - case "-verbose": - verbose = true; - break; - default: - cfgVisualizeLauncher.printError("Unknown command line argument: " + args[i]); - error = true; - break; - } - } - - if (error) { - System.exit(1); - } - - cfgVisualizeLauncher.generateDOTofCFGWithoutAnalysis( - input, output, method, clas, pdf, verbose); - } - - /** - * Generate the DOT representation of the CFG for a method without analysis. - * - * @param inputFile java source input file - * @param outputDir output directory - * @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 pdf also generate a PDF - * @param verbose show verbose information in CFG - */ - protected void generateDOTofCFGWithoutAnalysis( - String inputFile, - String outputDir, - String method, - String clas, - boolean pdf, - boolean verbose) { - generateDOTofCFG(inputFile, outputDir, method, clas, pdf, verbose, null); - } - - /** - * Generate the DOT representation of the CFG for a method. - * - * @param the abstract value type to be tracked by the analysis - * @param the store type used in the analysis - * @param the transfer function type that is used to approximated runtime behavior - * @param inputFile java source input file - * @param outputDir source output directory - * @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 pdf also generate a PDF - * @param verbose show verbose information in CFG - * @param analysis analysis to perform before the visualization (or {@code null} if no analysis - * is to be performed) - */ - public , S extends Store, T extends TransferFunction> - void generateDOTofCFG( - String inputFile, - String outputDir, - String method, - String clas, - boolean pdf, - boolean verbose, - @Nullable Analysis analysis) { - ControlFlowGraph cfg = generateMethodCFG(inputFile, clas, method); - if (analysis != null) { - analysis.performAnalysis(cfg); - } - - Map args = new HashMap<>(); - args.put("outdir", outputDir); - args.put("verbose", verbose); - - CFGVisualizer viz = new DOTCFGVisualizer<>(); - viz.init(args); - Map res = viz.visualize(cfg, cfg.getEntryBlock(), analysis); - viz.shutdown(); - - if (pdf && res != null) { - assert res.get("dotFileName") != null : "@AssumeAssertion(nullness): specification"; - producePDF((String) res.get("dotFileName")); - } - } - - /** - * Generate the control flow graph of a method in a class. - * - * @param file java source input file - * @param clas name of the class which includes the method to generate the CFG for - * @param method name of the method to generate the CFG for - * @return control flow graph of the specified method - */ - protected ControlFlowGraph generateMethodCFG(String file, String clas, final String method) { - - CFGProcessor cfgProcessor = new CFGProcessor(clas, method); - - Context context = new Context(); - Options.instance(context).put("compilePolicy", "ATTR_ONLY"); - JavaCompiler javac = new JavaCompiler(context); - - JavacFileManager fileManager = (JavacFileManager) context.get(JavaFileManager.class); - - JavaFileObject l = - fileManager.getJavaFileObjectsFromStrings(List.of(file)).iterator().next(); - - PrintStream err = System.err; - try { - // redirect syserr to nothing (and prevent the compiler from issuing - // warnings about our exception. - System.setErr( - new PrintStream( - new OutputStream() { - @Override - public void write(int b) throws IOException {} - })); - javac.compile(List.of(l), List.of(clas), List.of(cfgProcessor), List.nil()); - } catch (Throwable e) { - // ok - } finally { - System.setErr(err); - } - - CFGProcessResult res = cfgProcessor.getCFGProcessResult(); - - if (res == null) { - printError( - "internal error in type processor! method typeProcessOver() doesn't get called."); - System.exit(1); - } - - if (!res.isSuccess()) { - printError(res.getErrMsg()); - System.exit(1); - } - - return res.getCFG(); - } - - /** - * Invoke "dot" command to generate a PDF. - * - * @param file name of the dot file - */ - protected void producePDF(String file) { - try { - String command = "dot -Tpdf \"" + file + "\" -o \"" + file + ".pdf\""; - Process child = Runtime.getRuntime().exec(new String[] {"/bin/sh", "-c", command}); - child.waitFor(); - } catch (InterruptedException | IOException e) { - e.printStackTrace(); - System.exit(1); - } - } - - /** - * Generate the String representation of the CFG for a method. - * - * @param the abstract value type to be tracked by the analysis - * @param the store type used in the analysis - * @param the transfer function type that is used to approximated runtime behavior - * @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 - * @param analysis analysis to perform before the visualization (or {@code null} if no analysis - * is to be performed) - * @return a map which includes a key "stringGraph" and the String representation of CFG as the - * value - */ - public , S extends Store, T extends TransferFunction> - @Nullable Map generateStringOfCFG( - String inputFile, - String method, - String clas, - boolean verbose, - @Nullable Analysis analysis) { - ControlFlowGraph cfg = generateMethodCFG(inputFile, clas, method); - if (analysis != null) { - analysis.performAnalysis(cfg); - } - - Map args = new HashMap<>(); - args.put("verbose", verbose); - - CFGVisualizer viz = new StringCFGVisualizer<>(); - viz.init(args); - Map res = viz.visualize(cfg, cfg.getEntryBlock(), analysis); - viz.shutdown(); - return res; - } - - /** Print usage information. */ - protected void printUsage() { - System.out.println( - "Generate the control flow graph of a Java method, represented as a DOT graph."); - System.out.println( - "Parameters: [-method ] [-class ] [-pdf] [-verbose]"); - System.out.println(" -pdf: Also generate the PDF by invoking 'dot'."); - System.out.println( - " -method: The method to generate the CFG for (defaults to 'test')."); - 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')."); - } - - /** - * Print error message. - * - * @param string error message - */ - protected void printError(@Nullable String string) { - System.err.println("ERROR: " + string); - } -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/CFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/CFGVisualizer.java deleted file mode 100644 index 370254d12159..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/CFGVisualizer.java +++ /dev/null @@ -1,203 +0,0 @@ -package org.checkerframework.dataflow.cfg; - -import java.util.Map; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.analysis.AbstractValue; -import org.checkerframework.dataflow.analysis.Analysis; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.Store; -import org.checkerframework.dataflow.analysis.TransferFunction; -import org.checkerframework.dataflow.cfg.block.Block; -import org.checkerframework.dataflow.cfg.block.ConditionalBlock; -import org.checkerframework.dataflow.cfg.block.SpecialBlock; -import org.checkerframework.dataflow.cfg.node.Node; - -/** - * Perform some visualization on a control flow graph. The particular operations depend on the - * implementation. - * - * @param the abstract value type to be tracked by the analysis - * @param the store type used in the analysis - * @param the transfer function type that is used to approximate runtime behavior - */ -public interface CFGVisualizer< - V extends AbstractValue, S extends Store, T extends TransferFunction> { - /** - * Initialization method guaranteed to be called once before the first invocation of {@link - * #visualize}. - * - * @param args implementation-dependent options - */ - void init(Map args); - - /** - * Output a visualization representing the control flow graph starting at {@code entry}. The - * concrete actions are implementation dependent. - * - *

      An invocation {@code visualize(cfg, entry, null);} does not output stores at the beginning - * of basic blocks. - * - * @param cfg the CFG to visualize - * @param entry the entry node of the control flow graph to be represented - * @param analysis an analysis containing information about the program represented by the CFG. - * The information includes {@link Store}s that are valid at the beginning of basic blocks - * reachable from {@code entry} and per-node information for value producing {@link Node}s. - * Can also be {@code null} to indicate that this information should not be output. - * @return visualization results, e.g. generated file names ({@link DOTCFGVisualizer}) or a - * String representation of the CFG ({@link StringCFGVisualizer}) - */ - @Nullable Map visualize( - ControlFlowGraph cfg, Block entry, @Nullable Analysis analysis); - - /** - * Delegate the visualization responsibility to the passed {@link Store} instance, which will - * call back to this visualizer instance for sub-components. - * - * @param store the store to visualize - * @return the String representation of the given store - */ - String visualizeStore(S store); - - /** - * Called by a {@code CFAbstractStore} to visualize the class name before calling the {@code - * CFAbstractStore#internalVisualize()} method. - * - * @param classCanonicalName the canonical name of the class - * @return the String representation of the class name - */ - String visualizeStoreHeader(String classCanonicalName); - - /** - * Called by {@code CFAbstractStore#internalVisualize()} to visualize a local variable. - * - * @param localVar the local variable - * @param value the value of the local variable - * @return the String representation of the local variable - */ - String visualizeStoreLocalVar(FlowExpressions.LocalVariable localVar, V value); - - /** - * Called by {@code CFAbstractStore#internalVisualize()} to visualize the value of the current - * object {@code this} in this Store. - * - * @param value the value of the current object {@code this} - * @return the String representation of {@code this} - */ - String visualizeStoreThisVal(V value); - - /** - * Called by {@code CFAbstractStore#internalVisualize()} to visualize the value of fields - * collected by this Store. - * - * @param fieldAccess the field - * @param value the value of the field - * @return the String representation of the field - */ - String visualizeStoreFieldVals(FlowExpressions.FieldAccess fieldAccess, V value); - - /** - * Called by {@code CFAbstractStore#internalVisualize()} to visualize the value of arrays - * collected by this Store. - * - * @param arrayValue the array - * @param value the value of the array - * @return the String representation of the array - */ - String visualizeStoreArrayVal(FlowExpressions.ArrayAccess arrayValue, V value); - - /** - * Called by {@code CFAbstractStore#internalVisualize()} to visualize the value of pure method - * calls collected by this Store. - * - * @param methodCall the pure method call - * @param value the value of the pure method call - * @return the String representation of the pure method call - */ - String visualizeStoreMethodVals(FlowExpressions.MethodCall methodCall, V value); - - /** - * Called by {@code CFAbstractStore#internalVisualize()} to visualize the value of class names - * collected by this Store. - * - * @param className the class name - * @param value the value of the class name - * @return the String representation of the class name - */ - String visualizeStoreClassVals(FlowExpressions.ClassName className, V value); - - /** - * Called by {@code CFAbstractStore#internalVisualize()} to visualize the specific information - * collected according to the specific kind of Store. Currently, these Stores call this method: - * {@code LockStore}, {@code NullnessStore}, and {@code InitializationStore} to visualize - * additional information. - * - * @param keyName the name of the specific information to be visualized - * @param value the value of the specific information to be visualized - * @return the String representation of the specific information - */ - String visualizeStoreKeyVal(String keyName, Object value); - - /** - * Called by {@code CFAbstractStore} to visualize any information after the invocation of {@code - * CFAbstractStore#internalVisualize()}. - * - * @return the String representation of the footer - */ - String visualizeStoreFooter(); - - /** - * Visualize a block based on the analysis. - * - * @param bb the block - * @param analysis the current analysis - * @return the String representation of the given block - */ - String visualizeBlock(Block bb, @Nullable Analysis analysis); - - /** - * Visualize a SpecialBlock. - * - * @param sbb the special block - * @return the String representation of the type of the special block {@code sbb}: entry, exit, - * or exceptional-exit - */ - String visualizeSpecialBlock(SpecialBlock sbb); - - /** - * Visualize a ConditionalBlock. - * - * @param cbb the conditional block - * @return the String representation of the conditional block - */ - String visualizeConditionalBlock(ConditionalBlock cbb); - - /** - * Visualize the transferInput before a Block based on the analysis. - * - * @param bb the block - * @param analysis the current analysis - * @return the String representation of the transferInput before the given block - */ - String visualizeBlockTransferInputBefore(Block bb, Analysis analysis); - - /** - * Visualize the transferInput after a Block based on the analysis. - * - * @param bb the block - * @param analysis the current analysis - * @return the String representation of the transferInput after the given block - */ - String visualizeBlockTransferInputAfter(Block bb, Analysis analysis); - - /** - * Visualize a Node based on the analysis. - * - * @param t the node - * @param analysis the current analysis - * @return the String representation of the given node - */ - String visualizeBlockNode(Node t, @Nullable Analysis analysis); - - /** Shutdown method called once from the shutdown hook of the {@code BaseTypeChecker}. */ - void shutdown(); -} 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 abff30922fad..e6175f084606 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/ControlFlowGraph.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/ControlFlowGraph.java @@ -1,35 +1,57 @@ package org.checkerframework.dataflow.cfg; +import com.sun.source.tree.BinaryTree; import com.sun.source.tree.ClassTree; import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; import com.sun.source.tree.UnaryTree; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Deque; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Set; + +import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.AnalysisResult; 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.SingleSuccessorBlockImpl; import org.checkerframework.dataflow.cfg.block.SpecialBlock; import org.checkerframework.dataflow.cfg.block.SpecialBlockImpl; -import org.checkerframework.dataflow.cfg.node.AssignmentNode; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.cfg.node.ReturnNode; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizer; +import org.checkerframework.dataflow.cfg.visualize.StringCFGVisualizer; +import org.plumelib.util.UniqueId; +import org.plumelib.util.UnmodifiableIdentityHashMap; -/** A control flow graph (CFG for short) of a single method. */ -public class ControlFlowGraph { +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; +import java.util.StringJoiner; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; + +import javax.lang.model.type.TypeMirror; + +/** + * 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 implements UniqueId { /** The entry block of the control flow graph. */ protected final SpecialBlock entryBlock; @@ -41,21 +63,43 @@ public class ControlFlowGraph { protected final SpecialBlock exceptionalExitBlock; /** The AST this CFG corresponds to. */ - protected final UnderlyingAST underlyingAST; + public final UnderlyingAST underlyingAST; + + /** The unique ID for the next-created object. */ + private static final AtomicLong nextUid = new AtomicLong(0); + + /** The unique ID of this object. */ + private final transient long uid = nextUid.getAndIncrement(); + + @Override + public long getUid(@UnknownInitialization ControlFlowGraph this) { + return uid; + } /** - * Maps from AST {@link Tree}s to sets of {@link Node}s. Every Tree that produces a value will - * have at least one corresponding Node. Trees that undergo conversions, such as boxing or - * unboxing, can map to two distinct Nodes. The Node for the pre-conversion value is stored in - * treeLookup, while the Node for the post-conversion value is stored in convertedTreeLookup. + * Maps from AST {@link Tree}s to sets of {@link Node}s. + * + *

      + * + * Some of the mapped-to nodes (in both {@link #treeLookup} and {@link #convertedTreeLookup}) do + * not appear in {@link #getAllNodes} because their blocks are not reachable in the control flow + * graph. Dataflow will not compute abstract values for these nodes. */ protected final IdentityHashMap> treeLookup; /** Map from AST {@link Tree}s to post-conversion sets of {@link Node}s. */ protected final IdentityHashMap> convertedTreeLookup; - /** Map from AST {@link UnaryTree}s to corresponding {@link AssignmentNode}s. */ - protected final IdentityHashMap unaryAssignNodeLookup; + /** + * Map from postfix increment or decrement trees that are AST {@link UnaryTree}s to the + * synthetic tree that is {@code v + 1} or {@code v - 1}. + */ + protected final IdentityHashMap postfixNodeLookup; /** * All return nodes (if any) encountered. Only includes return statements that actually return @@ -82,7 +126,7 @@ public ControlFlowGraph( UnderlyingAST underlyingAST, IdentityHashMap> treeLookup, IdentityHashMap> convertedTreeLookup, - IdentityHashMap unaryAssignNodeLookup, + IdentityHashMap postfixNodeLookup, List returnNodes, List declaredClasses, List declaredLambdas) { @@ -90,7 +134,7 @@ public ControlFlowGraph( this.entryBlock = entryBlock; this.underlyingAST = underlyingAST; this.treeLookup = treeLookup; - this.unaryAssignNodeLookup = unaryAssignNodeLookup; + this.postfixNodeLookup = postfixNodeLookup; this.convertedTreeLookup = convertedTreeLookup; this.regularExitBlock = regularExitBlock; this.exceptionalExitBlock = exceptionalExitBlock; @@ -99,6 +143,54 @@ public ControlFlowGraph( this.declaredLambdas = declaredLambdas; } + /** + * Verify that this is a complete and well-formed CFG, i.e. that all internal invariants hold. + * + * @throws IllegalStateException if some internal invariant is violated + */ + public void checkInvariants() { + // TODO: this is a big data structure with many more invariants... + for (Block b : getAllBlocks()) { + + // Each node in the block should have this block as its parent. + for (Node n : b.getNodes()) { + if (!Objects.equals(n.getBlock(), b)) { + throw new IllegalStateException( + "Node " + + n + + " in block " + + b + + " incorrectly believes it belongs to " + + n.getBlock()); + } + } + + // Each successor should have this block in its predecessors. + for (Block succ : b.getSuccessors()) { + if (!succ.getPredecessors().contains(b)) { + throw new IllegalStateException( + "Block " + + b + + " has successor " + + succ + + " but does not appear in that successor's predecessors"); + } + } + + // Each predecessor should have this block in its successors. + for (Block pred : b.getPredecessors()) { + if (!pred.getSuccessors().contains(b)) { + throw new IllegalStateException( + "Block " + + b + + " has predecessor " + + pred + + " but does not appear in that predecessor's successors"); + } + } + } + } + /** * Returns the set of {@link Node}s to which the {@link Tree} {@code t} corresponds, or null for * trees that don't produce a value. @@ -146,12 +238,14 @@ public UnderlyingAST getUnderlyingAST() { } /** - * Returns the set of all basic block in this control flow graph. + * Returns the set of all basic blocks in this control flow graph. * - * @return the set of all basic block in this control flow graph + * @return the set of all basic blocks in this control flow graph */ - public Set getAllBlocks() { - Set visited = new HashSet<>(); + public Set getAllBlocks( + @UnknownInitialization(ControlFlowGraph.class) ControlFlowGraph this) { + Set visited = new LinkedHashSet<>(); + // worklist is always a subset of visited; any block in worklist is also in visited. Queue worklist = new ArrayDeque<>(); Block cur = entryBlock; visited.add(entryBlock); @@ -162,11 +256,8 @@ public Set getAllBlocks() { break; } - Deque succs = getSuccessors(cur); - - for (Block b : succs) { - if (!visited.contains(b)) { - visited.add(b); + for (Block b : cur.getSuccessors()) { + if (visited.add(b)) { worklist.add(b); } } @@ -178,8 +269,90 @@ public Set getAllBlocks() { } /** - * Rreturns the list of all basic block in this control flow graph in reversed depth-first - * postorder sequence. Blocks may appear more than once in the sequence. + * Returns all nodes in this control flow graph. + * + * @return all nodes in this control flow graph + */ + public List getAllNodes( + @UnknownInitialization(ControlFlowGraph.class) ControlFlowGraph this) { + List result = new ArrayList<>(); + for (Block b : getAllBlocks()) { + result.addAll(b.getNodes()); + } + return result; + } + + /** + * Returns the set of all basic blocks in this control flow graph, except those that are + * only reachable via an exception whose type is ignored by parameter {@code + * shouldIgnoreException}. + * + * @param shouldIgnoreException returns true if it is passed a {@code TypeMirror} that should be + * ignored + * @return the set of all basic blocks in this control flow graph, except those that are + * only reachable via an exception whose type is ignored by {@code shouldIgnoreException} + */ + public Set getAllBlocks( + @UnknownInitialization(ControlFlowGraph.class) ControlFlowGraph this, + Function shouldIgnoreException) { + // This is the return value of the method. + Set visited = new LinkedHashSet<>(); + // `worklist` is always a subset of `visited`; any block in `worklist` is also in `visited`. + Queue worklist = new ArrayDeque<>(); + Block cur = entryBlock; + visited.add(entryBlock); + + // Traverse the whole control flow graph. + while (cur != null) { + if (cur instanceof ExceptionBlock) { + for (Map.Entry> entry : + ((ExceptionBlock) cur).getExceptionalSuccessors().entrySet()) { + if (!shouldIgnoreException.apply(entry.getKey())) { + for (Block b : entry.getValue()) { + if (visited.add(b)) { + worklist.add(b); + } + } + } + } + Block b = ((SingleSuccessorBlockImpl) cur).getSuccessor(); + if (b != null && visited.add(b)) { + worklist.add(b); + } + + } else { + for (Block b : cur.getSuccessors()) { + if (visited.add(b)) { + worklist.add(b); + } + } + } + cur = worklist.poll(); + } + + return visited; + } + + /** + * Returns the list of all nodes in this control flow graph, except those that are only + * reachable via an exception whose type is ignored by parameter {@code shouldIgnoreException}. + * + * @param shouldIgnoreException returns true if it is passed a {@code TypeMirror} that should be + * ignored + * @return the list of all nodes in this control flow graph, except those that are only + * reachable via an exception whose type is ignored by {@code shouldIgnoreException} + */ + public List getAllNodes( + @UnknownInitialization(ControlFlowGraph.class) ControlFlowGraph this, + Function shouldIgnoreException) { + List result = new ArrayList<>(); + getAllBlocks(shouldIgnoreException).forEach(b -> result.addAll(b.getNodes())); + return result; + } + + /** + * Returns all basic blocks in this control flow graph, in reversed depth-first postorder. + * Blocks may appear more than once in the sequence. * * @return the list of all basic block in this control flow graph in reversed depth-first * postorder sequence @@ -187,6 +360,7 @@ public Set getAllBlocks() { public List getDepthFirstOrderedBlocks() { List dfsOrderResult = new ArrayList<>(); Set visited = new HashSet<>(); + // worklist can contain values that are not yet in visited. Deque worklist = new ArrayDeque<>(); worklist.add(entryBlock); while (!worklist.isEmpty()) { @@ -196,9 +370,12 @@ public List getDepthFirstOrderedBlocks() { worklist.removeLast(); } else { visited.add(cur); - Deque successors = getSuccessors(cur); - successors.removeAll(visited); - worklist.addAll(successors); + + for (Block b : cur.getSuccessors()) { + if (!visited.contains(b)) { + worklist.add(b); + } + } } } @@ -207,75 +384,50 @@ public List getDepthFirstOrderedBlocks() { } /** - * Get a list of all successor Blocks for cur. + * Returns an unmodifiable view of the tree-lookup map. Ignores convertedTreeLookup, though + * {@link #getNodesCorrespondingToTree} uses that field. * - * @return a Deque of successor Blocks + * @return the unmodifiable tree-lookup map */ - 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; + public UnmodifiableIdentityHashMap> getTreeLookup() { + return UnmodifiableIdentityHashMap.wrap(treeLookup); } /** - * Returns the copied tree-lookup map. + * Returns an unmodifiable view of the lookup-map of the binary tree for a postfix expression. * - * @return the copied tree-lookup map + * @return the unmodifiable lookup-map of the binary tree for a postfix expression */ - public IdentityHashMap> getTreeLookup() { - return new IdentityHashMap<>(treeLookup); - } - - /** - * Returns the copied lookup-map of the assign node for unary operation. - * - * @return the copied lookup-map of the assign node for unary operation - */ - public IdentityHashMap getUnaryAssignNodeLookup() { - return new IdentityHashMap<>(unaryAssignNodeLookup); + public UnmodifiableIdentityHashMap getPostfixNodeLookup() { + return UnmodifiableIdentityHashMap.wrap(postfixNodeLookup); } /** * Get the {@link MethodTree} of the CFG if the argument {@link Tree} maps to a {@link Node} in - * the CFG or null otherwise. + * the CFG, or null otherwise. + * + * @param t a tree that might correspond to a node in the CFG + * @return the method that contains {@code t}'s Node, or null */ public @Nullable MethodTree getContainingMethod(Tree t) { - if (treeLookup.containsKey(t)) { - if (underlyingAST.getKind() == UnderlyingAST.Kind.METHOD) { - UnderlyingAST.CFGMethod cfgMethod = (UnderlyingAST.CFGMethod) underlyingAST; - return cfgMethod.getMethod(); - } + if (treeLookup.containsKey(t) && underlyingAST.getKind() == UnderlyingAST.Kind.METHOD) { + UnderlyingAST.CFGMethod cfgMethod = (UnderlyingAST.CFGMethod) underlyingAST; + return cfgMethod.getMethod(); } return null; } /** * Get the {@link ClassTree} of the CFG if the argument {@link Tree} maps to a {@link Node} in - * the CFG or null otherwise. + * the CFG, or null otherwise. + * + * @param t a tree that might be within a class + * @return the class that contains the given tree, or null */ public @Nullable ClassTree getContainingClass(Tree t) { - if (treeLookup.containsKey(t)) { - if (underlyingAST.getKind() == UnderlyingAST.Kind.METHOD) { - UnderlyingAST.CFGMethod cfgMethod = (UnderlyingAST.CFGMethod) underlyingAST; - return cfgMethod.getClassTree(); - } + if (treeLookup.containsKey(t) && underlyingAST.getKind() == UnderlyingAST.Kind.METHOD) { + UnderlyingAST.CFGMethod cfgMethod = (UnderlyingAST.CFGMethod) underlyingAST; + return cfgMethod.getClassTree(); } return null; } @@ -290,17 +442,47 @@ public List getDeclaredLambdas() { @Override public String toString() { - Map args = new HashMap<>(); - args.put("verbose", true); - CFGVisualizer viz = new StringCFGVisualizer<>(); - viz.init(args); + viz.init(Collections.singletonMap("verbose", true)); Map res = viz.visualize(this, this.getEntryBlock(), null); viz.shutdown(); if (res == null) { - return super.toString(); + return "unvisualizable " + getClass().getCanonicalName(); } String stringGraph = (String) res.get("stringGraph"); - return stringGraph == null ? super.toString() : stringGraph; + return stringGraph == null + ? "unvisualizable " + getClass().getCanonicalName() + : stringGraph; + } + + /** + * Returns a verbose string representation of this, useful for debugging. + * + * @return a string representation of this + */ + public String toStringDebug() { + String className = this.getClass().getSimpleName(); + if (className.equals("ControlFlowGraph") && this.getClass() != ControlFlowGraph.class) { + className = this.getClass().getCanonicalName(); + } + + StringJoiner result = new StringJoiner(String.format("%n ")); + result.add(className + " #" + getUid() + " {"); + result.add("entryBlock=" + entryBlock); + result.add("regularExitBlock=" + regularExitBlock); + result.add("exceptionalExitBlock=" + exceptionalExitBlock); + String astString = underlyingAST.toString().replaceAll("\\s", " "); + if (astString.length() > 65) { + astString = "\"" + astString.substring(0, 60) + "\""; + } + result.add("underlyingAST=" + underlyingAST); + result.add("treeLookup=" + AnalysisResult.treeLookupToString(treeLookup)); + result.add("convertedTreeLookup=" + AnalysisResult.treeLookupToString(convertedTreeLookup)); + result.add("postfixLookup=" + postfixNodeLookup); + result.add("returnNodes=" + Node.nodeCollectionToString(returnNodes)); + result.add("declaredClasses=" + declaredClasses); + result.add("declaredLambdas=" + declaredLambdas); + result.add("}"); + return result.toString(); } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/DOTCFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/DOTCFGVisualizer.java deleted file mode 100644 index 654a4fe5de0c..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/DOTCFGVisualizer.java +++ /dev/null @@ -1,336 +0,0 @@ -package org.checkerframework.dataflow.cfg; - -import com.sun.tools.javac.tree.JCTree; -import java.io.BufferedWriter; -import java.io.FileWriter; -import java.io.IOException; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import org.checkerframework.checker.nullness.qual.KeyFor; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.analysis.AbstractValue; -import org.checkerframework.dataflow.analysis.Analysis; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.Store; -import org.checkerframework.dataflow.analysis.TransferFunction; -import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGMethod; -import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGStatement; -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.SpecialBlock; -import org.checkerframework.javacutil.BugInCF; -import org.checkerframework.javacutil.UserError; - -/** Generate a graph description in the DOT language of a control graph. */ -@SuppressWarnings("initialization.fields.uninitialized") // uses init method -public class DOTCFGVisualizer< - V extends AbstractValue, S extends Store, T extends TransferFunction> - extends AbstractCFGVisualizer { - - /** The output directory. */ - protected String outDir; - - /** The (optional) checker name. Used as a part of the name of the output dot file. */ - protected @Nullable String checkerName; - - /** Mapping from class/method representation to generated dot file. */ - protected Map generated; - - /** Terminator for lines that are left-justified. */ - protected static final String leftJustifiedTerminator = "\\l"; - - @Override - @SuppressWarnings("nullness") // assume arguments are set correctly - public void init(Map args) { - super.init(args); - this.outDir = (String) args.get("outdir"); - if (this.outDir == null) { - throw new BugInCF( - "outDir should never be null, provide it in args when calling DOTCFGVisualizer.init(args)."); - } - this.checkerName = (String) args.get("checkerName"); - this.generated = new HashMap<>(); - } - - @Override - public @Nullable Map visualize( - ControlFlowGraph cfg, Block entry, @Nullable Analysis analysis) { - - String dotGraph = visualizeGraph(cfg, entry, analysis); - String dotFileName = dotOutputFileName(cfg.underlyingAST); - - try { - FileWriter fStream = new FileWriter(dotFileName); - BufferedWriter out = new BufferedWriter(fStream); - out.write(dotGraph); - out.close(); - } catch (IOException e) { - throw new UserError("Error creating dot file (is the path valid?): " + dotFileName, e); - } - - Map res = new HashMap<>(); - res.put("dotFileName", dotFileName); - - return res; - } - - @SuppressWarnings("enhancedfor.type.incompatible") - @Override - public String visualizeNodes( - Set blocks, ControlFlowGraph cfg, @Nullable Analysis analysis) { - - StringBuilder sbDotNodes = new StringBuilder(); - sbDotNodes.append(lineSeparator); - - IdentityHashMap> processOrder = getProcessOrder(cfg); - - // Definition of all nodes including their labels. - for (@KeyFor("processOrder") Block v : blocks) { - sbDotNodes.append(" ").append(v.getId()).append(" ["); - if (v.getType() == BlockType.CONDITIONAL_BLOCK) { - sbDotNodes.append("shape=polygon sides=8 "); - } else if (v.getType() == BlockType.SPECIAL_BLOCK) { - sbDotNodes.append("shape=oval "); - } else { - sbDotNodes.append("shape=rectangle "); - } - sbDotNodes.append("label=\""); - if (verbose) { - sbDotNodes - .append(getProcessOrderSimpleString(processOrder.get(v))) - .append(leftJustifiedTerminator); - } - String strBlock = visualizeBlock(v, analysis); - if (strBlock.length() == 0) { - if (v.getType() == BlockType.CONDITIONAL_BLOCK) { - // The footer of the conditional block. - sbDotNodes.append("\"];").append(lineSeparator); - } else { - // The footer of the block which has no content and is not a special or - // conditional block. - sbDotNodes.append("?? empty ??\"];").append(lineSeparator); - } - } else { - sbDotNodes.append(strBlock).append("\"];").append(lineSeparator); - } - } - return sbDotNodes.toString(); - } - - @Override - protected String addEdge(long sId, long eId, String flowRule) { - return " " + sId + " -> " + eId + " [label=\"" + flowRule + "\"];" + lineSeparator; - } - - @Override - public String visualizeBlock(Block bb, @Nullable Analysis analysis) { - return super.visualizeBlockHelper(bb, analysis, leftJustifiedTerminator); - } - - @Override - public String visualizeSpecialBlock(SpecialBlock sbb) { - return super.visualizeSpecialBlockHelper(sbb, "\\n"); - } - - @Override - public String visualizeConditionalBlock(ConditionalBlock cbb) { - // No extra content in DOT output. - return ""; - } - - @Override - public String visualizeBlockTransferInputBefore(Block bb, Analysis analysis) { - return super.visualizeBlockTransferInputBeforeHelper(bb, analysis, leftJustifiedTerminator); - } - - @Override - public String visualizeBlockTransferInputAfter(Block bb, Analysis analysis) { - return super.visualizeBlockTransferInputAfterHelper(bb, analysis, leftJustifiedTerminator); - } - - /** - * Create a dot file and return its name. - * - * @param ast an abstract syntax tree - * @return the file name used for DOT output - */ - protected String dotOutputFileName(UnderlyingAST ast) { - StringBuilder srcLoc = new StringBuilder(); - StringBuilder outFile = new StringBuilder(outDir); - - outFile.append("/"); - - if (ast.getKind() == UnderlyingAST.Kind.ARBITRARY_CODE) { - CFGStatement cfgStatement = (CFGStatement) ast; - String clsName = cfgStatement.getClassTree().getSimpleName().toString(); - outFile.append(clsName); - outFile.append("-initializer-"); - outFile.append(ast.hashCode()); - - srcLoc.append("<"); - srcLoc.append(clsName); - srcLoc.append("::initializer::"); - srcLoc.append(((JCTree) cfgStatement.getCode()).pos); - srcLoc.append(">"); - } else if (ast.getKind() == UnderlyingAST.Kind.METHOD) { - CFGMethod cfgMethod = (CFGMethod) ast; - String clsName = cfgMethod.getClassTree().getSimpleName().toString(); - String methodName = cfgMethod.getMethod().getName().toString(); - outFile.append(clsName); - outFile.append("-"); - outFile.append(methodName); - - srcLoc.append("<"); - srcLoc.append(clsName); - srcLoc.append("::"); - srcLoc.append(methodName); - srcLoc.append("("); - srcLoc.append(cfgMethod.getMethod().getParameters()); - srcLoc.append(")::"); - srcLoc.append(((JCTree) cfgMethod.getMethod()).pos); - srcLoc.append(">"); - } else { - throw new BugInCF("Unexpected AST kind: " + ast.getKind() + " value: " + ast); - } - if (checkerName != null && !checkerName.isEmpty()) { - outFile.append('-'); - outFile.append(checkerName); - } - outFile.append(".dot"); - - // make path safe for Windows - String outFileName = outFile.toString().replace("<", "_").replace(">", ""); - - generated.put(srcLoc.toString(), outFileName); - - return outFileName; - } - - @Override - protected String format(Object obj) { - return escapeDoubleQuotes(obj); - } - - @Override - public String visualizeStoreThisVal(V value) { - return storeEntryIndent + "this > " + value + leftJustifiedTerminator; - } - - @Override - public String visualizeStoreLocalVar(FlowExpressions.LocalVariable localVar, V value) { - return storeEntryIndent - + localVar - + " > " - + escapeDoubleQuotes(value) - + leftJustifiedTerminator; - } - - @Override - public String visualizeStoreFieldVals(FlowExpressions.FieldAccess fieldAccess, V value) { - return storeEntryIndent - + fieldAccess - + " > " - + escapeDoubleQuotes(value) - + leftJustifiedTerminator; - } - - @Override - public String visualizeStoreArrayVal(FlowExpressions.ArrayAccess arrayValue, V value) { - return storeEntryIndent - + arrayValue - + " > " - + escapeDoubleQuotes(value) - + leftJustifiedTerminator; - } - - @Override - public String visualizeStoreMethodVals(FlowExpressions.MethodCall methodCall, V value) { - return storeEntryIndent - + escapeDoubleQuotes(methodCall) - + " > " - + value - + leftJustifiedTerminator; - } - - @Override - public String visualizeStoreClassVals(FlowExpressions.ClassName className, V value) { - return storeEntryIndent - + className - + " > " - + escapeDoubleQuotes(value) - + leftJustifiedTerminator; - } - - @Override - public String visualizeStoreKeyVal(String keyName, Object value) { - return storeEntryIndent + keyName + " = " + value + leftJustifiedTerminator; - } - - /** - * Escape the double quotes from the input String, replacing {@code "} by {@code \"}. - * - * @param str the string to be escaped - * @return the escaped version of the string - */ - private String escapeDoubleQuotes(final String str) { - return str.replace("\"", "\\\""); - } - - /** - * Escape the double quotes from the string representation of the given object. - * - * @param obj an object - * @return an escaped version of the string representation of the object - */ - private String escapeDoubleQuotes(final Object obj) { - return escapeDoubleQuotes(String.valueOf(obj)); - } - - @Override - public String visualizeStoreHeader(String classCanonicalName) { - return classCanonicalName + " (" + leftJustifiedTerminator; - } - - @Override - public String visualizeStoreFooter() { - return ")" + leftJustifiedTerminator; - } - - /** - * Write a file {@code methods.txt} that contains a mapping from source code location to - * generated dot file. - */ - @Override - public void shutdown() { - try { - // Open for append, in case of multiple sub-checkers. - FileWriter fstream = new FileWriter(outDir + "/methods.txt", true); - BufferedWriter out = new BufferedWriter(fstream); - for (Map.Entry kv : generated.entrySet()) { - out.write(kv.getKey()); - out.append("\t"); - out.write(kv.getValue()); - out.append(lineSeparator); - } - out.close(); - } catch (IOException e) { - throw new UserError( - "Error creating methods.txt file in: " + outDir + "; ensure the path is valid", - e); - } - } - - @Override - protected String visualizeGraphHeader() { - return "digraph {" + lineSeparator; - } - - @Override - protected String visualizeGraphFooter() { - return "}" + lineSeparator; - } -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/StringCFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/StringCFGVisualizer.java deleted file mode 100644 index f257c4dde774..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/StringCFGVisualizer.java +++ /dev/null @@ -1,173 +0,0 @@ -package org.checkerframework.dataflow.cfg; - -import java.util.HashMap; -import java.util.IdentityHashMap; -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; -import org.checkerframework.dataflow.analysis.Analysis; -import org.checkerframework.dataflow.analysis.FlowExpressions; -import org.checkerframework.dataflow.analysis.Store; -import org.checkerframework.dataflow.analysis.TransferFunction; -import org.checkerframework.dataflow.cfg.block.Block; -import org.checkerframework.dataflow.cfg.block.ConditionalBlock; -import org.checkerframework.dataflow.cfg.block.SpecialBlock; - -/** Generate the String representation of a control flow graph. */ -public class StringCFGVisualizer< - V extends AbstractValue, S extends Store, T extends TransferFunction> - extends AbstractCFGVisualizer { - - @Override - public Map visualize( - ControlFlowGraph cfg, Block entry, @Nullable Analysis analysis) { - String stringGraph = visualizeGraph(cfg, entry, analysis); - Map res = new HashMap<>(); - res.put("stringGraph", stringGraph); - return res; - } - - @SuppressWarnings("enhancedfor.type.incompatible") - @Override - public String visualizeNodes( - Set blocks, ControlFlowGraph cfg, @Nullable Analysis analysis) { - StringJoiner sjStringNodes = new StringJoiner(lineSeparator, lineSeparator, ""); - IdentityHashMap> processOrder = getProcessOrder(cfg); - - // Generate all the Nodes. - for (@KeyFor("processOrder") Block v : blocks) { - sjStringNodes.add(v.getId() + ":"); - if (verbose) { - sjStringNodes.add(getProcessOrderSimpleString(processOrder.get(v))); - } - sjStringNodes.add(visualizeBlock(v, analysis)); - } - - // Remove the line separator from the end of the string. - String stringNodes = sjStringNodes.toString(); - if (stringNodes.endsWith(lineSeparator)) { - stringNodes = stringNodes.substring(0, stringNodes.length() - lineSeparator.length()); - } - - return stringNodes; - } - - @Override - protected String addEdge(long sId, long eId, String flowRule) { - if (this.verbose) { - return sId + " -> " + eId + " " + flowRule + lineSeparator; - } - return sId + " -> " + eId + lineSeparator; - } - - @Override - public String visualizeBlock(Block bb, @Nullable Analysis analysis) { - return super.visualizeBlockHelper(bb, analysis, lineSeparator); - } - - @Override - public String visualizeSpecialBlock(SpecialBlock sbb) { - return super.visualizeSpecialBlockHelper(sbb, lineSeparator); - } - - @Override - public String visualizeConditionalBlock(ConditionalBlock cbb) { - return "ConditionalBlock: then: " - + cbb.getThenSuccessor().getId() - + ", else: " - + cbb.getElseSuccessor().getId() - + lineSeparator; - } - - @Override - public String visualizeBlockTransferInputBefore(Block bb, Analysis analysis) { - return super.visualizeBlockTransferInputBeforeHelper(bb, analysis, lineSeparator); - } - - @Override - public String visualizeBlockTransferInputAfter(Block bb, Analysis analysis) { - return super.visualizeBlockTransferInputAfterHelper(bb, analysis, lineSeparator); - } - - @Override - protected String format(Object obj) { - return obj.toString(); - } - - @Override - public String visualizeStoreThisVal(V value) { - return storeEntryIndent + "this > " + value + lineSeparator; - } - - @Override - public String visualizeStoreLocalVar(FlowExpressions.LocalVariable localVar, V value) { - return storeEntryIndent + localVar + " > " + value + lineSeparator; - } - - @Override - public String visualizeStoreFieldVals(FlowExpressions.FieldAccess fieldAccess, V value) { - return storeEntryIndent + fieldAccess + " > " + value + lineSeparator; - } - - @Override - public String visualizeStoreArrayVal(FlowExpressions.ArrayAccess arrayValue, V value) { - return storeEntryIndent + arrayValue + " > " + value + lineSeparator; - } - - @Override - public String visualizeStoreMethodVals(FlowExpressions.MethodCall methodCall, V value) { - return storeEntryIndent + methodCall + " > " + value + lineSeparator; - } - - @Override - public String visualizeStoreClassVals(FlowExpressions.ClassName className, V value) { - return storeEntryIndent + className + " > " + value + lineSeparator; - } - - @Override - public String visualizeStoreKeyVal(String keyName, Object value) { - return storeEntryIndent + keyName + " = " + value + lineSeparator; - } - - @Override - public String visualizeStoreHeader(String classCanonicalName) { - return classCanonicalName + " (" + lineSeparator; - } - - @Override - public String visualizeStoreFooter() { - return ")" + lineSeparator; - } - - /** - * {@inheritDoc} - * - *

      StringCFGVisualizer does not write into file, so left intentionally blank. - */ - @Override - public void shutdown() {} - - /** - * {@inheritDoc} - * - *

      StringCFGVisualizer does not need a specific header, so just return an empty string. - */ - @Override - protected String visualizeGraphHeader() { - return ""; - } - - /** - * {@inheritDoc} - * - *

      StringCFGVisualizer does not need a specific footer, so just return an empty string. - */ - @Override - protected String visualizeGraphFooter() { - return ""; - } -} 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 73150bc46668..d08e7174eb75 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/UnderlyingAST.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/UnderlyingAST.java @@ -4,13 +4,20 @@ import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; -import org.checkerframework.javacutil.SystemUtil; + +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.plumelib.util.StringsPlume; +import org.plumelib.util.UniqueId; + +import java.util.concurrent.atomic.AtomicLong; /** * Represents an abstract syntax tree of type {@link Tree} that underlies a given control flow * graph. */ -public abstract class UnderlyingAST { +public abstract class UnderlyingAST implements UniqueId { + /** The kinds of underlying ASTs. */ public enum Kind { /** The underlying code is a whole method. */ METHOD, @@ -21,14 +28,32 @@ public enum Kind { ARBITRARY_CODE, } + /** The kind of the underlying AST. */ protected final Kind kind; + /** The unique ID for the next-created object. */ + private static final AtomicLong nextUid = new AtomicLong(0); + + /** The unique ID of this object. */ + private final transient long uid = nextUid.getAndIncrement(); + + @Override + public long getUid(@UnknownInitialization UnderlyingAST this) { + return uid; + } + + /** + * Creates an UnderlyingAST. + * + * @param kind the kind of the AST + */ protected UnderlyingAST(Kind kind) { this.kind = kind; } /** - * Returns the code that corresponds to the CFG. + * Returns the code that corresponds to the CFG. For a method or lamdda, this returns the body. + * For other constructs, it returns the tree itself (a statement or expression). * * @return the code that corresponds to the CFG */ @@ -62,24 +87,66 @@ public MethodTree getMethod() { return method; } + /** + * Returns the name of the method. + * + * @return the name of the method + */ + public String getMethodName() { + return method.getName().toString(); + } + + /** + * Returns the class tree this method belongs to. + * + * @return the class tree this method belongs to + */ public ClassTree getClassTree() { return classTree; } + /** + * Returns the simple name of the enclosing class. + * + * @return the simple name of the enclosing class + */ + public String getSimpleClassName() { + return classTree.getSimpleName().toString(); + } + @Override public String toString() { - return SystemUtil.joinLines("CFGMethod(", method, ")"); + return StringsPlume.joinLines("CFGMethod(", method, ")"); } } /** 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 @Nullable MethodTree enclosingMethod; + + /** + * Create a new CFGLambda. + * + * @param lambda the lambda expression + * @param classTree the enclosing class of the lambda + * @param enclosingMethod the enclosing method of the lambda + */ + public CFGLambda( + LambdaExpressionTree lambda, + ClassTree classTree, + @Nullable MethodTree enclosingMethod) { super(Kind.LAMBDA); this.lambda = lambda; + this.enclosingMethod = enclosingMethod; + this.classTree = classTree; } @Override @@ -87,17 +154,63 @@ 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 simple name of the enclosing class. + * + * @return the simple name of the enclosing class + */ + public String getSimpleClassName() { + return classTree.getSimpleName().toString(); + } + + /** + * Returns the enclosing method of the lambda. + * + * @return the enclosing method of the lambda, or {@code null} if there is no enclosing + * method + */ + public @Nullable MethodTree getEnclosingMethod() { + return enclosingMethod; + } + + /** + * Returns the name of the enclosing method of the lambda. + * + * @return the name of the enclosing method of the lambda, or {@code null} if there is no + * enclosing method + */ + public @Nullable String getEnclosingMethodName() { + return enclosingMethod == null ? null : enclosingMethod.getName().toString(); + } + @Override public String toString() { - return SystemUtil.joinLines("CFGLambda(", lambda, ")"); + return StringsPlume.joinLines("CFGLambda(", lambda, ")"); } } - /** If the underlying AST is a statement or expression. */ + /** + * If the underlying AST is a statement or expression. This is for field definitions (with + * initializers) and initializer blocks. + */ public static class CFGStatement extends UnderlyingAST { protected final Tree code; @@ -120,9 +233,18 @@ public ClassTree getClassTree() { return classTree; } + /** + * Returns the simple name of the enclosing class. + * + * @return the simple name of the enclosing class + */ + public String getSimpleClassName() { + return classTree.getSimpleName().toString(); + } + @Override public String toString() { - return SystemUtil.joinLines("CFGStatement(", code, ")"); + return StringsPlume.joinLines("CFGStatement(", code, ")"); } } } 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 2db3de2894c5..007f48fe7553 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 @@ -1,12 +1,18 @@ package org.checkerframework.dataflow.cfg.block; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.qual.Pure; +import org.plumelib.util.UniqueId; + +import java.util.List; import java.util.Set; /** Represents a basic block in a control flow graph. */ -public interface Block { +public interface Block extends UniqueId { /** The types of basic blocks. */ - public static enum BlockType { + enum BlockType { /** A regular basic block. */ REGULAR_BLOCK, @@ -29,16 +35,37 @@ public static enum BlockType { BlockType getType(); /** - * Returns the unique identifier of this block. + * Returns the predecessors of this basic block. * - * @return the unique identifier of this block + * @return the predecessors of this basic block */ - long getId(); + Set getPredecessors(); /** - * Returns the predecessors of this basic block. + * Returns the successors of this basic block. * - * @return the predecessors of this basic block + * @return the successors of this basic block + */ + Set getSuccessors(); + + /** + * Returns the nodes contained within this basic block. The list may be empty. + * + *

      The following invariant holds. + * + *

      +     * forall n in getNodes() :: n.getBlock() == this
      +     * 
      + * + * @return the nodes contained within this basic block + */ + @Pure + List getNodes(); + + /** + * Returns the last node of this block, or null if none. + * + * @return the last node of this block or {@code null} */ - Set getPredecessors(); + @Nullable Node getLastNode(); } 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 1d1961f8487a..3e007e49f014 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 @@ -1,41 +1,40 @@ package org.checkerframework.dataflow.cfg.block; -import java.util.Collections; -import java.util.HashSet; +import org.checkerframework.checker.initialization.qual.UnknownInitialization; +import org.plumelib.util.ArraySet; + import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; /** Base class of the {@link Block} implementation hierarchy. */ public abstract class BlockImpl implements Block { - /** A unique ID for this node. */ - protected final long id = BlockImpl.uniqueID(); - - /** The last ID that has already been used. */ - protected static long lastId = 0; - /** The type of this basic block. */ protected final BlockType type; /** The set of predecessors. */ protected final Set predecessors; + /** The unique ID for the next-created object. */ + private static final AtomicLong nextUid = new AtomicLong(0); + + /** The unique ID of this object. */ + private final transient long uid = nextUid.getAndIncrement(); + + @Override + public long getUid(@UnknownInitialization BlockImpl this) { + return uid; + } + /** - * Returns a fresh identifier. + * Create a new BlockImpl. * - * @return a fresh identifier + * @param type the type of this basic block */ - private static long uniqueID() { - return lastId++; - } - protected BlockImpl(BlockType type) { this.type = type; - this.predecessors = new HashSet<>(); - } - - @Override - public long getId() { - return id; + // Most blocks have few predecessors. + this.predecessors = new ArraySet<>(2); } @Override @@ -44,8 +43,10 @@ public BlockType getType() { } @Override - public Set getPredecessors() { - return Collections.unmodifiableSet(predecessors); + public Set getPredecessors() { + // Not "Collections.unmodifiableSet(predecessors)" which has nondeterministic iteration + // order. + return new ArraySet<>(predecessors); } public void addPredecessor(BlockImpl pred) { diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ConditionalBlock.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ConditionalBlock.java index 726ef40dab46..334b8f1ffcef 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ConditionalBlock.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ConditionalBlock.java @@ -1,9 +1,12 @@ package org.checkerframework.dataflow.cfg.block; -import org.checkerframework.dataflow.analysis.Store; -import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.analysis.Store.FlowRule; -/** Represents a conditional basic block that contains exactly one boolean {@link Node}. */ +// Werner believes that a ConditionalBlock has to have exactly one RegularBlock (?) predecessor and +// the last node of that predecessor has to be a node of boolean type. He's not totally sure, +// though. We should check whether that property holds. + +/** Represents a conditional basic block. */ public interface ConditionalBlock extends Block { /** @@ -25,18 +28,26 @@ public interface ConditionalBlock extends Block { * * @return the flow rule for information flowing from this block to its then successor */ - Store.FlowRule getThenFlowRule(); + FlowRule getThenFlowRule(); /** * Returns the flow rule for information flowing from this block to its else successor. * * @return the flow rule for information flowing from this block to its else successor */ - Store.FlowRule getElseFlowRule(); + FlowRule getElseFlowRule(); - /** Set the flow rule for information flowing from this block to its then successor. */ - void setThenFlowRule(Store.FlowRule rule); + /** + * Set the flow rule for information flowing from this block to its then successor. + * + * @param rule the new flow rule for information flowing from this block to its then successor + */ + void setThenFlowRule(FlowRule rule); - /** Set the flow rule for information flowing from this block to its else successor. */ - void setElseFlowRule(Store.FlowRule rule); + /** + * Set the flow rule for information flowing from this block to its else successor. + * + * @param rule the new flow rule for information flowing from this block to its else successor + */ + void setElseFlowRule(FlowRule rule); } 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 bcdde13a59ed..7e0c08f1af15 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,8 +1,14 @@ package org.checkerframework.dataflow.cfg.block; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.analysis.Store.FlowRule; +import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.javacutil.BugInCF; +import org.plumelib.util.ArraySet; + +import java.util.Collections; +import java.util.List; +import java.util.Set; /** Implementation of a conditional basic block. */ public class ConditionalBlockImpl extends BlockImpl implements ConditionalBlock { @@ -14,13 +20,16 @@ public class ConditionalBlockImpl extends BlockImpl implements ConditionalBlock protected @Nullable BlockImpl elseSuccessor; /** - * The rules below say that the THEN store before a conditional block flows to BOTH of the - * stores of the then successor, while the ELSE store before a conditional block flows to BOTH - * of the stores of the else successor. + * The initial value says that the THEN store before a conditional block flows to BOTH of the + * stores of the then successor. */ - protected Store.FlowRule thenFlowRule = Store.FlowRule.THEN_TO_BOTH; + protected FlowRule thenFlowRule = FlowRule.THEN_TO_BOTH; - protected Store.FlowRule elseFlowRule = Store.FlowRule.ELSE_TO_BOTH; + /** + * The initial value says that the ELSE store before a conditional block flows to BOTH of the + * stores of the else successor. + */ + protected FlowRule elseFlowRule = FlowRule.ELSE_TO_BOTH; /** * Initialize an empty conditional basic block to be filled with contents and linked to other @@ -61,25 +70,48 @@ public Block getElseSuccessor() { } @Override - public Store.FlowRule getThenFlowRule() { + public Set getSuccessors() { + Set result = new ArraySet<>(2); + result.add(getThenSuccessor()); + result.add(getElseSuccessor()); + return result; + } + + @Override + public FlowRule getThenFlowRule() { return thenFlowRule; } @Override - public Store.FlowRule getElseFlowRule() { + public FlowRule getElseFlowRule() { return elseFlowRule; } @Override - public void setThenFlowRule(Store.FlowRule rule) { + public void setThenFlowRule(FlowRule rule) { thenFlowRule = rule; } @Override - public void setElseFlowRule(Store.FlowRule rule) { + public void setElseFlowRule(FlowRule rule) { elseFlowRule = rule; } + /** + * {@inheritDoc} + * + *

      This implementation returns an empty list. + */ + @Override + public List getNodes() { + return Collections.emptyList(); + } + + @Override + public @Nullable Node getLastNode() { + return null; + } + @Override public String toString() { return "ConditionalBlock()"; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ExceptionBlock.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ExceptionBlock.java index 7d9c8b1da420..83b028db7514 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ExceptionBlock.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/ExceptionBlock.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.block; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.qual.Pure; + import java.util.Map; import java.util.Set; + import javax.lang.model.type.TypeMirror; -import org.checkerframework.dataflow.cfg.node.Node; -import org.checkerframework.dataflow.qual.Pure; /** * Represents a basic block that contains exactly one {@link Node} which can throw an exception. 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 6b871d380b4f..7d1efd8aff36 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,16 +1,20 @@ package org.checkerframework.dataflow.cfg.block; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.javacutil.BugInCF; +import org.plumelib.util.ArrayMap; +import org.plumelib.util.ArraySet; +import org.plumelib.util.CollectionsPlume; + import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; + import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.cfg.node.Node; -import org.checkerframework.javacutil.BugInCF; -/** Base class of the {@link Block} implementation hierarchy. */ +/** Implementation of {@link ExceptionBlock}. */ public class ExceptionBlockImpl extends SingleSuccessorBlockImpl implements ExceptionBlock { /** The node of this block. */ @@ -22,7 +26,7 @@ public class ExceptionBlockImpl extends SingleSuccessorBlockImpl implements Exce /** Create an empty exceptional block. */ public ExceptionBlockImpl() { super(BlockType.EXCEPTION_BLOCK); - exceptionalSuccessors = new HashMap<>(); + exceptionalSuccessors = new ArrayMap<>(2); } /** Set the node. */ @@ -39,13 +43,29 @@ public Node getNode() { return node; } - /** Add an exceptional successor. */ + /** + * {@inheritDoc} + * + *

      This implementation returns a singleton list. + */ + @Override + public List getNodes() { + return Collections.singletonList(getNode()); + } + + @Override + public @Nullable Node getLastNode() { + return getNode(); + } + + /** + * Add an exceptional successor. + * + * @param b the successor + * @param cause the exception type that leads to the given block + */ public void addExceptionalSuccessor(BlockImpl b, TypeMirror cause) { - Set blocks = exceptionalSuccessors.get(cause); - if (blocks == null) { - blocks = new HashSet<>(); - exceptionalSuccessors.put(cause, blocks); - } + Set blocks = exceptionalSuccessors.computeIfAbsent(cause, __ -> new ArraySet<>(2)); blocks.add(b); b.addPredecessor(this); } @@ -58,6 +78,15 @@ public Map> getExceptionalSuccessors() { return Collections.unmodifiableMap(exceptionalSuccessors); } + @Override + public Set getSuccessors() { + Set result = new ArraySet<>(super.getSuccessors()); + for (Set blocks : getExceptionalSuccessors().values()) { + CollectionsPlume.adjoinAll(result, blocks); + } + return result; + } + @Override public String toString() { return "ExceptionBlock(" + node + ")"; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlock.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlock.java index 46a0da69a148..91648ac84564 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlock.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlock.java @@ -1,29 +1,11 @@ package org.checkerframework.dataflow.cfg.block; -import java.util.List; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.qual.Pure; -/** - * A regular basic block that contains a sequence of {@link Node}s. - * - *

      The following invariant holds. - * - *

      - * forall n in getContents() :: n.getBlock() == this
      - * 
      - */ +/** A regular basic block that contains a sequence of {@link Node}s. */ public interface RegularBlock extends SingleSuccessorBlock { - - /** - * Returns the unmodifiable sequence of {@link Node}s. - * - * @return the unmodifiable sequence of {@link Node}s - */ - @Pure - List getContents(); - /** * Returns the regular successor block. * diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlockImpl.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlockImpl.java index 366404d23918..1197ac77301a 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlockImpl.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/RegularBlockImpl.java @@ -1,10 +1,11 @@ package org.checkerframework.dataflow.cfg.block; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.node.Node; + import java.util.ArrayList; import java.util.Collections; import java.util.List; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.cfg.node.Node; /** Implementation of a regular basic block. */ public class RegularBlockImpl extends SingleSuccessorBlockImpl implements RegularBlock { @@ -34,11 +35,21 @@ public void addNodes(List ts) { } } + /** + * {@inheritDoc} + * + *

      This implementation returns an non-empty list. + */ @Override - public List getContents() { + public List getNodes() { return Collections.unmodifiableList(contents); } + @Override + public @Nullable Node getLastNode() { + return contents.get(contents.size() - 1); + } + @Override public @Nullable BlockImpl getRegularSuccessor() { return successor; 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 d39152bd7cdb..b7dd3f23df3c 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 @@ -1,16 +1,18 @@ package org.checkerframework.dataflow.cfg.block; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.analysis.Store.FlowRule; import org.checkerframework.dataflow.qual.Pure; -/** A basic block that has at exactly one non-exceptional successor. */ +/** A basic block that has exactly one non-exceptional successor. */ public interface SingleSuccessorBlock extends Block { /** - * 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(); @@ -21,8 +23,12 @@ public interface SingleSuccessorBlock extends Block { * @return the flow rule for information flowing from this block to its successor */ @Pure - Store.FlowRule getFlowRule(); + FlowRule getFlowRule(); - /** Set the flow rule for information flowing from this block to its successor. */ - void setFlowRule(Store.FlowRule rule); + /** + * Set the flow rule for information flowing from this block to its successor. + * + * @param rule the new flow rule for information flowing from this block to its successor + */ + void setFlowRule(FlowRule rule); } 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 073c889b11fd..429a17c14ce1 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,20 +1,35 @@ package org.checkerframework.dataflow.cfg.block; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.analysis.Store.FlowRule; -/** Implementation of a non-special basic block. */ +import java.util.Collections; +import java.util.Set; + +/** + * A basic block that has at most one successor. SpecialBlockImpl extends this, but exit blocks have + * no successor. + */ public abstract class SingleSuccessorBlockImpl extends BlockImpl implements SingleSuccessorBlock { - /** Internal representation of the successor. */ + /** + * Internal representation of the successor. + * + *

      Is set by {@link #setSuccessor}. + */ protected @Nullable BlockImpl successor; /** - * The rule below say that EACH store at the end of a single successor block flow to the - * corresponding store of the successor. + * The initial value for the rule below says that EACH store at the end of a single successor + * block flows to the corresponding store of the successor. */ - protected Store.FlowRule flowRule = Store.FlowRule.EACH_TO_EACH; + protected FlowRule flowRule = FlowRule.EACH_TO_EACH; + /** + * Creates a new SingleSuccessorBlock. + * + * @param type the type of this basic block + */ protected SingleSuccessorBlockImpl(BlockType type) { super(type); } @@ -24,19 +39,32 @@ protected SingleSuccessorBlockImpl(BlockType type) { return successor; } - /** Set a basic block as the successor of this block. */ + @Override + public Set getSuccessors() { + if (successor == null) { + return Collections.emptySet(); + } else { + return Collections.singleton(successor); + } + } + + /** + * 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); } @Override - public Store.FlowRule getFlowRule() { + public FlowRule getFlowRule() { return flowRule; } @Override - public void setFlowRule(Store.FlowRule rule) { + public void setFlowRule(FlowRule rule) { flowRule = rule; } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SpecialBlock.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SpecialBlock.java index 4f43bf5f7489..e3d26d7797bd 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SpecialBlock.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SpecialBlock.java @@ -12,7 +12,7 @@ public interface SpecialBlock extends SingleSuccessorBlock { /** The types of special basic blocks. */ - public static enum SpecialBlockType { + public enum SpecialBlockType { /** The entry block of a method. */ ENTRY, diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SpecialBlockImpl.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SpecialBlockImpl.java index c6ae18c6bdf7..cd461998c3ec 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SpecialBlockImpl.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/block/SpecialBlockImpl.java @@ -1,5 +1,12 @@ package org.checkerframework.dataflow.cfg.block; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.node.Node; + +import java.util.Collections; +import java.util.List; + +/** The implementation of a {@link SpecialBlock}. */ public class SpecialBlockImpl extends SingleSuccessorBlockImpl implements SpecialBlock { /** The type of this special basic block. */ @@ -15,6 +22,21 @@ public SpecialBlockType getSpecialType() { return specialType; } + /** + * {@inheritDoc} + * + *

      This implementation returns an empty list. + */ + @Override + public List getNodes() { + return Collections.emptyList(); + } + + @Override + public @Nullable Node getLastNode() { + return null; + } + @Override public String toString() { return "SpecialBlock(" + specialType + ")"; diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGBuilder.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGBuilder.java new file mode 100644 index 000000000000..57b3043c0b40 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGBuilder.java @@ -0,0 +1,188 @@ +package org.checkerframework.dataflow.cfg.builder; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.util.TreePath; + +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.UnderlyingAST; +import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGMethod; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ConditionalBlockImpl; +import org.checkerframework.dataflow.cfg.block.ExceptionBlockImpl; +import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlockImpl; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.BasicAnnotationProvider; +import org.checkerframework.javacutil.trees.TreeBuilder; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.type.TypeMirror; + +/** + * Builds the control flow graph of some Java code (either a method, or an arbitrary statement). + * + *

      The translation of the AST to the CFG is split into three phases: + * + *

        + *
      1. Phase one. In the first phase, the AST is translated into a sequence of {@link + * org.checkerframework.dataflow.cfg.builder.ExtendedNode}s. An extended node can either be a + * {@link Node}, or one of several meta elements such as a conditional or unconditional jump + * or a node with additional information about exceptions. Some of the extended nodes contain + * labels (e.g., for the jump target), and phase one additionally creates a mapping from + * labels to extended nodes. Finally, the list of leaders is computed: A leader is an extended + * node which will give rise to a basic block in phase two. + *
      2. Phase two. In this phase, the sequence of extended nodes is translated to a graph + * of control flow blocks that contain nodes. The meta elements from phase one are translated + * into the correct edges. + *
      3. Phase three. The control flow graph generated in phase two can contain degenerate + * basic blocks such as empty regular basic blocks or conditional basic blocks that have the + * same block as both 'then' and 'else' successor. This phase removes these cases while + * preserving the control flow structure. + *
      + */ +public abstract class CFGBuilder { + + /** Creates a CFGBuilder. */ + protected CFGBuilder() {} + + /** + * Build the control flow graph of some code. + * + * @param root the compilation unit + * @param underlyingAST the AST that underlies the control frow graph + * @param assumeAssertionsDisabled can assertions be assumed to be disabled? + * @param assumeAssertionsEnabled can assertions be assumed to be enabled? + * @param env annotation processing environment containing type utilities + * @return a control flow graph + */ + public static ControlFlowGraph build( + CompilationUnitTree root, + UnderlyingAST underlyingAST, + boolean assumeAssertionsEnabled, + boolean assumeAssertionsDisabled, + ProcessingEnvironment env) { + TreeBuilder builder = new TreeBuilder(env); + AnnotationProvider annotationProvider = new BasicAnnotationProvider(); + PhaseOneResult phase1result = + new CFGTranslationPhaseOne( + builder, + annotationProvider, + assumeAssertionsEnabled, + assumeAssertionsDisabled, + env) + .process(root, underlyingAST); + ControlFlowGraph phase2result = CFGTranslationPhaseTwo.process(phase1result); + ControlFlowGraph phase3result = CFGTranslationPhaseThree.process(phase2result); + return phase3result; + } + + /** + * Build the control flow graph of some code (method, initializer block, ...). bodyPath is the + * TreePath to the body of that code. + */ + public static ControlFlowGraph build( + TreePath bodyPath, + UnderlyingAST underlyingAST, + boolean assumeAssertionsEnabled, + boolean assumeAssertionsDisabled, + ProcessingEnvironment env) { + TreeBuilder builder = new TreeBuilder(env); + AnnotationProvider annotationProvider = new BasicAnnotationProvider(); + PhaseOneResult phase1result = + new CFGTranslationPhaseOne( + builder, + annotationProvider, + assumeAssertionsEnabled, + assumeAssertionsDisabled, + env) + .process(bodyPath, underlyingAST); + ControlFlowGraph phase2result = CFGTranslationPhaseTwo.process(phase1result); + ControlFlowGraph phase3result = CFGTranslationPhaseThree.process(phase2result); + return phase3result; + } + + /** Build the control flow graph of some code. */ + public static ControlFlowGraph build( + CompilationUnitTree root, UnderlyingAST underlyingAST, ProcessingEnvironment env) { + return build(root, underlyingAST, false, false, env); + } + + /** Build the control flow graph of a method. */ + public static ControlFlowGraph build( + CompilationUnitTree root, + MethodTree tree, + ClassTree classTree, + ProcessingEnvironment env) { + UnderlyingAST underlyingAST = new CFGMethod(tree, classTree); + return build(root, underlyingAST, false, false, env); + } + + /** + * Return a printed representation of a collection of extended nodes. + * + * @param nodes a collection of extended nodes to format + * @return a printed representation of the given collection + */ + public static String extendedNodeCollectionToStringDebug( + Collection nodes) { + StringJoiner result = new StringJoiner(", ", "[", "]"); + for (ExtendedNode n : nodes) { + result.add(n.toStringDebug()); + } + return result.toString(); + } + + /* --------------------------------------------------------- */ + /* Utility routines for debugging CFG building */ + /* --------------------------------------------------------- */ + + /** + * Print a set of {@link Block}s and the edges between them. This is useful for examining the + * results of phase two. + * + * @param blocks the blocks to print + */ + protected static void printBlocks(Set blocks) { + for (Block b : blocks) { + System.out.print(b.getUid() + ": " + b); + switch (b.getType()) { + case REGULAR_BLOCK: + case SPECIAL_BLOCK: + { + Block succ = ((SingleSuccessorBlockImpl) b).getSuccessor(); + System.out.println(" -> " + (succ != null ? succ.getUid() : "||")); + break; + } + case EXCEPTION_BLOCK: + { + Block succ = ((SingleSuccessorBlockImpl) b).getSuccessor(); + System.out.print(" -> " + (succ != null ? succ.getUid() : "||") + " {"); + for (Map.Entry> entry : + ((ExceptionBlockImpl) b).getExceptionalSuccessors().entrySet()) { + System.out.print(entry.getKey() + " : " + entry.getValue() + ", "); + } + System.out.println("}"); + break; + } + case CONDITIONAL_BLOCK: + { + Block tSucc = ((ConditionalBlockImpl) b).getThenSuccessor(); + Block eSucc = ((ConditionalBlockImpl) b).getElseSuccessor(); + System.out.println( + " -> T " + + (tSucc != null ? tSucc.getUid() : "||") + + " F " + + (eSucc != null ? eSucc.getUid() : "||")); + break; + } + } + } + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java new file mode 100644 index 000000000000..8f92a87c8ca4 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseOne.java @@ -0,0 +1,4604 @@ +package org.checkerframework.dataflow.cfg.builder; + +import com.sun.source.tree.AnnotatedTypeTree; +import com.sun.source.tree.AnnotationTree; +import com.sun.source.tree.ArrayAccessTree; +import com.sun.source.tree.ArrayTypeTree; +import com.sun.source.tree.AssertTree; +import com.sun.source.tree.AssignmentTree; +import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.BreakTree; +import com.sun.source.tree.CaseTree; +import com.sun.source.tree.CatchTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.CompoundAssignmentTree; +import com.sun.source.tree.ConditionalExpressionTree; +import com.sun.source.tree.ContinueTree; +import com.sun.source.tree.DoWhileLoopTree; +import com.sun.source.tree.EmptyStatementTree; +import com.sun.source.tree.EnhancedForLoopTree; +import com.sun.source.tree.ErroneousTree; +import com.sun.source.tree.ExpressionStatementTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.ForLoopTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.IfTree; +import com.sun.source.tree.ImportTree; +import com.sun.source.tree.InstanceOfTree; +import com.sun.source.tree.LabeledStatementTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.LiteralTree; +import com.sun.source.tree.MemberReferenceTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.ModifiersTree; +import com.sun.source.tree.NewArrayTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.ParameterizedTypeTree; +import com.sun.source.tree.ParenthesizedTree; +import com.sun.source.tree.PrimitiveTypeTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.tree.StatementTree; +import com.sun.source.tree.SwitchTree; +import com.sun.source.tree.SynchronizedTree; +import com.sun.source.tree.ThrowTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.TryTree; +import com.sun.source.tree.TypeCastTree; +import com.sun.source.tree.TypeParameterTree; +import com.sun.source.tree.UnaryTree; +import com.sun.source.tree.UnionTypeTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.tree.WhileLoopTree; +import com.sun.source.tree.WildcardTree; +import com.sun.source.util.TreePath; +import com.sun.source.util.TreeScanner; +import com.sun.source.util.Trees; +import com.sun.tools.javac.code.Type; + +import org.checkerframework.checker.interning.qual.FindDistinct; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.Store.FlowRule; +import org.checkerframework.dataflow.cfg.UnderlyingAST; +import org.checkerframework.dataflow.cfg.node.ArrayAccessNode; +import org.checkerframework.dataflow.cfg.node.ArrayCreationNode; +import org.checkerframework.dataflow.cfg.node.ArrayTypeNode; +import org.checkerframework.dataflow.cfg.node.AssertionErrorNode; +import org.checkerframework.dataflow.cfg.node.AssignmentNode; +import org.checkerframework.dataflow.cfg.node.BitwiseAndNode; +import org.checkerframework.dataflow.cfg.node.BitwiseComplementNode; +import org.checkerframework.dataflow.cfg.node.BitwiseOrNode; +import org.checkerframework.dataflow.cfg.node.BitwiseXorNode; +import org.checkerframework.dataflow.cfg.node.BooleanLiteralNode; +import org.checkerframework.dataflow.cfg.node.CaseNode; +import org.checkerframework.dataflow.cfg.node.CatchMarkerNode; +import org.checkerframework.dataflow.cfg.node.CharacterLiteralNode; +import org.checkerframework.dataflow.cfg.node.ClassDeclarationNode; +import org.checkerframework.dataflow.cfg.node.ClassNameNode; +import org.checkerframework.dataflow.cfg.node.ConditionalAndNode; +import org.checkerframework.dataflow.cfg.node.ConditionalNotNode; +import org.checkerframework.dataflow.cfg.node.ConditionalOrNode; +import org.checkerframework.dataflow.cfg.node.DeconstructorPatternNode; +import org.checkerframework.dataflow.cfg.node.DoubleLiteralNode; +import org.checkerframework.dataflow.cfg.node.EqualToNode; +import org.checkerframework.dataflow.cfg.node.ExplicitThisNode; +import org.checkerframework.dataflow.cfg.node.ExpressionStatementNode; +import org.checkerframework.dataflow.cfg.node.FieldAccessNode; +import org.checkerframework.dataflow.cfg.node.FloatLiteralNode; +import org.checkerframework.dataflow.cfg.node.FloatingDivisionNode; +import org.checkerframework.dataflow.cfg.node.FloatingRemainderNode; +import org.checkerframework.dataflow.cfg.node.FunctionalInterfaceNode; +import org.checkerframework.dataflow.cfg.node.GreaterThanNode; +import org.checkerframework.dataflow.cfg.node.GreaterThanOrEqualNode; +import org.checkerframework.dataflow.cfg.node.ImplicitThisNode; +import org.checkerframework.dataflow.cfg.node.InstanceOfNode; +import org.checkerframework.dataflow.cfg.node.IntegerDivisionNode; +import org.checkerframework.dataflow.cfg.node.IntegerLiteralNode; +import org.checkerframework.dataflow.cfg.node.IntegerRemainderNode; +import org.checkerframework.dataflow.cfg.node.LambdaResultExpressionNode; +import org.checkerframework.dataflow.cfg.node.LeftShiftNode; +import org.checkerframework.dataflow.cfg.node.LessThanNode; +import org.checkerframework.dataflow.cfg.node.LessThanOrEqualNode; +import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import org.checkerframework.dataflow.cfg.node.LongLiteralNode; +import org.checkerframework.dataflow.cfg.node.MarkerNode; +import org.checkerframework.dataflow.cfg.node.MethodAccessNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.NarrowingConversionNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.node.NotEqualNode; +import org.checkerframework.dataflow.cfg.node.NullChkNode; +import org.checkerframework.dataflow.cfg.node.NullLiteralNode; +import org.checkerframework.dataflow.cfg.node.NumericalAdditionNode; +import org.checkerframework.dataflow.cfg.node.NumericalMinusNode; +import org.checkerframework.dataflow.cfg.node.NumericalMultiplicationNode; +import org.checkerframework.dataflow.cfg.node.NumericalPlusNode; +import org.checkerframework.dataflow.cfg.node.NumericalSubtractionNode; +import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; +import org.checkerframework.dataflow.cfg.node.PackageNameNode; +import org.checkerframework.dataflow.cfg.node.ParameterizedTypeNode; +import org.checkerframework.dataflow.cfg.node.PrimitiveTypeNode; +import org.checkerframework.dataflow.cfg.node.ReturnNode; +import org.checkerframework.dataflow.cfg.node.SignedRightShiftNode; +import org.checkerframework.dataflow.cfg.node.StringConcatenateNode; +import org.checkerframework.dataflow.cfg.node.StringConversionNode; +import org.checkerframework.dataflow.cfg.node.StringLiteralNode; +import org.checkerframework.dataflow.cfg.node.SuperNode; +import org.checkerframework.dataflow.cfg.node.SwitchExpressionNode; +import org.checkerframework.dataflow.cfg.node.SynchronizedNode; +import org.checkerframework.dataflow.cfg.node.TernaryExpressionNode; +import org.checkerframework.dataflow.cfg.node.ThisNode; +import org.checkerframework.dataflow.cfg.node.ThrowNode; +import org.checkerframework.dataflow.cfg.node.TypeCastNode; +import org.checkerframework.dataflow.cfg.node.UnsignedRightShiftNode; +import org.checkerframework.dataflow.cfg.node.ValueLiteralNode; +import org.checkerframework.dataflow.cfg.node.VariableDeclarationNode; +import org.checkerframework.dataflow.cfg.node.WideningConversionNode; +import org.checkerframework.dataflow.qual.AssertMethod; +import org.checkerframework.dataflow.qual.TerminatesExecution; +import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.AnnotationUtils; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.SystemUtil; +import org.checkerframework.javacutil.TreePathUtil; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.BindingPatternUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.CaseUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.DeconstructionPatternUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.InstanceOfUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.SwitchExpressionUtils; +import org.checkerframework.javacutil.TreeUtilsAfterJava11.YieldUtils; +import org.checkerframework.javacutil.TypeAnnotationUtils; +import org.checkerframework.javacutil.TypeKindUtils; +import org.checkerframework.javacutil.TypesUtils; +import org.checkerframework.javacutil.trees.TreeBuilder; +import org.plumelib.util.ArrayMap; +import org.plumelib.util.ArraySet; +import org.plumelib.util.CollectionsPlume; +import org.plumelib.util.IPair; +import org.plumelib.util.IdentityArraySet; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; +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.ExecutableType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.ReferenceType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** + * Class that performs phase one of the translation process. It generates the following information: + * + *
        + *
      • A sequence of extended nodes. + *
      • A set of bindings from {@link Label}s to positions in the node sequence. + *
      • A set of leader nodes that give rise to basic blocks in phase two. + *
      • A mapping from AST tree nodes to {@link Node}s. + *
      + * + *

      The return type of this scanner is {@link Node}. For expressions, the corresponding node is + * returned to allow linking between different nodes. + * + *

      However, for statements there is usually no single {@link Node} that is created, and thus null + * is returned. + * + *

      Every {@code visit*} method is assumed to add at least one extended node to the list of nodes + * (which might only be a jump). + * + *

      The entry point to process a single body (e.g., method, lambda, top-level block) is {@link + * #process(TreePath, UnderlyingAST)}. + */ +@SuppressWarnings("nullness") // TODO +public class CFGTranslationPhaseOne extends TreeScanner { + + /** Path to the tree currently being scanned. */ + private TreePath path; + + /** Annotation processing environment. */ + protected final ProcessingEnvironment env; + + /** Element utilities. */ + protected final Elements elements; + + /** Type utilities. */ + protected final Types types; + + /** Tree utilities. */ + protected final Trees trees; + + /** TreeBuilder instance. */ + protected final TreeBuilder treeBuilder; + + /** The annotation provider, e.g., a type factory. */ + protected final AnnotationProvider annotationProvider; + + /** Can assertions be assumed to be disabled? */ + protected final boolean assumeAssertionsDisabled; + + /** Can assertions be assumed to be enabled? */ + protected final boolean assumeAssertionsEnabled; + + /* --------------------------------------------------------- */ + /* Extended Node Types and Labels */ + /* --------------------------------------------------------- */ + + /** Special label to identify the regular exit. */ + private final Label regularExitLabel; + + /** Special label to identify the exceptional exit. */ + private final Label exceptionalExitLabel; + + /** + * Current {@link LabelCell} to which a return statement should jump, or null if there is no + * valid destination. + */ + private @Nullable LabelCell returnTargetLC; + + /** + * Current {@link LabelCell} to which a break statement with no label should jump, or null if + * there is no valid destination. + */ + private @Nullable LabelCell breakTargetLC; + + /** + * Map from AST label Names to CFG {@link Label}s for breaks. Each labeled statement creates two + * CFG {@link Label}s, one for break and one for continue. + */ + private Map breakLabels; + + /** + * Current {@link LabelCell} to which a continue statement with no label should jump, or null if + * there is no valid destination. + */ + private @Nullable LabelCell continueTargetLC; + + /** + * Map from AST label Names to CFG {@link Label}s for continues. Each labeled statement creates + * two CFG {@link Label}s, one for break and one for continue. + */ + private Map continueLabels; + + /** Nested scopes of try-catch blocks in force at the current program point. */ + private final TryStack tryStack; + + /** + * SwitchBuilder for the current switch. Used to match yield statements to enclosing switches. + */ + private SwitchBuilder switchBuilder; + + /** + * Maps from AST {@link Tree}s to sets of {@link Node}s. Every Tree that produces a value will + * have at least one corresponding Node. Trees that undergo conversions, such as boxing or + * unboxing, can map to two distinct Nodes. The Node for the pre-conversion value is stored in + * the treeToCfgNodes, while the Node for the post-conversion value is stored in the + * treeToConvertedCfgNodes. + */ + private final IdentityHashMap> treeToCfgNodes; + + /** Map from AST {@link Tree}s to post-conversion sets of {@link Node}s. */ + private final IdentityHashMap> treeToConvertedCfgNodes; + + /** + * Map from postfix increment or decrement trees that are AST {@link UnaryTree}s to the + * synthetic tree that is {@code v + 1} or {@code v - 1}. + */ + private final IdentityHashMap postfixTreeToCfgNodes; + + /** The list of extended nodes. */ + private final ArrayList nodeList; + + /** The bindings of labels to positions (i.e., indices) in the {@code nodeList}. */ + private final Map bindings; + + /** The set of leaders (represented as indices into {@code nodeList}). */ + private final Set leaders; + + /** + * All return nodes (if any) encountered. Only includes return statements that actually return + * something. + */ + private final List returnNodes; + + /** + * Class declarations that have been encountered when building the control-flow graph for a + * method. + */ + private final List declaredClasses; + + /** + * Lambdas encountered when building the control-flow graph for a method, variable initializer, + * or initializer. + */ + private final List declaredLambdas; + + /** The ArithmeticException type. */ + protected final TypeMirror arithmeticExceptionType; + + /** The ArrayIndexOutOfBoundsException type. */ + protected final TypeMirror arrayIndexOutOfBoundsExceptionType; + + /** The AssertionError type. */ + protected final TypeMirror assertionErrorType; + + /** The ClassCastException type . */ + protected final TypeMirror classCastExceptionType; + + /** The Iterable type (erased). */ + protected final TypeMirror iterableType; + + /** The NegativeArraySizeException type. */ + protected final TypeMirror negativeArraySizeExceptionType; + + /** The NullPointerException type . */ + protected final TypeMirror nullPointerExceptionType; + + /** The OutOfMemoryError type. */ + protected final @Nullable TypeMirror outOfMemoryErrorType; + + /** The ClassCircularityError type. */ + protected final @Nullable TypeMirror classCircularityErrorType; + + /** The ClassFormatErrorType type. */ + protected final @Nullable TypeMirror classFormatErrorType; + + /** The NoClassDefFoundError type. */ + protected final @Nullable TypeMirror noClassDefFoundErrorType; + + /** The String type. */ + protected final TypeMirror stringType; + + /** The Throwable type. */ + protected final TypeMirror throwableType; + + /** + * Supertypes of all unchecked exceptions. The size is 2 and the contents are {@code + * RuntimeException} and {@code Error}. + */ + protected final Set uncheckedExceptionTypes; + + /** + * Exceptions that can be thrown by array creation "new SomeType[]". The size is 2 and the + * contents are {@code NegativeArraySizeException} and {@code OutOfMemoryError}. This list comes + * from JLS 15.10.1 "Run-Time Evaluation of Array Creation Expressions". + */ + protected final Set newArrayExceptionTypes; + + /** + * Creates {@link CFGTranslationPhaseOne}. + * + * @param treeBuilder builder for new AST nodes + * @param annotationProvider extracts annotations from AST nodes + * @param assumeAssertionsDisabled can assertions be assumed to be disabled? + * @param assumeAssertionsEnabled can assertions be assumed to be enabled? + * @param env annotation processing environment containing type utilities + */ + public CFGTranslationPhaseOne( + TreeBuilder treeBuilder, + AnnotationProvider annotationProvider, + boolean assumeAssertionsEnabled, + boolean assumeAssertionsDisabled, + ProcessingEnvironment env) { + this.env = env; + this.treeBuilder = treeBuilder; + this.annotationProvider = annotationProvider; + + assert !(assumeAssertionsDisabled && assumeAssertionsEnabled); + this.assumeAssertionsEnabled = assumeAssertionsEnabled; + this.assumeAssertionsDisabled = assumeAssertionsDisabled; + + elements = env.getElementUtils(); + types = env.getTypeUtils(); + trees = Trees.instance(env); + + // initialize lists and maps + treeToCfgNodes = new IdentityHashMap<>(); + treeToConvertedCfgNodes = new IdentityHashMap<>(); + postfixTreeToCfgNodes = new IdentityHashMap<>(); + nodeList = new ArrayList<>(); + bindings = new HashMap<>(); + leaders = new HashSet<>(); + + regularExitLabel = new Label(); + exceptionalExitLabel = new Label(); + tryStack = new TryStack(exceptionalExitLabel); + returnTargetLC = new LabelCell(regularExitLabel); + breakLabels = new HashMap<>(2); + continueLabels = new HashMap<>(2); + returnNodes = new ArrayList<>(); + declaredClasses = new ArrayList<>(); + declaredLambdas = new ArrayList<>(); + + arithmeticExceptionType = getTypeMirror(ArithmeticException.class); + arrayIndexOutOfBoundsExceptionType = getTypeMirror(ArrayIndexOutOfBoundsException.class); + assertionErrorType = getTypeMirror(AssertionError.class); + classCastExceptionType = getTypeMirror(ClassCastException.class); + iterableType = types.erasure(getTypeMirror(Iterable.class)); + negativeArraySizeExceptionType = getTypeMirror(NegativeArraySizeException.class); + nullPointerExceptionType = getTypeMirror(NullPointerException.class); + outOfMemoryErrorType = maybeGetTypeMirror(OutOfMemoryError.class); + classCircularityErrorType = maybeGetTypeMirror(ClassCircularityError.class); + classFormatErrorType = maybeGetTypeMirror(ClassFormatError.class); + noClassDefFoundErrorType = maybeGetTypeMirror(NoClassDefFoundError.class); + stringType = getTypeMirror(String.class); + throwableType = getTypeMirror(Throwable.class); + uncheckedExceptionTypes = new ArraySet<>(2); + uncheckedExceptionTypes.add(getTypeMirror(RuntimeException.class)); + uncheckedExceptionTypes.add(getTypeMirror(Error.class)); + newArrayExceptionTypes = new ArraySet<>(2); + newArrayExceptionTypes.add(negativeArraySizeExceptionType); + if (outOfMemoryErrorType != null) { + newArrayExceptionTypes.add(outOfMemoryErrorType); + } + } + + /** + * Performs the actual work of phase one: processing a single body (of a method, lambda, + * top-level block, etc.). + * + * @param bodyPath path to the body of the underlying AST's method + * @param underlyingAST the AST for which the CFG is to be built + * @return the result of phase one + */ + public PhaseOneResult process(TreePath bodyPath, UnderlyingAST underlyingAST) { + + // Set class variables + this.path = bodyPath; + + // Traverse AST of the method body. + try { // "finally" clause is "this.path = null" + Node finalNode = scan(path.getLeaf(), null); + + // If we are building the CFG for a lambda with a single expression as the body, then + // add an extra node for the result of that lambda. + if (underlyingAST.getKind() == UnderlyingAST.Kind.LAMBDA) { + LambdaExpressionTree lambdaTree = + ((UnderlyingAST.CFGLambda) underlyingAST).getLambdaTree(); + if (lambdaTree.getBodyKind() == LambdaExpressionTree.BodyKind.EXPRESSION) { + Node resultNode = + new LambdaResultExpressionNode( + (ExpressionTree) lambdaTree.getBody(), finalNode); + extendWithNode(resultNode); + } + } + + // Add marker to indicate that the next block will be the exit block. + // Note: if there is a return statement earlier in the method (which is always the case + // for non-void methods), then this is not strictly necessary. However, it is also not a + // problem, as it will just generate a degenerate control graph case that will be + // removed in a later phase. + nodeList.add(new UnconditionalJump(regularExitLabel)); + + return new PhaseOneResult( + underlyingAST, + treeToCfgNodes, + treeToConvertedCfgNodes, + postfixTreeToCfgNodes, + nodeList, + bindings, + leaders, + returnNodes, + regularExitLabel, + exceptionalExitLabel, + declaredClasses, + declaredLambdas, + types); + } finally { + this.path = null; + } + } + + /** + * Process a single body within {@code root}. This method does not process the entire given + * CompilationUnitTree. Rather, it processes one body (of a method/lambda/etc.) within it, which + * corresponds to {@code underlyingAST}. + * + * @param root the compilation unit + * @param underlyingAST the AST corresponding to the body to process + * @return a PhaseOneResult + */ + public PhaseOneResult process(CompilationUnitTree root, UnderlyingAST underlyingAST) { + // TODO: Isn't this costly? Is there no cache we can reuse? + TreePath bodyPath = trees.getPath(root, underlyingAST.getCode()); + assert bodyPath != null; + return process(bodyPath, underlyingAST); + } + + /** + * Perform any actions required when CFG translation creates a new Tree that is not part of the + * original AST. + * + * @param tree the newly created Tree + */ + public void handleArtificialTree(Tree tree) {} + + /** + * Returns the current path for the tree currently being scanned. + * + * @return the current path + */ + public TreePath getCurrentPath() { + return path; + } + + // TODO: remove method and instead use JCP to add version-specific methods. + // Switch expressions first appeared in 12, standard in 14, so don't use 17. + // TODO: look into changing back to TreePathScanner and removing path/getCurrentPath. + @Override + public Node scan(Tree tree, Void p) { + if (tree == null) { + return null; + } + + TreePath prev = path; + @SuppressWarnings("interning:not.interned") // Looking for exact match. + boolean treeIsLeaf = path.getLeaf() != tree; + if (treeIsLeaf) { + path = new TreePath(path, tree); + } + try { + // TODO: use JCP to add version-specific behavior + if (SystemUtil.jreVersion >= 14) { + // Must use String comparison to support compiling on JDK 11 and earlier. + // Features added between JDK 12 and JDK 17 inclusive. + switch (tree.getKind().name()) { + case "BINDING_PATTERN": + return visitBindingPattern17(path.getLeaf(), p); + case "SWITCH_EXPRESSION": + return visitSwitchExpression17(tree, p); + case "YIELD": + return visitYield17(tree, p); + case "DECONSTRUCTION_PATTERN": + return visitDeconstructionPattern21(tree, p); + default: + // fall through to generic behavior + } + } + + return tree.accept(this, p); + } finally { + path = prev; + } + } + + /** + * Visit a SwitchExpressionTree. + * + * @param yieldTree a YieldTree, typed as Tree to be backward-compatible + * @param p parameter + * @return the result of visiting the switch expression tree + */ + public Node visitYield17(Tree yieldTree, Void p) { + ExpressionTree resultExpression = YieldUtils.getValue(yieldTree); + switchBuilder.buildSwitchExpressionResult(resultExpression); + return null; + } + + /** + * Visit a SwitchExpressionTree + * + * @param switchExpressionTree a SwitchExpressionTree, typed as Tree to be backward-compatible + * @param p parameter + * @return the result of visiting the switch expression tree + */ + public Node visitSwitchExpression17(Tree switchExpressionTree, Void p) { + SwitchBuilder oldSwitchBuilder = switchBuilder; + switchBuilder = new SwitchBuilder(switchExpressionTree); + Node result = switchBuilder.build(); + switchBuilder = oldSwitchBuilder; + return result; + } + + /** + * Visit a BindingPatternTree + * + * @param bindingPatternTree a BindingPatternTree, typed as Tree to be backward-compatible + * @param p parameter + * @return the result of visiting the binding pattern tree + */ + public Node visitBindingPattern17(Tree bindingPatternTree, Void p) { + ClassTree enclosingClass = TreePathUtil.enclosingClass(getCurrentPath()); + TypeElement classElem = TreeUtils.elementFromDeclaration(enclosingClass); + Node receiver = new ImplicitThisNode(classElem.asType()); + VariableTree varTree = BindingPatternUtils.getVariable(bindingPatternTree); + VariableDeclarationNode variableDeclarationNode = new VariableDeclarationNode(varTree); + extendWithNode(variableDeclarationNode); + LocalVariableNode varNode = new LocalVariableNode(varTree, receiver); + extendWithNode(varNode); + return varNode; + } + + /** + * Visit a DeconstructionPatternTree. + * + * @param deconstructionPatternTree a DeconstructionPatternTree, typed as Tree so the Checker + * Framework compiles under JDK 20 and earlier + * @param p an unused parameter + * @return the result of visiting the tree + */ + public Node visitDeconstructionPattern21(Tree deconstructionPatternTree, Void p) { + List nestedPatternTrees = + DeconstructionPatternUtils.getNestedPatterns(deconstructionPatternTree); + List nestedPatterns = new ArrayList<>(nestedPatternTrees.size()); + for (Tree pattern : nestedPatternTrees) { + nestedPatterns.add(scan(pattern, p)); + } + + DeconstructorPatternNode dcpN = + new DeconstructorPatternNode( + TreeUtils.typeOf(deconstructionPatternTree), + deconstructionPatternTree, + nestedPatterns); + extendWithNode(dcpN); + return dcpN; + } + + /* --------------------------------------------------------- */ + /* Nodes and Labels Management */ + /* --------------------------------------------------------- */ + + /** + * Add a node to the lookup map if it not already present. + * + * @param node the node to add to the lookup map + */ + protected void addToLookupMap(Node node) { + Tree tree = node.getTree(); + if (tree == null) { + return; + } + Set existing = treeToCfgNodes.get(tree); + if (existing == null) { + Set newSet = new IdentityArraySet(1); + newSet.add(node); + treeToCfgNodes.put(tree, newSet); + } else { + existing.add(node); + } + + Tree enclosingParens = parenMapping.get(tree); + while (enclosingParens != null) { + Set exp = + treeToCfgNodes.computeIfAbsent(enclosingParens, k -> new IdentityArraySet<>(1)); + // `node` could already be in set `exp`, but it's probably as fast to just add again + exp.add(node); + enclosingParens = parenMapping.get(enclosingParens); + } + } + + /** + * Add a node in the post-conversion lookup map. The node should refer to a Tree and that Tree + * should already be in the pre-conversion lookup map. This method is used to update the + * Tree-Node mapping with conversion nodes. + * + * @param node the node to add to the lookup map + */ + protected void addToConvertedLookupMap(Node node) { + Tree tree = node.getTree(); + addToConvertedLookupMap(tree, node); + } + + /** + * Add a node in the post-conversion lookup map. The tree argument should already be in the + * pre-conversion lookup map. This method is used to update the Tree-Node mapping with + * conversion nodes. + * + * @param tree the tree used as a key in the map + * @param node the node to add to the lookup map + */ + protected void addToConvertedLookupMap(Tree tree, Node node) { + assert tree != null; + assert treeToCfgNodes.containsKey(tree); + Set existing = treeToConvertedCfgNodes.get(tree); + if (existing == null) { + Set newSet = new IdentityArraySet<>(1); + newSet.add(node); + treeToConvertedCfgNodes.put(tree, newSet); + } else { + existing.add(node); + } + } + + /** + * Extend the list of extended nodes with a node. + * + * @param node the node to add + */ + protected void extendWithNode(Node node) { + addToLookupMap(node); + extendWithExtendedNode(new NodeHolder(node)); + } + + /** + * Extend the list of extended nodes with a node, where {@code node} might throw the exception + * {@code cause}. + * + * @param node the node to add + * @param cause an exception that the node might throw + * @return the node holder + */ + protected NodeWithExceptionsHolder extendWithNodeWithException(Node node, TypeMirror cause) { + addToLookupMap(node); + return extendWithNodeWithExceptions(node, Collections.singleton(cause)); + } + + /** + * Extend the list of extended nodes with a node, where {@code node} might throw any of the + * exceptions in {@code causes}. + * + * @param node the node to add + * @param causes set of exceptions that the node might throw + * @return the node holder + */ + protected NodeWithExceptionsHolder extendWithNodeWithExceptions( + Node node, Set causes) { + addToLookupMap(node); + Map> exceptions = new ArrayMap<>(causes.size()); + for (TypeMirror cause : causes) { + exceptions.put(cause, tryStack.possibleLabels(cause)); + } + NodeWithExceptionsHolder exNode = new NodeWithExceptionsHolder(node, exceptions); + extendWithExtendedNode(exNode); + return exNode; + } + + /** + * Extend a list of extended nodes with a ClassName node. + * + *

      Evaluating a class literal kicks off class loading (JLS 15.8.2) which can fail and throw + * one of the specified subclasses of a LinkageError or an OutOfMemoryError (JLS 12.2.1). + * + * @param node the ClassName node to add + * @return the node holder + */ + protected NodeWithExceptionsHolder extendWithClassNameNode(ClassNameNode node) { + Set thrownSet = new ArraySet<>(4); + if (classCircularityErrorType != null) { + thrownSet.add(classCircularityErrorType); + } + if (classFormatErrorType != null) { + thrownSet.add(classFormatErrorType); + } + if (noClassDefFoundErrorType != null) { + thrownSet.add(noClassDefFoundErrorType); + } + if (outOfMemoryErrorType != null) { + thrownSet.add(outOfMemoryErrorType); + } + + return extendWithNodeWithExceptions(node, thrownSet); + } + + /** + * Insert {@code node} after {@code pred} in the list of extended nodes, or append to the list + * if {@code pred} is not present. + * + * @param node the node to add + * @param pred the desired predecessor of node + * @return the node holder + */ + protected T insertNodeAfter(T node, Node pred) { + addToLookupMap(node); + insertExtendedNodeAfter(new NodeHolder(node), pred); + return node; + } + + /** + * Insert a {@code node} that might throw the exceptions in {@code causes} after {@code pred} in + * the list of extended nodes, or append to the list if {@code pred} is not present. + * + * @param node the node to add + * @param causes set of exceptions that the node might throw + * @param pred the desired predecessor of node + * @return the node holder + */ + protected NodeWithExceptionsHolder insertNodeWithExceptionsAfter( + Node node, Set causes, Node pred) { + addToLookupMap(node); + Map> exceptions = new ArrayMap<>(causes.size()); + for (TypeMirror cause : causes) { + exceptions.put(cause, tryStack.possibleLabels(cause)); + } + NodeWithExceptionsHolder exNode = new NodeWithExceptionsHolder(node, exceptions); + insertExtendedNodeAfter(exNode, pred); + return exNode; + } + + /** + * Extend the list of extended nodes with an extended node. + * + * @param n the extended node + */ + protected void extendWithExtendedNode(ExtendedNode n) { + nodeList.add(n); + } + + /** + * Insert {@code n} after the node {@code pred} in the list of extended nodes, or append {@code + * n} if {@code pred} is not present. + * + * @param n the extended node + * @param pred the desired predecessor + */ + @SuppressWarnings("ModifyCollectionInEnhancedForLoop") + protected void insertExtendedNodeAfter(ExtendedNode n, @FindDistinct Node pred) { + int index = -1; + for (int i = 0; i < nodeList.size(); i++) { + ExtendedNode inList = nodeList.get(i); + if (inList instanceof NodeHolder || inList instanceof NodeWithExceptionsHolder) { + if (inList.getNode() == pred) { + index = i; + break; + } + } + } + if (index != -1) { + nodeList.add(index + 1, n); + // update bindings + for (Map.Entry e : bindings.entrySet()) { + if (e.getValue() >= index + 1) { + bindings.put(e.getKey(), e.getValue() + 1); + } + } + // update leaders + Set oldLeaders = new HashSet<>(leaders); + leaders.clear(); + for (Integer l : oldLeaders) { + if (l >= index + 1) { + leaders.add(l + 1); + } else { + leaders.add(l); + } + } + } else { + nodeList.add(n); + } + } + + /** + * Add the label {@code l} to the extended node that will be placed next in the sequence. + * + * @param l the node to add to the forthcoming extended node + */ + protected void addLabelForNextNode(Label l) { + if (bindings.containsKey(l)) { + throw new BugInCF("bindings already contains key %s: %s", l, bindings); + } + leaders.add(nodeList.size()); + bindings.put(l, nodeList.size()); + } + + /* --------------------------------------------------------- */ + /* Utility Methods */ + /* --------------------------------------------------------- */ + + /** The UID for the next unique name. */ + protected long uid = 0; + + /** + * Returns a unique name starting with {@code prefix}. + * + * @param prefix the prefix of the unique name + * @return a unique name starting with {@code prefix} + */ + protected String uniqueName(String prefix) { + return prefix + "#num" + uid++; + } + + /** + * If the input node is an unboxed primitive type, insert a call to the appropriate valueOf + * method, otherwise leave it alone. + * + * @param node in input node + * @return a Node representing the boxed version of the input, which may simply be the input + * node + */ + protected Node box(Node node) { + // For boxing conversion, see JLS 5.1.7 + if (TypesUtils.isPrimitive(node.getType())) { + PrimitiveType primitive = types.getPrimitiveType(node.getType().getKind()); + TypeMirror boxedType = types.getDeclaredType(types.boxedClass(primitive)); + + TypeElement boxedElement = (TypeElement) ((DeclaredType) boxedType).asElement(); + IdentifierTree classTree = treeBuilder.buildClassUse(boxedElement); + handleArtificialTree(classTree); + // No need to handle possible errors from evaluating a class literal here + // since this is synthetic code that can't fail. + ClassNameNode className = new ClassNameNode(classTree); + className.setInSource(false); + insertNodeAfter(className, node); + + MemberSelectTree valueOfSelect = treeBuilder.buildValueOfMethodAccess(classTree); + handleArtificialTree(valueOfSelect); + MethodAccessNode valueOfAccess = new MethodAccessNode(valueOfSelect, className); + valueOfAccess.setInSource(false); + insertNodeAfter(valueOfAccess, className); + + MethodInvocationTree valueOfCall = + treeBuilder.buildMethodInvocation( + valueOfSelect, (ExpressionTree) node.getTree()); + handleArtificialTree(valueOfCall); + Node boxed = + new MethodInvocationNode( + valueOfCall, + valueOfAccess, + Collections.singletonList(node), + getCurrentPath()); + boxed.setInSource(false); + // Add Throwable to account for unchecked exceptions + addToConvertedLookupMap(node.getTree(), boxed); + insertNodeWithExceptionsAfter(boxed, uncheckedExceptionTypes, valueOfAccess); + return boxed; + } else { + return node; + } + } + + /** + * If the input node is a boxed type, unbox it, otherwise leave it alone. + * + * @param node in input node + * @return a Node representing the unboxed version of the input, which may simply be the input + * node + */ + protected Node unbox(Node node) { + if (TypesUtils.isBoxedPrimitive(node.getType())) { + + MemberSelectTree primValueSelect = + treeBuilder.buildPrimValueMethodAccess(node.getTree()); + handleArtificialTree(primValueSelect); + MethodAccessNode primValueAccess = new MethodAccessNode(primValueSelect, node); + primValueAccess.setInSource(false); + // Method access may throw NullPointerException + insertNodeWithExceptionsAfter( + primValueAccess, Collections.singleton(nullPointerExceptionType), node); + + MethodInvocationTree primValueCall = treeBuilder.buildMethodInvocation(primValueSelect); + handleArtificialTree(primValueCall); + Node unboxed = + new MethodInvocationNode( + primValueCall, + primValueAccess, + Collections.emptyList(), + getCurrentPath()); + unboxed.setInSource(false); + + // Add Throwable to account for unchecked exceptions + addToConvertedLookupMap(node.getTree(), unboxed); + insertNodeWithExceptionsAfter(unboxed, uncheckedExceptionTypes, primValueAccess); + return unboxed; + } else { + return node; + } + } + + private TreeInfo getTreeInfo(Tree tree) { + TypeMirror type = TreeUtils.typeOf(tree); + boolean boxed = TypesUtils.isBoxedPrimitive(type); + TypeMirror unboxedType = boxed ? types.unboxedType(type) : type; + + boolean bool = TypesUtils.isBooleanType(type); + boolean numeric = TypesUtils.isNumeric(unboxedType); + + return new TreeInfo() { + @Override + public boolean isNumeric() { + return numeric; + } + + @Override + public boolean isBoxed() { + return boxed; + } + + @Override + public boolean isBoolean() { + return bool; + } + + @Override + public TypeMirror unboxedType() { + return unboxedType; + } + }; + } + + /** + * Returns the unboxed tree if necessary, as described in JLS 5.1.8. + * + * @return the unboxed tree if necessary, as described in JLS 5.1.8 + */ + private Node unboxAsNeeded(Node node, boolean boxed) { + return boxed ? unbox(node) : node; + } + + /** + * Convert the input node to String type, if it isn't already. + * + * @param node an input node + * @return a Node with the value promoted to String, which may be the input node + */ + protected Node stringConversion(Node node) { + // For string conversion, see JLS 5.1.11 + if (!TypesUtils.isString(node.getType())) { + Node converted = new StringConversionNode(node.getTree(), node, stringType); + addToConvertedLookupMap(converted); + insertNodeAfter(converted, node); + return converted; + } else { + return node; + } + } + + /** + * Perform unary numeric promotion on the input node. + * + * @param node a node producing a value of numeric primitive or boxed type + * @return a Node with the value promoted to the int, long, float, or double; may return be the + * input node + */ + protected Node unaryNumericPromotion(Node node) { + // For unary numeric promotion, see JLS 5.6.1 + node = unbox(node); + + switch (node.getType().getKind()) { + case BYTE: + case CHAR: + case SHORT: + { + TypeMirror intType = types.getPrimitiveType(TypeKind.INT); + Node widened = new WideningConversionNode(node.getTree(), node, intType); + addToConvertedLookupMap(widened); + insertNodeAfter(widened, node); + return widened; + } + default: + // Nothing to do. + break; + } + + return node; + } + + /** + * Returns true if the argument type is a numeric primitive or a boxed numeric primitive and + * false otherwise. + */ + protected boolean isNumericOrBoxed(TypeMirror type) { + if (TypesUtils.isBoxedPrimitive(type)) { + type = types.unboxedType(type); + } + return TypesUtils.isNumeric(type); + } + + /** + * Compute the type to which two numeric types must be promoted before performing a binary + * numeric operation on them. The input types must both be numeric and the output type is + * primitive. + * + * @param left the type of the left operand + * @param right the type of the right operand + * @return a TypeMirror representing the binary numeric promoted type + */ + protected TypeMirror binaryPromotedType(TypeMirror left, TypeMirror right) { + if (TypesUtils.isBoxedPrimitive(left)) { + left = types.unboxedType(left); + } + if (TypesUtils.isBoxedPrimitive(right)) { + right = types.unboxedType(right); + } + TypeKind promotedTypeKind = TypeKindUtils.widenedNumericType(left, right); + return types.getPrimitiveType(promotedTypeKind); + } + + /** + * Perform binary numeric promotion on the input node to make it match the expression type. + * + * @param node a node producing a value of numeric primitive or boxed type + * @param exprType the type to promote the value to + * @return a Node with the value promoted to the exprType, which may be the input node + */ + protected Node binaryNumericPromotion(Node node, TypeMirror exprType) { + // For binary numeric promotion, see JLS 5.6.2 + node = unbox(node); + + if (!types.isSameType(node.getType(), exprType)) { + Node widened = new WideningConversionNode(node.getTree(), node, exprType); + addToConvertedLookupMap(widened); + insertNodeAfter(widened, node); + return widened; + } else { + return node; + } + } + + /** + * Perform widening primitive conversion on the input node to make it match the destination + * type. + * + * @param node a node producing a value of numeric primitive type + * @param destType the type to widen the value to + * @return a Node with the value widened to the exprType, which may be the input node + */ + protected Node widen(Node node, TypeMirror destType) { + // For widening conversion, see JLS 5.1.2 + assert TypesUtils.isPrimitive(node.getType()) && TypesUtils.isPrimitive(destType) + : "widening must be applied to primitive types"; + if (types.isSubtype(node.getType(), destType) + && !types.isSameType(node.getType(), destType)) { + Node widened = new WideningConversionNode(node.getTree(), node, destType); + addToConvertedLookupMap(widened); + insertNodeAfter(widened, node); + return widened; + } else { + return node; + } + } + + /** + * Perform narrowing conversion on the input node to make it match the destination type. + * + * @param node a node producing a value of numeric primitive type + * @param destType the type to narrow the value to + * @return a Node with the value narrowed to the exprType, which may be the input node + */ + protected Node narrow(Node node, TypeMirror destType) { + // For narrowing conversion, see JLS 5.1.3 + assert TypesUtils.isPrimitive(node.getType()) && TypesUtils.isPrimitive(destType) + : "narrowing must be applied to primitive types"; + if (types.isSubtype(destType, node.getType()) + && !types.isSameType(destType, node.getType())) { + Node narrowed = new NarrowingConversionNode(node.getTree(), node, destType); + addToConvertedLookupMap(narrowed); + insertNodeAfter(narrowed, node); + return narrowed; + } else { + return node; + } + } + + /** + * Perform narrowing conversion and optionally boxing conversion on the input node to make it + * match the destination type. + * + * @param node a node producing a value of numeric primitive type + * @param destType the type to narrow the value to (possibly boxed) + * @return a Node with the value narrowed and boxed to the destType, which may be the input node + */ + protected Node narrowAndBox(Node node, TypeMirror destType) { + if (TypesUtils.isBoxedPrimitive(destType)) { + return box(narrow(node, types.unboxedType(destType))); + } else { + return narrow(node, destType); + } + } + + /** + * Return whether a conversion from the type of the node to varType requires narrowing. + * + * @param varType the type of a variable (or general LHS) to be converted to + * @param node a node whose value is being converted + * @return whether this conversion requires narrowing to succeed + */ + protected boolean conversionRequiresNarrowing(TypeMirror varType, Node node) { + // Narrowing is restricted to cases where the left hand side is byte, char, short or Byte, + // Char, Short and the right hand side is a constant. + TypeMirror unboxedVarType = + TypesUtils.isBoxedPrimitive(varType) ? types.unboxedType(varType) : varType; + TypeKind unboxedVarKind = unboxedVarType.getKind(); + boolean isLeftNarrowableTo = + unboxedVarKind == TypeKind.BYTE + || unboxedVarKind == TypeKind.SHORT + || unboxedVarKind == TypeKind.CHAR; + boolean isRightConstant = node instanceof ValueLiteralNode; + return isLeftNarrowableTo && isRightConstant; + } + + /** + * Assignment conversion and method invocation conversion are almost identical, except that + * assignment conversion allows narrowing. We factor out the common logic here. + * + * @param node a Node producing a value + * @param varType the type of a variable + * @param contextAllowsNarrowing whether to allow narrowing (for assignment conversion) or not + * (for method invocation conversion) + * @return a Node with the value converted to the type of the variable, which may be the input + * node itself + */ + protected Node commonConvert(Node node, TypeMirror varType, boolean contextAllowsNarrowing) { + // For assignment conversion, see JLS 5.2 + // For method invocation conversion, see JLS 5.3 + + // Check for identical types or "identity conversion" + TypeMirror nodeType = node.getType(); + boolean isSameType = types.isSameType(nodeType, varType); + if (isSameType) { + return node; + } + + boolean isRightNumeric = TypesUtils.isNumeric(nodeType); + boolean isRightPrimitive = TypesUtils.isPrimitive(nodeType); + boolean isRightBoxed = TypesUtils.isBoxedPrimitive(nodeType); + boolean isRightReference = nodeType instanceof ReferenceType; + boolean isLeftNumeric = TypesUtils.isNumeric(varType); + boolean isLeftPrimitive = TypesUtils.isPrimitive(varType); + // boolean isLeftBoxed = TypesUtils.isBoxedPrimitive(varType); + boolean isLeftReference = varType instanceof ReferenceType; + boolean isSubtype = types.isSubtype(nodeType, varType); + + if (isRightNumeric && isLeftNumeric && isSubtype) { + node = widen(node, varType); + } else if (isRightReference && isLeftReference && isSubtype) { + // widening reference conversion is a no-op, but if it + // applies, then later conversions do not. + } else if (isRightPrimitive && isLeftReference) { + if (contextAllowsNarrowing && conversionRequiresNarrowing(varType, node)) { + node = narrowAndBox(node, varType); + } else { + node = box(node); + } + } else if (isRightBoxed && isLeftPrimitive) { + node = unbox(node); + nodeType = node.getType(); + + if (types.isSubtype(nodeType, varType) && !types.isSameType(nodeType, varType)) { + node = widen(node, varType); + } + } else if (isRightPrimitive && isLeftPrimitive) { + if (contextAllowsNarrowing && conversionRequiresNarrowing(varType, node)) { + node = narrow(node, varType); + } + } + // `node` might have been re-assigned; if `nodeType` is needed, set it again. + // nodeType = node.getType(); + + // TODO: if checkers need to know about null references of + // a particular type, add logic for them here. + + return node; + } + + /** + * Perform assignment conversion so that it can be assigned to a variable of the given type. + * + * @param node a Node producing a value + * @param varType the type of a variable + * @return a Node with the value converted to the type of the variable, which may be the input + * node itself + */ + protected Node assignConvert(Node node, TypeMirror varType) { + return commonConvert(node, varType, true); + } + + /** + * Perform method invocation conversion so that the node can be passed as a formal parameter of + * the given type. + * + * @param node a Node producing a value + * @param formalType the type of a formal parameter + * @return a Node with the value converted to the type of the formal, which may be the input + * node itself + */ + protected Node methodInvocationConvert(Node node, TypeMirror formalType) { + return commonConvert(node, formalType, false); + } + + /** + * Given a method element, its type at the call site, and a list of argument expressions, return + * a list of {@link Node}s representing the arguments converted for a call of the method. This + * method applies to both method invocations and constructor calls. The argument of newClassTree + * is null when we visit {@link MethodInvocationTree}, and is non-null when we visit {@link + * NewClassTree}. + * + * @param tree the invocation tree for the call + * @param executable an ExecutableElement representing a method/constructor to be called + * @param executableType an ExecutableType representing the type of the method/constructor call; + * the type must be viewpoint-adapted to the call + * @param actualExprs a List of argument expressions to a call + * @param newClassTree the NewClassTree if the method is the invocation of a constructor + * @return a List of {@link Node}s representing arguments after conversions required by a call + * to this method + */ + protected List convertCallArguments( + ExpressionTree tree, + ExecutableElement executable, + ExecutableType executableType, + List actualExprs, + @Nullable NewClassTree newClassTree) { + // NOTE: It is important to convert one method argument before generating CFG nodes for the + // next argument, since label binding expects nodes to be generated in execution order. + // Therefore, this method first determines which conversions need to be applied and then + // iterates over the actual arguments. + List formals = executableType.getParameterTypes(); + int numFormals = formals.size(); + + ArrayList convertedNodes = new ArrayList<>(numFormals); + AssertMethodTuple assertMethodTuple = getAssertMethodTuple(executable); + + int numActuals = actualExprs.size(); + if (executable.isVarArgs()) { + // Create a new array argument if the actuals outnumber the formals, or if the last + // actual is not assignable to the last formal. + int lastArgIndex = numFormals - 1; + TypeMirror lastParamType = formals.get(lastArgIndex); + if (numActuals == numFormals + && types.isAssignable( + TreeUtils.typeOf(actualExprs.get(numActuals - 1)), lastParamType)) { + // Normal call with no array creation, apply method + // invocation conversion to all arguments. + for (int i = 0; i < numActuals; i++) { + Node actualVal = scan(actualExprs.get(i), null); + if (i == assertMethodTuple.booleanParam) { + treatMethodAsAssert( + (MethodInvocationTree) tree, assertMethodTuple, actualVal); + } + if (actualVal == null) { + throw new BugInCF( + "CFGBuilder: scan returned null for %s [%s]", + actualExprs.get(i), actualExprs.get(i).getClass()); + } + convertedNodes.add(methodInvocationConvert(actualVal, formals.get(i))); + } + } else { + assert lastParamType instanceof ArrayType + : "variable argument formal must be an array"; + // Handle anonymous constructors with an explicit enclosing expression. + // There is a mismatch between the number of parameters and arguments + // when the following conditions are met: + // 1. Java version >= 11, + // 2. the method is an anonymous constructor, + // 3. the executable element has varargs, + // 4. the constructor is invoked with an explicit enclosing expression. + // In this case, the parameters have an enclosing expression as its first parameter, + // while the arguments do not have such element. Hence, decrease the lastArgIndex + // to organize the arguments from the correct index later. + if (SystemUtil.jreVersion >= 11 + && newClassTree != null + && TreeUtils.isAnonymousConstructorWithExplicitEnclosingExpression( + executable, newClassTree)) { + lastArgIndex--; + } + + // Apply method invocation conversion to lastArgIndex arguments and use the + // remaining ones to initialize an array. + for (int i = 0; i < lastArgIndex; i++) { + Node actualVal = scan(actualExprs.get(i), null); + if (i == assertMethodTuple.booleanParam) { + treatMethodAsAssert( + (MethodInvocationTree) tree, assertMethodTuple, actualVal); + } + convertedNodes.add(methodInvocationConvert(actualVal, formals.get(i))); + } + + TypeMirror elemType = ((ArrayType) lastParamType).getComponentType(); + + List inits = new ArrayList<>(numActuals - lastArgIndex); + List initializers = new ArrayList<>(numActuals - lastArgIndex); + for (int i = lastArgIndex; i < numActuals; i++) { + inits.add(actualExprs.get(i)); + Node actualVal = scan(actualExprs.get(i), null); + initializers.add(assignConvert(actualVal, elemType)); + } + + NewArrayTree wrappedVarargs = treeBuilder.buildNewArray(elemType, inits); + handleArtificialTree(wrappedVarargs); + + Node lastArgument = + new ArrayCreationNode( + wrappedVarargs, + lastParamType, + /* dimensions= */ Collections.emptyList(), + initializers); + extendWithNode(lastArgument); + + convertedNodes.add(lastArgument); + } + } else { + for (int i = 0; i < numActuals; i++) { + Node actualVal = scan(actualExprs.get(i), null); + if (i == assertMethodTuple.booleanParam) { + treatMethodAsAssert((MethodInvocationTree) tree, assertMethodTuple, actualVal); + } + convertedNodes.add(methodInvocationConvert(actualVal, formals.get(i))); + } + } + + return convertedNodes; + } + + /** + * Returns the AssertMethodTuple for {@code method}. If {@code method} is not an assert method, + * then {@link AssertMethodTuple#NONE} is returned. + * + * @param method a method element that might be an assert method + * @return the AssertMethodTuple for {@code method} + */ + protected AssertMethodTuple getAssertMethodTuple(ExecutableElement method) { + AnnotationMirror assertMethodAnno = + annotationProvider.getDeclAnnotation(method, AssertMethod.class); + if (assertMethodAnno == null) { + return AssertMethodTuple.NONE; + } + + // Dataflow does not require checker-qual.jar to be on the users classpath, so + // AnnotationUtils.getElementValue(...) cannot be used. + + int booleanParam = + AnnotationUtils.getElementValueNotOnClasspath( + assertMethodAnno, "parameter", Integer.class, 1) + - 1; + + TypeMirror exceptionType = + AnnotationUtils.getElementValueNotOnClasspath( + assertMethodAnno, + "value", + Type.ClassType.class, + (Type.ClassType) assertionErrorType); + boolean isAssertFalse = + AnnotationUtils.getElementValueNotOnClasspath( + assertMethodAnno, "isAssertFalse", Boolean.class, false); + return new AssertMethodTuple(booleanParam, exceptionType, isAssertFalse); + } + + /** Holds the elements of an {@link AssertMethod} annotation. */ + protected static class AssertMethodTuple { + + /** A tuple representing the lack of an {@link AssertMethodTuple}. */ + protected static final AssertMethodTuple NONE = new AssertMethodTuple(-1, null, false); + + /** + * 0-based index of the parameter of the expression that is tested by the assert method. (Or + * -1 if this isn't an assert method.) + */ + public final int booleanParam; + + /** The type of the exception thrown by the assert method. */ + public final TypeMirror exceptionType; + + /** Is this an assert false method? */ + public final boolean isAssertFalse; + + /** + * Creates an AssertMethodTuple. + * + * @param booleanParam 0-based index of the parameter of the expression that is tested by + * the assert method + * @param exceptionType the type of the exception thrown by the assert method + * @param isAssertFalse is this an assert false method + */ + public AssertMethodTuple( + int booleanParam, TypeMirror exceptionType, boolean isAssertFalse) { + this.booleanParam = booleanParam; + this.exceptionType = exceptionType; + this.isAssertFalse = isAssertFalse; + } + } + + /** + * Convert an operand of a conditional expression to the type of the whole expression. + * + * @param node a node occurring as the second or third operand of a conditional expression + * @param destType the type to promote the value to + * @return a Node with the value promoted to the destType, which may be the input node + */ + protected Node conditionalExprPromotion(Node node, TypeMirror destType) { + // For rules on converting operands of conditional expressions, + // JLS 15.25 + TypeMirror nodeType = node.getType(); + + // If the operand is already the same type as the whole + // expression, then do nothing. + if (types.isSameType(nodeType, destType)) { + return node; + } + + // If the operand is a primitive and the whole expression is + // boxed, then apply boxing. + if (TypesUtils.isPrimitive(nodeType) && TypesUtils.isBoxedPrimitive(destType)) { + return box(node); + } + + // If the operand is byte or Byte and the whole expression is + // short, then convert to short. + boolean isBoxedPrimitive = TypesUtils.isBoxedPrimitive(nodeType); + TypeMirror unboxedNodeType = isBoxedPrimitive ? types.unboxedType(nodeType) : nodeType; + TypeMirror unboxedDestType = + TypesUtils.isBoxedPrimitive(destType) ? types.unboxedType(destType) : destType; + if (TypesUtils.isNumeric(unboxedNodeType) && TypesUtils.isNumeric(unboxedDestType)) { + if (unboxedNodeType.getKind() == TypeKind.BYTE + && destType.getKind() == TypeKind.SHORT) { + if (isBoxedPrimitive) { + node = unbox(node); + } + return widen(node, destType); + } + + // If the operand is Byte, Short or Character and the whole expression + // is the unboxed version of it, then apply unboxing. + TypeKind destKind = destType.getKind(); + if (destKind == TypeKind.BYTE + || destKind == TypeKind.CHAR + || destKind == TypeKind.SHORT) { + if (isBoxedPrimitive) { + return unbox(node); + } else if (nodeType.getKind() == TypeKind.INT) { + return narrow(node, destType); + } + } + + return binaryNumericPromotion(node, destType); + } + + // For the final case in JLS 15.25, apply boxing but not lub. + if (TypesUtils.isPrimitive(nodeType) + && (destType.getKind() == TypeKind.DECLARED + || destType.getKind() == TypeKind.UNION + || destType.getKind() == TypeKind.INTERSECTION)) { + return box(node); + } + + return node; + } + + /** + * Returns the label {@link Name} of the leaf in the argument path, or null if the leaf is not a + * labeled statement. + */ + protected @Nullable Name getLabel(TreePath path) { + if (path.getParentPath() != null) { + Tree parent = path.getParentPath().getLeaf(); + if (parent.getKind() == Tree.Kind.LABELED_STATEMENT) { + return ((LabeledStatementTree) parent).getLabel(); + } + } + return null; + } + + /* --------------------------------------------------------- */ + /* Visitor Methods */ + /* --------------------------------------------------------- */ + + @Override + public Node visitAnnotatedType(AnnotatedTypeTree tree, Void p) { + return scan(tree.getUnderlyingType(), p); + } + + @Override + public Node visitAnnotation(AnnotationTree tree, Void p) { + throw new BugInCF("AnnotationTree is unexpected in AST to CFG translation"); + } + + @Override + public MethodInvocationNode visitMethodInvocation(MethodInvocationTree tree, Void p) { + + // see JLS 15.12.4 + + // First, compute the receiver, if any (15.12.4.1). + // Second, evaluate the actual arguments, left to right and possibly some arguments are + // stored into an array for varargs calls (15.12.4.2). + // Third, test the receiver, if any, for nullness (15.12.4.4). + // Fourth, convert the arguments to the type of the formal parameters (15.12.4.5). + // Fifth, if the method is synchronized, lock the receiving object or class (15.12.4.5). + ExecutableElement method = TreeUtils.elementFromUse(tree); + if (method == null) { + // The method wasn't found, e.g. because of a compilation error. + return null; + } + + ExpressionTree methodSelect = tree.getMethodSelect(); + assert TreeUtils.isMethodAccess(methodSelect) + : "Expected a method access, but got: " + methodSelect; + + List actualExprs = tree.getArguments(); + + // Look up method to invoke and possibly throw NullPointerException + Node receiver = getReceiver(methodSelect); + + MethodAccessNode target = new MethodAccessNode(methodSelect, method, receiver); + + if (ElementUtils.isStatic(method) || receiver instanceof ThisNode) { + // No NullPointerException can be thrown, use normal node + extendWithNode(target); + } else { + extendWithNodeWithException(target, nullPointerExceptionType); + } + + List arguments; + if (TreeUtils.isEnumSuperCall(tree)) { + // Don't convert arguments for enum super calls. The AST contains no actual arguments, + // while the method element expects two arguments, leading to an exception in + // convertCallArguments. + // Since no actual arguments are present in the AST that is being checked, it shouldn't + // cause any harm to omit the conversions. + // See also BaseTypeVisitor.visitMethodInvocation and QualifierPolymorphism.annotate. + arguments = Collections.emptyList(); + } else { + arguments = + convertCallArguments( + tree, method, TreeUtils.typeFromUse(tree), actualExprs, null); + } + + // TODO: lock the receiver for synchronized methods + + MethodInvocationNode node = + new MethodInvocationNode(tree, target, arguments, getCurrentPath()); + + ExtendedNode extendedNode = extendWithMethodInvocationNode(method, node); + + /* Check for the TerminatesExecution annotation. */ + boolean terminatesExecution = + annotationProvider.getDeclAnnotation(method, TerminatesExecution.class) != null; + if (terminatesExecution) { + extendedNode.setTerminatesExecution(true); + } + return node; + } + + /** + * Extends the CFG with a MethodInvocationNode, accounting for potential exceptions thrown by + * the invocation. + * + * @param method the invoked method + * @param node the invocation + * @return an ExtendedNode representing the invocation and its possible thrown exceptions + */ + private ExtendedNode extendWithMethodInvocationNode( + ExecutableElement method, MethodInvocationNode node) { + List thrownTypes = method.getThrownTypes(); + Set thrownSet = + new LinkedHashSet<>(thrownTypes.size() + uncheckedExceptionTypes.size()); + // Add exceptions explicitly mentioned in the throws clause. + thrownSet.addAll(thrownTypes); + // Add types to account for unchecked exceptions + thrownSet.addAll(uncheckedExceptionTypes); + + return extendWithNodeWithExceptions(node, thrownSet); + } + + @Override + public Node visitAssert(AssertTree tree, Void p) { + + // see JLS 14.10 + + // If assertions are enabled, then we can just translate the assertion. + if (assumeAssertionsEnabled || assumeAssertionsEnabledFor(tree)) { + translateAssertWithAssertionsEnabled(tree); + return null; + } + + // If assertions are disabled, then nothing is executed. + if (assumeAssertionsDisabled) { + return null; + } + + // Otherwise, we don't know if assertions are enabled, so we use a + // variable "ea" and case-split on it. One branch does execute the + // assertion, while the other assumes assertions are disabled. + VariableTree ea = getAssertionsEnabledVariable(); + + // all necessary labels + Label assertionEnabled = new Label(); + Label assertionDisabled = new Label(); + + extendWithNode(new LocalVariableNode(ea)); + extendWithExtendedNode(new ConditionalJump(assertionEnabled, assertionDisabled)); + + // 'then' branch (i.e. check the assertion) + addLabelForNextNode(assertionEnabled); + + translateAssertWithAssertionsEnabled(tree); + + // 'else' branch + addLabelForNextNode(assertionDisabled); + + return null; + } + + /** + * Should assertions be assumed to be executed for a given {@link AssertTree}? False by default. + */ + protected boolean assumeAssertionsEnabledFor(AssertTree tree) { + return false; + } + + /** The {@link VariableTree} that indicates whether assertions are enabled or not. */ + protected VariableTree ea = null; + + /** + * Get a synthetic {@link VariableTree} that indicates whether assertions are enabled or not. + */ + protected VariableTree getAssertionsEnabledVariable() { + if (ea == null) { + String name = uniqueName("assertionsEnabled"); + Element owner = findOwner(); + ExpressionTree initializer = null; + ea = + treeBuilder.buildVariableDecl( + types.getPrimitiveType(TypeKind.BOOLEAN), name, owner, initializer); + handleArtificialTree(ea); + } + return ea; + } + + /** + * Find nearest owner element (Method or Class) which holds current tree. + * + * @return nearest owner element of current tree + */ + private Element findOwner() { + MethodTree enclosingMethod = TreePathUtil.enclosingMethod(getCurrentPath()); + if (enclosingMethod != null) { + return TreeUtils.elementFromDeclaration(enclosingMethod); + } else { + ClassTree enclosingClass = TreePathUtil.enclosingClass(getCurrentPath()); + return TreeUtils.elementFromDeclaration(enclosingClass); + } + } + + /** + * Translates an assertion statement to the correct CFG nodes. The translation assumes that + * assertions are enabled. + */ + protected void translateAssertWithAssertionsEnabled(AssertTree tree) { + + // all necessary labels + Label assertEnd = new Label(); + Label elseEntry = new Label(); + + // basic block for the condition + Node condition = unbox(scan(tree.getCondition(), null)); + ConditionalJump cjump = new ConditionalJump(assertEnd, elseEntry); + extendWithExtendedNode(cjump); + + // else branch + Node detail = null; + addLabelForNextNode(elseEntry); + if (tree.getDetail() != null) { + detail = scan(tree.getDetail(), null); + } + AssertionErrorNode assertNode = + new AssertionErrorNode(tree, condition, detail, assertionErrorType); + extendWithNode(assertNode); + NodeWithExceptionsHolder exNode = + extendWithNodeWithException( + new ThrowNode(null, assertNode, env.getTypeUtils()), assertionErrorType); + exNode.setTerminatesExecution(true); + + // then branch (nothing happens) + addLabelForNextNode(assertEnd); + } + + /** + * Translates a method marked as {@link AssertMethod} into CFG nodes corresponding to an {@code + * assert} statement. + * + * @param tree the method invocation tree for a method marked as {@link AssertMethod} + * @param assertMethodTuple the assert method tuple for the method + * @param condition the boolean expression node for the argument that the method tests + */ + protected void treatMethodAsAssert( + MethodInvocationTree tree, AssertMethodTuple assertMethodTuple, Node condition) { + // all necessary labels + Label thenLabel = new Label(); + Label elseLabel = new Label(); + ConditionalJump cjump = new ConditionalJump(thenLabel, elseLabel); + extendWithExtendedNode(cjump); + + addLabelForNextNode(assertMethodTuple.isAssertFalse ? thenLabel : elseLabel); + AssertionErrorNode assertNode = + new AssertionErrorNode(tree, condition, null, assertMethodTuple.exceptionType); + extendWithNode(assertNode); + NodeWithExceptionsHolder exNode = + extendWithNodeWithException( + new ThrowNode(null, assertNode, env.getTypeUtils()), + assertMethodTuple.exceptionType); + exNode.setTerminatesExecution(true); + + addLabelForNextNode(assertMethodTuple.isAssertFalse ? elseLabel : thenLabel); + } + + @Override + public Node visitAssignment(AssignmentTree tree, Void p) { + + // see JLS 15.26.1 + + AssignmentNode assignmentNode; + ExpressionTree variable = tree.getVariable(); + TypeMirror varType = TreeUtils.typeOf(variable); + + // case 1: lhs is field access + if (TreeUtils.isFieldAccess(variable)) { + // visit receiver + Node receiver = getReceiver(variable); + + // visit expression + Node expression = scan(tree.getExpression(), p); + expression = assignConvert(expression, varType); + + // visit field access (throws null-pointer exception) + FieldAccessNode target = new FieldAccessNode(variable, receiver); + target.setLValue(); + + Element element = TreeUtils.elementFromUse(variable); + if (ElementUtils.isStatic(element) || receiver instanceof ThisNode) { + // No NullPointerException can be thrown, use normal node + extendWithNode(target); + } else { + extendWithNodeWithException(target, nullPointerExceptionType); + } + + // add assignment node + assignmentNode = new AssignmentNode(tree, target, expression); + extendWithNode(assignmentNode); + } + + // case 2: lhs is not a field access + else { + Node target = scan(variable, p); + target.setLValue(); + + assignmentNode = translateAssignment(tree, target, tree.getExpression()); + } + + return assignmentNode; + } + + /** Translate an assignment. */ + protected AssignmentNode translateAssignment(Tree tree, Node target, ExpressionTree rhs) { + Node expression = scan(rhs, null); + return translateAssignment(tree, target, expression); + } + + /** Translate an assignment where the RHS has already been scanned. */ + protected AssignmentNode translateAssignment(Tree tree, Node target, Node expression) { + assert tree instanceof AssignmentTree || tree instanceof VariableTree; + target.setLValue(); + expression = assignConvert(expression, target.getType()); + AssignmentNode assignmentNode = new AssignmentNode(tree, target, expression); + extendWithNode(assignmentNode); + return assignmentNode; + } + + /** + * Note 1: Requires {@code tree} to be a field or method access tree. + * + *

      Note 2: Visits the receiver and adds all necessary blocks to the CFG. + * + * @param tree the field or method access tree containing the receiver: one of + * MethodInvocationTree, AssignmentTree, or IdentifierTree + * @return the receiver of the field or method access + */ + private Node getReceiver(ExpressionTree tree) { + assert TreeUtils.isFieldAccess(tree) || TreeUtils.isMethodAccess(tree); + if (tree.getKind() == Tree.Kind.MEMBER_SELECT) { + // `tree` has an explicit receiver. + MemberSelectTree mtree = (MemberSelectTree) tree; + return scan(mtree.getExpression(), null); + } + + // Access through an implicit receiver + Element ele = TreeUtils.elementFromUse(tree); + TypeElement declClassElem = ElementUtils.enclosingTypeElement(ele); + TypeMirror declClassType = ElementUtils.getType(declClassElem); + + if (ElementUtils.isStatic(ele)) { + ClassNameNode node = new ClassNameNode(declClassType, declClassElem); + extendWithClassNameNode(node); + return node; + } + + // Access through an implicit `this` + TreePath enclClassPath = TreePathUtil.pathTillClass(getCurrentPath()); + ClassTree enclClassTree = (ClassTree) enclClassPath.getLeaf(); + TypeElement enclClassElem = TreeUtils.elementFromDeclaration(enclClassTree); + TypeMirror enclClassType = enclClassElem.asType(); + while (!TypesUtils.isErasedSubtype(enclClassType, declClassType, types)) { + enclClassPath = TreePathUtil.pathTillClass(enclClassPath.getParentPath()); + if (enclClassPath == null) { + enclClassType = declClassType; + break; + } + enclClassTree = (ClassTree) enclClassPath.getLeaf(); + enclClassElem = TreeUtils.elementFromDeclaration(enclClassTree); + enclClassType = enclClassElem.asType(); + } + Node node = new ImplicitThisNode(enclClassType); + extendWithNode(node); + return node; + } + + /** + * Map an operation with assignment to the corresponding operation without assignment. + * + * @param kind a Tree.Kind representing an operation with assignment + * @return the Tree.Kind for the same operation without assignment + */ + protected Tree.Kind withoutAssignment(Tree.Kind kind) { + switch (kind) { + case DIVIDE_ASSIGNMENT: + return Tree.Kind.DIVIDE; + case MULTIPLY_ASSIGNMENT: + return Tree.Kind.MULTIPLY; + case REMAINDER_ASSIGNMENT: + return Tree.Kind.REMAINDER; + case MINUS_ASSIGNMENT: + return Tree.Kind.MINUS; + case PLUS_ASSIGNMENT: + return Tree.Kind.PLUS; + case LEFT_SHIFT_ASSIGNMENT: + return Tree.Kind.LEFT_SHIFT; + case RIGHT_SHIFT_ASSIGNMENT: + return Tree.Kind.RIGHT_SHIFT; + case UNSIGNED_RIGHT_SHIFT_ASSIGNMENT: + return Tree.Kind.UNSIGNED_RIGHT_SHIFT; + case AND_ASSIGNMENT: + return Tree.Kind.AND; + case OR_ASSIGNMENT: + return Tree.Kind.OR; + case XOR_ASSIGNMENT: + return Tree.Kind.XOR; + default: + return Tree.Kind.ERRONEOUS; + } + } + + @Override + public Node visitCompoundAssignment(CompoundAssignmentTree tree, Void p) { + // According the JLS 15.26.2, E1 op= E2 is equivalent to + // E1 = (T) ((E1) op (E2)), where T is the type of E1, + // except that E1 is evaluated only once. + // + + Tree.Kind kind = tree.getKind(); + switch (kind) { + case DIVIDE_ASSIGNMENT: + case MULTIPLY_ASSIGNMENT: + case REMAINDER_ASSIGNMENT: + { + // see JLS 15.17 and 15.26.2 + Node targetLHS = scan(tree.getVariable(), p); + Node value = scan(tree.getExpression(), p); + + TypeMirror exprType = TreeUtils.typeOf(tree); + TypeMirror leftType = TreeUtils.typeOf(tree.getVariable()); + TypeMirror rightType = TreeUtils.typeOf(tree.getExpression()); + TypeMirror promotedType = binaryPromotedType(leftType, rightType); + Node targetRHS = binaryNumericPromotion(targetLHS, promotedType); + value = binaryNumericPromotion(value, promotedType); + + BinaryTree operTree = + treeBuilder.buildBinary( + promotedType, + withoutAssignment(kind), + tree.getVariable(), + tree.getExpression()); + handleArtificialTree(operTree); + Node operNode; + if (kind == Tree.Kind.MULTIPLY_ASSIGNMENT) { + operNode = new NumericalMultiplicationNode(operTree, targetRHS, value); + } else if (kind == Tree.Kind.DIVIDE_ASSIGNMENT) { + if (TypesUtils.isIntegralPrimitive(exprType)) { + operNode = new IntegerDivisionNode(operTree, targetRHS, value); + + extendWithNodeWithException(operNode, arithmeticExceptionType); + } else { + operNode = new FloatingDivisionNode(operTree, targetRHS, value); + } + } else { + assert kind == Tree.Kind.REMAINDER_ASSIGNMENT; + if (TypesUtils.isIntegralPrimitive(exprType)) { + operNode = new IntegerRemainderNode(operTree, targetRHS, value); + + extendWithNodeWithException(operNode, arithmeticExceptionType); + } else { + operNode = new FloatingRemainderNode(operTree, targetRHS, value); + } + } + extendWithNode(operNode); + + TypeMirror castType = TypeAnnotationUtils.unannotatedType(leftType); + TypeCastTree castTree = treeBuilder.buildTypeCast(castType, operTree); + handleArtificialTree(castTree); + TypeCastNode castNode = new TypeCastNode(castTree, operNode, castType, types); + castNode.setInSource(false); + extendWithNode(castNode); + + AssignmentNode assignNode = new AssignmentNode(tree, targetLHS, castNode); + extendWithNode(assignNode); + return assignNode; + } + + case MINUS_ASSIGNMENT: + case PLUS_ASSIGNMENT: + { + // see JLS 15.18 and 15.26.2 + + Node targetLHS = scan(tree.getVariable(), p); + Node value = scan(tree.getExpression(), p); + + TypeMirror leftType = TreeUtils.typeOf(tree.getVariable()); + TypeMirror rightType = TreeUtils.typeOf(tree.getExpression()); + + if (TypesUtils.isString(leftType) || TypesUtils.isString(rightType)) { + assert (kind == Tree.Kind.PLUS_ASSIGNMENT); + Node targetRHS = stringConversion(targetLHS); + value = stringConversion(value); + BinaryTree operTree = + treeBuilder.buildBinary( + leftType, + withoutAssignment(kind), + tree.getVariable(), + tree.getExpression()); + handleArtificialTree(operTree); + Node operNode = new StringConcatenateNode(operTree, targetRHS, value); + extendWithNode(operNode); + AssignmentNode assignNode = new AssignmentNode(tree, targetLHS, operNode); + extendWithNode(assignNode); + return assignNode; + } else { + TypeMirror promotedType = binaryPromotedType(leftType, rightType); + Node targetRHS = binaryNumericPromotion(targetLHS, promotedType); + value = binaryNumericPromotion(value, promotedType); + + BinaryTree operTree = + treeBuilder.buildBinary( + promotedType, + withoutAssignment(kind), + tree.getVariable(), + tree.getExpression()); + handleArtificialTree(operTree); + Node operNode; + if (kind == Tree.Kind.PLUS_ASSIGNMENT) { + operNode = new NumericalAdditionNode(operTree, targetRHS, value); + } else { + assert kind == Tree.Kind.MINUS_ASSIGNMENT; + operNode = new NumericalSubtractionNode(operTree, targetRHS, value); + } + extendWithNode(operNode); + + TypeMirror castType = TypeAnnotationUtils.unannotatedType(leftType); + TypeCastTree castTree = treeBuilder.buildTypeCast(castType, operTree); + handleArtificialTree(castTree); + TypeCastNode castNode = + new TypeCastNode(castTree, operNode, castType, types); + castNode.setInSource(false); + extendWithNode(castNode); + + // Map the compound assignment tree to an assignment node, which + // will have the correct type. + AssignmentNode assignNode = new AssignmentNode(tree, targetLHS, castNode); + extendWithNode(assignNode); + return assignNode; + } + } + + case LEFT_SHIFT_ASSIGNMENT: + case RIGHT_SHIFT_ASSIGNMENT: + case UNSIGNED_RIGHT_SHIFT_ASSIGNMENT: + { + // see JLS 15.19 and 15.26.2 + Node targetLHS = scan(tree.getVariable(), p); + Node value = scan(tree.getExpression(), p); + + TypeMirror leftType = TreeUtils.typeOf(tree.getVariable()); + + Node targetRHS = unaryNumericPromotion(targetLHS); + value = unaryNumericPromotion(value); + + BinaryTree operTree = + treeBuilder.buildBinary( + leftType, + withoutAssignment(kind), + tree.getVariable(), + tree.getExpression()); + handleArtificialTree(operTree); + Node operNode; + if (kind == Tree.Kind.LEFT_SHIFT_ASSIGNMENT) { + operNode = new LeftShiftNode(operTree, targetRHS, value); + } else if (kind == Tree.Kind.RIGHT_SHIFT_ASSIGNMENT) { + operNode = new SignedRightShiftNode(operTree, targetRHS, value); + } else { + assert kind == Tree.Kind.UNSIGNED_RIGHT_SHIFT_ASSIGNMENT; + operNode = new UnsignedRightShiftNode(operTree, targetRHS, value); + } + extendWithNode(operNode); + + TypeMirror castType = TypeAnnotationUtils.unannotatedType(leftType); + TypeCastTree castTree = treeBuilder.buildTypeCast(castType, operTree); + handleArtificialTree(castTree); + TypeCastNode castNode = new TypeCastNode(castTree, operNode, castType, types); + castNode.setInSource(false); + extendWithNode(castNode); + + AssignmentNode assignNode = new AssignmentNode(tree, targetLHS, castNode); + extendWithNode(assignNode); + return assignNode; + } + + case AND_ASSIGNMENT: + case OR_ASSIGNMENT: + case XOR_ASSIGNMENT: + // see JLS 15.22 + Node targetLHS = scan(tree.getVariable(), p); + Node value = scan(tree.getExpression(), p); + + TypeMirror leftType = TreeUtils.typeOf(tree.getVariable()); + TypeMirror rightType = TreeUtils.typeOf(tree.getExpression()); + + Node targetRHS = null; + if (isNumericOrBoxed(leftType) && isNumericOrBoxed(rightType)) { + TypeMirror promotedType = binaryPromotedType(leftType, rightType); + targetRHS = binaryNumericPromotion(targetLHS, promotedType); + value = binaryNumericPromotion(value, promotedType); + } else if (TypesUtils.isBooleanType(leftType) + && TypesUtils.isBooleanType(rightType)) { + targetRHS = unbox(targetLHS); + value = unbox(value); + } else { + throw new BugInCF( + "Both arguments to logical operation must be numeric or boolean"); + } + + BinaryTree operTree = + treeBuilder.buildBinary( + leftType, + withoutAssignment(kind), + tree.getVariable(), + tree.getExpression()); + handleArtificialTree(operTree); + Node operNode; + if (kind == Tree.Kind.AND_ASSIGNMENT) { + operNode = new BitwiseAndNode(operTree, targetRHS, value); + } else if (kind == Tree.Kind.OR_ASSIGNMENT) { + operNode = new BitwiseOrNode(operTree, targetRHS, value); + } else { + assert kind == Tree.Kind.XOR_ASSIGNMENT; + operNode = new BitwiseXorNode(operTree, targetRHS, value); + } + extendWithNode(operNode); + + TypeMirror castType = TypeAnnotationUtils.unannotatedType(leftType); + TypeCastTree castTree = treeBuilder.buildTypeCast(castType, operTree); + handleArtificialTree(castTree); + TypeCastNode castNode = new TypeCastNode(castTree, operNode, castType, types); + castNode.setInSource(false); + extendWithNode(castNode); + + AssignmentNode assignNode = new AssignmentNode(tree, targetLHS, castNode); + extendWithNode(assignNode); + return assignNode; + default: + throw new BugInCF("unexpected compound assignment type"); + } + } + + @Override + public Node visitBinary(BinaryTree tree, Void p) { + // Note that for binary operations it is important to perform any required promotion on the + // left operand before generating any Nodes for the right operand, because labels must be + // inserted AFTER ALL preceding Nodes and BEFORE ALL following Nodes. + Node r = null; + Tree leftTree = tree.getLeftOperand(); + Tree rightTree = tree.getRightOperand(); + + Tree.Kind kind = tree.getKind(); + switch (kind) { + case DIVIDE: + case MULTIPLY: + case REMAINDER: + { + // see JLS 15.17 + + TypeMirror exprType = TreeUtils.typeOf(tree); + TypeMirror leftType = TreeUtils.typeOf(leftTree); + TypeMirror rightType = TreeUtils.typeOf(rightTree); + TypeMirror promotedType = binaryPromotedType(leftType, rightType); + + Node left = binaryNumericPromotion(scan(leftTree, p), promotedType); + Node right = binaryNumericPromotion(scan(rightTree, p), promotedType); + + if (kind == Tree.Kind.MULTIPLY) { + r = new NumericalMultiplicationNode(tree, left, right); + } else if (kind == Tree.Kind.DIVIDE) { + if (TypesUtils.isIntegralPrimitive(exprType)) { + r = new IntegerDivisionNode(tree, left, right); + + extendWithNodeWithException(r, arithmeticExceptionType); + } else { + r = new FloatingDivisionNode(tree, left, right); + } + } else { + assert kind == Tree.Kind.REMAINDER; + if (TypesUtils.isIntegralPrimitive(exprType)) { + r = new IntegerRemainderNode(tree, left, right); + + extendWithNodeWithException(r, arithmeticExceptionType); + } else { + r = new FloatingRemainderNode(tree, left, right); + } + } + break; + } + + case MINUS: + case PLUS: + { + // see JLS 15.18 + + // TypeMirror exprType = InternalUtils.typeOf(tree); + TypeMirror leftType = TreeUtils.typeOf(leftTree); + TypeMirror rightType = TreeUtils.typeOf(rightTree); + + if (TypesUtils.isString(leftType) || TypesUtils.isString(rightType)) { + assert (kind == Tree.Kind.PLUS); + Node left = stringConversion(scan(leftTree, p)); + Node right = stringConversion(scan(rightTree, p)); + r = new StringConcatenateNode(tree, left, right); + } else { + TypeMirror promotedType = binaryPromotedType(leftType, rightType); + Node left = binaryNumericPromotion(scan(leftTree, p), promotedType); + Node right = binaryNumericPromotion(scan(rightTree, p), promotedType); + + // TODO: Decide whether to deal with floating-point value + // set conversion. + if (kind == Tree.Kind.PLUS) { + r = new NumericalAdditionNode(tree, left, right); + } else { + assert kind == Tree.Kind.MINUS; + r = new NumericalSubtractionNode(tree, left, right); + } + } + break; + } + + case LEFT_SHIFT: + case RIGHT_SHIFT: + case UNSIGNED_RIGHT_SHIFT: + { + // see JLS 15.19 + + Node left = unaryNumericPromotion(scan(leftTree, p)); + Node right = unaryNumericPromotion(scan(rightTree, p)); + + if (kind == Tree.Kind.LEFT_SHIFT) { + r = new LeftShiftNode(tree, left, right); + } else if (kind == Tree.Kind.RIGHT_SHIFT) { + r = new SignedRightShiftNode(tree, left, right); + } else { + assert kind == Tree.Kind.UNSIGNED_RIGHT_SHIFT; + r = new UnsignedRightShiftNode(tree, left, right); + } + break; + } + + case GREATER_THAN: + case GREATER_THAN_EQUAL: + case LESS_THAN: + case LESS_THAN_EQUAL: + { + // see JLS 15.20.1 + TypeMirror leftType = TreeUtils.typeOf(leftTree); + if (TypesUtils.isBoxedPrimitive(leftType)) { + leftType = types.unboxedType(leftType); + } + + TypeMirror rightType = TreeUtils.typeOf(rightTree); + if (TypesUtils.isBoxedPrimitive(rightType)) { + rightType = types.unboxedType(rightType); + } + + TypeMirror promotedType = binaryPromotedType(leftType, rightType); + Node left = binaryNumericPromotion(scan(leftTree, p), promotedType); + Node right = binaryNumericPromotion(scan(rightTree, p), promotedType); + + Node node; + if (kind == Tree.Kind.GREATER_THAN) { + node = new GreaterThanNode(tree, left, right); + } else if (kind == Tree.Kind.GREATER_THAN_EQUAL) { + node = new GreaterThanOrEqualNode(tree, left, right); + } else if (kind == Tree.Kind.LESS_THAN) { + node = new LessThanNode(tree, left, right); + } else { + assert kind == Tree.Kind.LESS_THAN_EQUAL; + node = new LessThanOrEqualNode(tree, left, right); + } + + extendWithNode(node); + + return node; + } + + case EQUAL_TO: + case NOT_EQUAL_TO: + { + // see JLS 15.21 + TreeInfo leftInfo = getTreeInfo(leftTree); + TreeInfo rightInfo = getTreeInfo(rightTree); + Node left = scan(leftTree, p); + Node right = scan(rightTree, p); + + if (leftInfo.isNumeric() + && rightInfo.isNumeric() + && !(leftInfo.isBoxed() && rightInfo.isBoxed())) { + // JLS 15.21.1 numerical equality + TypeMirror promotedType = + binaryPromotedType(leftInfo.unboxedType(), rightInfo.unboxedType()); + left = binaryNumericPromotion(left, promotedType); + right = binaryNumericPromotion(right, promotedType); + } else if (leftInfo.isBoolean() + && rightInfo.isBoolean() + && !(leftInfo.isBoxed() && rightInfo.isBoxed())) { + // JSL 15.21.2 boolean equality + left = unboxAsNeeded(left, leftInfo.isBoxed()); + right = unboxAsNeeded(right, rightInfo.isBoxed()); + } + + Node node; + if (kind == Tree.Kind.EQUAL_TO) { + node = new EqualToNode(tree, left, right); + } else { + assert kind == Tree.Kind.NOT_EQUAL_TO; + node = new NotEqualNode(tree, left, right); + } + extendWithNode(node); + + return node; + } + + case AND: + case OR: + case XOR: + { + // see JLS 15.22 + TypeMirror leftType = TreeUtils.typeOf(leftTree); + TypeMirror rightType = TreeUtils.typeOf(rightTree); + boolean isBooleanOp = + TypesUtils.isBooleanType(leftType) + && TypesUtils.isBooleanType(rightType); + + Node left; + Node right; + + if (isBooleanOp) { + left = unbox(scan(leftTree, p)); + right = unbox(scan(rightTree, p)); + } else if (isNumericOrBoxed(leftType) && isNumericOrBoxed(rightType)) { + TypeMirror promotedType = binaryPromotedType(leftType, rightType); + left = binaryNumericPromotion(scan(leftTree, p), promotedType); + right = binaryNumericPromotion(scan(rightTree, p), promotedType); + } else { + left = unbox(scan(leftTree, p)); + right = unbox(scan(rightTree, p)); + } + + Node node; + if (kind == Tree.Kind.AND) { + node = new BitwiseAndNode(tree, left, right); + } else if (kind == Tree.Kind.OR) { + node = new BitwiseOrNode(tree, left, right); + } else { + assert kind == Tree.Kind.XOR; + node = new BitwiseXorNode(tree, left, right); + } + + extendWithNode(node); + + return node; + } + + case CONDITIONAL_AND: + case CONDITIONAL_OR: + { + // see JLS 15.23 and 15.24 + + // all necessary labels + Label rightStartLabel = new Label(); + Label shortCircuitLabel = new Label(); + + // left-hand side + Node left = scan(leftTree, p); + + ConditionalJump cjump; + if (kind == Tree.Kind.CONDITIONAL_AND) { + cjump = new ConditionalJump(rightStartLabel, shortCircuitLabel); + cjump.setFalseFlowRule(FlowRule.ELSE_TO_ELSE); + } else { + cjump = new ConditionalJump(shortCircuitLabel, rightStartLabel); + cjump.setTrueFlowRule(FlowRule.THEN_TO_THEN); + } + extendWithExtendedNode(cjump); + + // right-hand side + addLabelForNextNode(rightStartLabel); + Node right = scan(rightTree, p); + + // conditional expression itself + addLabelForNextNode(shortCircuitLabel); + Node node; + if (kind == Tree.Kind.CONDITIONAL_AND) { + node = new ConditionalAndNode(tree, left, right); + } else { + node = new ConditionalOrNode(tree, left, right); + } + extendWithNode(node); + return node; + } + default: + throw new BugInCF("unexpected binary tree: " + kind); + } + assert r != null : "unexpected binary tree"; + extendWithNode(r); + return r; + } + + @Override + public Node visitBlock(BlockTree tree, Void p) { + for (StatementTree n : tree.getStatements()) { + scan(n, null); + } + return null; + } + + @Override + public Node visitBreak(BreakTree tree, Void p) { + Name label = tree.getLabel(); + if (label == null) { + assert breakTargetLC != null : "no target for break statement"; + + extendWithExtendedNode(new UnconditionalJump(breakTargetLC.accessLabel())); + } else { + assert breakLabels.containsKey(label); + + extendWithExtendedNode(new UnconditionalJump(breakLabels.get(label))); + } + + return null; + } + + // This visits a switch statement. + // Switch expressions are visited by visitSwitchExpression17. + @Override + public Node visitSwitch(SwitchTree tree, Void p) { + SwitchBuilder builder = new SwitchBuilder(tree); + builder.build(); + return null; + } + + /** + * Helper class for handling switch statements and switch expressions, including all their + * substatements such as case labels. + */ + private class SwitchBuilder { + + /** + * The tree for the switch statement or switch expression. Its type may be {@link + * SwitchTree} (for a switch statement) or {@code SwitchExpressionTree}. + */ + private final Tree switchTree; + + /** The case trees of {@code switchTree} */ + private final List caseTrees; + + /** + * The Tree for the selector expression. + * + *

      +         *   switch ( selector expression ) { ... }
      +         * 
      + */ + private final ExpressionTree selectorExprTree; + + /** The labels for the case bodies. */ + private final Label[] caseBodyLabels; + + /** + * The Node for the assignment of the switch selector expression to a synthetic local + * variable. + */ + private AssignmentNode selectorExprAssignment; + + /** + * If {@link #switchTree} is a switch expression, then this is a result variable: the + * synthetic variable that all results of {@code #switchTree} are assigned to. Otherwise, + * this is null. + */ + private @Nullable VariableTree switchExprVarTree; + + /** + * Construct a SwitchBuilder. + * + * @param switchTree a {@link SwitchTree} or a {@code SwitchExpressionTree} + */ + private SwitchBuilder(Tree switchTree) { + this.switchTree = switchTree; + if (TreeUtils.isSwitchStatement(switchTree)) { + SwitchTree switchStatementTree = (SwitchTree) switchTree; + this.caseTrees = switchStatementTree.getCases(); + this.selectorExprTree = switchStatementTree.getExpression(); + } else { + this.caseTrees = SwitchExpressionUtils.getCases(switchTree); + this.selectorExprTree = SwitchExpressionUtils.getExpression(switchTree); + } + // "+ 1" for the default case. If the switch has an explicit default case, then + // the last element of the array is never used. + this.caseBodyLabels = new Label[caseTrees.size() + 1]; + } + + /** + * Build up the CFG for the switchTree. + * + * @return if the switch is a switch expression, then a {@link SwitchExpressionNode}; + * otherwise, null + */ + public @Nullable SwitchExpressionNode build() { + LabelCell oldBreakTargetLC = breakTargetLC; + breakTargetLC = new LabelCell(new Label()); + int numCases = caseTrees.size(); + + for (int i = 0; i < numCases; ++i) { + caseBodyLabels[i] = new Label(); + } + caseBodyLabels[numCases] = breakTargetLC.peekLabel(); + + buildSelector(); + + buildSwitchExpressionVar(); + + if (TreeUtils.isSwitchStatement(switchTree)) { + // It's a switch statement, not a switch expression. + extendWithNode( + new MarkerNode( + switchTree, + "start of switch statement #" + TreeUtils.treeUids.get(switchTree), + env.getTypeUtils())); + } + + // JSL 14.11.2 + // https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-14.11.2 + // states "For compatibility reasons, switch statements that are not enhanced switch + // statements are not required to be exhaustive". + // Switch expressions and enhanced switch statements are exhaustive. + boolean switchExprOrEnhanced = + !TreeUtils.isSwitchStatement(switchTree) + || TreeUtils.isEnhancedSwitchStatement((SwitchTree) switchTree); + // Build CFG for the cases. + int defaultIndex = -1; + for (int i = 0; i < numCases; ++i) { + CaseTree caseTree = caseTrees.get(i); + if (CaseUtils.isDefaultCaseTree(caseTree)) { + // Per the Java Language Specification, the checks of all cases must happen + // before the default case, no matter where `default:` is written. Therefore, + // build the default case last. + defaultIndex = i; + } else if (i == numCases - 1 && defaultIndex == -1) { + // This is the last case, and there is no default case. + // Switch expressions and enhanced switch statements are exhaustive. + buildCase(caseTree, i, switchExprOrEnhanced); + } else { + buildCase(caseTree, i, false); + } + } + + if (defaultIndex != -1) { + // The checks of all cases must happen before the default case, therefore we build + // the default case last. + // Fallthrough is still handled correctly with the caseBodyLabels. + buildCase(caseTrees.get(defaultIndex), defaultIndex, false); + } + + addLabelForNextNode(breakTargetLC.peekLabel()); + breakTargetLC = oldBreakTargetLC; + if (TreeUtils.isSwitchStatement(switchTree)) { + // It's a switch statement, not a switch expression. + extendWithNode( + new MarkerNode( + switchTree, + "end of switch statement #" + TreeUtils.treeUids.get(switchTree), + env.getTypeUtils())); + } + + if (!TreeUtils.isSwitchStatement(switchTree)) { + // It's a switch expression, not a switch statement. + IdentifierTree switchExprVarUseTree = + treeBuilder.buildVariableUse(switchExprVarTree); + handleArtificialTree(switchExprVarUseTree); + + LocalVariableNode switchExprVarUseNode = + new LocalVariableNode(switchExprVarUseTree); + switchExprVarUseNode.setInSource(false); + extendWithNode(switchExprVarUseNode); + SwitchExpressionNode switchExpressionNode = + new SwitchExpressionNode( + TreeUtils.typeOf(switchTree), switchTree, switchExprVarUseNode); + extendWithNode(switchExpressionNode); + return switchExpressionNode; + } else { + return null; + } + } + + /** + * Builds the CFG for the selector expression. It also creates a synthetic variable and + * assigns the selector expression to the variable. This assignment node is stored in {@link + * #selectorExprAssignment}. It can later be used to refine the selector expression in case + * bodies. + */ + private void buildSelector() { + // Create a synthetic variable to which the switch selector expression will be assigned + TypeMirror selectorExprType = TreeUtils.typeOf(selectorExprTree); + VariableTree selectorVarTree = + treeBuilder.buildVariableDecl( + selectorExprType, uniqueName("switch"), findOwner(), null); + handleArtificialTree(selectorVarTree); + + VariableDeclarationNode selectorVarNode = new VariableDeclarationNode(selectorVarTree); + selectorVarNode.setInSource(false); + extendWithNode(selectorVarNode); + + IdentifierTree selectorVarUseTree = treeBuilder.buildVariableUse(selectorVarTree); + handleArtificialTree(selectorVarUseTree); + + LocalVariableNode selectorVarUseNode = new LocalVariableNode(selectorVarUseTree); + selectorVarUseNode.setInSource(false); + extendWithNode(selectorVarUseNode); + + Node selectorExprNode = unbox(scan(selectorExprTree, null)); + + AssignmentTree assign = + treeBuilder.buildAssignment(selectorVarUseTree, selectorExprTree); + handleArtificialTree(assign); + + selectorExprAssignment = + new AssignmentNode(assign, selectorVarUseNode, selectorExprNode); + selectorExprAssignment.setInSource(false); + extendWithNode(selectorExprAssignment); + } + + /** + * If {@link #switchTree} is a switch expression tree, this method creates a synthetic + * variable whose value is the value of the switch expression. + */ + private void buildSwitchExpressionVar() { + if (TreeUtils.isSwitchStatement(switchTree)) { + // A switch statement does not have a value, so do nothing. + return; + } + TypeMirror switchExprType = TreeUtils.typeOf(switchTree); + switchExprVarTree = + treeBuilder.buildVariableDecl( + switchExprType, uniqueName("switchExpr"), findOwner(), null); + handleArtificialTree(switchExprVarTree); + + VariableDeclarationNode switchExprVarNode = + new VariableDeclarationNode(switchExprVarTree); + switchExprVarNode.setInSource(false); + extendWithNode(switchExprVarNode); + } + + /** + * Build the CFG for the given case tree. + * + * @param caseTree a case tree whose CFG to build + * @param index the index of the case tree in {@link #caseBodyLabels} + * @param isLastCaseOfExhaustive true if this is the last case of an exhaustive switch + * statement, with no fallthrough to it. In other words, no test of the labels is + * necessary. + */ + private void buildCase(CaseTree caseTree, int index, boolean isLastCaseOfExhaustive) { + boolean isDefaultCase = CaseUtils.isDefaultCaseTree(caseTree); + // If true, no test of labels is necessary. + // Unfortunately, if isLastCaseOfExhaustive==TRUE, no flow-sensitive refinement occurs + // within the body of the CaseNode. In the future, that can be performed, but it + // requires addition of InfeasibleExitBlock, a new SpecialBlock in the CFG. + boolean isTerminalCase = isDefaultCase || isLastCaseOfExhaustive; + + Label thisBodyLabel = caseBodyLabels[index]; + Label nextBodyLabel = caseBodyLabels[index + 1]; + // `nextCaseLabel` is not used if isTerminalCase==FALSE. + Label nextCaseLabel = new Label(); + + // Handle the case expressions + if (!isTerminalCase) { + // A case expression exists, and it needs to be tested. + ArrayList exprs = new ArrayList<>(); + for (Tree exprTree : CaseUtils.getLabels(caseTree)) { + exprs.add(scan(exprTree, null)); + } + + ExpressionTree guardTree = CaseUtils.getGuard(caseTree); + Node guard = (guardTree == null) ? null : scan(guardTree, null); + + CaseNode test = + new CaseNode( + caseTree, selectorExprAssignment, exprs, guard, env.getTypeUtils()); + extendWithNode(test); + extendWithExtendedNode(new ConditionalJump(thisBodyLabel, nextCaseLabel)); + } + + // Handle the case body + addLabelForNextNode(thisBodyLabel); + if (caseTree.getStatements() != null) { + // This is a switch labeled statement group. + // A "switch labeled statement group" is a "case L:" label along with its code. + // The code either ends with a "yield" statement, or it falls through. + for (StatementTree stmt : caseTree.getStatements()) { + scan(stmt, null); + } + // Handle possible fallthrough by adding jump to next body. + if (!isTerminalCase) { + extendWithExtendedNode(new UnconditionalJump(nextBodyLabel)); + } + } else { + // This is either the default case or a switch labeled rule (which appears in a + // switch expression). + // A "switch labeled rule" is a "case L ->" label along with its code. + Tree bodyTree = CaseUtils.getBody(caseTree); + if (!TreeUtils.isSwitchStatement(switchTree) + && bodyTree instanceof ExpressionTree) { + buildSwitchExpressionResult((ExpressionTree) bodyTree); + } else { + scan(bodyTree, null); + // Switch rules never fall through so add jump to the break target. + assert breakTargetLC != null : "no target for case statement"; + extendWithExtendedNode(new UnconditionalJump(breakTargetLC.accessLabel())); + } + } + + if (!isTerminalCase) { + addLabelForNextNode(nextCaseLabel); + } + } + + /** + * Does the following for the result expression of a switch expression, {@code + * resultExpression}: + * + *
        + *
      1. Builds the CFG for the switch expression result. + *
      2. Creates an assignment node for the assignment of {@code resultExpression} to {@code + * switchExprVarTree}. + *
      3. Adds an unconditional jump to {@link #breakTargetLC} (the end of the switch + * expression). + *
      + * + * @param resultExpression the result of a switch expression; either from a yield or an + * expression in a case rule + */ + /*package-private*/ void buildSwitchExpressionResult(ExpressionTree resultExpression) { + IdentifierTree switchExprVarUseTree = treeBuilder.buildVariableUse(switchExprVarTree); + handleArtificialTree(switchExprVarUseTree); + + LocalVariableNode switchExprVarUseNode = new LocalVariableNode(switchExprVarUseTree); + switchExprVarUseNode.setInSource(false); + extendWithNode(switchExprVarUseNode); + + Node resultExprNode = scan(resultExpression, null); + + AssignmentTree assign = + treeBuilder.buildAssignment(switchExprVarUseTree, resultExpression); + handleArtificialTree(assign); + + AssignmentNode assignmentNode = + new AssignmentNode(assign, switchExprVarUseNode, resultExprNode); + assignmentNode.setInSource(false); + extendWithNode(assignmentNode); + // Switch rules never fall through so add jump to the break target. + assert breakTargetLC != null : "no target for case statement"; + extendWithExtendedNode(new UnconditionalJump(breakTargetLC.accessLabel())); + } + } + + @Override + public Node visitCase(CaseTree tree, Void p) { + // This assertion assumes that `case` appears only within a switch statement, + throw new AssertionError("case visitor is implemented in SwitchBuilder"); + } + + @Override + public Node visitCatch(CatchTree tree, Void p) { + scan(tree.getParameter(), p); + scan(tree.getBlock(), p); + return null; + } + + // This is not invoked for top-level classes. Maybe it is, for classes defined within method + // bodies. + @Override + public Node visitClass(ClassTree tree, Void p) { + declaredClasses.add(tree); + Node classbody = new ClassDeclarationNode(tree); + extendWithNode(classbody); + return classbody; + } + + @Override + public Node visitConditionalExpression(ConditionalExpressionTree tree, Void p) { + // see JLS 15.25 + TypeMirror exprType = TreeUtils.typeOf(tree); + + Label trueStart = new Label(); + Label falseStart = new Label(); + Label merge = new Label(); + + // create a synthetic variable for the value of the conditional expression + VariableTree condExprVarTree = + treeBuilder.buildVariableDecl(exprType, uniqueName("condExpr"), findOwner(), null); + handleArtificialTree(condExprVarTree); + VariableDeclarationNode condExprVarNode = new VariableDeclarationNode(condExprVarTree); + condExprVarNode.setInSource(false); + extendWithNode(condExprVarNode); + + Node condition = unbox(scan(tree.getCondition(), p)); + ConditionalJump cjump = new ConditionalJump(trueStart, falseStart); + extendWithExtendedNode(cjump); + + addLabelForNextNode(trueStart); + ExpressionTree trueExprTree = tree.getTrueExpression(); + Node trueExprNode = scan(trueExprTree, p); + trueExprNode = conditionalExprPromotion(trueExprNode, exprType); + extendWithAssignmentForConditionalExpr(condExprVarTree, trueExprTree, trueExprNode); + extendWithExtendedNode(new UnconditionalJump(merge)); + + addLabelForNextNode(falseStart); + ExpressionTree falseExprTree = tree.getFalseExpression(); + Node falseExprNode = scan(falseExprTree, p); + falseExprNode = conditionalExprPromotion(falseExprNode, exprType); + extendWithAssignmentForConditionalExpr(condExprVarTree, falseExprTree, falseExprNode); + extendWithExtendedNode(new UnconditionalJump(merge)); + + addLabelForNextNode(merge); + IPair treeAndLocalVarNode = + buildVarUseNode(condExprVarTree); + Node node = + new TernaryExpressionNode( + tree, condition, trueExprNode, falseExprNode, treeAndLocalVarNode.second); + extendWithNode(node); + + return node; + } + + /** + * Extend the CFG with an assignment for either the true or false case of a conditional + * expression, assigning the value of the expression for the case to the synthetic variable for + * the conditional expression + * + * @param condExprVarTree tree for synthetic variable for conditional expression + * @param caseExprTree expression tree for the case + * @param caseExprNode node for the case + */ + private void extendWithAssignmentForConditionalExpr( + VariableTree condExprVarTree, ExpressionTree caseExprTree, Node caseExprNode) { + IPair treeAndLocalVarNode = + buildVarUseNode(condExprVarTree); + + AssignmentTree assign = + treeBuilder.buildAssignment(treeAndLocalVarNode.first, caseExprTree); + handleArtificialTree(assign); + + // Build a "synthetic" assignment node, allowing special handling in transfer functions + AssignmentNode assignmentNode = + new AssignmentNode(assign, treeAndLocalVarNode.second, caseExprNode, true); + assignmentNode.setInSource(false); + extendWithNode(assignmentNode); + } + + /** + * Build a pair of {@link IdentifierTree} and {@link LocalVariableNode} to represent a use of + * some variable. Does not add the node to the CFG. + * + * @param varTree tree for the variable + * @return a pair whose first element is the synthetic {@link IdentifierTree} for the use, and + * whose second element is the {@link LocalVariableNode} representing the use + */ + private IPair buildVarUseNode(VariableTree varTree) { + IdentifierTree condExprVarUseTree = treeBuilder.buildVariableUse(varTree); + handleArtificialTree(condExprVarUseTree); + LocalVariableNode condExprVarUseNode = new LocalVariableNode(condExprVarUseTree); + condExprVarUseNode.setInSource(false); + // Do not actually add the node to the CFG. + return IPair.of(condExprVarUseTree, condExprVarUseNode); + } + + @Override + public Node visitContinue(ContinueTree tree, Void p) { + Name label = tree.getLabel(); + if (label == null) { + assert continueTargetLC != null : "no target for continue statement"; + + extendWithExtendedNode(new UnconditionalJump(continueTargetLC.accessLabel())); + } else { + assert continueLabels.containsKey(label); + + extendWithExtendedNode(new UnconditionalJump(continueLabels.get(label))); + } + + return null; + } + + @Override + public Node visitDoWhileLoop(DoWhileLoopTree tree, Void p) { + Name parentLabel = getLabel(getCurrentPath()); + + Label loopEntry = new Label(); + Label loopExit = new Label(); + + // If the loop is a labeled statement, then its continue target is identical for continues + // with no label and continues with the loop's label. + Label conditionStart; + if (parentLabel != null) { + conditionStart = continueLabels.get(parentLabel); + } else { + conditionStart = new Label(); + } + + LabelCell oldBreakTargetLC = breakTargetLC; + breakTargetLC = new LabelCell(loopExit); + + LabelCell oldContinueTargetLC = continueTargetLC; + continueTargetLC = new LabelCell(conditionStart); + + // Loop body + addLabelForNextNode(loopEntry); + assert tree.getStatement() != null; + scan(tree.getStatement(), p); + + // Condition + addLabelForNextNode(conditionStart); + assert tree.getCondition() != null; + unbox(scan(tree.getCondition(), p)); + ConditionalJump cjump = new ConditionalJump(loopEntry, loopExit); + extendWithExtendedNode(cjump); + + // Loop exit + addLabelForNextNode(loopExit); + + breakTargetLC = oldBreakTargetLC; + continueTargetLC = oldContinueTargetLC; + + return null; + } + + @Override + public Node visitErroneous(ErroneousTree tree, Void p) { + throw new BugInCF("ErroneousTree is unexpected in AST to CFG translation: " + tree); + } + + @Override + public Node visitExpressionStatement(ExpressionStatementTree tree, Void p) { + ExpressionTree exprTree = tree.getExpression(); + scan(exprTree, p); + extendWithNode(new ExpressionStatementNode(exprTree)); + return null; + } + + @Override + public Node visitEnhancedForLoop(EnhancedForLoopTree tree, Void p) { + // see JLS 14.14.2 + Name parentLabel = getLabel(getCurrentPath()); + + Label conditionStart = new Label(); + Label loopEntry = new Label(); + Label loopExit = new Label(); + + // If the loop is a labeled statement, then its continue target is identical for continues + // with no label and continues with the loop's label. + Label updateStart; + if (parentLabel != null) { + updateStart = continueLabels.get(parentLabel); + } else { + updateStart = new Label(); + } + + LabelCell oldBreakTargetLC = breakTargetLC; + breakTargetLC = new LabelCell(loopExit); + + LabelCell oldContinueTargetLC = continueTargetLC; + continueTargetLC = new LabelCell(updateStart); + + // Distinguish loops over Iterables from loops over arrays. + + VariableTree variable = tree.getVariable(); + VariableElement variableElement = TreeUtils.elementFromDeclaration(variable); + ExpressionTree expression = tree.getExpression(); + StatementTree statement = tree.getStatement(); + + TypeMirror exprType = TreeUtils.typeOf(expression); + + if (types.isSubtype(exprType, iterableType)) { + // Take the upper bound of a type variable or wildcard + exprType = TypesUtils.upperBound(exprType); + + assert (exprType instanceof DeclaredType) : "an Iterable must be a DeclaredType"; + DeclaredType declaredExprType = (DeclaredType) exprType; + declaredExprType.getTypeArguments(); + + MemberSelectTree iteratorSelect = treeBuilder.buildIteratorMethodAccess(expression); + handleArtificialTree(iteratorSelect); + + MethodInvocationTree iteratorCall = treeBuilder.buildMethodInvocation(iteratorSelect); + handleArtificialTree(iteratorCall); + + VariableTree iteratorVariable = + createEnhancedForLoopIteratorVariable(iteratorCall, variableElement); + handleArtificialTree(iteratorVariable); + + VariableDeclarationNode iteratorVariableDecl = + new VariableDeclarationNode(iteratorVariable); + iteratorVariableDecl.setInSource(false); + + extendWithNode(iteratorVariableDecl); + + Node expressionNode = scan(expression, p); + + MethodAccessNode iteratorAccessNode = + new MethodAccessNode(iteratorSelect, expressionNode); + iteratorAccessNode.setInSource(false); + extendWithNode(iteratorAccessNode); + MethodInvocationNode iteratorCallNode = + new MethodInvocationNode( + iteratorCall, + iteratorAccessNode, + Collections.emptyList(), + getCurrentPath()); + iteratorCallNode.setInSource(false); + extendWithNode(iteratorCallNode); + + translateAssignment( + iteratorVariable, new LocalVariableNode(iteratorVariable), iteratorCallNode); + + // Test the loop ending condition + addLabelForNextNode(conditionStart); + IdentifierTree iteratorUse1 = treeBuilder.buildVariableUse(iteratorVariable); + handleArtificialTree(iteratorUse1); + + LocalVariableNode iteratorReceiverNode = new LocalVariableNode(iteratorUse1); + iteratorReceiverNode.setInSource(false); + extendWithNode(iteratorReceiverNode); + + MemberSelectTree hasNextSelect = treeBuilder.buildHasNextMethodAccess(iteratorUse1); + handleArtificialTree(hasNextSelect); + + MethodAccessNode hasNextAccessNode = + new MethodAccessNode(hasNextSelect, iteratorReceiverNode); + hasNextAccessNode.setInSource(false); + extendWithNode(hasNextAccessNode); + + MethodInvocationTree hasNextCall = treeBuilder.buildMethodInvocation(hasNextSelect); + handleArtificialTree(hasNextCall); + + MethodInvocationNode hasNextCallNode = + new MethodInvocationNode( + hasNextCall, + hasNextAccessNode, + Collections.emptyList(), + getCurrentPath()); + hasNextCallNode.setInSource(false); + extendWithNode(hasNextCallNode); + extendWithExtendedNode(new ConditionalJump(loopEntry, loopExit)); + + // Loop body, starting with declaration of the loop iteration variable + addLabelForNextNode(loopEntry); + extendWithNode(new VariableDeclarationNode(variable)); + + IdentifierTree iteratorUse2 = treeBuilder.buildVariableUse(iteratorVariable); + handleArtificialTree(iteratorUse2); + + LocalVariableNode iteratorReceiverNode2 = new LocalVariableNode(iteratorUse2); + iteratorReceiverNode2.setInSource(false); + extendWithNode(iteratorReceiverNode2); + + MemberSelectTree nextSelect = treeBuilder.buildNextMethodAccess(iteratorUse2); + handleArtificialTree(nextSelect); + + MethodAccessNode nextAccessNode = + new MethodAccessNode(nextSelect, iteratorReceiverNode2); + nextAccessNode.setInSource(false); + extendWithNode(nextAccessNode); + + MethodInvocationTree nextCall = treeBuilder.buildMethodInvocation(nextSelect); + handleArtificialTree(nextCall); + + MethodInvocationNode nextCallNode = + new MethodInvocationNode( + nextCall, nextAccessNode, Collections.emptyList(), getCurrentPath()); + // If the type of iteratorVariable is a capture, its type tree may be missing + // annotations, so save the expression in the node so that the full type can be + // found later. + nextCallNode.setIterableExpression(expression); + nextCallNode.setInSource(false); + extendWithNode(nextCallNode); + + AssignmentNode assignNode = + translateAssignment(variable, new LocalVariableNode(variable), nextCall); + // translateAssignment() scans variable and creates new nodes, so set the expression + // there, too. + ((MethodInvocationNode) assignNode.getExpression()).setIterableExpression(expression); + + assert statement != null; + scan(statement, p); + + // Loop back edge + addLabelForNextNode(updateStart); + extendWithExtendedNode(new UnconditionalJump(conditionStart)); + + } else { + // TODO: Shift any labels after the initialization of the + // temporary array variable. + + VariableTree arrayVariable = + createEnhancedForLoopArrayVariable(expression, variableElement); + handleArtificialTree(arrayVariable); + + VariableDeclarationNode arrayVariableNode = new VariableDeclarationNode(arrayVariable); + arrayVariableNode.setInSource(false); + extendWithNode(arrayVariableNode); + Node expressionNode = scan(expression, p); + + translateAssignment( + arrayVariable, new LocalVariableNode(arrayVariable), expressionNode); + + // Declare and initialize the loop index variable + TypeMirror intType = types.getPrimitiveType(TypeKind.INT); + + LiteralTree zero = treeBuilder.buildLiteral(Integer.valueOf(0)); + handleArtificialTree(zero); + + VariableTree indexVariable = + treeBuilder.buildVariableDecl( + intType, + uniqueName("index"), + variableElement.getEnclosingElement(), + zero); + handleArtificialTree(indexVariable); + VariableDeclarationNode indexVariableNode = new VariableDeclarationNode(indexVariable); + indexVariableNode.setInSource(false); + extendWithNode(indexVariableNode); + IntegerLiteralNode zeroNode = new IntegerLiteralNode(zero); + extendWithNode(zeroNode); + + translateAssignment(indexVariable, new LocalVariableNode(indexVariable), zeroNode); + + // Compare index to array length + addLabelForNextNode(conditionStart); + IdentifierTree indexUse1 = treeBuilder.buildVariableUse(indexVariable); + handleArtificialTree(indexUse1); + LocalVariableNode indexNode1 = new LocalVariableNode(indexUse1); + indexNode1.setInSource(false); + extendWithNode(indexNode1); + + IdentifierTree arrayUse1 = treeBuilder.buildVariableUse(arrayVariable); + handleArtificialTree(arrayUse1); + LocalVariableNode arrayNode1 = new LocalVariableNode(arrayUse1); + extendWithNode(arrayNode1); + + MemberSelectTree lengthSelect = treeBuilder.buildArrayLengthAccess(arrayUse1); + handleArtificialTree(lengthSelect); + FieldAccessNode lengthAccessNode = new FieldAccessNode(lengthSelect, arrayNode1); + lengthAccessNode.setInSource(false); + extendWithNode(lengthAccessNode); + + BinaryTree lessThan = treeBuilder.buildLessThan(indexUse1, lengthSelect); + handleArtificialTree(lessThan); + + LessThanNode lessThanNode = new LessThanNode(lessThan, indexNode1, lengthAccessNode); + lessThanNode.setInSource(false); + extendWithNode(lessThanNode); + extendWithExtendedNode(new ConditionalJump(loopEntry, loopExit)); + + // Loop body, starting with declaration of the loop iteration variable + addLabelForNextNode(loopEntry); + extendWithNode(new VariableDeclarationNode(variable)); + + IdentifierTree arrayUse2 = treeBuilder.buildVariableUse(arrayVariable); + handleArtificialTree(arrayUse2); + LocalVariableNode arrayNode2 = new LocalVariableNode(arrayUse2); + arrayNode2.setInSource(false); + extendWithNode(arrayNode2); + + IdentifierTree indexUse2 = treeBuilder.buildVariableUse(indexVariable); + handleArtificialTree(indexUse2); + LocalVariableNode indexNode2 = new LocalVariableNode(indexUse2); + indexNode2.setInSource(false); + extendWithNode(indexNode2); + + ArrayAccessTree arrayAccess = treeBuilder.buildArrayAccess(arrayUse2, indexUse2); + handleArtificialTree(arrayAccess); + ArrayAccessNode arrayAccessNode = + new ArrayAccessNode(arrayAccess, arrayNode2, indexNode2); + arrayAccessNode.setArrayExpression(expression); + arrayAccessNode.setInSource(false); + extendWithNode(arrayAccessNode); + AssignmentNode arrayAccessAssignNode = + translateAssignment(variable, new LocalVariableNode(variable), arrayAccessNode); + extendWithNodeWithException(arrayAccessNode, nullPointerExceptionType); + // translateAssignment() scans variable and creates new nodes, so set the expression + // there, too. + Node arrayAccessAssignNodeExpr = arrayAccessAssignNode.getExpression(); + if (arrayAccessAssignNodeExpr instanceof ArrayAccessNode) { + ((ArrayAccessNode) arrayAccessAssignNodeExpr).setArrayExpression(expression); + } else if (arrayAccessAssignNodeExpr instanceof MethodInvocationNode) { + // If the array component type is a primitive, there may be a boxing or unboxing + // conversion. Treat that as an iterator. + MethodInvocationNode boxingNode = (MethodInvocationNode) arrayAccessAssignNodeExpr; + boxingNode.setIterableExpression(expression); + } + + assert statement != null; + scan(statement, p); + + // Loop back edge + addLabelForNextNode(updateStart); + + IdentifierTree indexUse3 = treeBuilder.buildVariableUse(indexVariable); + handleArtificialTree(indexUse3); + LocalVariableNode indexNode3 = new LocalVariableNode(indexUse3); + indexNode3.setInSource(false); + extendWithNode(indexNode3); + + LiteralTree oneTree = treeBuilder.buildLiteral(Integer.valueOf(1)); + handleArtificialTree(oneTree); + Node one = new IntegerLiteralNode(oneTree); + one.setInSource(false); + extendWithNode(one); + + BinaryTree addOneTree = + treeBuilder.buildBinary(intType, Tree.Kind.PLUS, indexUse3, oneTree); + handleArtificialTree(addOneTree); + Node addOneNode = new NumericalAdditionNode(addOneTree, indexNode3, one); + addOneNode.setInSource(false); + extendWithNode(addOneNode); + + AssignmentTree assignTree = treeBuilder.buildAssignment(indexUse3, addOneTree); + handleArtificialTree(assignTree); + Node assignNode = new AssignmentNode(assignTree, indexNode3, addOneNode); + assignNode.setInSource(false); + extendWithNode(assignNode); + + extendWithExtendedNode(new UnconditionalJump(conditionStart)); + } + + // Loop exit + addLabelForNextNode(loopExit); + + breakTargetLC = oldBreakTargetLC; + continueTargetLC = oldContinueTargetLC; + + return null; + } + + protected VariableTree createEnhancedForLoopIteratorVariable( + MethodInvocationTree iteratorCall, VariableElement variableElement) { + TypeMirror iteratorType = TreeUtils.typeOf(iteratorCall); + + // Declare and initialize a new, unique iterator variable + VariableTree iteratorVariable = + treeBuilder.buildVariableDecl( + iteratorType, // annotatedIteratorTypeTree, + uniqueName("iter"), + variableElement.getEnclosingElement(), + iteratorCall); + return iteratorVariable; + } + + protected VariableTree createEnhancedForLoopArrayVariable( + ExpressionTree expression, VariableElement variableElement) { + TypeMirror arrayType = TreeUtils.typeOf(expression); + + // Declare and initialize a temporary array variable + VariableTree arrayVariable = + treeBuilder.buildVariableDecl( + arrayType, + uniqueName("array"), + variableElement.getEnclosingElement(), + expression); + return arrayVariable; + } + + @Override + public Node visitForLoop(ForLoopTree tree, Void p) { + Name parentLabel = getLabel(getCurrentPath()); + + Label conditionStart = new Label(); + Label loopEntry = new Label(); + Label loopExit = new Label(); + + // If the loop is a labeled statement, then its continue target is identical for continues + // with no label and continues with the loop's label. + Label updateStart; + if (parentLabel != null) { + updateStart = continueLabels.get(parentLabel); + } else { + updateStart = new Label(); + } + + LabelCell oldBreakTargetLC = breakTargetLC; + breakTargetLC = new LabelCell(loopExit); + + LabelCell oldContinueTargetLC = continueTargetLC; + continueTargetLC = new LabelCell(updateStart); + + // Initializer + for (StatementTree init : tree.getInitializer()) { + scan(init, p); + } + + // Condition + addLabelForNextNode(conditionStart); + if (tree.getCondition() != null) { + unbox(scan(tree.getCondition(), p)); + ConditionalJump cjump = new ConditionalJump(loopEntry, loopExit); + extendWithExtendedNode(cjump); + } + + // Loop body + addLabelForNextNode(loopEntry); + assert tree.getStatement() != null; + scan(tree.getStatement(), p); + + // Update + addLabelForNextNode(updateStart); + for (ExpressionStatementTree update : tree.getUpdate()) { + scan(update, p); + } + + extendWithExtendedNode(new UnconditionalJump(conditionStart)); + + // Loop exit + addLabelForNextNode(loopExit); + + breakTargetLC = oldBreakTargetLC; + continueTargetLC = oldContinueTargetLC; + + return null; + } + + @Override + public Node visitIdentifier(IdentifierTree tree, Void p) { + Node node; + if (TreeUtils.isFieldAccess(tree)) { + Node receiver = getReceiver(tree); + node = new FieldAccessNode(tree, receiver); + } else { + Element element = TreeUtils.elementFromUse(tree); + switch (element.getKind()) { + case FIELD: + // Note that "this"/"super" is a field, but not a field access. + if (element.getSimpleName().contentEquals("this")) { + node = new ExplicitThisNode(tree); + } else { + node = new SuperNode(tree); + } + break; + case EXCEPTION_PARAMETER: + case LOCAL_VARIABLE: + case RESOURCE_VARIABLE: + case PARAMETER: + node = new LocalVariableNode(tree); + break; + case PACKAGE: + node = new PackageNameNode(tree); + break; + default: + if (ElementUtils.isTypeDeclaration(element)) { + node = new ClassNameNode(tree); + break; + } else if (ElementUtils.isBindingVariable(element)) { + // Note: BINDING_VARIABLE should be added as a direct case above when + // instanceof pattern matching and Java15 are supported. + node = new LocalVariableNode(tree); + break; + } + throw new BugInCF("bad element kind " + element.getKind()); + } + } + if (node instanceof ClassNameNode) { + extendWithClassNameNode((ClassNameNode) node); + } else { + extendWithNode(node); + } + return node; + } + + @Override + public Node visitIf(IfTree tree, Void p) { + // all necessary labels + Label thenEntry = new Label(); + Label elseEntry = new Label(); + Label endIf = new Label(); + + // basic block for the condition + unbox(scan(tree.getCondition(), p)); + + ConditionalJump cjump = new ConditionalJump(thenEntry, elseEntry); + extendWithExtendedNode(cjump); + + // then branch + addLabelForNextNode(thenEntry); + StatementTree thenStatement = tree.getThenStatement(); + scan(thenStatement, p); + extendWithExtendedNode(new UnconditionalJump(endIf)); + + // else branch + addLabelForNextNode(elseEntry); + StatementTree elseStatement = tree.getElseStatement(); + if (elseStatement != null) { + scan(elseStatement, p); + } + + // label the end of the if statement + addLabelForNextNode(endIf); + + return null; + } + + @Override + public Node visitImport(ImportTree tree, Void p) { + throw new BugInCF("ImportTree is unexpected in AST to CFG translation: " + tree); + } + + @Override + public Node visitArrayAccess(ArrayAccessTree tree, Void p) { + Node array = scan(tree.getExpression(), p); + Node index = unaryNumericPromotion(scan(tree.getIndex(), p)); + Node arrayAccess = new ArrayAccessNode(tree, array, index); + extendWithNode(arrayAccess); + extendWithNodeWithException(arrayAccess, arrayIndexOutOfBoundsExceptionType); + extendWithNodeWithException(arrayAccess, nullPointerExceptionType); + return arrayAccess; + } + + @Override + public Node visitLabeledStatement(LabeledStatementTree tree, Void p) { + // This method can set the break target after generating all Nodes in the contained + // statement, but it can't set the continue target, which may be in the middle of a + // sequence of nodes. Labeled loops must look up and use the continue Labels. + Name labelName = tree.getLabel(); + + Label breakLabel = new Label(labelName + "_break"); + Label continueLabel = new Label(labelName + "_continue"); + + breakLabels.put(labelName, breakLabel); + continueLabels.put(labelName, continueLabel); + + scan(tree.getStatement(), p); + + addLabelForNextNode(breakLabel); + + breakLabels.remove(labelName); + continueLabels.remove(labelName); + + return null; + } + + @Override + public Node visitLiteral(LiteralTree tree, Void p) { + Node r = null; + switch (tree.getKind()) { + case BOOLEAN_LITERAL: + r = new BooleanLiteralNode(tree); + break; + case CHAR_LITERAL: + r = new CharacterLiteralNode(tree); + break; + case DOUBLE_LITERAL: + r = new DoubleLiteralNode(tree); + break; + case FLOAT_LITERAL: + r = new FloatLiteralNode(tree); + break; + case INT_LITERAL: + r = new IntegerLiteralNode(tree); + break; + case LONG_LITERAL: + r = new LongLiteralNode(tree); + break; + case NULL_LITERAL: + r = new NullLiteralNode(tree); + break; + case STRING_LITERAL: + r = new StringLiteralNode(tree); + break; + default: + throw new BugInCF("unexpected literal tree: " + tree); + } + assert r != null : "unexpected literal tree"; + extendWithNode(r); + return r; + } + + @Override + public Node visitMethod(MethodTree tree, Void p) { + throw new BugInCF("MethodTree is unexpected in AST to CFG translation"); + } + + @Override + public Node visitModifiers(ModifiersTree tree, Void p) { + throw new BugInCF("ModifiersTree is unexpected in AST to CFG translation"); + } + + @Override + public Node visitNewArray(NewArrayTree tree, Void p) { + // see JLS 15.10 + + ArrayType type = (ArrayType) TreeUtils.typeOf(tree); + TypeMirror elemType = type.getComponentType(); + + List dimensions = tree.getDimensions(); + List initializers = tree.getInitializers(); + assert dimensions != null; + + List dimensionNodes = + CollectionsPlume.mapList(dim -> unaryNumericPromotion(scan(dim, p)), dimensions); + + List initializerNodes; + if (initializers == null) { + initializerNodes = Collections.emptyList(); + } else { + initializerNodes = + CollectionsPlume.mapList( + init -> assignConvert(scan(init, p), elemType), initializers); + } + + Node node = new ArrayCreationNode(tree, type, dimensionNodes, initializerNodes); + + extendWithNodeWithExceptions(node, newArrayExceptionTypes); + return node; + } + + @Override + public Node visitNewClass(NewClassTree tree, Void p) { + // see JLS 15.9 + + DeclaredType classType = (DeclaredType) TreeUtils.typeOf(tree); + TypeMirror enclosingType = classType.getEnclosingType(); + Tree enclosingExpr = tree.getEnclosingExpression(); + Node enclosingExprNode; + if (enclosingExpr != null) { + enclosingExprNode = scan(enclosingExpr, p); + } else if (enclosingType.getKind() == TypeKind.DECLARED) { + // This is an inner class (instance nested class). + // As there is no explicit enclosing expression, create a node for the implicit this + // argument. + enclosingExprNode = new ImplicitThisNode(enclosingType); + extendWithNode(enclosingExprNode); + } else { + // For static nested classes, the kind would be Typekind.None. + + enclosingExprNode = null; + } + + // Convert constructor arguments + ExecutableElement constructor = TreeUtils.elementFromUse(tree); + + List actualExprs = tree.getArguments(); + + List arguments = + convertCallArguments( + tree, constructor, TreeUtils.typeFromUse(tree), actualExprs, tree); + + // TODO: for anonymous classes, don't use the identifier alone. + // See https://github.com/typetools/checker-framework/issues/890 . + Node constructorNode = scan(tree.getIdentifier(), p); + + // Handle anonymous classes in visitClass. + // Note that getClassBody() and therefore classbody can be null. + ClassDeclarationNode classbody = (ClassDeclarationNode) scan(tree.getClassBody(), p); + + Node node = + new ObjectCreationNode( + tree, enclosingExprNode, constructorNode, arguments, classbody); + List thrownTypes = constructor.getThrownTypes(); + Set thrownSet = + ArraySet.newArraySetOrLinkedHashSet( + thrownTypes.size() + uncheckedExceptionTypes.size()); + // Add exceptions explicitly mentioned in the throws clause. + thrownSet.addAll(thrownTypes); + // Add types to account for unchecked exceptions + thrownSet.addAll(uncheckedExceptionTypes); + + extendWithNodeWithExceptions(node, thrownSet); + + return node; + } + + /** + * Maps a {@code Tree} to its directly enclosing {@code ParenthesizedTree} if one exists. + * + *

      This can be used to handle system types that are not present. For example, in Java code + * that is translated to JavaScript using j2cl, the custom bootclasspath contains APIs that are + * emulated in JavaScript, so some types such as OutOfMemoryError are deliberately not present. + * + * @param clazz a class, which must have a canonical name + * @return the TypeMirror for the class, or {@code null} if the type is not present + */ + protected @Nullable TypeMirror maybeGetTypeMirror(Class clazz) { + String name = clazz.getCanonicalName(); + assert name != null : clazz + " does not have a canonical name"; + TypeElement element = elements.getTypeElement(name); + if (element == null) { + return null; + } + return element.asType(); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseThree.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseThree.java new file mode 100644 index 000000000000..f874279870e7 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseThree.java @@ -0,0 +1,382 @@ +package org.checkerframework.dataflow.cfg.builder; + +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.Block.BlockType; +import org.checkerframework.dataflow.cfg.block.BlockImpl; +import org.checkerframework.dataflow.cfg.block.ConditionalBlockImpl; +import org.checkerframework.dataflow.cfg.block.ExceptionBlockImpl; +import org.checkerframework.dataflow.cfg.block.RegularBlockImpl; +import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlockImpl; +import org.checkerframework.javacutil.BugInCF; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import javax.lang.model.type.TypeMirror; + +/** + * Class that performs phase three of the translation process. In particular, the following + * degenerate cases of basic blocks are removed: + * + *

        + *
      1. Empty regular basic blocks: These blocks will be removed and their predecessors linked + * directly to the successor. + *
      2. Conditional basic blocks that have the same basic block as the 'then' and 'else' successor: + * The conditional basic block will be removed in this case. + *
      3. Two consecutive, non-empty, regular basic blocks where the second block has exactly one + * predecessor (namely the other of the two blocks): In this case, the two blocks are merged. + *
      4. Some basic blocks might not be reachable from the entryBlock. These basic blocks are + * removed, and the list of predecessors (in the doubly-linked structure of basic blocks) are + * adapted correctly. + *
      + * + * Eliminating the second type of degenerate cases might introduce cases of the third problem. These + * are also removed. + */ +public class CFGTranslationPhaseThree { + + /** A simple wrapper object that holds a basic block and allows to set one of its successors. */ + protected interface PredecessorHolder { + void setSuccessor(BlockImpl b); + + BlockImpl getBlock(); + } + + /** + * Perform phase three on the control flow graph {@code cfg}. + * + * @param cfg the control flow graph. Ownership is transfered to this method and the caller is + * not allowed to read or modify {@code cfg} after the call to {@code process} any more. + * @return the resulting control flow graph + */ + @SuppressWarnings("nullness") // TODO: successors + public static ControlFlowGraph process(ControlFlowGraph cfg) { + Set worklist = cfg.getAllBlocks(); + + // note: this method has to be careful when relinking basic blocks + // to not forget to adjust the predecessors, too + + // fix predecessor lists by removing any unreachable predecessors + for (Block c : worklist) { + BlockImpl cur = (BlockImpl) c; + for (Block pred : cur.getPredecessors()) { + if (!worklist.contains(pred)) { + cur.removePredecessor((BlockImpl) pred); + } + } + } + + // remove empty blocks + Set dontVisit = new HashSet<>(); + for (Block cur : worklist) { + if (dontVisit.contains(cur)) { + continue; + } + + if (cur.getType() == BlockType.REGULAR_BLOCK) { + RegularBlockImpl b = (RegularBlockImpl) cur; + if (b.isEmpty()) { + Set emptyBlocks = new HashSet<>(); + Set predecessors = new LinkedHashSet<>(); + BlockImpl succ = computeNeighborhoodOfEmptyBlock(b, emptyBlocks, predecessors); + for (RegularBlockImpl e : emptyBlocks) { + succ.removePredecessor(e); + dontVisit.add(e); + } + for (PredecessorHolder p : predecessors) { + BlockImpl block = p.getBlock(); + dontVisit.add(block); + succ.removePredecessor(block); + p.setSuccessor(succ); + } + } + } + } + + // remove useless conditional blocks + /* Issue 3267 revealed that this is a dangerous optimization: + it merges a block that evaluates one condition onto an unrelated following block, + which can also be a condition. The then/else stores from the first block are still + set, leading to incorrect results for the then/else stores in the following block. + The correct result would be to merge the then/else stores from the previous block. + However, as this is late in the CFG construction, I didn't see how to add e.g. a + dummy variable declaration node in a dummy regular block, which would cause a merge. + So for now, let's not perform this optimization. + It would be interesting to know how large the impact of this optimization is. + + worklist = cfg.getAllBlocks(); + for (Block c : worklist) { + BlockImpl cur = (BlockImpl) c; + + if (cur.getType() == BlockType.CONDITIONAL_BLOCK) { + ConditionalBlockImpl cb = (ConditionalBlockImpl) cur; + assert cb.getPredecessors().size() == 1; + if (cb.getThenSuccessor() == cb.getElseSuccessor()) { + BlockImpl pred = cb.getPredecessors().iterator().next(); + PredecessorHolder predecessorHolder = getPredecessorHolder(pred, cb); + BlockImpl succ = (BlockImpl) cb.getThenSuccessor(); + succ.removePredecessor(cb); + predecessorHolder.setSuccessor(succ); + } + } + } + */ + + mergeConsecutiveBlocks(cfg); + return cfg; + } + + /** + * Simplify the CFG by merging consecutive single-successor blocks. + * + * @param cfg the control flow graph + */ + @SuppressWarnings({ + "interning:not.interned", // CFG node comparisons + "nullness" // TODO: successors + }) + protected static void mergeConsecutiveBlocks(ControlFlowGraph cfg) { + Set worklist = cfg.getAllBlocks(); + + // This transformation removes blocks from the CFG. If those blocks appear in `worklist` + // then we might visit a block AFTER it has been removed and its nodes have been moved + // somewhere else. When this happens the correct behavior is to just skip the removed + // block; to do so, we need to remember which blocks have been removed. + Set removedBlocks = new HashSet<>(); + + for (Block cur : worklist) { + // Skip this block if it was already merged into another. + if (removedBlocks.contains(cur)) { + continue; + } + + // There may be many blocks to merge in series. + // + // ... \ /> ... + // ... --> cur -> b2 -> b3 -> ... + // ... / \> ... + // + // This loop merges the successor into `cur` until it can't do so anymore. + boolean didMerge; + do { + didMerge = false; + if (cur.getType() == BlockType.REGULAR_BLOCK) { + RegularBlockImpl b = (RegularBlockImpl) cur; + Block succ = b.getRegularSuccessor(); + if (succ.getType() == BlockType.REGULAR_BLOCK) { + RegularBlockImpl rs = (RegularBlockImpl) succ; + if (rs.getRegularSuccessor() == rs) { + // An infinite loop, do not try to merge. + break; + } + if (rs.getPredecessors().size() == 1) { + b.setSuccessor(rs.getRegularSuccessor()); + b.addNodes(rs.getNodes()); + rs.getRegularSuccessor().removePredecessor(rs); + removedBlocks.add(rs); + didMerge = true; + } + } + } + } while (didMerge); + } + } + + /** + * Compute the set of empty regular basic blocks {@code emptyBlocks}, starting at {@code start} + * and going both forward and backwards. Furthermore, compute the predecessors of these empty + * blocks ({@code predecessors} ), and their single successor (return value). + * + * @param start the starting point of the search (an empty, regular basic block) + * @param emptyBlocks a set to be filled by this method with all empty basic blocks found + * (including {@code start}). + * @param predecessors a 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", // CFG node comparisons + "nullness" // successors + }) + protected static BlockImpl computeNeighborhoodOfEmptyBlock( + RegularBlockImpl start, + Set emptyBlocks, + Set predecessors) { + + // get empty neighborhood that come before 'start' + computeNeighborhoodOfEmptyBlockBackwards(start, emptyBlocks, predecessors); + + // go forward + BlockImpl succ = (BlockImpl) start.getSuccessor(); + while (succ.getType() == BlockType.REGULAR_BLOCK) { + RegularBlockImpl cur = (RegularBlockImpl) succ; + if (cur.isEmpty()) { + computeNeighborhoodOfEmptyBlockBackwards(cur, emptyBlocks, predecessors); + assert emptyBlocks.contains(cur) : "cur ought to be in emptyBlocks"; + succ = (BlockImpl) cur.getSuccessor(); + if (succ == cur) { + // An infinite loop, making exit block unreachable + break; + } + } else { + break; + } + } + return succ; + } + + /** + * Compute the set of empty regular basic blocks {@code emptyBlocks}, starting at {@code start} + * and looking only backwards in the control flow graph. Furthermore, compute the predecessors + * of these empty blocks ({@code predecessors}). + * + * @param start the starting point of the search (an empty, regular basic block) + * @param emptyBlocks a set to be filled by this method with all empty basic blocks found + * (including {@code start}). + * @param predecessors a set to be filled by this method with all predecessors + */ + protected static void computeNeighborhoodOfEmptyBlockBackwards( + RegularBlockImpl start, + Set emptyBlocks, + Set predecessors) { + + RegularBlockImpl cur = start; + emptyBlocks.add(cur); + for (Block p : cur.getPredecessors()) { + BlockImpl pred = (BlockImpl) p; + switch (pred.getType()) { + case SPECIAL_BLOCK: + // add pred correctly to predecessor list + predecessors.add(getPredecessorHolder(pred, cur)); + break; + case CONDITIONAL_BLOCK: + // add pred correctly to predecessor list + predecessors.add(getPredecessorHolder(pred, cur)); + break; + case EXCEPTION_BLOCK: + // add pred correctly to predecessor list + predecessors.add(getPredecessorHolder(pred, cur)); + break; + case REGULAR_BLOCK: + RegularBlockImpl r = (RegularBlockImpl) pred; + if (r.isEmpty()) { + // recursively look backwards + if (!emptyBlocks.contains(r)) { + computeNeighborhoodOfEmptyBlockBackwards(r, emptyBlocks, predecessors); + } + } else { + // add pred correctly to predecessor list + predecessors.add(getPredecessorHolder(pred, cur)); + } + break; + } + } + } + + /** + * Return a predecessor holder that can be used to set the successor of {@code pred} in the + * 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(BlockImpl pred, BlockImpl cur) { + switch (pred.getType()) { + case SPECIAL_BLOCK: + SingleSuccessorBlockImpl s = (SingleSuccessorBlockImpl) pred; + return singleSuccessorHolder(s, cur); + case CONDITIONAL_BLOCK: + // add pred correctly to predecessor list + ConditionalBlockImpl c = (ConditionalBlockImpl) pred; + if (c.getThenSuccessor() == cur) { + return new PredecessorHolder() { + @Override + public void setSuccessor(BlockImpl b) { + c.setThenSuccessor(b); + cur.removePredecessor(pred); + } + + @Override + public BlockImpl getBlock() { + return c; + } + }; + } else { + assert c.getElseSuccessor() == cur; + return new PredecessorHolder() { + @Override + public void setSuccessor(BlockImpl b) { + c.setElseSuccessor(b); + cur.removePredecessor(pred); + } + + @Override + public BlockImpl getBlock() { + return c; + } + }; + } + case EXCEPTION_BLOCK: + // add pred correctly to predecessor list + ExceptionBlockImpl e = (ExceptionBlockImpl) pred; + if (e.getSuccessor() == cur) { + return singleSuccessorHolder(e, cur); + } else { + @SuppressWarnings("keyfor:assignment.type.incompatible") // ignore keyfor type + Set>> entrySet = + e.getExceptionalSuccessors().entrySet(); + for (Map.Entry> entry : entrySet) { + if (entry.getValue().contains(cur)) { + return new PredecessorHolder() { + @Override + public void setSuccessor(BlockImpl b) { + e.addExceptionalSuccessor(b, entry.getKey()); + cur.removePredecessor(pred); + } + + @Override + public BlockImpl getBlock() { + return e; + } + }; + } + } + } + throw new BugInCF("Unreachable"); + case REGULAR_BLOCK: + RegularBlockImpl r = (RegularBlockImpl) pred; + return singleSuccessorHolder(r, cur); + default: + throw new BugInCF("Unexpected block type " + pred.getType()); + } + } + + /** + * Returns a {@link PredecessorHolder} that sets the successor of a single successor block + * {@code s}. + * + * @return a {@link PredecessorHolder} that sets the successor of a single successor block + * {@code s} + */ + protected static PredecessorHolder singleSuccessorHolder( + SingleSuccessorBlockImpl s, BlockImpl old) { + return new PredecessorHolder() { + @Override + public void setSuccessor(BlockImpl b) { + s.setSuccessor(b); + old.removePredecessor(s); + } + + @Override + public BlockImpl getBlock() { + return s; + } + }; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseTwo.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseTwo.java new file mode 100644 index 000000000000..a1818b191015 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/CFGTranslationPhaseTwo.java @@ -0,0 +1,236 @@ +package org.checkerframework.dataflow.cfg.builder; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.block.BlockImpl; +import org.checkerframework.dataflow.cfg.block.ConditionalBlockImpl; +import org.checkerframework.dataflow.cfg.block.ExceptionBlockImpl; +import org.checkerframework.dataflow.cfg.block.RegularBlockImpl; +import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlockImpl; +import org.checkerframework.dataflow.cfg.block.SpecialBlock.SpecialBlockType; +import org.checkerframework.dataflow.cfg.block.SpecialBlockImpl; +import org.checkerframework.dataflow.cfg.node.CatchMarkerNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.javacutil.BugInCF; +import org.plumelib.util.ArraySet; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.lang.model.type.TypeMirror; + +/** Class that performs phase two of the translation process. */ +@SuppressWarnings("nullness") // TODO +public class CFGTranslationPhaseTwo { + + private CFGTranslationPhaseTwo() {} + + /** + * Perform phase two of the translation. + * + * @param in the result of phase one + * @return a control flow graph that might still contain degenerate basic blocks (such as 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; + List nodeList = in.nodeList; + // A leader is an extended node which will give rise to a basic block in phase two. + Set leaders = in.leaders; + + assert !in.nodeList.isEmpty(); + + // exit blocks + SpecialBlockImpl regularExitBlock = new SpecialBlockImpl(SpecialBlockType.EXIT); + SpecialBlockImpl exceptionalExitBlock = + new SpecialBlockImpl(SpecialBlockType.EXCEPTIONAL_EXIT); + + // record missing edges that will be added later + Set missingEdges = new ArraySet<>(1); + + // missing exceptional edges + Set missingExceptionalEdges = new LinkedHashSet<>(); + + // create start block + SpecialBlockImpl startBlock = new SpecialBlockImpl(SpecialBlockType.ENTRY); + missingEdges.add(new MissingEdge(startBlock, 0)); + + // Loop through all 'leaders' (while dynamically detecting the leaders). + @NonNull RegularBlockImpl block = new RegularBlockImpl(); // block being processed/built + int i = 0; + for (ExtendedNode node : nodeList) { + switch (node.getType()) { + case NODE: + if (leaders.contains(i)) { + RegularBlockImpl b = new RegularBlockImpl(); + block.setSuccessor(b); + block = b; + } + block.addNode(node.getNode()); + node.setBlock(block); + + // does this node end the execution (modeled as an edge to + // the exceptional exit block) + boolean terminatesExecution = node.getTerminatesExecution(); + if (terminatesExecution) { + block.setSuccessor(exceptionalExitBlock); + block = new RegularBlockImpl(); + } + break; + case CONDITIONAL_JUMP: + { + ConditionalJump cj = (ConditionalJump) node; + // Exception nodes may fall through to conditional jumps, so we set the + // block which is required for the insertion of missing edges. + node.setBlock(block); + assert block != null; + ConditionalBlockImpl cb = new ConditionalBlockImpl(); + if (cj.getTrueFlowRule() != null) { + cb.setThenFlowRule(cj.getTrueFlowRule()); + } + if (cj.getFalseFlowRule() != null) { + cb.setElseFlowRule(cj.getFalseFlowRule()); + } + block.setSuccessor(cb); + block = new RegularBlockImpl(); + + // use two anonymous SingleSuccessorBlockImpl that set the + // 'then' and 'else' successor of the conditional block + Label thenLabel = cj.getThenLabel(); + Label elseLabel = cj.getElseLabel(); + Integer target = bindings.get(thenLabel); + assert target != null; + missingEdges.add( + new MissingEdge( + new RegularBlockImpl() { + @Override + public void setSuccessor(BlockImpl successor) { + cb.setThenSuccessor(successor); + } + }, + target)); + target = bindings.get(elseLabel); + if (target == null) { + throw new BugInCF( + String.format( + "in conditional jump %s, no binding for elseLabel %s: %s", + cj, elseLabel, bindings)); + } + missingEdges.add( + new MissingEdge( + new RegularBlockImpl() { + @Override + public void setSuccessor(BlockImpl successor) { + cb.setElseSuccessor(successor); + } + }, + target)); + break; + } + case UNCONDITIONAL_JUMP: + UnconditionalJump uj = (UnconditionalJump) node; + if (leaders.contains(i)) { + RegularBlockImpl b = new RegularBlockImpl(); + block.setSuccessor(b); + block = b; + } + node.setBlock(block); + if (node.getLabel() == in.regularExitLabel) { + block.setSuccessor(regularExitBlock); + block.setFlowRule(uj.getFlowRule()); + } else if (node.getLabel() == in.exceptionalExitLabel) { + block.setSuccessor(exceptionalExitBlock); + block.setFlowRule(uj.getFlowRule()); + } else { + int target = bindings.get(node.getLabel()); + missingEdges.add(new MissingEdge(block, target, uj.getFlowRule())); + } + block = new RegularBlockImpl(); + break; + case EXCEPTION_NODE: + NodeWithExceptionsHolder en = (NodeWithExceptionsHolder) node; + // create new exception block and link with previous block + ExceptionBlockImpl e = new ExceptionBlockImpl(); + Node nn = en.getNode(); + e.setNode(nn); + node.setBlock(e); + block.setSuccessor(e); + block = new RegularBlockImpl(); + + // Ensure linking between e and next block (normal edge). + // Note: do not link to the next block for throw statements (these throw + // exceptions for sure). + if (!node.getTerminatesExecution()) { + missingEdges.add(new MissingEdge(e, i + 1)); + } + + // exceptional edges + for (Map.Entry> entry : en.getExceptions().entrySet()) { + TypeMirror cause = entry.getKey(); + for (Label label : entry.getValue()) { + Integer target = bindings.get(label); + // TODO: `target` is sometimes null; is this a problem? + // assert target != null; + missingExceptionalEdges.add(new MissingEdge(e, target, cause)); + } + } + break; + } + i++; + } + + // add missing edges + for (MissingEdge p : missingEdges) { + Integer index = p.index; + assert index != null : "CFGBuilder: problem in CFG construction " + p.source; + ExtendedNode extendedNode = nodeList.get(index); + BlockImpl target = extendedNode.getBlock(); + SingleSuccessorBlockImpl source = p.source; + source.setSuccessor(target); + if (p.flowRule != null) { + source.setFlowRule(p.flowRule); + } + } + + // add missing exceptional edges + for (MissingEdge p : missingExceptionalEdges) { + Integer index = p.index; + TypeMirror cause = p.cause; + ExceptionBlockImpl source = (ExceptionBlockImpl) p.source; + if (index == null) { + // edge to exceptional exit + source.addExceptionalSuccessor(exceptionalExitBlock, cause); + } else { + // edge to specific target + ExtendedNode extendedNode = nodeList.get(index); + BlockImpl target = extendedNode.getBlock(); + List targetNodes = target.getNodes(); + Node firstNode = targetNodes.isEmpty() ? null : targetNodes.get(0); + if (firstNode instanceof CatchMarkerNode) { + TypeMirror catchType = ((CatchMarkerNode) firstNode).getCatchType(); + if (in.types.isSubtype(catchType, cause)) { + cause = catchType; + } + } + source.addExceptionalSuccessor(target, cause); + } + } + + return new ControlFlowGraph( + startBlock, + regularExitBlock, + exceptionalExitBlock, + in.underlyingAST, + in.treeToCfgNodes, + in.treeToConvertedCfgNodes, + in.postfixTreeToCfgNodes, + in.returnNodes, + in.declaredClasses, + in.declaredLambdas); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/ConditionalJump.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/ConditionalJump.java new file mode 100644 index 000000000000..f7b152f89a9d --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/ConditionalJump.java @@ -0,0 +1,102 @@ +package org.checkerframework.dataflow.cfg.builder; + +import org.checkerframework.dataflow.analysis.Store.FlowRule; +import org.checkerframework.dataflow.cfg.builder.ExtendedNode.ExtendedNodeType; + +/** + * An extended node of type {@link ExtendedNodeType#CONDITIONAL_JUMP}. + * + *

      Important: In the list of extended nodes, there should not be any labels that point + * to a conditional jump. Furthermore, the node directly ahead of any conditional jump has to be a + * {@link NodeWithExceptionsHolder} or {@link NodeHolder}, and the node held by that extended node + * is required to be of boolean type. + */ +@SuppressWarnings("nullness") // TODO +public class ConditionalJump extends ExtendedNode { + + /** The true successor label. */ + protected final Label trueSucc; + + /** The false successor label. */ + protected final Label falseSucc; + + /** The true branch flow rule. */ + protected FlowRule trueFlowRule; + + /** The false branch flow rule. */ + protected FlowRule falseFlowRule; + + /** + * Construct a ConditionalJump. + * + * @param trueSucc true successor label + * @param falseSucc false successor label + */ + public ConditionalJump(Label trueSucc, Label falseSucc) { + super(ExtendedNodeType.CONDITIONAL_JUMP); + assert trueSucc != null; + this.trueSucc = trueSucc; + assert falseSucc != null; + this.falseSucc = falseSucc; + } + + public Label getThenLabel() { + return trueSucc; + } + + public Label getElseLabel() { + return falseSucc; + } + + /** + * Returns the true branch flow rule. + * + * @return the true branch flow rule + */ + public FlowRule getTrueFlowRule() { + return trueFlowRule; + } + + /** + * Returns the false branch flow rule. + * + * @return the false branch flow rule + */ + public FlowRule getFalseFlowRule() { + return falseFlowRule; + } + + /** + * Sets the true branch flow rule. + * + * @param rule the new true branch flow rule + */ + public void setTrueFlowRule(FlowRule rule) { + trueFlowRule = rule; + } + + /** + * Sets the false branch flow rule. + * + * @param rule the new false branch flow rule + */ + public void setFalseFlowRule(FlowRule rule) { + falseFlowRule = rule; + } + + /** + * Produce a string representation. + * + * @return a string representation + * @see org.checkerframework.dataflow.cfg.builder.PhaseOneResult#nodeToString + */ + @Override + public String toString() { + return "TwoTargetConditionalJump(" + getThenLabel() + ", " + getElseLabel() + ")"; + } + + @Override + public String toStringDebug() { + return toString(); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/ExtendedNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/ExtendedNode.java new file mode 100644 index 000000000000..1c1c40f6e699 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/ExtendedNode.java @@ -0,0 +1,107 @@ +package org.checkerframework.dataflow.cfg.builder; + +import org.checkerframework.dataflow.cfg.block.BlockImpl; +import org.checkerframework.dataflow.cfg.builder.ExtendedNode.ExtendedNodeType; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.javacutil.BugInCF; + +/** + * An extended node can be one of several things (depending on its {@code type}): + * + *

        + *
      • NODE: {@link NodeHolder}. An extended node of this type is just a wrapper for a + * {@link Node} (that cannot throw exceptions). + *
      • EXCEPTION_NODE: {@link 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 UnconditionalJump}. An unconditional jump to a label. + *
      • TWO_TARGET_CONDITIONAL_JUMP: {@link ConditionalJump}. A conditional jump with two + * targets for both the 'then' and 'else' branch. + *
      + * + * Note that this class is deliberately public, to enable users of the dataflow library to customize + * CFG construction. + */ +@SuppressWarnings("nullness") // TODO +public abstract class ExtendedNode { + + /** The basic block this extended node belongs to (as determined in phase two). */ + protected BlockImpl block; + + /** Type of this node. */ + protected final ExtendedNodeType type; + + /** Does this node terminate the execution? (e.g., "System.exit()") */ + protected boolean terminatesExecution = false; + + /** + * Create a new ExtendedNode. + * + * @param type the type of this node + */ + protected ExtendedNode(ExtendedNodeType type) { + this.type = type; + } + + /** Extended node types (description see above). */ + public enum ExtendedNodeType { + NODE, + EXCEPTION_NODE, + UNCONDITIONAL_JUMP, + CONDITIONAL_JUMP + } + + public ExtendedNodeType getType() { + return type; + } + + public boolean getTerminatesExecution() { + return terminatesExecution; + } + + public void setTerminatesExecution(boolean terminatesExecution) { + this.terminatesExecution = terminatesExecution; + } + + /** + * Returns the node contained in this extended node (only applicable if the type is {@code NODE} + * or {@code EXCEPTION_NODE}). + * + * @return the node contained in this extended node (only applicable if the type is {@code NODE} + * or {@code EXCEPTION_NODE}) + */ + public Node getNode() { + throw new BugInCF("Do not call"); + } + + /** + * Returns the label associated with this extended node (only applicable if type is {@link + * ExtendedNodeType#CONDITIONAL_JUMP} or {@link ExtendedNodeType#UNCONDITIONAL_JUMP}). + * + * @return the label associated with this extended node (only applicable if type is {@link + * ExtendedNodeType#CONDITIONAL_JUMP} or {@link ExtendedNodeType#UNCONDITIONAL_JUMP}) + */ + public Label getLabel() { + throw new BugInCF("Do not call"); + } + + public BlockImpl getBlock() { + return block; + } + + public void setBlock(BlockImpl b) { + this.block = b; + } + + @Override + public String toString() { + throw new BugInCF("DO NOT CALL ExtendedNode.toString(). Write your own."); + } + + /** + * Returns a verbose string representation of this, useful for debugging. + * + * @return a string representation of this + */ + public abstract String toStringDebug(); +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/Label.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/Label.java new file mode 100644 index 000000000000..3ce9e3dcda3b --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/Label.java @@ -0,0 +1,39 @@ +package org.checkerframework.dataflow.cfg.builder; + +/** + * A label is used to refer to other extended nodes using a mapping from labels to extended nodes. + * Labels get their names either from labeled statements in the source code or from internally + * generated unique names. + * + *

      Note that this class is deliberately public, to enable users of the dataflow library to + * customize CFG construction. + */ +public class Label { + + /** Unique id counter that incremented in {@code #uniqueName}. */ + private static int uid = 0; + + protected final String name; + + public Label(String name) { + this.name = name; + } + + public Label() { + this.name = uniqueName(); + } + + @Override + public String toString() { + return name; + } + + /** + * Return a new unique label name that cannot be confused with a Java source code label. + * + * @return a new unique label name + */ + private static String uniqueName() { + return "%L" + uid++; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/LabelCell.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/LabelCell.java new file mode 100644 index 000000000000..a948fcac7749 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/LabelCell.java @@ -0,0 +1,48 @@ +package org.checkerframework.dataflow.cfg.builder; + +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.javacutil.BugInCF; + +/** Storage cell for a single Label, with tracking whether it was accessed. */ +/*package-private*/ class LabelCell { + /** The label. If it is null, then it will be lazily set if {@link #accessLabel} is called. */ + private @MonotonicNonNull Label label; + + /** True if the label has been accessed. */ + private boolean accessed; + + /** Create a LabelCell with no label; the label will be lazily created if needed. */ + protected LabelCell() { + this.accessed = false; + } + + /** + * Create a LabelCell with the given label. + * + * @param label the label + */ + protected LabelCell(Label label) { + assert label != null; + this.label = label; + this.accessed = false; + } + + public Label accessLabel() { + if (label == null) { + label = new Label(); + } + accessed = true; + return label; + } + + public Label peekLabel() { + if (label == null) { + throw new BugInCF("called peekLabel prematurely"); + } + return label; + } + + public boolean wasAccessed() { + return accessed; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/MissingEdge.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/MissingEdge.java new file mode 100644 index 000000000000..a49169ca59fb --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/MissingEdge.java @@ -0,0 +1,84 @@ +package org.checkerframework.dataflow.cfg.builder; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.Store.FlowRule; +import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlockImpl; + +import javax.lang.model.type.TypeMirror; + +/* --------------------------------------------------------- */ +/* Phase Two */ +/* --------------------------------------------------------- */ + +/** Represents a missing edge that will be added later. */ +/*package-private*/ class MissingEdge { + /** The source of the edge. */ + /*package-private*/ final SingleSuccessorBlockImpl source; + + /** The index (target?) of the edge. Null means go to exceptional exit. */ + /*package-private*/ final @Nullable Integer index; + + /** The cause exception type, for an exceptional edge; otherwise null. */ + /*package-private*/ final @Nullable TypeMirror cause; + + /** The flow rule for this edge. */ + /*package-private*/ final @Nullable FlowRule flowRule; + + /** + * Create a new MissingEdge. + * + * @param source the source of the edge + * @param index the index (target?) of the edge + */ + public MissingEdge(SingleSuccessorBlockImpl source, int index) { + this(source, index, null, FlowRule.EACH_TO_EACH); + } + + /** + * Create a new MissingEdge. + * + * @param source the source of the edge + * @param index the index (target?) of the edge + * @param flowRule the flow rule for this edge + */ + public MissingEdge(SingleSuccessorBlockImpl source, int index, FlowRule flowRule) { + this(source, index, null, flowRule); + } + + /** + * Create a new MissingEdge. + * + * @param source the source of the edge + * @param index the index (target?) of the edge; null means go to exceptional exit + * @param cause the cause exception type, for an exceptional edge; otherwise null + */ + public MissingEdge( + SingleSuccessorBlockImpl source, @Nullable Integer index, @Nullable TypeMirror cause) { + this(source, index, cause, FlowRule.EACH_TO_EACH); + } + + /** + * Create a new MissingEdge. + * + * @param source the source of the edge + * @param index the index (target?) of the edge; null means go to exceptional exit + * @param cause the cause exception type, for an exceptional edge; otherwise null + * @param flowRule the flow rule for this edge + */ + public MissingEdge( + SingleSuccessorBlockImpl source, + @Nullable Integer index, + @Nullable TypeMirror cause, + FlowRule flowRule) { + assert (index != null) || (cause != null); + this.source = source; + this.index = index; + this.cause = cause; + this.flowRule = flowRule; + } + + @Override + public String toString() { + return "MissingEdge(" + source + ", " + index + ", " + cause + ")"; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/NodeHolder.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/NodeHolder.java new file mode 100644 index 000000000000..33dc0e97689d --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/NodeHolder.java @@ -0,0 +1,36 @@ +package org.checkerframework.dataflow.cfg.builder; + +import org.checkerframework.dataflow.cfg.builder.ExtendedNode.ExtendedNodeType; +import org.checkerframework.dataflow.cfg.node.Node; + +/** An extended node of type {@code NODE}. */ +/*package-private*/ class NodeHolder extends ExtendedNode { + + /** The node to hold. */ + protected final Node node; + + /** + * Construct a NodeHolder for the given Node. + * + * @param node the node to hold + */ + public NodeHolder(Node node) { + super(ExtendedNodeType.NODE); + this.node = node; + } + + @Override + public Node getNode() { + return node; + } + + @Override + public String toString() { + return "NodeHolder(" + node + ")"; + } + + @Override + public String toStringDebug() { + return "NodeHolder(" + node.toStringDebug() + ")"; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/NodeWithExceptionsHolder.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/NodeWithExceptionsHolder.java new file mode 100644 index 000000000000..c499126d428d --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/NodeWithExceptionsHolder.java @@ -0,0 +1,70 @@ +package org.checkerframework.dataflow.cfg.builder; + +import org.checkerframework.dataflow.cfg.builder.ExtendedNode.ExtendedNodeType; +import org.checkerframework.dataflow.cfg.node.Node; + +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; + +import javax.lang.model.type.TypeMirror; + +/** An extended node of type {@code EXCEPTION_NODE}. */ +/*package-private*/ class NodeWithExceptionsHolder extends ExtendedNode { + + /** The node to hold. */ + protected final Node node; + + /** + * Map from exception type to labels of successors that may be reached as a result of that + * exception. + * + *

      This map's keys are the exception types that a Java expression or statement is declared to + * throw -- say, in the {@code throws} clause of the declaration of a method being called. The + * expression might be within a {@code try} statement with {@code catch} blocks that are + * different (either finer-grained or coarser). + */ + protected final Map> exceptions; + + /** + * Construct a NodeWithExceptionsHolder for the given node and exceptions. + * + * @param node the node to hold + * @param exceptions the exceptions to hold + */ + public NodeWithExceptionsHolder(Node node, Map> exceptions) { + super(ExtendedNodeType.EXCEPTION_NODE); + this.node = node; + this.exceptions = exceptions; + } + + /** + * Get the exceptions for the node. + * + * @return exceptions for the node + */ + public Map> getExceptions() { + return exceptions; + } + + @Override + public Node getNode() { + return node; + } + + @Override + public String toString() { + return "NodeWithExceptionsHolder(" + node + ")"; + } + + @Override + public String toStringDebug() { + StringJoiner sj = new StringJoiner(String.format("%n ")); + sj.add("NodeWithExceptionsHolder(" + node.toStringDebug() + ") {"); + for (Map.Entry> entry : exceptions.entrySet()) { + sj.add(entry.getKey() + " => " + entry.getValue()); + } + sj.add("}"); + return sj.toString(); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/PhaseOneResult.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/PhaseOneResult.java new file mode 100644 index 000000000000..e9a4537ac6d7 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/PhaseOneResult.java @@ -0,0 +1,208 @@ +package org.checkerframework.dataflow.cfg.builder; + +import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.UnaryTree; + +import org.checkerframework.dataflow.cfg.UnderlyingAST; +import org.checkerframework.dataflow.cfg.builder.ExtendedNode.ExtendedNodeType; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.node.ReturnNode; + +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; + +import javax.lang.model.util.Types; + +/** A wrapper object to pass around the result of phase one. */ +public class PhaseOneResult { + + /** AST for which the CFG is to be built. */ + /*package-private*/ final UnderlyingAST underlyingAST; + + /** + * Maps from AST {@link Tree}s to sets of {@link Node}s. Every Tree that produces a value will + * have at least one corresponding Node. Trees that undergo conversions, such as boxing or + * unboxing, can map to two distinct Nodes. The Node for the pre-conversion value is stored in + * the treeToCfgNodes, while the Node for the post-conversion value is stored in the + * treeToConvertedCfgNodes. + */ + /*package-private*/ final IdentityHashMap> treeToCfgNodes; + + /** Map from AST {@link Tree}s to post-conversion sets of {@link Node}s. */ + /*package-private*/ final IdentityHashMap> treeToConvertedCfgNodes; + + /** + * Map from postfix increment or decrement trees that are AST {@link UnaryTree}s to the + * synthetic tree that is {@code v + 1} or {@code v - 1}. + */ + /*package-private*/ final IdentityHashMap postfixTreeToCfgNodes; + + /** The list of extended nodes. */ + /*package-private*/ final List nodeList; + + /** The bindings of labels to positions (i.e., indices) in the {@code nodeList}. */ + /*package-private*/ final Map bindings; + + /** The set of leaders (represented as indices into {@code nodeList}). */ + /*package-private*/ final Set leaders; + + /** + * All return nodes (if any) encountered. Only includes return statements that actually return + * something. + */ + /*package-private*/ final List returnNodes; + + /** Special label to identify the regular exit. */ + /*package-private*/ final Label regularExitLabel; + + /** Special label to identify the exceptional exit. */ + /*package-private*/ final Label exceptionalExitLabel; + + /** + * Class declarations that have been encountered when building the control-flow graph for a + * method. + */ + /*package-private*/ final List declaredClasses; + + /** + * Lambdas encountered when building the control-flow graph for a method, variable initializer, + * or initializer. + */ + /*package-private*/ final List declaredLambdas; + + /** The javac type utilities. */ + /*package-private*/ final Types types; + + /** + * Create a PhaseOneResult with the given data. + * + * @param underlyingAST the underlying AST + * @param treeToCfgNodes the tree to nodes mapping + * @param treeToConvertedCfgNodes the tree to converted nodes mapping + * @param postfixTreeToCfgNodes the postfix tree to nodes mapping + * @param nodeList the list of nodes + * @param bindings the label bindings + * @param leaders the leaders + * @param returnNodes the return nodes + * @param regularExitLabel the regular exit labels + * @param exceptionalExitLabel the exceptional exit labels + * @param declaredClasses the declared classes + * @param declaredLambdas the declared lambdas + * @param types the javac type utilities + */ + public PhaseOneResult( + UnderlyingAST underlyingAST, + IdentityHashMap> treeToCfgNodes, + IdentityHashMap> treeToConvertedCfgNodes, + IdentityHashMap postfixTreeToCfgNodes, + List nodeList, + Map bindings, + Set leaders, + List returnNodes, + Label regularExitLabel, + Label exceptionalExitLabel, + List declaredClasses, + List declaredLambdas, + Types types) { + this.underlyingAST = underlyingAST; + this.treeToCfgNodes = treeToCfgNodes; + this.treeToConvertedCfgNodes = treeToConvertedCfgNodes; + this.postfixTreeToCfgNodes = postfixTreeToCfgNodes; + this.nodeList = nodeList; + this.bindings = bindings; + this.leaders = leaders; + this.returnNodes = returnNodes; + this.regularExitLabel = regularExitLabel; + this.exceptionalExitLabel = exceptionalExitLabel; + this.declaredClasses = declaredClasses; + this.declaredLambdas = declaredLambdas; + this.types = types; + } + + @Override + public String toString() { + StringJoiner sj = new StringJoiner(System.lineSeparator()); + for (ExtendedNode n : nodeList) { + sj.add(nodeToString(n)); + } + return sj.toString(); + } + + protected String nodeToString(ExtendedNode n) { + if (n.getType() == ExtendedNodeType.CONDITIONAL_JUMP) { + ConditionalJump t = (ConditionalJump) n; + return "TwoTargetConditionalJump(" + + resolveLabel(t.getThenLabel()) + + ", " + + resolveLabel(t.getElseLabel()) + + ")"; + } else if (n.getType() == ExtendedNodeType.UNCONDITIONAL_JUMP) { + return "UnconditionalJump(" + resolveLabel(n.getLabel()) + ")"; + } else { + return n.toString(); + } + } + + private String resolveLabel(Label label) { + Integer index = bindings.get(label); + if (index == null) { + return "unbound label: " + label; + } + return nodeToString(nodeList.get(index)); + } + + /** + * Returns a representation of a map, one entry per line. + * + * @param the key type of the map + * @param the value type of the map + * @param map a map + * @return a representation of a map, one entry per line + */ + private String mapToString(Map map) { + if (map.isEmpty()) { + return "{}"; + } + StringJoiner result = + new StringJoiner( + String.format("%n "), + String.format("{%n "), + String.format("%n }")); + for (Map.Entry entry : map.entrySet()) { + result.add(entry.getKey() + " => " + entry.getValue()); + } + return result.toString(); + } + + /** + * Returns a verbose string representation of this, useful for debugging. + * + * @return a string representation of this + */ + public String toStringDebug() { + StringJoiner result = + new StringJoiner( + String.format("%n "), + String.format("PhaseOneResult{%n "), + String.format("%n }")); + result.add("treeToCfgNodes=" + mapToString(treeToCfgNodes)); + result.add("treeToConvertedCfgNodes=" + mapToString(treeToConvertedCfgNodes)); + result.add("postfixTreeToCfgNodes=" + mapToString(postfixTreeToCfgNodes)); + result.add("underlyingAST=" + underlyingAST); + result.add("bindings=" + bindings); + result.add("nodeList=" + CFGBuilder.extendedNodeCollectionToStringDebug(nodeList)); + result.add("leaders=" + leaders); + result.add("returnNodes=" + Node.nodeCollectionToString(returnNodes)); + result.add("regularExitLabel=" + regularExitLabel); + result.add("exceptionalExitLabel=" + exceptionalExitLabel); + result.add("declaredClasses=" + declaredClasses); + result.add("declaredLambdas=" + declaredLambdas); + return result.toString(); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TreeInfo.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TreeInfo.java new file mode 100644 index 000000000000..e4a0df096909 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TreeInfo.java @@ -0,0 +1,34 @@ +package org.checkerframework.dataflow.cfg.builder; + +import javax.lang.model.type.TypeMirror; + +/** A tuple with 4 named elements. */ +/*package-private*/ interface TreeInfo { + /** + * Returns true if this is boxed. + * + * @return true if this is boxed + */ + boolean isBoxed(); + + /** + * Returns true if this is numeric. + * + * @return true if this is numeric + */ + boolean isNumeric(); + + /** + * Returns true if this is boolean. + * + * @return true if this is boolean + */ + boolean isBoolean(); + + /** + * Returns the unboxed type that this wraps. + * + * @return the unboxed type that this wraps + */ + TypeMirror unboxedType(); +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryCatchFrame.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryCatchFrame.java new file mode 100644 index 000000000000..d390653a7892 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/builder/TryCatchFrame.java @@ -0,0 +1,121 @@ +package org.checkerframework.dataflow.cfg.builder; + +import org.plumelib.util.IPair; + +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; + +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.type.UnionType; +import javax.lang.model.util.Types; + +/** + * A TryCatchFrame contains an ordered list of catch labels that apply to exceptions with specific + * types. + */ +/*package-private*/ class TryCatchFrame implements TryFrame { + /** The Types utilities. */ + protected final Types types; + + /** An ordered list of pairs because catch blocks are ordered. */ + protected final List> catchLabels; + + /** + * Construct a TryCatchFrame. + * + * @param types the Types utilities + * @param catchLabels the catch labels + */ + public TryCatchFrame(Types types, List> catchLabels) { + this.types = types; + this.catchLabels = catchLabels; + } + + @Override + public String toString() { + if (this.catchLabels.isEmpty()) { + return "TryCatchFrame: no catch labels."; + } else { + StringJoiner sb = new StringJoiner(System.lineSeparator(), "TryCatchFrame: ", ""); + for (IPair ptml : this.catchLabels) { + sb.add(ptml.first.toString() + " -> " + ptml.second.toString()); + } + return sb.toString(); + } + } + + /** + * Given a type of thrown exception, add the set of possible control flow successor {@link + * Label}s to the argument set. Return true if the exception is known to be caught by one of + * those labels and false if it may propagate still further. + */ + @Override + public boolean possibleLabels(TypeMirror thrown, Set

      This is useful to implement a visitor that performs the same operation (e.g., nothing) for @@ -158,12 +158,6 @@ public R visitBitwiseXor(BitwiseXorNode n, P p) { return visitNode(n, p); } - // Compound assignments - @Override - public R visitStringConcatenateAssignment(StringConcatenateAssignmentNode n, P p) { - return visitNode(n, p); - } - // Comparison operations @Override public R visitLessThan(LessThanNode n, P p) { @@ -216,6 +210,11 @@ public R visitTernaryExpression(TernaryExpressionNode n, P p) { return visitNode(n, p); } + @Override + public R visitSwitchExpressionNode(SwitchExpressionNode n, P p) { + return visitNode(n, p); + } + @Override public R visitAssignment(AssignmentNode n, P p) { return visitNode(n, p); @@ -246,18 +245,18 @@ public R visitArrayAccess(ArrayAccessNode n, P p) { return visitNode(n, p); } - public R visitThisLiteral(ThisLiteralNode n, P p) { + public R visitThis(ThisNode n, P p) { return visitNode(n, p); } @Override - public R visitImplicitThisLiteral(ImplicitThisLiteralNode n, P p) { - return visitThisLiteral(n, p); + public R visitImplicitThis(ImplicitThisNode n, P p) { + return visitThis(n, p); } @Override - public R visitExplicitThisLiteral(ExplicitThisLiteralNode n, P p) { - return visitThisLiteral(n, p); + public R visitExplicitThis(ExplicitThisNode n, P p) { + return visitThis(n, p); } @Override @@ -281,12 +280,12 @@ public R visitStringConversion(StringConversionNode n, P p) { } @Override - public R visitNarrowingConversion(NarrowingConversionNode n, P p) { + public R visitWideningConversion(WideningConversionNode n, P p) { return visitNode(n, p); } @Override - public R visitWideningConversion(WideningConversionNode n, P p) { + public R visitNarrowingConversion(NarrowingConversionNode n, P p) { return visitNode(n, p); } @@ -380,4 +379,14 @@ public R visitParameterizedType(ParameterizedTypeNode n, P p) { public R visitMarker(MarkerNode n, P p) { return visitNode(n, p); } + + @Override + public R visitExpressionStatement(ExpressionStatementNode n, P p) { + return visitNode(n, p); + } + + @Override + public R visitDeconstructorPattern(DeconstructorPatternNode n, P p) { + return visitNode(n, p); + } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ArrayAccessNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ArrayAccessNode.java index 4a28f8cbaade..681fd6c723fd 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ArrayAccessNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ArrayAccessNode.java @@ -1,13 +1,17 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.ArrayAccessTree; +import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.Tree; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Objects; + import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; import org.checkerframework.javacutil.TreeUtils; +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; + /** * A node for an array access: * @@ -19,18 +23,65 @@ */ public class ArrayAccessNode extends Node { - protected final Tree tree; + /** The corresponding ArrayAccessTree. */ + protected final ArrayAccessTree tree; + + /** The array expression being accessed. */ protected final Node array; + + /** The index expresssion used to access the array. */ protected final Node index; - public ArrayAccessNode(Tree t, Node array, Node index) { + /** + * If this ArrayAccessNode is a node for an array desugared from an enhanced for loop, then the + * {@code arrayExpression} field is the expression in the for loop, e.g., {@code arr} in {@code + * for(Object o: arr}. + * + *

      Is set by {@link #setArrayExpression}. + */ + protected @Nullable ExpressionTree arrayExpression; + + /** + * Create an ArrayAccessNode. + * + * @param t tree for the array access + * @param array the node for the array expression being accessed + * @param index the node for the index used to access the array + */ + public ArrayAccessNode(ArrayAccessTree t, Node array, Node index) { super(TreeUtils.typeOf(t)); - assert t instanceof ArrayAccessTree; this.tree = t; this.array = array; this.index = index; } + /** + * If this ArrayAccessNode is a node for an array desugared from an enhanced for loop, then + * return the expression in the for loop, e.g., {@code arr} in {@code for(Object o: arr}. + * Otherwise, return null. + * + * @return the array expression, or null if this is not an array desugared from an enhanced for + * loop + */ + public @Nullable ExpressionTree getArrayExpression() { + return arrayExpression; + } + + /** + * Set the array expression from a for loop. + * + * @param arrayExpression array expression + * @see #getArrayExpression() + */ + public void setArrayExpression(@Nullable ExpressionTree arrayExpression) { + this.arrayExpression = arrayExpression; + } + + /** + * Get the node that represents the array expression being accessed. + * + * @return the array expression node + */ public Node getArray() { return array; } @@ -40,7 +91,7 @@ public Node getIndex() { } @Override - public Tree getTree() { + public ArrayAccessTree getTree() { return tree; } @@ -70,10 +121,8 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { - ArrayList list = new ArrayList<>(2); - list.add(getArray()); - list.add(getIndex()); - return list; + return Arrays.asList(getArray(), getIndex()); } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ArrayCreationNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ArrayCreationNode.java index 7b1802b53de0..9e4d33e905bc 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ArrayCreationNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ArrayCreationNode.java @@ -2,19 +2,24 @@ import com.sun.source.tree.NewArrayTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.plumelib.util.StringsPlume; + import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; + import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for new array creation. * *

      - *   new type [1][2]
      - *   new type [] = { expr1, expr2, ... }
      + *   new type[1][2]
      + *   new type[] = { expr1, expr2, ... }
        * 
      */ public class ArrayCreationNode extends Node { @@ -24,7 +29,8 @@ public class ArrayCreationNode extends Node { /** * The length of this list is the number of dimensions in the array. Each element is the size of - * the given dimension. + * the given dimension. It can be empty if initializers is non-empty, as in {@code new + * SomeType[] = { expr1, expr2, ... }}. */ protected final List dimensions; @@ -72,27 +78,13 @@ public String toString() { StringBuilder sb = new StringBuilder(); sb.append("new " + type); if (!dimensions.isEmpty()) { - boolean needComma = false; sb.append(" ("); - for (Node dim : dimensions) { - if (needComma) { - sb.append(", "); - } - sb.append(dim); - needComma = true; - } + sb.append(StringsPlume.join(", ", dimensions)); sb.append(")"); } if (!initializers.isEmpty()) { - boolean needComma = false; sb.append(" = {"); - for (Node init : initializers) { - if (needComma) { - sb.append(", "); - } - sb.append(init); - needComma = true; - } + sb.append(StringsPlume.join(", ", initializers)); sb.append("}"); } return sb.toString(); @@ -115,6 +107,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { ArrayList list = new ArrayList<>(dimensions.size() + initializers.size()); list.addAll(dimensions); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ArrayTypeNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ArrayTypeNode.java index b7a7f17fcaf0..088c84fbbaa6 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ArrayTypeNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ArrayTypeNode.java @@ -2,15 +2,19 @@ import com.sun.source.tree.ArrayTypeTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Collection; import java.util.Collections; import java.util.Objects; + import javax.lang.model.util.Types; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; /** - * A node representing a array type used in an expression such as a field access. + * A node representing an array type used in an expression such as a field access. * *

      type .class */ @@ -57,6 +61,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssertionErrorNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssertionErrorNode.java index ba9b194b23a5..de2af1c76e49 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssertionErrorNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssertionErrorNode.java @@ -1,15 +1,21 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; -import java.util.ArrayList; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.dataflow.qual.SideEffectFree; + +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Objects; + import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.Nullable; /** - * A node for the {@link AssertionError} when an assertion fails. + * A node for the {@link AssertionError} when an assertion fails or when a method call marked {@link + * org.checkerframework.dataflow.qual.AssertMethod} fails. * *

        *   assert condition : detail ;
      @@ -17,25 +23,49 @@
        */
       public class AssertionErrorNode extends Node {
       
      +    /** Tree for the assert statement or assert method. */
           protected final Tree tree;
      +
      +    /** The condition that if it is false, the assertion exception is thrown. */
           protected final Node condition;
      -    protected final Node detail;
       
      -    public AssertionErrorNode(Tree tree, Node condition, Node detail, TypeMirror type) {
      +    /** The node for the expression after {@code :} in the assert statement, or null. */
      +    protected final @Nullable Node detail;
      +
      +    /**
      +     * Creates an AssertionErrorNode.
      +     *
      +     * @param tree tree for the assert statement or assert method
      +     * @param condition the node of the condition when if false the assertion exception is thrown
      +     * @param detail node for the expression after {@code :} in the assert statement, or null
      +     * @param type the type of the exception thrown
      +     */
      +    public AssertionErrorNode(Tree tree, Node condition, @Nullable Node detail, TypeMirror type) {
               // TODO: Find out the correct "type" for statements.
               // Is it TypeKind.NONE?
               super(type);
      -        assert tree.getKind() == Kind.ASSERT;
               this.tree = tree;
               this.condition = condition;
               this.detail = detail;
           }
       
      +    /**
      +     * The node of the condition that if it is false, the assertion exception is thrown.
      +     *
      +     * @return the node of the condition that if it is false, the assertion exception is thrown
      +     */
      +    @Pure
           public Node getCondition() {
               return condition;
           }
       
      -    public Node getDetail() {
      +    /**
      +     * The node for the expression after {@code :} in the assert statement, or null.
      +     *
      +     * @return node for the expression after {@code :} in the assert statement, or null
      +     */
      +    @Pure
      +    public @Nullable Node getDetail() {
               return detail;
           }
       
      @@ -70,10 +100,11 @@ public int hashCode() {
           }
       
           @Override
      +    @SideEffectFree
           public Collection getOperands() {
      -        ArrayList list = new ArrayList<>(2);
      -        list.add(getCondition());
      -        list.add(getDetail());
      -        return list;
      +        if (getDetail() == null) {
      +            return Collections.singleton(getCondition());
      +        }
      +        return Arrays.asList(getCondition(), getDetail());
           }
       }
      diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentContext.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentContext.java
      deleted file mode 100644
      index 254782705219..000000000000
      --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentContext.java
      +++ /dev/null
      @@ -1,126 +0,0 @@
      -package org.checkerframework.dataflow.cfg.node;
      -
      -import com.sun.source.tree.ExpressionTree;
      -import com.sun.source.tree.MethodTree;
      -import com.sun.source.tree.Tree;
      -import com.sun.source.tree.VariableTree;
      -import javax.lang.model.element.Element;
      -import javax.lang.model.element.ExecutableElement;
      -import org.checkerframework.checker.nullness.qual.Nullable;
      -import org.checkerframework.javacutil.TreeUtils;
      -
      -/**
      - * An assignment context for a node, which represents the place to which the node with this context
      - * is 'assigned' to. An 'assignment' (as we use the term here) can occur for Java assignments,
      - * method calls (for all the actual parameters which get assigned to their formal parameters) or
      - * method return statements.
      - *
      - * 

      The main use of {@link AssignmentContext} is to be able to get the declared type of the - * left-hand side of the assignment for proper type-refinement. - */ -public abstract class AssignmentContext { - - /** An assignment context for an assignment 'lhs = rhs'. */ - public static class AssignmentLhsContext extends AssignmentContext { - - protected final Node node; - - public AssignmentLhsContext(Node node) { - this.node = node; - } - - @Override - public @Nullable Element getElementForType() { - Tree tree = node.getTree(); - if (tree == null) { - return null; - } else if (tree instanceof ExpressionTree) { - return TreeUtils.elementFromUse((ExpressionTree) tree); - } else if (tree instanceof VariableTree) { - return TreeUtils.elementFromDeclaration((VariableTree) tree); - } else { - assert false : "unexpected tree"; - return null; - } - } - - @Override - public @Nullable Tree getContextTree() { - return node.getTree(); - } - } - - /** An assignment context for a method parameter. */ - public static class MethodParameterContext extends AssignmentContext { - - protected final ExecutableElement method; - protected final int paramNum; - - public MethodParameterContext(ExecutableElement method, int paramNum) { - this.method = method; - this.paramNum = paramNum; - } - - @Override - public Element getElementForType() { - return method.getParameters().get(paramNum); - } - - @Override - public @Nullable Tree getContextTree() { - // TODO: what is the right assignment context? We might not have - // a tree for the invoked method. - return null; - } - } - - /** An assignment context for method return statements. */ - public static class MethodReturnContext extends AssignmentContext { - - protected final ExecutableElement method; - protected final Tree ret; - - public MethodReturnContext(MethodTree method) { - this.method = TreeUtils.elementFromDeclaration(method); - this.ret = method.getReturnType(); - } - - @Override - public Element getElementForType() { - return method; - } - - @Override - public Tree getContextTree() { - return ret; - } - } - - /** An assignment context for lambda return statements. */ - public static class LambdaReturnContext extends AssignmentContext { - - protected final ExecutableElement method; - - public LambdaReturnContext(ExecutableElement method) { - this.method = method; - } - - @Override - public Element getElementForType() { - return method; - } - - @Override - public @Nullable Tree getContextTree() { - // TODO: what is the right assignment context? We might not have - // a tree for the invoked method. - return null; - } - } - - /** Returns an {@link Element} that has the type of this assignment context. */ - public abstract @Nullable Element getElementForType(); - - /** Returns the context tree. */ - public abstract @Nullable Tree getContextTree(); -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java index 02d73ea48259..6cd525436df0 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/AssignmentNode.java @@ -5,31 +5,70 @@ import com.sun.source.tree.Tree; import com.sun.source.tree.UnaryTree; import com.sun.source.tree.VariableTree; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Objects; + import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.cfg.node.AssignmentContext.AssignmentLhsContext; +import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.dataflow.qual.SideEffectFree; import org.checkerframework.javacutil.TreeUtils; +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; + /** * A node for an assignment: * *

        *   variable = expression
      + *   variable += expression
        *   expression . field = expression
        *   expression [ index ] = expression
        * 
      * * We allow assignments without corresponding AST {@link Tree}s. + * + *

      Some desugarings create additional assignments to synthetic local variables. Such assignment + * nodes are marked as synthetic to allow special handling in transfer functions. + * + *

      String concatenation compound assignments are desugared to an assignment and a string + * concatenation. + * + *

      Numeric compound assignments are desugared to an assignment and a numeric operation. */ public class AssignmentNode extends Node { + /** The underlying assignment tree. */ protected final Tree tree; + + /** The node for the LHS of the assignment tree. */ protected final Node lhs; + + /** The node for the RHS of the assignment tree. */ protected final Node rhs; + /** Whether the assignment node is synthetic */ + protected final boolean synthetic; + + /** + * Create a (non-synthetic) AssignmentNode. + * + * @param tree the {@code AssignmentTree} corresponding to the {@code AssignmentNode} + * @param target the lhs of {@code tree} + * @param expression the rhs of {@code tree} + */ public AssignmentNode(Tree tree, Node target, Node expression) { + this(tree, target, expression, false); + } + + /** + * Create an AssignmentNode. + * + * @param tree the {@code AssignmentTree} corresponding to the {@code AssignmentNode} + * @param target the lhs of {@code tree} + * @param expression the rhs of {@code tree} + * @param synthetic whether the assignment node is synthetic + */ + public AssignmentNode(Tree tree, Node target, Node expression, boolean synthetic) { super(TreeUtils.typeOf(tree)); assert tree instanceof AssignmentTree || tree instanceof VariableTree @@ -41,33 +80,59 @@ public AssignmentNode(Tree tree, Node target, Node expression) { this.tree = tree; this.lhs = target; this.rhs = expression; - rhs.setAssignmentContext(new AssignmentLhsContext(lhs)); + this.synthetic = synthetic; } + /** + * Returns the left-hand-side of the assignment. + * + * @return the left-hand-side of the assignment + */ + @Pure public Node getTarget() { return lhs; } + /** + * Returns the right-hand-side of the assignment. + * + * @return the right-hand-side of the assignment + */ + @Pure public Node getExpression() { return rhs; } @Override + @Pure public Tree getTree() { return tree; } + /** + * Check if the assignment node is synthetic, e.g. the synthetic assignment in a ternary + * expression. + * + * @return true if the assignment node is synthetic + */ + @Pure + public boolean isSynthetic() { + return synthetic; + } + @Override public R accept(NodeVisitor visitor, P p) { return visitor.visitAssignment(this, p); } @Override + @Pure public String toString() { - return getTarget() + " = " + getExpression(); + return getTarget() + " = " + getExpression() + (synthetic ? " (synthetic)" : ""); } @Override + @Pure public boolean equals(@Nullable Object obj) { if (!(obj instanceof AssignmentNode)) { return false; @@ -78,15 +143,14 @@ public boolean equals(@Nullable Object obj) { } @Override + @Pure public int hashCode() { return Objects.hash(getTarget(), getExpression()); } @Override + @SideEffectFree public Collection getOperands() { - ArrayList list = new ArrayList<>(2); - list.add(getTarget()); - list.add(getExpression()); - return list; + return Arrays.asList(getTarget(), getExpression()); } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BinaryOperationNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BinaryOperationNode.java index db045509ca38..d5ad0c2d3a7d 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BinaryOperationNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BinaryOperationNode.java @@ -1,10 +1,13 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import java.util.ArrayList; -import java.util.Collection; + +import org.checkerframework.dataflow.qual.SideEffectFree; import org.checkerframework.javacutil.TreeUtils; +import java.util.Arrays; +import java.util.Collection; + /** * A node for a binary expression. * @@ -41,10 +44,8 @@ public BinaryTree getTree() { } @Override + @SideEffectFree public Collection getOperands() { - ArrayList list = new ArrayList<>(2); - list.add(getLeftOperand()); - list.add(getRightOperand()); - return list; + return Arrays.asList(getLeftOperand(), getRightOperand()); } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseAndNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseAndNode.java index ca00aa2c66b8..35818d894108 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseAndNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseAndNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the bitwise or logical (single bit) and operation: * @@ -14,9 +16,16 @@ */ public class BitwiseAndNode extends BinaryOperationNode { + /** + * Constructs a {@link BitwiseAndNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public BitwiseAndNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.AND; + assert tree.getKind() == Tree.Kind.AND; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseComplementNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseComplementNode.java index 43bbe3776b03..527827f373f6 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseComplementNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseComplementNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; -import com.sun.source.tree.Tree.Kind; +import com.sun.source.tree.Tree; import com.sun.source.tree.UnaryTree; -import java.util.Objects; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the bitwise complement operation: * @@ -14,9 +16,15 @@ */ public class BitwiseComplementNode extends UnaryOperationNode { + /** + * Constructs a {@link BitwiseComplementNode}. + * + * @param tree the tree of the bitwise complement + * @param operand the operand of the bitwise complement + */ public BitwiseComplementNode(UnaryTree tree, Node operand) { super(tree, operand); - assert tree.getKind() == Kind.BITWISE_COMPLEMENT; + assert tree.getKind() == Tree.Kind.BITWISE_COMPLEMENT; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseOrNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseOrNode.java index e117e1323fb1..d85a3522eccc 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseOrNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseOrNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the bitwise or logical (single bit) or operation: * @@ -14,9 +16,16 @@ */ public class BitwiseOrNode extends BinaryOperationNode { + /** + * Constructs a {@link BitwiseOrNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public BitwiseOrNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.OR; + assert tree.getKind() == Tree.Kind.OR; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseXorNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseXorNode.java index 547fc5d8a5a1..847e0aaac697 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseXorNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BitwiseXorNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the bitwise or logical (single bit) xor operation: * @@ -14,9 +16,16 @@ */ public class BitwiseXorNode extends BinaryOperationNode { + /** + * Constructs a {@link BitwiseXorNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public BitwiseXorNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.XOR; + assert tree.getKind() == Tree.Kind.XOR; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BooleanLiteralNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BooleanLiteralNode.java index 0749eeff12df..8aa75272d258 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BooleanLiteralNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/BooleanLiteralNode.java @@ -2,9 +2,12 @@ import com.sun.source.tree.LiteralTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for a boolean literal: @@ -47,6 +50,7 @@ public boolean equals(@Nullable Object obj) { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CaseNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CaseNode.java index 0ee2ac94dacc..540d37684ecb 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CaseNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CaseNode.java @@ -1,13 +1,18 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.CaseTree; -import com.sun.source.tree.Tree.Kind; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.plumelib.util.StringsPlume; + import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Objects; + import javax.lang.model.type.TypeKind; import javax.lang.model.util.Types; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for a case in a switch statement. Although a case has no abstract value, it can imply @@ -21,33 +26,71 @@ public class CaseNode extends Node { /** The tree for this node. */ protected final CaseTree tree; - /** The switch expression. */ - protected final Node switchExpr; - /** The case expression to match the switch expression against. */ - protected final Node caseExpr; + + /** + * The Node for the assignment of the switch selector expression to a synthetic local variable. + */ + protected final AssignmentNode selectorExprAssignment; + + /** + * The case expressions to match the switch expression against: the operands of (possibly + * multiple) case labels. + */ + protected final List caseExprs; + + /** The guard (the expression in the {@code when} clause) for this case. */ + protected final @Nullable Node guard; /** * Create a new CaseNode. * * @param tree the tree for this node - * @param switchExpr the switch expression - * @param caseExpr the case expression to match the switch expression against + * @param selectorExprAssignment the Node for the assignment of the switch selector expression + * to a synthetic local variable + * @param caseExprs the case expression(s) to match the switch expression against + * @param guard the guard expression or null * @param types a factory of utility methods for operating on types */ - public CaseNode(CaseTree tree, Node switchExpr, Node caseExpr, Types types) { + public CaseNode( + CaseTree tree, + AssignmentNode selectorExprAssignment, + List caseExprs, + @Nullable Node guard, + Types types) { super(types.getNoType(TypeKind.NONE)); - assert tree.getKind() == Kind.CASE; this.tree = tree; - this.switchExpr = switchExpr; - this.caseExpr = caseExpr; + this.selectorExprAssignment = selectorExprAssignment; + this.caseExprs = caseExprs; + this.guard = guard; } - public Node getSwitchOperand() { - return switchExpr; + /** + * The Node for the assignment of the switch selector expression to a synthetic local variable. + * This is used to refine the type of the switch selector expression in a case block. + * + * @return the assignment of the switch selector expression to a synthetic local variable + */ + public AssignmentNode getSwitchOperand() { + return selectorExprAssignment; } - public Node getCaseOperand() { - return caseExpr; + /** + * Gets the nodes corresponding to the case expressions. There can be multiple expressions since + * Java 12. + * + * @return the nodes corresponding to the (potentially multiple) case expressions + */ + public List getCaseOperands() { + return caseExprs; + } + + /** + * Gets the node for the guard (the expression in the {@code when} clause). + * + * @return the node for the guard + */ + public @Nullable Node getGuard() { + return guard; } @Override @@ -62,7 +105,7 @@ public R accept(NodeVisitor visitor, P p) { @Override public String toString() { - return "case " + getCaseOperand() + ":"; + return "case " + StringsPlume.join(", ", getCaseOperands()) + ":"; } @Override @@ -72,19 +115,21 @@ public boolean equals(@Nullable Object obj) { } CaseNode other = (CaseNode) obj; return getSwitchOperand().equals(other.getSwitchOperand()) - && getCaseOperand().equals(other.getCaseOperand()); + && getCaseOperands().equals(other.getCaseOperands()); } @Override public int hashCode() { - return Objects.hash(getSwitchOperand(), getCaseOperand()); + return Objects.hash(getSwitchOperand(), getCaseOperands()); } @Override + @SideEffectFree public Collection getOperands() { - ArrayList list = new ArrayList<>(2); - list.add(getSwitchOperand()); - list.add(getCaseOperand()); - return list; + List caseOperands = getCaseOperands(); + ArrayList operands = new ArrayList<>(caseOperands.size() + 1); + operands.add(getSwitchOperand()); + operands.addAll(caseOperands); + return operands; } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CatchMarkerNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CatchMarkerNode.java new file mode 100644 index 000000000000..95d42cbe51d9 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CatchMarkerNode.java @@ -0,0 +1,67 @@ +package org.checkerframework.dataflow.cfg.node; + +import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; + +import java.util.Objects; + +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; + +/** A CatchMarkerNode is a marker node for the beginning or end of a catch block. */ +public class CatchMarkerNode extends MarkerNode { + + /** The type of the exception parameter. */ + private final TypeMirror catchType; + + /** The type utilities. */ + private final Types types; + + /** + * Creates a new CatchMarkerNode. + * + * @param tree the tree + * @param startOrEnd {@code "start"} or {@code "end"} + * @param catchType the type of the exception parameter + * @param types the type utilities + */ + public CatchMarkerNode( + @Nullable Tree tree, String startOrEnd, TypeMirror catchType, Types types) { + super( + tree, + startOrEnd + + " of catch block for " + + TypesUtils.simpleTypeName(catchType) + + " #" + + (tree == null ? "null" : TreeUtils.treeUids.get(tree)), + types); + this.catchType = catchType; + this.types = types; + } + + /** + * Returns the type of the exception parameter. + * + * @return the type of the exception parameter + */ + public TypeMirror getCatchType() { + return catchType; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof CatchMarkerNode)) { + return false; + } + CatchMarkerNode other = (CatchMarkerNode) obj; + return types.isSameType(getCatchType(), other.getCatchType()) && super.equals(other); + } + + @Override + public int hashCode() { + return Objects.hash(tree, getMessage(), catchType); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CharacterLiteralNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CharacterLiteralNode.java index 8b0fa194d083..54d14bdf1aa8 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CharacterLiteralNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/CharacterLiteralNode.java @@ -2,9 +2,12 @@ import com.sun.source.tree.LiteralTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for a character literal. For example: @@ -48,6 +51,7 @@ public boolean equals(@Nullable Object obj) { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ClassDeclarationNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ClassDeclarationNode.java index 4a12044dadcf..4a526b42e1db 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ClassDeclarationNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ClassDeclarationNode.java @@ -1,11 +1,14 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.ClassTree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Collection; import java.util.Collections; import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; /** * A node representing a class declaration that occurs within a method, for example, an anonymous @@ -55,6 +58,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ClassNameNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ClassNameNode.java index 03318bb4b555..b34299862fcf 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ClassNameNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ClassNameNode.java @@ -4,13 +4,19 @@ import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Collection; import java.util.Collections; import java.util.Objects; + import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; /** * A node representing a class name used in an expression such as a static method invocation. @@ -22,7 +28,7 @@ public class ClassNameNode extends Node { /** The tree for this node. */ protected final @Nullable Tree tree; - /** The class named by this node. */ + /** The class named by this node. Either a TypeElement or a TypeParameterElement. */ protected final Element element; /** The parent name, if any. */ @@ -33,7 +39,10 @@ public ClassNameNode(IdentifierTree tree) { assert tree.getKind() == Tree.Kind.IDENTIFIER; this.tree = tree; assert TreeUtils.isUseOfElement(tree) : "@AssumeAssertion(nullness): tree kind"; - this.element = TreeUtils.elementFromUse(tree); + Element element = TreeUtils.elementFromUse(tree); + assert element instanceof TypeElement || element instanceof TypeParameterElement + : "@AssumeAssertion(nullness)"; + this.element = element; this.parent = null; } @@ -53,7 +62,10 @@ public ClassNameNode(MemberSelectTree tree, Node parent) { super(TreeUtils.typeOf(tree)); this.tree = tree; assert TreeUtils.isUseOfElement(tree) : "@AssumeAssertion(nullness): tree kind"; - this.element = TreeUtils.elementFromUse(tree); + Element element = TreeUtils.elementFromUse(tree); + assert element instanceof TypeElement || element instanceof TypeParameterElement + : "@AssumeAssertion(nullness)"; + this.element = element; this.parent = parent; } @@ -61,6 +73,7 @@ public ClassNameNode(TypeMirror type, Element element) { super(type); this.tree = null; this.element = element; + assert element instanceof TypeElement || element instanceof TypeParameterElement; this.parent = null; } @@ -104,6 +117,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { if (parent == null) { return Collections.emptyList(); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ConditionalAndNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ConditionalAndNode.java index 89aeef01e4bb..d87073ca647f 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ConditionalAndNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ConditionalAndNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for a conditional and expression: * @@ -23,7 +25,7 @@ public class ConditionalAndNode extends BinaryOperationNode { */ public ConditionalAndNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.CONDITIONAL_AND; + assert tree.getKind() == Tree.Kind.CONDITIONAL_AND; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ConditionalNotNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ConditionalNotNode.java index d190d8aea338..f69404878133 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ConditionalNotNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ConditionalNotNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; -import com.sun.source.tree.Tree.Kind; +import com.sun.source.tree.Tree; import com.sun.source.tree.UnaryTree; -import java.util.Objects; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for a conditional not expression: * @@ -22,7 +24,7 @@ public class ConditionalNotNode extends UnaryOperationNode { */ public ConditionalNotNode(UnaryTree tree, Node operand) { super(tree, operand); - assert tree.getKind() == Kind.LOGICAL_COMPLEMENT; + assert tree.getKind() == Tree.Kind.LOGICAL_COMPLEMENT; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ConditionalOrNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ConditionalOrNode.java index d97870e2775e..38704da607c5 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ConditionalOrNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ConditionalOrNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for a conditional or expression: * @@ -23,7 +25,7 @@ public class ConditionalOrNode extends BinaryOperationNode { */ public ConditionalOrNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.CONDITIONAL_OR; + assert tree.getKind() == Tree.Kind.CONDITIONAL_OR; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/DeconstructorPatternNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/DeconstructorPatternNode.java new file mode 100644 index 000000000000..33ae66cc71e8 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/DeconstructorPatternNode.java @@ -0,0 +1,98 @@ +package org.checkerframework.dataflow.cfg.node; + +import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.Pure; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import javax.lang.model.type.TypeMirror; + +/** A node for a deconstrutor pattern. */ +public class DeconstructorPatternNode extends Node { + /** + * The {@code DeconstructorPatternTree}, declared as {@link Tree} to permit this file to compile + * under JDK 20 and earlier. + */ + protected final Tree deconstructorPattern; + + /** A list of nested pattern nodes. */ + protected final List nestedPatterns; + + /** + * Creates a {@code DeconstructorPatternNode}. + * + * @param type the type of the node + * @param deconstructorPattern the {@code DeconstructorPatternTree} + * @param nestedPatterns a list of nested pattern nodes + */ + public DeconstructorPatternNode( + TypeMirror type, Tree deconstructorPattern, List nestedPatterns) { + super(type); + this.deconstructorPattern = deconstructorPattern; + this.nestedPatterns = nestedPatterns; + } + + @Override + @Pure + public @Nullable Tree getTree() { + return deconstructorPattern; + } + + /** + * Returns the nested patterns. + * + * @return the nested patterns + */ + @Pure + public List getNestedPatterns() { + return nestedPatterns; + } + + @Override + public R accept(NodeVisitor visitor, P p) { + return visitor.visitDeconstructorPattern(this, p); + } + + @Override + @Pure + public Collection getOperands() { + return nestedPatterns; + } + + /** + * A list of nested binding variables. This is lazily initialized and should only be accessed by + * {@link #getBindingVariables()}. + */ + protected @MonotonicNonNull List bindingVariables = null; + + /** + * Return all the binding variables in this pattern. + * + * @return all the binding variables in this pattern + */ + public List getBindingVariables() { + if (bindingVariables == null) { + if (nestedPatterns.isEmpty()) { + bindingVariables = Collections.emptyList(); + } else { + bindingVariables = new ArrayList<>(nestedPatterns.size()); + for (Node patternNode : nestedPatterns) { + if (patternNode instanceof LocalVariableNode) { + bindingVariables.add((LocalVariableNode) patternNode); + } else { + bindingVariables.addAll( + ((DeconstructorPatternNode) patternNode).getBindingVariables()); + } + } + bindingVariables = Collections.unmodifiableList(bindingVariables); + } + } + return bindingVariables; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/DoubleLiteralNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/DoubleLiteralNode.java index 03f7c7238b62..773aec3888d8 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/DoubleLiteralNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/DoubleLiteralNode.java @@ -2,9 +2,12 @@ import com.sun.source.tree.LiteralTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for a double literal. For example: @@ -47,6 +50,7 @@ public boolean equals(@Nullable Object obj) { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/EqualToNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/EqualToNode.java index 22d524b83610..389da3c4fdd9 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/EqualToNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/EqualToNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for an equality check: * @@ -23,7 +25,7 @@ public class EqualToNode extends BinaryOperationNode { */ public EqualToNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.EQUAL_TO; + assert tree.getKind() == Tree.Kind.EQUAL_TO; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ExplicitThisLiteralNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ExplicitThisLiteralNode.java deleted file mode 100644 index 6beb50d43b61..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ExplicitThisLiteralNode.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.checkerframework.dataflow.cfg.node; - -import com.sun.source.tree.IdentifierTree; -import com.sun.source.tree.Tree; -import org.checkerframework.javacutil.TreeUtils; - -/** - * A node for a reference to 'this'. - * - *

      - *   this
      - * 
      - */ -public class ExplicitThisLiteralNode extends ThisLiteralNode { - - protected final Tree tree; - - public ExplicitThisLiteralNode(Tree t) { - super(TreeUtils.typeOf(t)); - assert t instanceof IdentifierTree && ((IdentifierTree) t).getName().contentEquals("this"); - tree = t; - } - - @Override - public Tree getTree() { - return tree; - } - - @Override - public R accept(NodeVisitor visitor, P p) { - return visitor.visitExplicitThisLiteral(this, p); - } - - @Override - public String toString() { - return getName(); - } -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ExplicitThisNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ExplicitThisNode.java new file mode 100644 index 000000000000..1176d970713f --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ExplicitThisNode.java @@ -0,0 +1,38 @@ +package org.checkerframework.dataflow.cfg.node; + +import com.sun.source.tree.IdentifierTree; + +import org.checkerframework.javacutil.TreeUtils; + +/** + * A node for a reference to 'this'. + * + *
      + *   this
      + * 
      + */ +public class ExplicitThisNode extends ThisNode { + + protected final IdentifierTree tree; + + public ExplicitThisNode(IdentifierTree t) { + super(TreeUtils.typeOf(t)); + assert t.getName().contentEquals("this"); + tree = t; + } + + @Override + public IdentifierTree getTree() { + return tree; + } + + @Override + public R accept(NodeVisitor visitor, P p) { + return visitor.visitExplicitThis(this, p); + } + + @Override + public String toString() { + return "this"; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ExpressionStatementNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ExpressionStatementNode.java new file mode 100644 index 000000000000..60c12afeb706 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ExpressionStatementNode.java @@ -0,0 +1,67 @@ +package org.checkerframework.dataflow.cfg.node; + +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.javacutil.TreeUtils; + +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; + +/** + * A node for an expression that is used as a statement. + * + *
      + *   expression;
      + * 
      + * + * An {@code ExpressionStatementNode} appears in the CFG after the node(s) of the expression. The + * node can be used for special-handling of expression statements or can be seen as a no-op marker + * node. + */ +public class ExpressionStatementNode extends Node { + /** The expression constituting this ExpressionStatementNode. */ + protected final ExpressionTree tree; + + /** + * Construct a ExpressionStatementNode. + * + * @param t the expression constituting this ExpressionStatementNode + */ + public ExpressionStatementNode(ExpressionTree t) { + super(TreeUtils.typeOf(t)); + tree = t; + } + + @Override + public @Nullable Tree getTree() { + return null; + } + + @Override + public Collection getOperands() { + return Collections.emptyList(); + } + + @Override + public R accept(NodeVisitor visitor, P p) { + return visitor.visitExpressionStatement(this, p); + } + + @Override + public String toString() { + return "expression statement " + tree.toString(); + } + + @Override + public boolean equals(@Nullable Object obj) { + return this == obj; + } + + @Override + public int hashCode() { + return Objects.hash(toString()); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FieldAccessNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FieldAccessNode.java index e2a981652288..1cf3af49fe3c 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FieldAccessNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FieldAccessNode.java @@ -3,13 +3,18 @@ import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Collection; import java.util.Collections; import java.util.Objects; + import javax.lang.model.element.VariableElement; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.ElementUtils; -import org.checkerframework.javacutil.TreeUtils; /** * A node for a field access, including a method accesses: @@ -19,14 +24,24 @@ *
      */ public class FieldAccessNode extends Node { - + /** The tree of the field access. */ protected final Tree tree; + + /** The element of the accessed field. */ protected final VariableElement element; + + /** The name of the accessed field. */ protected final String field; - protected final Node receiver; - // TODO: add method to get modifiers (static, access level, ..) + /** The receiver node of the field access. */ + protected final Node receiver; + /** + * Creates a new FieldAccessNode. + * + * @param tree the tree from which to create a FieldAccessNode + * @param receiver the receiver for the resulting FieldAccessNode + */ public FieldAccessNode(Tree tree, Node receiver) { super(TreeUtils.typeOf(tree)); assert TreeUtils.isFieldAccess(tree); @@ -37,12 +52,13 @@ public FieldAccessNode(Tree tree, Node receiver) { if (tree instanceof MemberSelectTree) { MemberSelectTree mstree = (MemberSelectTree) tree; assert TreeUtils.isUseOfElement(mstree) : "@AssumeAssertion(nullness): tree kind"; - this.element = (VariableElement) TreeUtils.elementFromUse(mstree); - } else { - assert tree instanceof IdentifierTree; + this.element = TreeUtils.variableElementFromUse(mstree); + } else if (tree instanceof IdentifierTree) { IdentifierTree itree = (IdentifierTree) tree; assert TreeUtils.isUseOfElement(itree) : "@AssumeAssertion(nullness): tree kind"; - this.element = (VariableElement) TreeUtils.elementFromUse(itree); + this.element = TreeUtils.variableElementFromUse(itree); + } else { + throw new BugInCF("unexpected tree %s [%s]", tree, tree.getClass()); } } @@ -81,7 +97,11 @@ public String toString() { return getReceiver() + "." + field; } - /** Is this a static field? */ + /** + * Determine whether the field is static or not. + * + * @return whether the field is static or not + */ public boolean isStatic() { return ElementUtils.isStatic(getElement()); } @@ -102,6 +122,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.singletonList(receiver); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatLiteralNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatLiteralNode.java index 3dd7a7d70f6c..0b10c4173619 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatLiteralNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatLiteralNode.java @@ -2,9 +2,12 @@ import com.sun.source.tree.LiteralTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for a float literal. For example: @@ -47,6 +50,7 @@ public boolean equals(@Nullable Object obj) { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingDivisionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingDivisionNode.java index 6bebc7c2dce4..a9fcd2748efc 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingDivisionNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingDivisionNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the floating-point division: * @@ -14,9 +16,16 @@ */ public class FloatingDivisionNode extends BinaryOperationNode { + /** + * Constructs a {@link FloatingDivisionNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public FloatingDivisionNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.DIVIDE; + assert tree.getKind() == Tree.Kind.DIVIDE; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingRemainderNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingRemainderNode.java index 42a4dfd84862..fb3f25eca42a 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingRemainderNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FloatingRemainderNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the floating-point remainder: * @@ -14,9 +16,16 @@ */ public class FloatingRemainderNode extends BinaryOperationNode { + /** + * Constructs a {@link FloatingRemainderNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public FloatingRemainderNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.REMAINDER; + assert tree.getKind() == Tree.Kind.REMAINDER; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FunctionalInterfaceNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FunctionalInterfaceNode.java index 9448233af88a..8e5ce2340ed2 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FunctionalInterfaceNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/FunctionalInterfaceNode.java @@ -3,13 +3,16 @@ import com.sun.source.tree.LambdaExpressionTree; import com.sun.source.tree.MemberReferenceTree; import com.sun.source.tree.Tree; -import java.util.Collection; -import java.util.Collections; -import java.util.Objects; + import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; import org.checkerframework.javacutil.BugInCF; import org.checkerframework.javacutil.TreeUtils; +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; + /** * A node for member references and lambdas. * @@ -84,6 +87,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanNode.java index ccf036e93dbc..5679d665fa47 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the greater than comparison: * @@ -14,9 +16,16 @@ */ public class GreaterThanNode extends BinaryOperationNode { + /** + * Constructs a {@link GreaterThanNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public GreaterThanNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.GREATER_THAN; + assert tree.getKind() == Tree.Kind.GREATER_THAN; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanOrEqualNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanOrEqualNode.java index df71eedb3a26..3f76c748a5cb 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanOrEqualNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/GreaterThanOrEqualNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the greater than or equal comparison: * @@ -14,9 +16,16 @@ */ public class GreaterThanOrEqualNode extends BinaryOperationNode { + /** + * Constructs a {@link GreaterThanOrEqualNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public GreaterThanOrEqualNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.GREATER_THAN_EQUAL; + assert tree.getKind() == Tree.Kind.GREATER_THAN_EQUAL; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ImplicitThisLiteralNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ImplicitThisLiteralNode.java deleted file mode 100644 index 9f984fb5b4c8..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ImplicitThisLiteralNode.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.checkerframework.dataflow.cfg.node; - -import com.sun.source.tree.Tree; -import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** A node to model the implicit {@code this}, e.g., in a field access. */ -public class ImplicitThisLiteralNode extends ThisLiteralNode { - - public ImplicitThisLiteralNode(TypeMirror type) { - super(type); - } - - @Override - public @Nullable Tree getTree() { - return null; - } - - @Override - public R accept(NodeVisitor visitor, P p) { - return visitor.visitImplicitThisLiteral(this, p); - } - - @Override - public String toString() { - return "(" + getName() + ")"; - } -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ImplicitThisNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ImplicitThisNode.java new file mode 100644 index 000000000000..09b555d316c7 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ImplicitThisNode.java @@ -0,0 +1,33 @@ +package org.checkerframework.dataflow.cfg.node; + +import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import javax.lang.model.type.TypeMirror; + +/** A node to model the implicit {@code this}, e.g., in a field access. */ +public class ImplicitThisNode extends ThisNode { + + public ImplicitThisNode(TypeMirror type) { + super(type); + } + + @Override + public @Nullable Tree getTree() { + return null; + } + + @Override + public R accept(NodeVisitor visitor, P p) { + return visitor.visitImplicitThis(this, p); + } + + // In an inner class context, an implicit this may need to be represented as "Outer.this" rather + // than just as "this". This is context-dependent, and toString doesn't know if it is being + // used in an inner class context. + @Override + public String toString() { + return "(this)"; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/InstanceOfNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/InstanceOfNode.java index 461139bad6ee..c72bb91886b2 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/InstanceOfNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/InstanceOfNode.java @@ -1,14 +1,20 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.InstanceOfTree; -import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TypesUtils; + import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Objects; + import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for the instanceof operator: @@ -26,24 +32,103 @@ public class InstanceOfNode extends Node { /** The tree associated with this node. */ protected final InstanceOfTree tree; + /** The node of the pattern if one exists. */ + protected final @Nullable Node patternNode; + /** For Types.isSameType. */ protected final Types types; - /** Create an InstanceOfNode. */ - public InstanceOfNode(Tree tree, Node operand, TypeMirror refType, Types types) { + /** + * Create an InstanceOfNode. + * + * @param tree instanceof tree + * @param operand the expression in the instanceof tree + * @param refType the type in the instanceof + * @param types types util + */ + public InstanceOfNode(InstanceOfTree tree, Node operand, TypeMirror refType, Types types) { + this(tree, operand, null, refType, types); + } + + /** + * Create an InstanceOfNode. + * + * @param tree instanceof tree + * @param operand the expression in the instanceof tree + * @param patternNode the pattern node or null if there is none + * @param refType the type in the instanceof + * @param types types util + */ + public InstanceOfNode( + InstanceOfTree tree, + Node operand, + @Nullable Node patternNode, + TypeMirror refType, + Types types) { super(types.getPrimitiveType(TypeKind.BOOLEAN)); - assert tree.getKind() == Tree.Kind.INSTANCE_OF; - this.tree = (InstanceOfTree) tree; + this.tree = tree; this.operand = operand; this.refType = refType; this.types = types; + this.patternNode = patternNode; } public Node getOperand() { return operand; } - /** The reference type being tested against. */ + /** + * Returns the binding variable for this instanceof, or null if one does not exist. + * + * @return the binding variable for this instanceof, or null if one does not exist + * @deprecated Use {@link #getPatternNode()} or {@link #getBindingVariables()} instead. + */ + @Deprecated // 2023-09-24 + public @Nullable LocalVariableNode getBindingVariable() { + if (patternNode instanceof LocalVariableNode) { + return (LocalVariableNode) patternNode; + } + return null; + } + + /** + * A list of all binding variables in this instanceof. This is lazily initialized, use {@link + * #getBindingVariables()}. + */ + protected @MonotonicNonNull List bindingVariables = null; + + /** + * Return all the binding variables in this instanceof. + * + * @return all the binding variables in this instanceof + */ + public List getBindingVariables() { + if (bindingVariables == null) { + if (patternNode instanceof DeconstructorPatternNode) { + bindingVariables = ((DeconstructorPatternNode) patternNode).getBindingVariables(); + } else if (patternNode instanceof LocalVariableNode) { + bindingVariables = Collections.singletonList((LocalVariableNode) patternNode); + } else { + bindingVariables = Collections.emptyList(); + } + } + return bindingVariables; + } + + /** + * Returns the pattern for this instanceof, or null if one does not exist. + * + * @return the pattern for this instanceof, or null if one does not exist + */ + public @Nullable Node getPatternNode() { + return patternNode; + } + + /** + * The reference type being tested against. + * + * @return the reference type + */ public TypeMirror getRefType() { return refType; } @@ -60,7 +145,12 @@ public R accept(NodeVisitor visitor, P p) { @Override public String toString() { - return "(" + getOperand() + " instanceof " + getRefType() + ")"; + return "(" + + getOperand() + + " instanceof " + + TypesUtils.simpleTypeName(getRefType()) + + (patternNode == null ? "" : " " + getPatternNode()) + + ")"; } @Override @@ -81,6 +171,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.singletonList(getOperand()); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerDivisionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerDivisionNode.java index 7e425ed497d2..8307fc95edc3 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerDivisionNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerDivisionNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the integer division: * @@ -14,9 +16,16 @@ */ public class IntegerDivisionNode extends BinaryOperationNode { + /** + * Constructs an {@link IntegerDivisionNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public IntegerDivisionNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.DIVIDE; + assert tree.getKind() == Tree.Kind.DIVIDE; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerLiteralNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerLiteralNode.java index 6296df040bc0..470a43b182c9 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerLiteralNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerLiteralNode.java @@ -2,9 +2,12 @@ import com.sun.source.tree.LiteralTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for an integer literal. For example: @@ -46,6 +49,7 @@ public boolean equals(@Nullable Object obj) { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerRemainderNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerRemainderNode.java index b428441cc2e1..d80fc1339a23 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerRemainderNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/IntegerRemainderNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the integer remainder: * @@ -14,9 +16,16 @@ */ public class IntegerRemainderNode extends BinaryOperationNode { + /** + * Constructs an {@link IntegerRemainderNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public IntegerRemainderNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.REMAINDER; + assert tree.getKind() == Tree.Kind.REMAINDER; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LambdaResultExpressionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LambdaResultExpressionNode.java index 12281b5bdb02..27beb525617e 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LambdaResultExpressionNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LambdaResultExpressionNode.java @@ -1,20 +1,31 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.ExpressionTree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Collection; import java.util.Collections; import java.util.Objects; -import javax.lang.model.util.Types; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; -/** A node for the single expression body of a single expression lambda. */ +/** A node for the single expression body of a single-expression lambda. */ public class LambdaResultExpressionNode extends Node { + /** Tree for the lambda expression body. */ protected final ExpressionTree tree; - protected final @Nullable Node result; - public LambdaResultExpressionNode(ExpressionTree t, @Nullable Node result, Types types) { + /** Final CFG node corresponding to the lambda expression body. */ + protected final Node result; + + /** + * Creates a LambdaResultExpressionNode. + * + * @param t tree for the lambda expression body + * @param result final CFG node corresponding to the lambda expression body + */ + public LambdaResultExpressionNode(ExpressionTree t, Node result) { super(TreeUtils.typeOf(t)); this.result = result; tree = t; @@ -23,14 +34,19 @@ public LambdaResultExpressionNode(ExpressionTree t, @Nullable Node result, Types /** * Returns the final node of the CFG corresponding to the lambda expression body (see {@link * #getTree()}). + * + * @return the final node of the CFG corresponding to the lambda expression body */ - public @Nullable Node getResult() { + public Node getResult() { return result; } /** * Returns the {@link ExpressionTree} corresponding to the body of a lambda expression with an * expression body (e.g. X for ({@code o -> X}) where X is an expression and not a {...} block). + * + * @return the {@link ExpressionTree} corresponding to the body of a lambda expression with an + * expression body */ @Override public ExpressionTree getTree() { @@ -44,10 +60,7 @@ public R accept(NodeVisitor visitor, P p) { @Override public String toString() { - if (result != null) { - return "-> " + result; - } - return "-> ()"; + return "-> " + result; } @Override @@ -69,11 +82,8 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { - if (result == null) { - return Collections.emptyList(); - } else { - return Collections.singletonList(result); - } + return Collections.singletonList(result); } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LeftShiftNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LeftShiftNode.java index 84cb3231a168..8ab462127e3d 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LeftShiftNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LeftShiftNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for bitwise left shift operations: * @@ -14,9 +16,16 @@ */ public class LeftShiftNode extends BinaryOperationNode { + /** + * Constructs a {@link LeftShiftNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public LeftShiftNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.LEFT_SHIFT; + assert tree.getKind() == Tree.Kind.LEFT_SHIFT; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanNode.java index ad972486e51a..5237ee99d2ad 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanNode.java @@ -2,10 +2,11 @@ import com.sun.source.tree.BinaryTree; import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the less than comparison: * @@ -17,9 +18,16 @@ */ public class LessThanNode extends BinaryOperationNode { + /** + * Constructs a {@link LessThanNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public LessThanNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.LESS_THAN; + assert tree.getKind() == Tree.Kind.LESS_THAN; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanOrEqualNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanOrEqualNode.java index 8fa56eedb338..cb29c4adbcf8 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanOrEqualNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LessThanOrEqualNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the less than or equal comparison: * @@ -14,9 +16,16 @@ */ public class LessThanOrEqualNode extends BinaryOperationNode { + /** + * Constructs a {@link LessThanOrEqualNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public LessThanOrEqualNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.LESS_THAN_EQUAL; + assert tree.getKind() == Tree.Kind.LESS_THAN_EQUAL; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LocalVariableNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LocalVariableNode.java index bc40ac6cdb98..6d477b7962ac 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LocalVariableNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LocalVariableNode.java @@ -3,12 +3,16 @@ import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.Tree; import com.sun.source.tree.VariableTree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Collection; import java.util.Collections; import java.util.Objects; -import javax.lang.model.element.Element; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; + +import javax.lang.model.element.VariableElement; /** * A node for a local variable or a parameter: @@ -18,7 +22,7 @@ *

      This map is used by {@link CFGTranslationPhaseOne#addToLookupMap(Node)} to associate a + * {@code ParenthesizedTree} with the dataflow {@code Node} that was used during inference. This + * map is necessary because dataflow does not create a {@code Node} for a {@code + * ParenthesizedTree}. + */ + private final Map parenMapping = new HashMap<>(); + + @Override + public Node visitParenthesized(ParenthesizedTree tree, Void p) { + parenMapping.put(tree.getExpression(), tree); + return scan(tree.getExpression(), p); + } + + @Override + public Node visitReturn(ReturnTree tree, Void p) { + ExpressionTree ret = tree.getExpression(); + // TODO: also have a return-node if nothing is returned + ReturnNode result = null; + if (ret != null) { + Node node = scan(ret, p); + result = new ReturnNode(tree, node, env.getTypeUtils()); + returnNodes.add(result); + extendWithNode(result); + } + + extendWithExtendedNode(new UnconditionalJump(this.returnTargetLC.accessLabel())); + + return result; + } + + @Override + public Node visitMemberSelect(MemberSelectTree tree, Void p) { + Node expr = scan(tree.getExpression(), p); + if (!TreeUtils.isFieldAccess(tree)) { + // Could be a selector of a class or package + Element element = TreeUtils.elementFromUse(tree); + if (ElementUtils.isTypeElement(element)) { + ClassNameNode result = new ClassNameNode(tree, expr); + extendWithClassNameNode(result); + return result; + } else if (element.getKind() == ElementKind.PACKAGE) { + Node result = new PackageNameNode(tree, (PackageNameNode) expr); + extendWithNode(result); + return result; + } else { + throw new BugInCF("Unexpected element kind: " + element.getKind()); + } + } + + Node node = new FieldAccessNode(tree, expr); + + Element element = TreeUtils.elementFromUse(tree); + if (ElementUtils.isStatic(element) || expr instanceof ThisNode) { + // No NullPointerException can be thrown, use normal node + extendWithNode(node); + } else { + extendWithNodeWithException(node, nullPointerExceptionType); + } + + return node; + } + + @Override + public Node visitEmptyStatement(EmptyStatementTree tree, Void p) { + return null; + } + + @Override + public Node visitSynchronized(SynchronizedTree tree, Void p) { + // see JLS 14.19 + + Node synchronizedExpr = scan(tree.getExpression(), p); + SynchronizedNode synchronizedStartNode = + new SynchronizedNode(tree, synchronizedExpr, true, env.getTypeUtils()); + extendWithNode(synchronizedStartNode); + scan(tree.getBlock(), p); + SynchronizedNode synchronizedEndNode = + new SynchronizedNode(tree, synchronizedExpr, false, env.getTypeUtils()); + extendWithNode(synchronizedEndNode); + + return null; + } + + @Override + public Node visitThrow(ThrowTree tree, Void p) { + Node expression = scan(tree.getExpression(), p); + TypeMirror exception = expression.getType(); + ThrowNode throwsNode = new ThrowNode(tree, expression, env.getTypeUtils()); + NodeWithExceptionsHolder exNode = extendWithNodeWithException(throwsNode, exception); + exNode.setTerminatesExecution(true); + return throwsNode; + } + + @Override + public Node visitCompilationUnit(CompilationUnitTree tree, Void p) { + throw new BugInCF("CompilationUnitTree is unexpected in AST to CFG translation"); + } + + /** + * Return the first argument if it is non-null, otherwise return the second argument. Throws an + * exception if both arguments are null. + * + * @param the type of the arguments + * @param first a reference + * @param second a reference + * @return the first argument that is non-null + */ + private static A firstNonNull(A first, A second) { + if (first != null) { + return first; + } else if (second != null) { + return second; + } else { + throw new NullPointerException(); + } + } + + @Override + public Node visitTry(TryTree tree, Void p) { + List catches = tree.getCatches(); + BlockTree finallyBlock = tree.getFinallyBlock(); + + extendWithNode( + new MarkerNode( + tree, + "start of try statement #" + TreeUtils.treeUids.get(tree), + env.getTypeUtils())); + + List> catchLabels = + CollectionsPlume.mapList( + (CatchTree c) -> + IPair.of(TreeUtils.typeOf(c.getParameter().getType()), new Label()), + catches); + + // Store return/break/continue labels, just in case we need them for a finally block. + LabelCell oldReturnTargetLC = returnTargetLC; + LabelCell oldBreakTargetLC = breakTargetLC; + Map oldBreakLabels = breakLabels; + LabelCell oldContinueTargetLC = continueTargetLC; + Map oldContinueLabels = continueLabels; + + Label finallyLabel = null; + Label exceptionalFinallyLabel = null; + + if (finallyBlock != null) { + finallyLabel = new Label(); + + exceptionalFinallyLabel = new Label(); + tryStack.pushFrame(new TryFinallyFrame(exceptionalFinallyLabel)); + + returnTargetLC = new LabelCell(); + + breakTargetLC = new LabelCell(); + breakLabels = new TryFinallyScopeMap(); + + continueTargetLC = new LabelCell(); + continueLabels = new TryFinallyScopeMap(); + } + + Label doneLabel = new Label(); + + tryStack.pushFrame(new TryCatchFrame(types, catchLabels)); + + extendWithNode( + new MarkerNode( + tree, + "start of try block #" + TreeUtils.treeUids.get(tree), + env.getTypeUtils())); + + handleTryResourcesAndBlock(tree, p, tree.getResources()); + + extendWithNode( + new MarkerNode( + tree, + "end of try block #" + TreeUtils.treeUids.get(tree), + env.getTypeUtils())); + + extendWithExtendedNode(new UnconditionalJump(firstNonNull(finallyLabel, doneLabel))); + + // This pops the try-catch frame + tryStack.popFrame(); + + int catchIndex = 0; + for (CatchTree c : catches) { + addLabelForNextNode(catchLabels.get(catchIndex).second); + TypeMirror catchType = TreeUtils.typeOf(c.getParameter().getType()); + extendWithNode(new CatchMarkerNode(tree, "start", catchType, env.getTypeUtils())); + scan(c, p); + extendWithNode(new CatchMarkerNode(tree, "end", catchType, env.getTypeUtils())); + + catchIndex++; + extendWithExtendedNode(new UnconditionalJump(firstNonNull(finallyLabel, doneLabel))); + } + + if (finallyLabel != null) { + handleFinally( + tree, + doneLabel, + finallyLabel, + exceptionalFinallyLabel, + () -> scan(finallyBlock, p), + oldReturnTargetLC, + oldBreakTargetLC, + oldBreakLabels, + oldContinueTargetLC, + oldContinueLabels); + } + + addLabelForNextNode(doneLabel); + + return null; + } + + /** + * A recursive helper method to handle the resource declarations (if any) in a {@link TryTree} + * and its main block. If the {@code resources} list is empty, the method scans the main block + * of the try statement and returns. Otherwise, the first resource declaration in {@code + * resources} is desugared, following the logic in JLS 14.20.3.1. A resource declaration + * r is desugared by adding the nodes for r itself to the CFG, followed by a + * synthetic nested {@code try} block and {@code finally} block. The synthetic {@code try} block + * contains any remaining resource declarations and the original try block (handled via + * recursion). The synthetic {@code finally} block contains a call to {@code close} for + * r, guaranteeing that on every path through the CFG, r is closed. + * + * @param tryTree the original try tree (with 0 or more resources) from the AST + * @param p the value to pass to calls to {@code scan} + * @param resources the remaining resource declarations to handle + */ + private void handleTryResourcesAndBlock( + TryTree tryTree, Void p, List resources) { + if (resources.isEmpty()) { + // Either `tryTree` was not a try-with-resources, or this method was called recursively + // and all the resources have been handled. Just scan the main try block. + scan(tryTree.getBlock(), p); + return; + } + + // Handle the first resource declaration in the list. The rest will be handled by a + // recursive call. + Tree resourceDeclarationTree = resources.get(0); + + extendWithNode( + new MarkerNode( + resourceDeclarationTree, + "start of try for resource #" + + TreeUtils.treeUids.get(resourceDeclarationTree), + env.getTypeUtils())); + + // Store return/break/continue labels. Generating a synthetic finally block for closing the + // resource requires creating fresh return/break/continue labels and then restoring the old + // labels afterward. + LabelCell oldReturnTargetLC = returnTargetLC; + LabelCell oldBreakTargetLC = breakTargetLC; + Map oldBreakLabels = breakLabels; + LabelCell oldContinueTargetLC = continueTargetLC; + Map oldContinueLabels = continueLabels; + + // Add nodes for the resource declaration to the CFG. NOTE: it is critical to add these + // nodes *before* pushing a TryFinallyFrame for the finally block that will close the + // resource. + // If any exception occurs due to code within the resource declaration, the corresponding + // variable or field is *not* automatically closed (as it was never assigned a value). + Node resourceCloseNode = scan(resourceDeclarationTree, p); + + // Now, set things up for our synthetic finally block that closes the resource. + Label doneLabel = new Label(); + Label finallyLabel = new Label(); + + Label exceptionalFinallyLabel = new Label(); + tryStack.pushFrame(new TryFinallyFrame(exceptionalFinallyLabel)); + + returnTargetLC = new LabelCell(); + + breakTargetLC = new LabelCell(); + breakLabels = new TryFinallyScopeMap(); + + continueTargetLC = new LabelCell(); + continueLabels = new TryFinallyScopeMap(); + + extendWithNode( + new MarkerNode( + resourceDeclarationTree, + "start of try block for resource #" + + TreeUtils.treeUids.get(resourceDeclarationTree), + env.getTypeUtils())); + // Recursively handle any remaining resource declarations and the main block of the try + handleTryResourcesAndBlock(tryTree, p, resources.subList(1, resources.size())); + extendWithNode( + new MarkerNode( + resourceDeclarationTree, + "end of try block for resource #" + + TreeUtils.treeUids.get(resourceDeclarationTree), + env.getTypeUtils())); + + extendWithExtendedNode(new UnconditionalJump(finallyLabel)); + + // Generate the finally block that closes the resource + handleFinally( + resourceDeclarationTree, + doneLabel, + finallyLabel, + exceptionalFinallyLabel, + () -> addCloseCallForResource(resourceDeclarationTree, resourceCloseNode), + oldReturnTargetLC, + oldBreakTargetLC, + oldBreakLabels, + oldContinueTargetLC, + oldContinueLabels); + + addLabelForNextNode(doneLabel); + } + + /** + * Adds a synthetic {@code close} call to the CFG to close some resource variable declared or + * used in a try-with-resources. + * + * @param resourceDeclarationTree the resource declaration + * @param resourceToCloseNode node represented the variable or field on which {@code close} + * should be invoked + */ + private void addCloseCallForResource(Tree resourceDeclarationTree, Node resourceToCloseNode) { + Tree receiverTree = resourceDeclarationTree; + if (receiverTree instanceof VariableTree) { + receiverTree = treeBuilder.buildVariableUse((VariableTree) receiverTree); + handleArtificialTree(receiverTree); + } + + MemberSelectTree closeSelect = + treeBuilder.buildCloseMethodAccess((ExpressionTree) receiverTree); + handleArtificialTree(closeSelect); + + MethodInvocationTree closeCall = treeBuilder.buildMethodInvocation(closeSelect); + handleArtificialTree(closeCall); + + Node receiverNode = resourceToCloseNode; + if (receiverNode instanceof AssignmentNode) { + // variable declaration; use the LHS + receiverNode = ((AssignmentNode) resourceToCloseNode).getTarget(); + } + // TODO do we need to insert some kind of node representing a use of receiverNode + // (which can be either a LocalVariableNode or a FieldAccessNode)? + MethodAccessNode closeAccessNode = new MethodAccessNode(closeSelect, receiverNode); + closeAccessNode.setInSource(false); + extendWithNode(closeAccessNode); + MethodInvocationNode closeCallNode = + new MethodInvocationNode( + closeCall, closeAccessNode, Collections.emptyList(), getCurrentPath()); + closeCallNode.setInSource(false); + extendWithMethodInvocationNode(TreeUtils.elementFromUse(closeCall), closeCallNode); + } + + /** + * Shared logic for CFG generation for a finally block. The block may correspond to a {@link + * TryTree} originally in the source code, or it may be a synthetic finally block used to model + * closing of a resource due to try-with-resources. + * + * @param markerTree tree to reference when creating {@link MarkerNode}s for the finally block + * @param doneLabel label for the normal successor of the try block (no exceptions, returns, + * breaks, or continues) + * @param finallyLabel label for the entry of the finally block for the normal case + * @param exceptionalFinallyLabel label for entry of the finally block for when the try block + * throws an exception + * @param finallyBlockCFGGenerator generates CFG nodes and edges for the finally block + * @param oldReturnTargetLC old return target label cell, which gets restored to {@link + * #returnTargetLC} while handling the finally block + * @param oldBreakTargetLC old break target label cell, which gets restored to {@link + * #breakTargetLC} while handling the finally block + * @param oldBreakLabels old break labels, which get restored to {@link #breakLabels} while + * handling the finally block + * @param oldContinueTargetLC old continue target label cell, which gets restored to {@link + * #continueTargetLC} while handling the finally block + * @param oldContinueLabels old continue labels, which get restored to {@link #continueLabels} + * while handling the finally block + */ + private void handleFinally( + Tree markerTree, + Label doneLabel, + Label finallyLabel, + Label exceptionalFinallyLabel, + Runnable finallyBlockCFGGenerator, + LabelCell oldReturnTargetLC, + LabelCell oldBreakTargetLC, + Map oldBreakLabels, + LabelCell oldContinueTargetLC, + Map oldContinueLabels) { + // Reset values before analyzing the finally block! + + tryStack.popFrame(); + + { // Scan 'finallyBlock' for only 'finallyLabel' (a successful path) + addLabelForNextNode(finallyLabel); + extendWithNode( + new MarkerNode( + markerTree, + "start of finally block #" + TreeUtils.treeUids.get(markerTree), + env.getTypeUtils())); + finallyBlockCFGGenerator.run(); + extendWithNode( + new MarkerNode( + markerTree, + "end of finally block #" + TreeUtils.treeUids.get(markerTree), + env.getTypeUtils())); + extendWithExtendedNode(new UnconditionalJump(doneLabel)); + } + + if (hasExceptionalPath(exceptionalFinallyLabel)) { + // If an exceptional path exists, scan 'finallyBlock' for 'exceptionalFinallyLabel', + // and scan copied 'finallyBlock' for 'finallyLabel' (a successful path). If there + // is no successful path, it will be removed in later phase. + // TODO: Don't we need a separate finally block for each kind of exception? + addLabelForNextNode(exceptionalFinallyLabel); + extendWithNode( + new MarkerNode( + markerTree, + "start of finally block for Throwable #" + + TreeUtils.treeUids.get(markerTree), + env.getTypeUtils())); + + finallyBlockCFGGenerator.run(); + + NodeWithExceptionsHolder throwing = + extendWithNodeWithException( + new MarkerNode( + markerTree, + "end of finally block for Throwable #" + + TreeUtils.treeUids.get(markerTree), + env.getTypeUtils()), + throwableType); + + throwing.setTerminatesExecution(true); + } + + if (returnTargetLC.wasAccessed()) { + addLabelForNextNode(returnTargetLC.peekLabel()); + returnTargetLC = oldReturnTargetLC; + + extendWithNode( + new MarkerNode( + markerTree, + "start of finally block for return #" + + TreeUtils.treeUids.get(markerTree), + env.getTypeUtils())); + finallyBlockCFGGenerator.run(); + extendWithNode( + new MarkerNode( + markerTree, + "end of finally block for return #" + + TreeUtils.treeUids.get(markerTree), + env.getTypeUtils())); + extendWithExtendedNode(new UnconditionalJump(returnTargetLC.accessLabel())); + } else { + returnTargetLC = oldReturnTargetLC; + } + + if (breakTargetLC.wasAccessed()) { + addLabelForNextNode(breakTargetLC.peekLabel()); + breakTargetLC = oldBreakTargetLC; + + extendWithNode( + new MarkerNode( + markerTree, + "start of finally block for break #" + + TreeUtils.treeUids.get(markerTree), + env.getTypeUtils())); + finallyBlockCFGGenerator.run(); + extendWithNode( + new MarkerNode( + markerTree, + "end of finally block for break #" + TreeUtils.treeUids.get(markerTree), + env.getTypeUtils())); + extendWithExtendedNode(new UnconditionalJump(breakTargetLC.accessLabel())); + } else { + breakTargetLC = oldBreakTargetLC; + } + + Map accessedBreakLabels = + ((TryFinallyScopeMap) breakLabels).getAccessedNames(); + if (!accessedBreakLabels.isEmpty()) { + breakLabels = oldBreakLabels; + + for (Map.Entry access : accessedBreakLabels.entrySet()) { + addLabelForNextNode(access.getValue()); + extendWithNode( + new MarkerNode( + markerTree, + "start of finally block for break label " + + access.getKey() + + " #" + + TreeUtils.treeUids.get(markerTree), + env.getTypeUtils())); + finallyBlockCFGGenerator.run(); + extendWithNode( + new MarkerNode( + markerTree, + "end of finally block for break label " + + access.getKey() + + " #" + + TreeUtils.treeUids.get(markerTree), + env.getTypeUtils())); + extendWithExtendedNode(new UnconditionalJump(breakLabels.get(access.getKey()))); + } + } else { + breakLabels = oldBreakLabels; + } + + if (continueTargetLC.wasAccessed()) { + addLabelForNextNode(continueTargetLC.peekLabel()); + continueTargetLC = oldContinueTargetLC; + + extendWithNode( + new MarkerNode( + markerTree, + "start of finally block for continue #" + + TreeUtils.treeUids.get(markerTree), + env.getTypeUtils())); + finallyBlockCFGGenerator.run(); + extendWithNode( + new MarkerNode( + markerTree, + "end of finally block for continue #" + + TreeUtils.treeUids.get(markerTree), + env.getTypeUtils())); + extendWithExtendedNode(new UnconditionalJump(continueTargetLC.accessLabel())); + } else { + continueTargetLC = oldContinueTargetLC; + } + + Map accessedContinueLabels = + ((TryFinallyScopeMap) continueLabels).getAccessedNames(); + if (!accessedContinueLabels.isEmpty()) { + continueLabels = oldContinueLabels; + + for (Map.Entry access : accessedContinueLabels.entrySet()) { + addLabelForNextNode(access.getValue()); + extendWithNode( + new MarkerNode( + markerTree, + "start of finally block for continue label " + + access.getKey() + + " #" + + TreeUtils.treeUids.get(markerTree), + env.getTypeUtils())); + finallyBlockCFGGenerator.run(); + extendWithNode( + new MarkerNode( + markerTree, + "end of finally block for continue label " + + access.getKey() + + " #" + + TreeUtils.treeUids.get(markerTree), + env.getTypeUtils())); + extendWithExtendedNode(new UnconditionalJump(continueLabels.get(access.getKey()))); + } + } else { + continueLabels = oldContinueLabels; + } + } + + /** + * Returns whether an exceptional node for {@code target} exists in {@link #nodeList} or not. + * + * @param target label for exception + * @return true when an exceptional node for {@code target} exists in {@link #nodeList} + */ + private boolean hasExceptionalPath(Label target) { + for (ExtendedNode node : nodeList) { + if (node instanceof NodeWithExceptionsHolder) { + NodeWithExceptionsHolder exceptionalNode = (NodeWithExceptionsHolder) node; + for (Set

      * * We allow local variable uses introduced by the {@link - * org.checkerframework.dataflow.cfg.CFGBuilder} without corresponding AST {@link Tree}s. + * org.checkerframework.dataflow.cfg.builder.CFGBuilder} without corresponding AST {@link Tree}s. */ // TODO: don't use for parameters, as they don't have a tree public class LocalVariableNode extends Node { @@ -29,28 +33,42 @@ public class LocalVariableNode extends Node { /** The receiver node for the local variable, {@code null} otherwise. */ protected final @Nullable Node receiver; - /** Create a new local variable node for the given tree. */ - public LocalVariableNode(Tree t) { - this(t, null); + /** + * Create a new local variable node for the given tree. + * + * @param tree the tree for the local variable: a VariableTree or an IdentifierTree + */ + public LocalVariableNode(Tree tree) { + this(tree, null); } - /** Create a new local variable node for the given tree and receiver. */ - public LocalVariableNode(Tree t, @Nullable Node receiver) { - super(TreeUtils.typeOf(t)); + /** + * Create a new local variable node for the given tree and receiver. + * + * @param tree the tree for the local variable: a VariableTree or an IdentifierTree + * @param receiver the receiver for the local variable, or null if none + */ + public LocalVariableNode(Tree tree, @Nullable Node receiver) { + super(TreeUtils.typeOf(tree)); // IdentifierTree for normal uses of the local variable or parameter, - // and VariableTree for the translation of an initializer block - assert t != null; - assert t instanceof IdentifierTree || t instanceof VariableTree; - this.tree = t; + // and VariableTree for declarations or the translation of an initializer block + assert tree != null; + assert tree instanceof IdentifierTree || tree instanceof VariableTree; + this.tree = tree; this.receiver = receiver; } - public Element getElement() { - Element el; + /** + * Returns the element associated with this local variable. + * + * @return the element associated with this local variable + */ + public VariableElement getElement() { + VariableElement el; if (tree instanceof IdentifierTree) { IdentifierTree itree = (IdentifierTree) tree; assert TreeUtils.isUseOfElement(itree) : "@AssumeAssertion(nullness): tree kind"; - el = TreeUtils.elementFromUse(itree); + el = TreeUtils.variableElementFromUse(itree); } else { assert tree instanceof VariableTree; el = TreeUtils.elementFromDeclaration((VariableTree) tree); @@ -100,6 +118,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LongLiteralNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LongLiteralNode.java index 33a1433d5737..17d9bb36f22f 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LongLiteralNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/LongLiteralNode.java @@ -2,9 +2,12 @@ import com.sun.source.tree.LiteralTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for a long literal. For example: @@ -47,6 +50,7 @@ public boolean equals(@Nullable Object obj) { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MarkerNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MarkerNode.java index 1d67f6c92881..658551a458eb 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MarkerNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MarkerNode.java @@ -1,12 +1,16 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; import java.util.Objects; + import javax.lang.model.type.TypeKind; import javax.lang.model.util.Types; -import org.checkerframework.checker.nullness.qual.Nullable; /** * MarkerNodes are no-op Nodes used for debugging information. They can hold a Tree and a message, @@ -64,6 +68,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MethodAccessNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MethodAccessNode.java index 6a698f08f141..b35eabe3d044 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MethodAccessNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MethodAccessNode.java @@ -2,34 +2,58 @@ import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Collection; import java.util.Collections; import java.util.Objects; + import javax.lang.model.element.ExecutableElement; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; /** - * A node for a method access, including a method accesses: + * A node for a method access, including a receiver: * *
        *   expression . method ()
        * 
      */ public class MethodAccessNode extends Node { - + /** The tree of the method access. */ protected final ExpressionTree tree; + + /** The element of the accessed method. */ protected final ExecutableElement method; - protected final Node receiver; - // TODO: add method to get modifiers (static, access level, ..) + /** The receiver node of the method access. */ + protected final Node receiver; + /** + * Create a new MethodAccessNode. + * + * @param tree the expression that is a method access + * @param receiver the receiver + */ public MethodAccessNode(ExpressionTree tree, Node receiver) { + this(tree, (ExecutableElement) TreeUtils.elementFromUse(tree), receiver); + } + + /** + * Create a new MethodAccessNode. + * + * @param tree the expression that is a method access + * @param method the element for the method + * @param receiver the receiver + */ + public MethodAccessNode(ExpressionTree tree, ExecutableElement method, Node receiver) { super(TreeUtils.typeOf(tree)); assert TreeUtils.isMethodAccess(tree); this.tree = tree; assert TreeUtils.isUseOfElement(tree) : "@AssumeAssertion(nullness): tree kind"; - this.method = (ExecutableElement) TreeUtils.elementFromUse(tree); + this.method = method; this.receiver = receiver; } @@ -71,7 +95,17 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.singletonList(receiver); } + + /** + * Determine whether the method is static or not. + * + * @return whether the method is static or not + */ + public boolean isStatic() { + return ElementUtils.isStatic(getMethod()); + } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MethodInvocationNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MethodInvocationNode.java index 28f08b8f91ac..5c444b2e4ecb 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MethodInvocationNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/MethodInvocationNode.java @@ -1,15 +1,20 @@ package org.checkerframework.dataflow.cfg.node; +import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.Tree; import com.sun.source.util.TreePath; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TreeUtils; +import org.plumelib.util.StringsPlume; + import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.cfg.node.AssignmentContext.MethodParameterContext; -import org.checkerframework.javacutil.TreeUtils; /** * A node for method invocation. @@ -25,14 +30,36 @@ public class MethodInvocationNode extends Node { /** The tree for the method invocation. */ protected final @Nullable MethodInvocationTree tree; - /** The target of the method invocation. */ + + /** + * The MethodAccessNode for the method being invoked. Includes the receiver if any. For a static + * method, the receiver may be a class name. + */ protected final MethodAccessNode target; + /** The arguments of the method invocation. */ protected final List arguments; + /** The tree path to the method invocation. */ protected final TreePath treePath; - /** Create a MethodInvocationNode. */ + /** + * If this MethodInvocationNode is a node for an {@link Iterator#next()} desugared from an + * enhanced for loop, then the {@code iterExpression} field is the expression in the for loop, + * e.g., {@code iter} in {@code for(Object o: iter}. + * + *

      Is set by {@link #setIterableExpression}. + */ + protected @Nullable ExpressionTree iterableExpression; + + /** + * Create a MethodInvocationNode. + * + * @param tree for the method invocation + * @param target the MethodAccessNode for the method being invoked + * @param arguments arguments of the method invocation + * @param treePath path to the method invocation + */ public MethodInvocationNode( @Nullable MethodInvocationTree tree, MethodAccessNode target, @@ -43,13 +70,6 @@ public MethodInvocationNode( this.target = target; this.arguments = arguments; this.treePath = treePath; - - // set assignment contexts for parameters - int i = 0; - for (Node arg : arguments) { - AssignmentContext ctx = new MethodParameterContext(target.getMethod(), i++); - arg.setAssignmentContext(ctx); - } } public MethodInvocationNode(MethodAccessNode target, List arguments, TreePath treePath) { @@ -72,6 +92,28 @@ public TreePath getTreePath() { return treePath; } + /** + * If this MethodInvocationNode is a node for an {@link Iterator#next()} desugared from an + * enhanced for loop, then return the expression in the for loop, e.g., {@code iter} in {@code + * for(Object o: iter}. Otherwise, return null. + * + * @return the iter expression, or null if this is not a {@link Iterator#next()} from an + * enhanced for loop + */ + public @Nullable ExpressionTree getIterableExpression() { + return iterableExpression; + } + + /** + * Set the iterable expression from a for loop. + * + * @param iterableExpression iterable expression + * @see #getIterableExpression() + */ + public void setIterableExpression(@Nullable ExpressionTree iterableExpression) { + this.iterableExpression = iterableExpression; + } + @Override public @Nullable MethodInvocationTree getTree() { return tree; @@ -84,19 +126,7 @@ public R accept(NodeVisitor visitor, P p) { @Override public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(target); - sb.append("("); - boolean needComma = false; - for (Node arg : arguments) { - if (needComma) { - sb.append(", "); - } - sb.append(arg); - needComma = true; - } - sb.append(")"); - return sb.toString(); + return target + "(" + StringsPlume.join(", ", arguments) + ")"; } @Override @@ -115,6 +145,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { List list = new ArrayList<>(1 + arguments.size()); list.add(target); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NarrowingConversionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NarrowingConversionNode.java index 76c00ea7a1a0..9f187c334262 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NarrowingConversionNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NarrowingConversionNode.java @@ -1,12 +1,16 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TypesUtils; + import java.util.Collection; import java.util.Collections; import java.util.Objects; + import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TypesUtils; /** * A node for the narrowing primitive conversion operation. See JLS 5.1.3 for the definition of @@ -63,6 +67,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.singletonList(getOperand()); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/Node.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/Node.java index 8d310aa7719d..0fdd5dfbe346 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/Node.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/Node.java @@ -1,13 +1,21 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.Tree; -import java.util.ArrayDeque; -import java.util.Collection; -import javax.lang.model.type.TypeMirror; + +import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.cfg.CFGBuilder; import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.builder.CFGBuilder; import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.plumelib.util.UniqueId; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.StringJoiner; +import java.util.concurrent.atomic.AtomicLong; + +import javax.lang.model.type.TypeMirror; /** * A node in the abstract representation used for Java code inside a basic block. @@ -16,9 +24,7 @@ * *

        * block == null || block instanceof RegularBlock || block instanceof ExceptionBlock
      - * block instanceof RegularBlock ⇒ block.getContents().contains(this)
      - * block instanceof ExceptionBlock ⇒ block.getNode() == this
      - * block == null ⇔ "This object represents a parameter of the method."
      + * block != null ⇔ block.getNodes().contains(this)
        * 
      * *
      @@ -28,24 +34,30 @@
        *
        * Note that two {@code Node}s can be {@code .equals} but represent different CFG nodes. Take care
        * to use reference equality, maps that handle identity {@code IdentityHashMap}, and sets like
      - * {@code IdentityMostlySingleton}.
      - *
      - * @see org.checkerframework.dataflow.util.IdentityMostlySingleton
      + * {@code IdentityArraySet}.
        */
      -public abstract class Node {
      +public abstract class Node implements UniqueId {
       
      -    /** The basic block this node belongs to (see invariant about this field above). */
      +    /**
      +     * The basic block this node belongs to. If null, this object represents a method formal
      +     * parameter.
      +     *
      +     * 

      Is set by {@link #setBlock}. + */ protected @Nullable Block block; - /** Is this node an l-value? */ + /** + * Is this node an l-value? + * + *

      Is set by {@link #setLValue}. + */ protected boolean lvalue = false; - /** The assignment context of this node. See {@link AssignmentContext}. */ - protected @Nullable AssignmentContext assignmentContext; - /** * Does this node represent a tree that appears in the source code (true) or one that the CFG * builder added while desugaring (false). + * + *

      Is set by {@link #setInSource}. */ protected boolean inSource = true; @@ -55,6 +67,23 @@ public abstract class Node { */ protected final TypeMirror type; + /** The unique ID for the next-created object. */ + private static final AtomicLong nextUid = new AtomicLong(0); + + /** The unique ID of this object. */ + private final transient long uid = nextUid.getAndIncrement(); + + @Override + @Pure + public long getUid(@UnknownInitialization Node this) { + return uid; + } + + /** + * Creates a new Node. + * + * @param type the type of the node + */ protected Node(TypeMirror type) { assert type != null; this.type = type; @@ -67,6 +96,7 @@ protected Node(TypeMirror type) { * @return the basic block this node belongs to (or {@code null} if it represents the parameter * of a method) */ + @Pure public @Nullable Block getBlock() { return block; } @@ -78,19 +108,20 @@ public void setBlock(Block b) { /** * Returns the {@link Tree} in the abstract syntax tree, or {@code null} if no corresponding - * tree exists. For instance, this is the case for an {@link ImplicitThisLiteralNode}. + * tree exists. For instance, this is the case for an {@link ImplicitThisNode}. * - * @return the corresponding {@link Tree} or {@code null}. + * @return the corresponding {@link Tree} or {@code null} */ @Pure public abstract @Nullable Tree getTree(); /** - * Returns a {@link TypeMirror} representing the type of a {@link Node} A {@link Node} will + * Returns a {@link TypeMirror} representing the type of a {@link Node}. A {@link Node} will * always have a type even when it has no {@link Tree}. * * @return a {@link TypeMirror} representing the type of this {@link Node} */ + @Pure public TypeMirror getType() { return type; } @@ -116,6 +147,13 @@ public void setLValue() { lvalue = true; } + /** + * Return whether this node represents a tree that appears in the source code (true) or one that + * the CFG or builder added while desugaring (false). + * + * @return whether this node represents a tree that appears in the source code + */ + @Pure public boolean getInSource() { return inSource; } @@ -124,20 +162,12 @@ public void setInSource(boolean inSrc) { inSource = inSrc; } - /** The assignment context for the node. */ - public @Nullable AssignmentContext getAssignmentContext() { - return assignmentContext; - } - - public void setAssignmentContext(AssignmentContext assignmentContext) { - this.assignmentContext = assignmentContext; - } - /** * Returns a collection containing all of the operand {@link Node}s of this {@link Node}. * * @return a collection containing all of the operand {@link Node}s of this {@link Node} */ + @SideEffectFree public abstract Collection getOperands(); /** @@ -147,6 +177,7 @@ public void setAssignmentContext(AssignmentContext assignmentContext) { * @return a collection containing all of the operand {@link Node}s of this {@link Node}, as * well as (transitively) the operands of its operands */ + @Pure public Collection getTransitiveOperands() { ArrayDeque operands = new ArrayDeque<>(getOperands()); ArrayDeque transitiveOperands = new ArrayDeque<>(operands.size()); @@ -157,4 +188,29 @@ public Collection getTransitiveOperands() { } return transitiveOperands; } + + /** + * Returns a verbose string representation of this, useful for debugging. + * + * @return a printed representation of this + */ + @Pure + public String toStringDebug() { + return String.format("%s [%s]", this, this.getClassAndUid()); + } + + /** + * Returns a verbose string representation of a collection of nodes, useful for debugging.. + * + * @param nodes a collection of nodes to format + * @return a printed representation of the given collection + */ + @Pure + public static String nodeCollectionToString(Collection nodes) { + StringJoiner result = new StringJoiner(", ", "[", "]"); + for (Node n : nodes) { + result.add(n.toStringDebug()); + } + return result.toString(); + } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NodeVisitor.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NodeVisitor.java index 111b3a690c8e..20211a9619ba 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NodeVisitor.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NodeVisitor.java @@ -66,9 +66,6 @@ public interface NodeVisitor { R visitBitwiseXor(BitwiseXorNode n, P p); - // Compound assignments - R visitStringConcatenateAssignment(StringConcatenateAssignmentNode n, P p); - // Comparison operations R visitLessThan(LessThanNode n, P p); @@ -91,6 +88,8 @@ public interface NodeVisitor { R visitTernaryExpression(TernaryExpressionNode n, P p); + R visitSwitchExpressionNode(SwitchExpressionNode n, P p); + R visitAssignment(AssignmentNode n, P p); R visitLocalVariable(LocalVariableNode n, P p); @@ -103,9 +102,9 @@ public interface NodeVisitor { R visitArrayAccess(ArrayAccessNode n, P p); - R visitImplicitThisLiteral(ImplicitThisLiteralNode n, P p); + R visitImplicitThis(ImplicitThisNode n, P p); - R visitExplicitThisLiteral(ExplicitThisLiteralNode n, P p); + R visitExplicitThis(ExplicitThisNode n, P p); R visitSuper(SuperNode n, P p); @@ -115,10 +114,10 @@ public interface NodeVisitor { R visitStringConversion(StringConversionNode n, P p); - R visitNarrowingConversion(NarrowingConversionNode n, P p); - R visitWideningConversion(WideningConversionNode n, P p); + R visitNarrowingConversion(NarrowingConversionNode n, P p); + R visitInstanceOf(InstanceOfNode n, P p); R visitTypeCast(TypeCastNode n, P p); @@ -159,6 +158,31 @@ public interface NodeVisitor { // Marker nodes R visitMarker(MarkerNode n, P p); - // Anonymous/inner/nested class declaration within a method + /** + * Visits an anonymous/inner/nested class declaration within a method. + * + * @param classDeclarationNode the {@link ClassDeclarationNode} to be visited + * @param p the argument for the operation implemented by this visitor + * @return the return value of the operation implemented by this visitor + */ R visitClassDeclaration(ClassDeclarationNode classDeclarationNode, P p); + + /** + * Visits an expression that is used as a statement. This node is a marker after the expression + * node(s). + * + * @param n the {@link ExpressionStatementNode} to be visited + * @param p the argument for the operation implemented by this visitor + * @return the return value of the operation implemented by this visitor + */ + R visitExpressionStatement(ExpressionStatementNode n, P p); + + /** + * Visits a deconstructor pattern node. + * + * @param n the {@link DeconstructorPatternNode} to be visited + * @param p the argument for the operation implemented by this visitor + * @return the return value of the operation implemented by this visitor + */ + R visitDeconstructorPattern(DeconstructorPatternNode n, P p); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NotEqualNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NotEqualNode.java index a17d264879ec..b7c2f5ed0ec8 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NotEqualNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NotEqualNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the not equal comparison: * @@ -14,9 +16,16 @@ */ public class NotEqualNode extends BinaryOperationNode { + /** + * Constructs a {@link NotEqualNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public NotEqualNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.NOT_EQUAL_TO; + assert tree.getKind() == Tree.Kind.NOT_EQUAL_TO; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NullChkNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NullChkNode.java index 51384b05b3d8..386bc977fd0a 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NullChkNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NullChkNode.java @@ -1,12 +1,14 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Collection; import java.util.Collections; import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; /** * A node for the unary 'nullchk' operation (generated by the Java compiler): @@ -16,13 +18,21 @@ *

      */ public class NullChkNode extends Node { - + /** The entire tree of the null check */ protected final Tree tree; + + /** The operand of the null check */ protected final Node operand; + /** + * Constructs a {@link NullChkNode}. + * + * @param tree the nullchk tree + * @param operand the operand of the null check + */ public NullChkNode(Tree tree, Node operand) { super(TreeUtils.typeOf(tree)); - assert tree.getKind() == Kind.OTHER; + assert tree.getKind() == Tree.Kind.OTHER; this.tree = tree; this.operand = operand; } @@ -61,6 +71,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.singletonList(getOperand()); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NullLiteralNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NullLiteralNode.java index 18206dde1a25..bf83ae785e4d 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NullLiteralNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NullLiteralNode.java @@ -2,9 +2,12 @@ import com.sun.source.tree.LiteralTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for the null literal. @@ -37,7 +40,9 @@ public R accept(NodeVisitor visitor, P p) { @Override public boolean equals(@Nullable Object obj) { - // test that obj is a NullLiteralNode + if (this == obj) { + return true; + } if (!(obj instanceof NullLiteralNode)) { return false; } @@ -46,6 +51,7 @@ public boolean equals(@Nullable Object obj) { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalAdditionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalAdditionNode.java index 91e1287b1585..fd26ab1c3cc7 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalAdditionNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalAdditionNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the numerical addition: * @@ -14,9 +16,16 @@ */ public class NumericalAdditionNode extends BinaryOperationNode { + /** + * Constructs a {@link NumericalAdditionNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public NumericalAdditionNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.PLUS || tree.getKind() == Kind.PLUS_ASSIGNMENT; + assert tree.getKind() == Tree.Kind.PLUS; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMinusNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMinusNode.java index a91c482cd1cb..0a7a02f5949d 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMinusNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMinusNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; -import com.sun.source.tree.Tree.Kind; +import com.sun.source.tree.Tree; import com.sun.source.tree.UnaryTree; -import java.util.Objects; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the unary minus operation: * @@ -14,9 +16,15 @@ */ public class NumericalMinusNode extends UnaryOperationNode { + /** + * Constructs a {@link NumericalMinusNode}. + * + * @param tree the unary tree + * @param operand the operand of the unary minus + */ public NumericalMinusNode(UnaryTree tree, Node operand) { super(tree, operand); - assert tree.getKind() == Kind.UNARY_MINUS; + assert tree.getKind() == Tree.Kind.UNARY_MINUS; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMultiplicationNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMultiplicationNode.java index 853ecbc4efe8..a1abe1c8403f 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMultiplicationNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalMultiplicationNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the numerical multiplication: * @@ -14,9 +16,16 @@ */ public class NumericalMultiplicationNode extends BinaryOperationNode { + /** + * Constructs a {@link NumericalMultiplicationNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public NumericalMultiplicationNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.MULTIPLY; + assert tree.getKind() == Tree.Kind.MULTIPLY; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalPlusNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalPlusNode.java index 4198b2f659e1..6f611d97cbe8 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalPlusNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalPlusNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; -import com.sun.source.tree.Tree.Kind; +import com.sun.source.tree.Tree; import com.sun.source.tree.UnaryTree; -import java.util.Objects; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the unary plus operation: * @@ -14,9 +16,15 @@ */ public class NumericalPlusNode extends UnaryOperationNode { + /** + * Constructs a {@link NumericalPlusNode}. + * + * @param tree the tree of the whole operation + * @param operand the operand of the operation + */ public NumericalPlusNode(UnaryTree tree, Node operand) { super(tree, operand); - assert tree.getKind() == Kind.UNARY_PLUS; + assert tree.getKind() == Tree.Kind.UNARY_PLUS; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalSubtractionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalSubtractionNode.java index 97d83debe948..af72519ffe7e 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalSubtractionNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/NumericalSubtractionNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for the numerical subtraction: * @@ -14,9 +16,16 @@ */ public class NumericalSubtractionNode extends BinaryOperationNode { + /** + * Constructs a {@link NumericalSubtractionNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public NumericalSubtractionNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.MINUS; + assert tree.getKind() == Tree.Kind.MINUS; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ObjectCreationNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ObjectCreationNode.java index 69cd8c1c4b6e..a14856f89d00 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ObjectCreationNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ObjectCreationNode.java @@ -1,58 +1,148 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.NewClassTree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.Pure; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TreeUtils; +import org.plumelib.util.StringsPlume; + import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; /** - * A node for new object creation. + * A node for a new object creation. * *
      - *   new constructor(arg1, arg2, ...)
      + *   new typeToInstantiate(arg1, arg2, ...)
      + *   enclosingExpression.new typeToInstantiate(arg1, arg2, ...)
      + *   enclosingExpression.new <Ts>typeToInstantiate(arg1, arg2, ...)
        * 
      + * + *

      We use the term "typeToInstantiate" to represent what is called the "identifier" in {@link + * NewClassTree} and what is called "ClassOrInterfaceTypeToInstantiate" in the + * "ClassInstanceCreationExpression" in the JLS. The former term "identifier" is misleading, as this + * can be a type with type arguments. The latter term "ClassOrInterfaceTypeToInstantiate" is rather + * long and we shortened it to "typeToInstantiate". + * + *

      Class type arguments can be accessed through the "typeToInstantiate" node. To access + * constructor type arguments one needs to use the {@link NewClassTree}. */ public class ObjectCreationNode extends Node { + /** The tree for the object creation. */ protected final NewClassTree tree; - protected final Node constructor; + + /** The enclosing expression of the object creation or null. */ + protected final @Nullable Node enclosingExpression; + + /** + * The type to instantiate node of the object creation. A non-generic typeToInstantiate node + * will refer to a {@link ClassNameNode}, while a generic typeToInstantiate node will refer to a + * {@link ParameterizedTypeNode}. + */ + protected final Node typeToInstantiate; + + /** The arguments of the object creation. */ protected final List arguments; - // Class body for anonymous classes, otherwise null. + /** Class body for anonymous classes, otherwise null. */ protected final @Nullable ClassDeclarationNode classbody; + /** + * Constructs a {@link ObjectCreationNode}. + * + * @param tree the NewClassTree + * @param enclosingExpr the enclosing expression Node if it exists, or null + * @param typeToInstantiate the typeToInstantiate node + * @param arguments the passed arguments + * @param classbody the ClassDeclarationNode + */ public ObjectCreationNode( NewClassTree tree, - Node constructor, + @Nullable Node enclosingExpr, + Node typeToInstantiate, List arguments, @Nullable ClassDeclarationNode classbody) { super(TreeUtils.typeOf(tree)); this.tree = tree; - this.constructor = constructor; + this.enclosingExpression = enclosingExpr; + this.typeToInstantiate = typeToInstantiate; this.arguments = arguments; this.classbody = classbody; } + /** + * Returns the constructor node. + * + * @return the constructor node + * @deprecated use {@link #getTypeToInstantiate()} + */ + @Pure + @Deprecated // 2023-06-06 public Node getConstructor() { - return constructor; + return typeToInstantiate; + } + + /** + * Returns the typeToInstantiate node. A non-generic typeToInstantiate node can refer to a + * {@link ClassNameNode}, while a generic typeToInstantiate node can refer to a {@link + * ParameterizedTypeNode}. + * + * @return the typeToInstantiate node + */ + @Pure + public Node getTypeToInstantiate() { + return typeToInstantiate; } + /** + * Returns the explicit arguments to the object creation. + * + * @return the arguments + */ + @Pure public List getArguments() { return arguments; } + /** + * Returns the i-th explicit argument to the object creation. + * + * @param i the index of the argument + * @return the argument + */ + @Pure public Node getArgument(int i) { return arguments.get(i); } + /** + * Returns the enclosing expression node, which only exists if it is an inner class + * instantiation. + * + * @return the enclosing type expression node + */ + @Pure + public @Nullable Node getEnclosingExpression() { + return enclosingExpression; + } + + /** + * Returns the classbody. + * + * @return the classbody + */ + @Pure public @Nullable Node getClassBody() { return classbody; } @Override + @Pure public NewClassTree getTree() { return tree; } @@ -63,17 +153,20 @@ public R accept(NodeVisitor visitor, P p) { } @Override + @SideEffectFree public String toString() { StringBuilder sb = new StringBuilder(); - sb.append("new " + constructor + "("); - boolean needComma = false; - for (Node arg : arguments) { - if (needComma) { - sb.append(", "); - } - sb.append(arg); - needComma = true; + if (enclosingExpression != null) { + sb.append(enclosingExpression + "."); } + sb.append("new "); + if (!tree.getTypeArguments().isEmpty()) { + sb.append("<"); + sb.append(StringsPlume.join(", ", tree.getTypeArguments())); + sb.append(">"); + } + sb.append(typeToInstantiate + "("); + sb.append(StringsPlume.join(", ", arguments)); sb.append(")"); if (classbody != null) { // TODO: maybe this can be done nicer... @@ -84,28 +177,38 @@ public String toString() { } @Override + @Pure public boolean equals(@Nullable Object obj) { if (!(obj instanceof ObjectCreationNode)) { return false; } ObjectCreationNode other = (ObjectCreationNode) obj; - if (constructor == null && other.getConstructor() != null) { + // TODO: See issue 470. + if (typeToInstantiate == null && other.getTypeToInstantiate() != null) { return false; } - return getConstructor().equals(other.getConstructor()) - && getArguments().equals(other.getArguments()); + return getTypeToInstantiate().equals(other.getTypeToInstantiate()) + && getArguments().equals(other.getArguments()) + && (getEnclosingExpression() == null + ? null == other.getEnclosingExpression() + : getEnclosingExpression().equals(other.getEnclosingExpression())); } @Override + @SideEffectFree public int hashCode() { - return Objects.hash(constructor, arguments); + return Objects.hash(enclosingExpression, typeToInstantiate, arguments); } @Override + @SideEffectFree public Collection getOperands() { - ArrayList list = new ArrayList<>(1 + arguments.size()); - list.add(constructor); + ArrayList list = new ArrayList<>(2 + arguments.size()); + if (enclosingExpression != null) { + list.add(enclosingExpression); + } + list.add(typeToInstantiate); list.addAll(arguments); return list; } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/PackageNameNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/PackageNameNode.java index 17d1e369bde6..6f8347a72cb1 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/PackageNameNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/PackageNameNode.java @@ -3,12 +3,17 @@ import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Collection; import java.util.Collections; import java.util.Objects; -import javax.lang.model.element.Element; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; + +import javax.lang.model.element.PackageElement; /** * A node representing a package name used in an expression such as a constructor invocation. @@ -19,9 +24,11 @@ */ public class PackageNameNode extends Node { + /** The package name, which is an IdentifierTree or a MemberSelectTree. */ protected final Tree tree; + /** The package named by this node. */ - protected final Element element; + protected final PackageElement element; /** The parent name, if any. */ protected final @Nullable PackageNameNode parent; @@ -30,7 +37,11 @@ public PackageNameNode(IdentifierTree tree) { super(TreeUtils.typeOf(tree)); this.tree = tree; assert TreeUtils.isUseOfElement(tree) : "@AssumeAssertion(nullness): tree kind"; - this.element = TreeUtils.elementFromUse(tree); + PackageElement element = (PackageElement) TreeUtils.elementFromUse(tree); + if (element == null) { + throw new BugInCF("null element for %s [%s]", tree, tree.getClass()); + } + this.element = element; this.parent = null; } @@ -38,11 +49,20 @@ public PackageNameNode(MemberSelectTree tree, PackageNameNode parent) { super(TreeUtils.typeOf(tree)); this.tree = tree; assert TreeUtils.isUseOfElement(tree) : "@AssumeAssertion(nullness): tree kind"; - this.element = TreeUtils.elementFromUse(tree); + PackageElement element = (PackageElement) TreeUtils.elementFromUse(tree); + if (element == null) { + throw new BugInCF("null element for %s [%s]", tree, tree.getClass()); + } + this.element = element; this.parent = parent; } - public Element getElement() { + /** + * Returns the element for this package. + * + * @return the element for this package + */ + public PackageElement getElement() { return element; } @@ -82,6 +102,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { if (parent == null) { return Collections.emptyList(); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ParameterizedTypeNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ParameterizedTypeNode.java index 7af50e100cc9..b5931f6de7f5 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ParameterizedTypeNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ParameterizedTypeNode.java @@ -1,12 +1,14 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.ParameterizedTypeTree; -import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Collection; import java.util.Collections; import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; /** * A node for a parameterized type occurring in an expression: @@ -21,16 +23,15 @@ */ public class ParameterizedTypeNode extends Node { - protected final Tree tree; + protected final ParameterizedTypeTree tree; - public ParameterizedTypeNode(Tree t) { + public ParameterizedTypeNode(ParameterizedTypeTree t) { super(TreeUtils.typeOf(t)); - assert t instanceof ParameterizedTypeTree; tree = t; } @Override - public Tree getTree() { + public ParameterizedTypeTree getTree() { return tree; } @@ -59,6 +60,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/PrimitiveTypeNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/PrimitiveTypeNode.java index 844251eeef19..a69081a99ab4 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/PrimitiveTypeNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/PrimitiveTypeNode.java @@ -1,13 +1,16 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.PrimitiveTypeTree; -import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Collection; import java.util.Collections; import java.util.Objects; + import javax.lang.model.util.Types; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; /** * A node representing a primitive type used in an expression such as a field access. @@ -28,7 +31,7 @@ public PrimitiveTypeNode(PrimitiveTypeTree tree, Types types) { } @Override - public Tree getTree() { + public PrimitiveTypeTree getTree() { return tree; } @@ -57,6 +60,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ReturnNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ReturnNode.java index 326136e9a8ce..8ede588a75b2 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ReturnNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ReturnNode.java @@ -1,17 +1,16 @@ package org.checkerframework.dataflow.cfg.node; -import com.sun.source.tree.LambdaExpressionTree; -import com.sun.source.tree.MethodTree; import com.sun.source.tree.ReturnTree; -import com.sun.tools.javac.code.Symbol.MethodSymbol; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; import java.util.Objects; + import javax.lang.model.type.TypeKind; import javax.lang.model.util.Types; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.cfg.node.AssignmentContext.LambdaReturnContext; -import org.checkerframework.dataflow.cfg.node.AssignmentContext.MethodReturnContext; /** * A node for a return statement: @@ -20,33 +19,28 @@ * return * return expression * + * + * No ReturnNode is created for implicit return statements. */ public class ReturnNode extends Node { - protected final ReturnTree tree; - protected final @Nullable Node result; + /** The return tree. */ + protected final ReturnTree returnTree; - public ReturnNode(ReturnTree t, @Nullable Node result, Types types, MethodTree methodTree) { - super(types.getNoType(TypeKind.NONE)); - this.result = result; - tree = t; - if (result != null) { - result.setAssignmentContext(new MethodReturnContext(methodTree)); - } - } + /** The node of the returned expression. */ + protected final @Nullable Node result; - public ReturnNode( - ReturnTree t, - @Nullable Node result, - Types types, - LambdaExpressionTree lambda, - MethodSymbol methodSymbol) { + /** + * Creates a node for the given return statement. + * + * @param returnTree return tree + * @param result the returned expression + * @param types types util + */ + public ReturnNode(ReturnTree returnTree, @Nullable Node result, Types types) { super(types.getNoType(TypeKind.NONE)); this.result = result; - tree = t; - if (result != null) { - result.setAssignmentContext(new LambdaReturnContext(methodSymbol)); - } + this.returnTree = returnTree; } /** The result of the return node, {@code null} otherwise. */ @@ -56,7 +50,7 @@ public ReturnNode( @Override public ReturnTree getTree() { - return tree; + return returnTree; } @Override @@ -87,6 +81,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { if (result == null) { return Collections.emptyList(); diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ShortLiteralNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ShortLiteralNode.java index 8506c4133f4e..1368d5757356 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ShortLiteralNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ShortLiteralNode.java @@ -2,9 +2,12 @@ import com.sun.source.tree.LiteralTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for a short literal. For example: @@ -15,9 +18,9 @@ * * * Java source and the AST representation do not have "short" literals. They have integer literals - * that may be narrowed to shorts depending on context. If we use explicit NarrowingConversionNodes, - * do we need ShortLiteralNodes too? TODO: Decide this question. + * that may be narrowed to shorts depending on context. */ +// TODO: If we use explicit NarrowingConversionNodes, do we need ShortLiteralNodes too? public class ShortLiteralNode extends ValueLiteralNode { /** @@ -51,6 +54,7 @@ public boolean equals(@Nullable Object obj) { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SignedRightShiftNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SignedRightShiftNode.java index e28fb5397a84..227104118503 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SignedRightShiftNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SignedRightShiftNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for bitwise right shift operations with sign extension: * @@ -14,9 +16,16 @@ */ public class SignedRightShiftNode extends BinaryOperationNode { + /** + * Constructs a {@link SignedRightShiftNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public SignedRightShiftNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.RIGHT_SHIFT; + assert tree.getKind() == Tree.Kind.RIGHT_SHIFT; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateAssignmentNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateAssignmentNode.java deleted file mode 100644 index c559f196d5d0..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateAssignmentNode.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.checkerframework.dataflow.cfg.node; - -import com.sun.source.tree.Tree; -import com.sun.source.tree.Tree.Kind; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; - -/** - * A node for the string concatenation compound assignment: - * - *

      - *   variable += expression
      - * 
      - */ -public class StringConcatenateAssignmentNode extends Node { - protected final Tree tree; - protected final Node left; - protected final Node right; - - public StringConcatenateAssignmentNode(Tree tree, Node left, Node right) { - super(TreeUtils.typeOf(tree)); - assert tree.getKind() == Kind.PLUS_ASSIGNMENT; - this.tree = tree; - this.left = left; - this.right = right; - } - - public Node getLeftOperand() { - return left; - } - - public Node getRightOperand() { - return right; - } - - @Override - public Tree getTree() { - return tree; - } - - @Override - public R accept(NodeVisitor visitor, P p) { - return visitor.visitStringConcatenateAssignment(this, p); - } - - @Override - public Collection getOperands() { - ArrayList list = new ArrayList<>(2); - list.add(getLeftOperand()); - list.add(getRightOperand()); - return list; - } - - @Override - public String toString() { - return "(" + getLeftOperand() + " += " + getRightOperand() + ")"; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (obj == null || !(obj instanceof StringConcatenateAssignmentNode)) { - return false; - } - StringConcatenateAssignmentNode other = (StringConcatenateAssignmentNode) obj; - return getLeftOperand().equals(other.getLeftOperand()) - && getRightOperand().equals(other.getRightOperand()); - } - - @Override - public int hashCode() { - return Objects.hash(getLeftOperand(), getRightOperand()); - } -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateNode.java index 50cfb4118939..c6615ad56056 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConcatenateNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for string concatenation: * @@ -14,9 +16,16 @@ */ public class StringConcatenateNode extends BinaryOperationNode { + /** + * Constructs a {@link StringConcatenateNode}. + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public StringConcatenateNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.PLUS; + assert tree.getKind() == Tree.Kind.PLUS; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConversionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConversionNode.java index edfee1eadf1a..4d55bb0171ab 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConversionNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringConversionNode.java @@ -1,11 +1,15 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; import java.util.Objects; + import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for the string conversion operation. See JLS 5.1.11 for the definition of string @@ -68,6 +72,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.singletonList(getOperand()); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringLiteralNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringLiteralNode.java index c11f10c31df7..6613dcbedb76 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringLiteralNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/StringLiteralNode.java @@ -2,9 +2,12 @@ import com.sun.source.tree.LiteralTree; import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for an string literal. For example: @@ -46,6 +49,7 @@ public boolean equals(@Nullable Object obj) { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SuperNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SuperNode.java index 4d012c5ffda3..05ef67c0eb41 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SuperNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SuperNode.java @@ -1,13 +1,14 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.IdentifierTree; -import com.sun.source.tree.Tree; -import java.util.Collection; -import java.util.Collections; -import java.util.Objects; + import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; import org.checkerframework.javacutil.TreeUtils; +import java.util.Collection; +import java.util.Collections; + /** * A node for a reference to 'super'. * @@ -17,16 +18,16 @@ */ public class SuperNode extends Node { - protected final Tree tree; + protected final IdentifierTree tree; - public SuperNode(Tree t) { + public SuperNode(IdentifierTree t) { super(TreeUtils.typeOf(t)); - assert t instanceof IdentifierTree && ((IdentifierTree) t).getName().contentEquals("super"); + assert t.getName().contentEquals("super"); tree = t; } @Override - public Tree getTree() { + public IdentifierTree getTree() { return tree; } @@ -35,13 +36,9 @@ public R accept(NodeVisitor visitor, P p) { return visitor.visitSuper(this, p); } - public String getName() { - return "super"; - } - @Override public String toString() { - return getName(); + return "super"; } @Override @@ -51,10 +48,11 @@ public boolean equals(@Nullable Object obj) { @Override public int hashCode() { - return Objects.hash(getName()); + return 109801370; // Objects.hash("super"); } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SwitchExpressionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SwitchExpressionNode.java new file mode 100644 index 000000000000..ef2a1dea1b9b --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SwitchExpressionNode.java @@ -0,0 +1,104 @@ +package org.checkerframework.dataflow.cfg.node; + +import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.SystemUtil; + +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; + +import javax.lang.model.type.TypeMirror; + +/** A node for a switch expression. */ +public class SwitchExpressionNode extends Node { + + /** The {@code SwitchExpressionTree} corresponding to this node. */ + private final Tree switchExpressionTree; + + /** + * This is a variable created by dataflow to which each result expression of the switch + * expression is assigned. Its value should be used for the value of the switch expression. + */ + private final LocalVariableNode switchExpressionVar; + + /** + * Creates a new SwitchExpressionNode. + * + * @param type the type of the node + * @param switchExpressionTree the {@code SwitchExpressionTree} for this node + * @param switchExpressionVar a variable created by dataflow to which each result expression of + * the switch expression is assigned. Its value should be used for the value of the switch + * expression + */ + public SwitchExpressionNode( + TypeMirror type, Tree switchExpressionTree, LocalVariableNode switchExpressionVar) { + super(type); + + // TODO: use JCP to add version-specific behavior + if (SystemUtil.jreVersion < 14 + || !switchExpressionTree.getKind().name().equals("SWITCH_EXPRESSION")) { + throw new BugInCF( + "switchExpressionTree is not a SwitchExpressionTree found tree with kind %s" + + " instead.", + switchExpressionTree.getKind()); + } + + this.switchExpressionTree = switchExpressionTree; + this.switchExpressionVar = switchExpressionVar; + } + + @Override + public Tree getTree() { + return switchExpressionTree; + } + + /** + * This is a variable created by dataflow to which each result expression of the switch + * expression is assigned. Its value should be used for the value of the switch expression. + * + * @return the variable for this switch expression + */ + public LocalVariableNode getSwitchExpressionVar() { + return switchExpressionVar; + } + + @Override + public R accept(NodeVisitor visitor, P p) { + return visitor.visitSwitchExpressionNode(this, p); + } + + @Override + @SideEffectFree + public Collection getOperands() { + return Collections.singleton(switchExpressionVar); + } + + @Override + public String toString() { + return "SwitchExpressionNode{" + + "switchExpressionTree=" + + switchExpressionTree + + ", switchExpressionVar=" + + switchExpressionVar + + '}'; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof SwitchExpressionNode)) { + return false; + } + SwitchExpressionNode other = (SwitchExpressionNode) obj; + return getTree().equals(other.getTree()) + && getSwitchExpressionVar().equals(other.getSwitchExpressionVar()); + } + + @Override + public int hashCode() { + return Objects.hash(getTree(), getSwitchExpressionVar()); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SynchronizedNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SynchronizedNode.java index d8493f9eaa26..fd636d47d65f 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SynchronizedNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/SynchronizedNode.java @@ -1,26 +1,30 @@ package org.checkerframework.dataflow.cfg.node; -import com.sun.source.tree.Tree; +import com.sun.source.tree.SynchronizedTree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; import java.util.Objects; + import javax.lang.model.type.TypeKind; import javax.lang.model.util.Types; -import org.checkerframework.checker.nullness.qual.Nullable; /** - * This represents the start and end of synchronized code block. If startOfBlock == true it is the + * This represents the start and end of a synchronized code block. If startOfBlock == true it is the * node preceding a synchronized code block. Otherwise it is the node immediately after a * synchronized code block. */ public class SynchronizedNode extends Node { - protected final @Nullable Tree tree; + protected final SynchronizedTree tree; protected final Node expression; protected final boolean startOfBlock; public SynchronizedNode( - @Nullable Tree tree, Node expression, boolean startOfBlock, Types types) { + SynchronizedTree tree, Node expression, boolean startOfBlock, Types types) { super(types.getNoType(TypeKind.NONE)); this.tree = tree; this.expression = expression; @@ -28,7 +32,7 @@ public SynchronizedNode( } @Override - public @Nullable Tree getTree() { + public SynchronizedTree getTree() { return tree; } @@ -71,6 +75,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/TernaryExpressionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/TernaryExpressionNode.java index ee9786a8c3d9..8301cfd9460d 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/TernaryExpressionNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/TernaryExpressionNode.java @@ -1,13 +1,15 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.ConditionalExpressionTree; -import com.sun.source.tree.Tree.Kind; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Objects; + import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; import org.checkerframework.javacutil.TreeUtils; +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; + /** * A node for a conditional expression: * @@ -17,33 +19,86 @@ */ public class TernaryExpressionNode extends Node { + /** The {@code ConditionalExpressionTree} corresponding to this node */ protected final ConditionalExpressionTree tree; + + /** Node representing the condition checked by the expression */ protected final Node condition; + + /** Node representing the "then" case of the expression */ protected final Node thenOperand; + + /** Node representing the "else" case of the expression */ protected final Node elseOperand; + /** + * This is a variable created by dataflow to which each case expression of the ternary + * expression is assigned. Its value should be used for the value of the switch expression. + */ + private final LocalVariableNode ternaryExpressionVar; + + /** + * Creates a new TernaryExpressionNode. + * + * @param tree the {@code ConditionalExpressionTree} for the node + * @param condition node representing the condition checked by the expression + * @param thenOperand node representing the "then" case of the expression + * @param elseOperand node representing the "else" case of the expression + * @param ternaryExpressionVar a variable created by dataflow to which each case expression of + * the ternary expression is assigned. Its value should be used for the value of the switch + * expression. + */ public TernaryExpressionNode( - ConditionalExpressionTree tree, Node condition, Node thenOperand, Node elseOperand) { + ConditionalExpressionTree tree, + Node condition, + Node thenOperand, + Node elseOperand, + LocalVariableNode ternaryExpressionVar) { super(TreeUtils.typeOf(tree)); - assert tree.getKind() == Kind.CONDITIONAL_EXPRESSION; this.tree = tree; this.condition = condition; this.thenOperand = thenOperand; this.elseOperand = elseOperand; + this.ternaryExpressionVar = ternaryExpressionVar; } + /** + * Gets the node representing the conditional operand for this node + * + * @return the condition operand node + */ public Node getConditionOperand() { return condition; } + /** + * Gets the node representing the "then" operand for this node + * + * @return the "then" operand node + */ public Node getThenOperand() { return thenOperand; } + /** + * Gets the node representing the "else" operand for this node + * + * @return the "else" operand node + */ public Node getElseOperand() { return elseOperand; } + /** + * This is a variable created by dataflow to which each case expression of the ternary + * expression is assigned. Its value should be used for the value of the switch expression. + * + * @return the variable for this ternary expression + */ + public LocalVariableNode getTernaryExpressionVar() { + return ternaryExpressionVar; + } + @Override public ConditionalExpressionTree getTree() { return tree; @@ -82,11 +137,8 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { - ArrayList list = new ArrayList<>(3); - list.add(getConditionOperand()); - list.add(getThenOperand()); - list.add(getElseOperand()); - return list; + return Arrays.asList(getConditionOperand(), getThenOperand(), getElseOperand()); } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ThisLiteralNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ThisLiteralNode.java deleted file mode 100644 index a6fc55fcff08..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ThisLiteralNode.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.checkerframework.dataflow.cfg.node; - -import java.util.Collection; -import java.util.Collections; -import java.util.Objects; -import javax.lang.model.type.TypeMirror; -import org.checkerframework.checker.nullness.qual.Nullable; - -/** - * A node for a reference to 'this', either implicit or explicit. - * - *
      - *   this
      - * 
      - */ -public abstract class ThisLiteralNode extends Node { - - protected ThisLiteralNode(TypeMirror type) { - super(type); - } - - public String getName() { - return "this"; - } - - @Override - public boolean equals(@Nullable Object obj) { - return obj instanceof ThisLiteralNode; - } - - @Override - public int hashCode() { - return Objects.hash(getName()); - } - - @Override - public Collection getOperands() { - return Collections.emptyList(); - } -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ThisNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ThisNode.java new file mode 100644 index 000000000000..e7d5f3c589b1 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ThisNode.java @@ -0,0 +1,39 @@ +package org.checkerframework.dataflow.cfg.node; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + +import java.util.Collection; +import java.util.Collections; + +import javax.lang.model.type.TypeMirror; + +/** + * A node for a reference to 'this', either implicit or explicit. + * + *
      + *   this
      + * 
      + */ +public abstract class ThisNode extends Node { + + protected ThisNode(TypeMirror type) { + super(type); + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof ThisNode; + } + + @Override + public int hashCode() { + return 3559101; // Objects.hash("this"); + } + + @Override + @SideEffectFree + public Collection getOperands() { + return Collections.emptyList(); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ThrowNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ThrowNode.java index 0a03bc57c7de..3adc6307d065 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ThrowNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ThrowNode.java @@ -1,13 +1,16 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.ThrowTree; -import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; import java.util.Objects; + import javax.lang.model.type.TypeKind; import javax.lang.model.util.Types; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for exception throws: @@ -32,7 +35,7 @@ public Node getExpression() { } @Override - public Tree getTree() { + public ThrowTree getTree() { return tree; } @@ -61,6 +64,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.singletonList(expression); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/TypeCastNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/TypeCastNode.java index b72ebcb3b9d8..55e359498279 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/TypeCastNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/TypeCastNode.java @@ -1,12 +1,16 @@ package org.checkerframework.dataflow.cfg.node; -import com.sun.source.tree.Tree; +import com.sun.source.tree.TypeCastTree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; + import java.util.Collection; import java.util.Collections; import java.util.Objects; + import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; -import org.checkerframework.checker.nullness.qual.Nullable; /** * A node for the cast operator: @@ -15,13 +19,13 @@ */ public class TypeCastNode extends Node { - protected final Tree tree; + protected final TypeCastTree tree; protected final Node operand; /** For Types.isSameType. */ protected final Types types; - public TypeCastNode(Tree tree, Node operand, TypeMirror type, Types types) { + public TypeCastNode(TypeCastTree tree, Node operand, TypeMirror type, Types types) { super(type); this.tree = tree; this.operand = operand; @@ -33,7 +37,7 @@ public Node getOperand() { } @Override - public Tree getTree() { + public TypeCastTree getTree() { return tree; } @@ -63,6 +67,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.singletonList(getOperand()); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/UnaryOperationNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/UnaryOperationNode.java index f3ad94472a95..37921a065315 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/UnaryOperationNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/UnaryOperationNode.java @@ -1,9 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.UnaryTree; + +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Collection; import java.util.Collections; -import org.checkerframework.javacutil.TreeUtils; /** * A node for a postfix or an unary expression. @@ -37,6 +40,7 @@ public UnaryTree getTree() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.singletonList(getOperand()); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/UnsignedRightShiftNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/UnsignedRightShiftNode.java index 505b66a580c9..661de7e83a8a 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/UnsignedRightShiftNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/UnsignedRightShiftNode.java @@ -1,10 +1,12 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.BinaryTree; -import com.sun.source.tree.Tree.Kind; -import java.util.Objects; +import com.sun.source.tree.Tree; + import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Objects; + /** * A node for bitwise right shift operations with zero extension: * @@ -14,9 +16,16 @@ */ public class UnsignedRightShiftNode extends BinaryOperationNode { + /** + * Constructs an {@link UnsignedRightShiftNode} + * + * @param tree the binary tree + * @param left the left operand + * @param right the right operand + */ public UnsignedRightShiftNode(BinaryTree tree, Node left, Node right) { super(tree, left, right); - assert tree.getKind() == Kind.UNSIGNED_RIGHT_SHIFT; + assert tree.getKind() == Tree.Kind.UNSIGNED_RIGHT_SHIFT; } @Override diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ValueLiteralNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ValueLiteralNode.java index c09ab3cb33e7..b412449bd0ea 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ValueLiteralNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/ValueLiteralNode.java @@ -1,11 +1,14 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.LiteralTree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Collection; import java.util.Collections; import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; /** * A node for a literals that have some form of value: @@ -48,9 +51,11 @@ public String toString() { return String.valueOf(getValue()); } - /** Compare the value of this nodes. */ @Override public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } if (!(obj instanceof ValueLiteralNode)) { return false; } @@ -67,6 +72,7 @@ public int hashCode() { } @Override + @SideEffectFree public Collection getOperands() { return Collections.emptyList(); } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/VariableDeclarationNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/VariableDeclarationNode.java index 6c229a8dabe2..3fe7294cd571 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/VariableDeclarationNode.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/VariableDeclarationNode.java @@ -1,14 +1,17 @@ package org.checkerframework.dataflow.cfg.node; import com.sun.source.tree.VariableTree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.qual.SideEffectFree; +import org.checkerframework.javacutil.TreeUtils; + import java.util.Collection; import java.util.Collections; import java.util.Objects; -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.javacutil.TreeUtils; /** - * A node for a local variable declaration: + * A node for a variable declaration, including local variables and fields: * *
        *   modifier type identifier;
      @@ -64,6 +67,7 @@ public int hashCode() {
           }
       
           @Override
      +    @SideEffectFree
           public Collection getOperands() {
               return Collections.emptyList();
           }
      diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/WideningConversionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/WideningConversionNode.java
      index fad859cc3924..07e5e031c70b 100644
      --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/WideningConversionNode.java
      +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/node/WideningConversionNode.java
      @@ -1,12 +1,16 @@
       package org.checkerframework.dataflow.cfg.node;
       
       import com.sun.source.tree.Tree;
      +
      +import org.checkerframework.checker.nullness.qual.Nullable;
      +import org.checkerframework.dataflow.qual.SideEffectFree;
      +import org.checkerframework.javacutil.TypesUtils;
      +
       import java.util.Collection;
       import java.util.Collections;
       import java.util.Objects;
      +
       import javax.lang.model.type.TypeMirror;
      -import org.checkerframework.checker.nullness.qual.Nullable;
      -import org.checkerframework.javacutil.TypesUtils;
       
       /**
        * A node for the widening primitive conversion operation. See JLS 5.1.2 for the definition of
      @@ -63,6 +67,7 @@ public int hashCode() {
           }
       
           @Override
      +    @SideEffectFree
           public Collection getOperands() {
               return Collections.singletonList(getOperand());
           }
      diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/playground/BusyExpressionPlayground.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/playground/BusyExpressionPlayground.java
      new file mode 100644
      index 000000000000..005a4c60d5e1
      --- /dev/null
      +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/playground/BusyExpressionPlayground.java
      @@ -0,0 +1,38 @@
      +package org.checkerframework.dataflow.cfg.playground;
      +
      +import org.checkerframework.dataflow.analysis.BackwardAnalysis;
      +import org.checkerframework.dataflow.analysis.BackwardAnalysisImpl;
      +import org.checkerframework.dataflow.analysis.UnusedAbstractValue;
      +import org.checkerframework.dataflow.busyexpr.BusyExprStore;
      +import org.checkerframework.dataflow.busyexpr.BusyExprTransfer;
      +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeLauncher;
      +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeOptions;
      +
      +/**
      + * The playground for busy expression analysis. As an example, try {@code
      + * dataflow/manual/examples/BusyExprSimple.java}.
      + */
      +public class BusyExpressionPlayground {
      +
      +    /** Class cannot be instantiated. */
      +    private BusyExpressionPlayground() {
      +        throw new AssertionError("Class BusyExpressionPlayground cannot be instantiated.");
      +    }
      +
      +    /**
      +     * Run busy expression analysis on a file.
      +     *
      +     * @param args command-line arguments
      +     */
      +    public static void main(String[] args) {
      +
      +        // Parse the arguments.
      +        CFGVisualizeOptions config = CFGVisualizeOptions.parseArgs(args);
      +
      +        // Run the analysis and create a PDF file
      +        BusyExprTransfer transfer = new BusyExprTransfer();
      +        BackwardAnalysis backwardAnalysis =
      +                new BackwardAnalysisImpl<>(transfer);
      +        CFGVisualizeLauncher.performAnalysis(config, backwardAnalysis);
      +    }
      +}
      diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/playground/ConstantPropagationPlayground.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/playground/ConstantPropagationPlayground.java
      index 308f6acf819e..9a901bf5fd07 100644
      --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/playground/ConstantPropagationPlayground.java
      +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/playground/ConstantPropagationPlayground.java
      @@ -2,28 +2,34 @@
       
       import org.checkerframework.dataflow.analysis.ForwardAnalysis;
       import org.checkerframework.dataflow.analysis.ForwardAnalysisImpl;
      -import org.checkerframework.dataflow.cfg.CFGVisualizeLauncher;
      +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeLauncher;
      +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeOptions;
       import org.checkerframework.dataflow.constantpropagation.Constant;
       import org.checkerframework.dataflow.constantpropagation.ConstantPropagationStore;
       import org.checkerframework.dataflow.constantpropagation.ConstantPropagationTransfer;
       
      +/** The playground for constant propagation analysis. */
       public class ConstantPropagationPlayground {
       
      -    /** Run constant propagation for a specific file and create a PDF of the CFG in the end. */
      +    /** Class cannot be instantiated. */
      +    private ConstantPropagationPlayground() {
      +        throw new AssertionError("Class ConstantPropagationPlayground cannot be instantiated.");
      +    }
      +
      +    /**
      +     * Run constant propagation analysis on a file.
      +     *
      +     * @param args command-line arguments
      +     */
           public static void main(String[] args) {
       
      -        /* Configuration: change as appropriate */
      -        String inputFile = "cfg-input.java"; // input file name and path
      -        String outputDir = "cfg"; // output directory
      -        String method = "test"; // name of the method to analyze
      -        String clazz = "Test"; // name of the class to consider
      +        // Parse the arguments.
      +        CFGVisualizeOptions config = CFGVisualizeOptions.parseArgs(args);
       
               // run the analysis and create a PDF file
               ConstantPropagationTransfer transfer = new ConstantPropagationTransfer();
               ForwardAnalysis
                       forwardAnalysis = new ForwardAnalysisImpl<>(transfer);
      -        CFGVisualizeLauncher cfgVisualizeLauncher = new CFGVisualizeLauncher();
      -        cfgVisualizeLauncher.generateDOTofCFG(
      -                inputFile, outputDir, method, clazz, true, false, forwardAnalysis);
      +        CFGVisualizeLauncher.performAnalysis(config, forwardAnalysis);
           }
       }
      diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/playground/LiveVariablePlayground.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/playground/LiveVariablePlayground.java
      index 0b209aa01a42..80682f13291e 100644
      --- a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/playground/LiveVariablePlayground.java
      +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/playground/LiveVariablePlayground.java
      @@ -2,33 +2,34 @@
       
       import org.checkerframework.dataflow.analysis.BackwardAnalysis;
       import org.checkerframework.dataflow.analysis.BackwardAnalysisImpl;
      -import org.checkerframework.dataflow.cfg.CFGVisualizeLauncher;
      +import org.checkerframework.dataflow.analysis.UnusedAbstractValue;
      +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeLauncher;
      +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeOptions;
       import org.checkerframework.dataflow.livevariable.LiveVarStore;
       import org.checkerframework.dataflow.livevariable.LiveVarTransfer;
      -import org.checkerframework.dataflow.livevariable.LiveVarValue;
       
       /** The playground of live variable analysis. */
       public class LiveVariablePlayground {
       
      +    /** Do not instantiate. */
      +    private LiveVariablePlayground() {
      +        throw new Error("do not instantiate");
      +    }
      +
           /**
      -     * Run live variable analysis for a specific file and create a PDF of the CFG in the end.
      +     * Run live variable analysis on a file.
            *
      -     * @param args input arguments
      +     * @param args command-line arguments
            */
           public static void main(String[] args) {
       
      -        /* Configuration: change as appropriate */
      -        String inputFile = "Test.java"; // input file name and path
      -        String outputDir = "cfg"; // output directory
      -        String method = "test"; // name of the method to analyze
      -        String clazz = "Test"; // name of the class to consider
      +        // Parse the arguments.
      +        CFGVisualizeOptions config = CFGVisualizeOptions.parseArgs(args);
       
               // Run the analysis and create a PDF file
               LiveVarTransfer transfer = new LiveVarTransfer();
      -        BackwardAnalysis backwardAnalysis =
      +        BackwardAnalysis backwardAnalysis =
                       new BackwardAnalysisImpl<>(transfer);
      -        CFGVisualizeLauncher cfgVisualizeLauncher = new CFGVisualizeLauncher();
      -        cfgVisualizeLauncher.generateDOTofCFG(
      -                inputFile, outputDir, method, clazz, true, true, backwardAnalysis);
      +        CFGVisualizeLauncher.performAnalysis(config, backwardAnalysis);
           }
       }
      diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/playground/ReachingDefinitionPlayground.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/playground/ReachingDefinitionPlayground.java
      new file mode 100644
      index 000000000000..04e52030dc51
      --- /dev/null
      +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/playground/ReachingDefinitionPlayground.java
      @@ -0,0 +1,38 @@
      +package org.checkerframework.dataflow.cfg.playground;
      +
      +import org.checkerframework.dataflow.analysis.ForwardAnalysis;
      +import org.checkerframework.dataflow.analysis.ForwardAnalysisImpl;
      +import org.checkerframework.dataflow.analysis.UnusedAbstractValue;
      +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeLauncher;
      +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeOptions;
      +import org.checkerframework.dataflow.reachingdef.ReachingDefinitionStore;
      +import org.checkerframework.dataflow.reachingdef.ReachingDefinitionTransfer;
      +
      +/**
      + * The playground for reaching definition analysis. As an example, try {@code
      + * dataflow/manual/examples/ReachSimple.java}.
      + */
      +public class ReachingDefinitionPlayground {
      +
      +    /** Class cannot be instantiated. */
      +    private ReachingDefinitionPlayground() {
      +        throw new AssertionError("Class ReachingDefinitionPlayground cannot be instantiated.");
      +    }
      +
      +    /**
      +     * Run reaching definition analysis on a file.
      +     *
      +     * @param args command-line arguments
      +     */
      +    public static void main(String[] args) {
      +
      +        // Parse the arguments.
      +        CFGVisualizeOptions config = CFGVisualizeOptions.parseArgs(args);
      +
      +        // Run the analysis and create a PDF file
      +        ReachingDefinitionTransfer transfer = new ReachingDefinitionTransfer();
      +        ForwardAnalysis
      +                forwardAnalysis = new ForwardAnalysisImpl<>(transfer);
      +        CFGVisualizeLauncher.performAnalysis(config, forwardAnalysis);
      +    }
      +}
      diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/AbstractCFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/AbstractCFGVisualizer.java
      new file mode 100644
      index 000000000000..04a8ea20cdce
      --- /dev/null
      +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/AbstractCFGVisualizer.java
      @@ -0,0 +1,473 @@
      +package org.checkerframework.dataflow.cfg.visualize;
      +
      +import org.checkerframework.checker.nullness.qual.NonNull;
      +import org.checkerframework.checker.nullness.qual.Nullable;
      +import org.checkerframework.dataflow.analysis.AbstractValue;
      +import org.checkerframework.dataflow.analysis.Analysis;
      +import org.checkerframework.dataflow.analysis.Analysis.Direction;
      +import org.checkerframework.dataflow.analysis.Store;
      +import org.checkerframework.dataflow.analysis.TransferFunction;
      +import org.checkerframework.dataflow.analysis.TransferInput;
      +import org.checkerframework.dataflow.cfg.ControlFlowGraph;
      +import org.checkerframework.dataflow.cfg.block.Block;
      +import org.checkerframework.dataflow.cfg.block.ConditionalBlock;
      +import org.checkerframework.dataflow.cfg.block.ExceptionBlock;
      +import org.checkerframework.dataflow.cfg.block.SingleSuccessorBlock;
      +import org.checkerframework.dataflow.cfg.block.SpecialBlock;
      +import org.checkerframework.dataflow.cfg.node.Node;
      +import org.checkerframework.javacutil.BugInCF;
      +import org.plumelib.util.StringsPlume;
      +import org.plumelib.util.UniqueId;
      +
      +import java.util.ArrayDeque;
      +import java.util.ArrayList;
      +import java.util.IdentityHashMap;
      +import java.util.LinkedHashSet;
      +import java.util.List;
      +import java.util.Map;
      +import java.util.Queue;
      +import java.util.Set;
      +import java.util.StringJoiner;
      +
      +import javax.lang.model.type.TypeMirror;
      +
      +/**
      + * This abstract class makes implementing a {@link CFGVisualizer} easier. Some of the methods in
      + * {@link CFGVisualizer} are already implemented in this abstract class, but can be overridden if
      + * necessary.
      + *
      + * @param  the abstract value type to be tracked by the analysis
      + * @param  the store type used in the analysis
      + * @param  the transfer function type that is used to approximate runtime behavior
      + * @see DOTCFGVisualizer
      + * @see StringCFGVisualizer
      + */
      +public abstract class AbstractCFGVisualizer<
      +                V extends AbstractValue, S extends Store, T extends TransferFunction>
      +        implements CFGVisualizer {
      +
      +    /**
      +     * If {@code true}, {@link CFGVisualizer} returns more detailed information.
      +     *
      +     * 

      Initialized in {@link #init(Map)}. + */ + protected boolean verbose; + + /** The line separator. */ + protected static final String lineSeparator = System.lineSeparator(); + + /** The indentation for elements of the store. */ + protected static final String storeEntryIndent = " "; + + @Override + public void init(Map args) { + 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; + } + + /** + * Visualize a control flow graph. + * + * @param cfg the current control flow graph + * @param entry the entry block of the control flow graph + * @param analysis the current analysis + * @return the representation of the control flow graph + */ + protected String visualizeGraph( + ControlFlowGraph cfg, Block entry, @Nullable Analysis analysis) { + return visualizeGraphHeader() + + visualizeGraphWithoutHeaderAndFooter(cfg, entry, analysis) + + visualizeGraphFooter(); + } + + /** + * Helper method to visualize a control flow graph, without outputting a header or footer. + * + * @param cfg the control flow graph + * @param entry the entry block of the control flow graph + * @param analysis the current analysis + * @return the String representation of the control flow graph + */ + protected String visualizeGraphWithoutHeaderAndFooter( + ControlFlowGraph cfg, Block entry, @Nullable Analysis analysis) { + Set visited = new LinkedHashSet<>(); + StringBuilder sbGraph = new StringBuilder(); + Queue workList = new ArrayDeque<>(); + Block cur = entry; + visited.add(entry); + while (cur != null) { + handleSuccessorsHelper(cur, visited, workList, sbGraph); + cur = workList.poll(); + } + sbGraph.append(lineSeparator); + sbGraph.append(visualizeNodes(visited, cfg, analysis)); + return sbGraph.toString(); + } + + /** + * 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; 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) { + if (cur.getType() == Block.BlockType.CONDITIONAL_BLOCK) { + ConditionalBlock ccur = ((ConditionalBlock) cur); + Block thenSuccessor = ccur.getThenSuccessor(); + sbGraph.append( + visualizeEdge( + ccur.getUid(), + thenSuccessor.getUid(), + ccur.getThenFlowRule().toString())); + sbGraph.append(lineSeparator); + addBlock(thenSuccessor, visited, workList); + Block elseSuccessor = ccur.getElseSuccessor(); + sbGraph.append( + visualizeEdge( + ccur.getUid(), + elseSuccessor.getUid(), + ccur.getElseFlowRule().toString())); + sbGraph.append(lineSeparator); + addBlock(elseSuccessor, visited, workList); + } else { + SingleSuccessorBlock sscur = (SingleSuccessorBlock) cur; + Block succ = sscur.getSuccessor(); + if (succ != null) { + sbGraph.append( + visualizeEdge(cur.getUid(), succ.getUid(), sscur.getFlowRule().name())); + sbGraph.append(lineSeparator); + addBlock(succ, visited, workList); + } + } + if (cur.getType() == Block.BlockType.EXCEPTION_BLOCK) { + ExceptionBlock ecur = (ExceptionBlock) cur; + for (Map.Entry> e : ecur.getExceptionalSuccessors().entrySet()) { + TypeMirror cause = e.getKey(); + String exception = cause.toString(); + if (exception.startsWith("java.lang.")) { + exception = exception.replace("java.lang.", ""); + } + for (Block b : e.getValue()) { + sbGraph.append(visualizeEdge(cur.getUid(), b.getUid(), exception)); + sbGraph.append(lineSeparator); + addBlock(b, visited, workList); + } + } + } + } + + /** + * Checks whether a block exists in the visited blocks list, and, if not, adds it to the visited + * blocks list and the work list. + * + * @param b the block to check + * @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 + */ + protected void addBlock(Block b, Set visited, Queue workList) { + if (visited.add(b)) { + workList.add(b); + } + } + + /** + * Helper method to visualize a block. + * + *

      NOTE: The output ends with a separator, only if an "after" store is visualized. The client + * {@link #visualizeBlock} should correct this if needed. + * + * @param bb the block + * @param analysis the current analysis + * @param separator the line separator. Examples: "\\l" for left justification in {@link + * DOTCFGVisualizer} (this is really a terminator, not a separator), "\n" to add a new line + * in {@link StringCFGVisualizer} + * @return the String representation of the block + */ + protected String visualizeBlockHelper( + Block bb, @Nullable Analysis analysis, String separator) { + StringBuilder sbBlock = new StringBuilder(); + String contents = loopOverBlockContents(bb, analysis, separator); + if (!contents.isEmpty()) { + sbBlock.append(contents); + } + if (sbBlock.length() == 0) { + // Nothing got appended; use default text for empty block + if (bb.getType() == Block.BlockType.SPECIAL_BLOCK) { + sbBlock.append(visualizeSpecialBlock((SpecialBlock) bb)); + } else if (bb.getType() == Block.BlockType.CONDITIONAL_BLOCK) { + sbBlock.append(visualizeConditionalBlock((ConditionalBlock) bb)); + } else { + sbBlock.append(""); + } + } + + // Visualize transfer input if necessary. + if (analysis != null) { + sbBlock.insert(0, visualizeBlockTransferInputBefore(bb, analysis) + separator); + if (verbose) { + Node lastNode = bb.getLastNode(); + if (lastNode != null) { + if (!sbBlock.toString().endsWith(separator)) { + sbBlock.append(separator); + } + sbBlock.append(visualizeBlockTransferInputAfter(bb, analysis) + separator); + } + } + } + return sbBlock.toString(); + } + + /** + * Iterates over the block content and visualizes all the nodes in it. + * + * @param bb the block + * @param analysis the current analysis + * @param separator the separator between the nodes of the block + * @return the String representation of the contents of the block + */ + protected String loopOverBlockContents( + Block bb, @Nullable Analysis analysis, String separator) { + + List contents = addBlockContent(bb); + StringJoiner sjBlockContents = new StringJoiner(separator); + for (Node t : contents) { + sjBlockContents.add(visualizeBlockNode(t, analysis)); + } + return sjBlockContents.toString(); + } + + /** + * Returns the contents of the block. + * + * @param bb the block + * @return the contents of the block, as a list of nodes + */ + protected List addBlockContent(Block bb) { + return bb.getNodes(); + } + + /** + * Format the given object as a String suitable for the output format, i.e. with format-specific + * characters escaped. + * + * @param obj an object + * @return the formatted String from the given object + */ + protected abstract String format(Object obj); + + @Override + public String visualizeBlockNode(Node t, @Nullable Analysis analysis) { + StringBuilder sbBlockNode = new StringBuilder(); + sbBlockNode.append(format(t)).append(" [ ").append(getNodeSimpleName(t)).append(" ]"); + if (analysis != null) { + V value = analysis.getValue(t); + if (value != null) { + sbBlockNode.append(" > ").append(format(value)); + } + } + return sbBlockNode.toString(); + } + + /** Whether to visualize before or after a block. */ + protected enum VisualizeWhere { + /** Visualize before the block. */ + BEFORE, + /** Visualize after the block. */ + AFTER + } + + /** + * Visualize the transfer input before or after the given block. + * + * @param where either BEFORE or AFTER + * @param bb a block + * @param analysis the current analysis + * @param separator the line separator. Examples: "\\l" for left justification in {@link + * DOTCFGVisualizer} (which is actually a line TERMINATOR, not a separator!), "\n" to add a + * new line in {@link StringCFGVisualizer} + * @return the visualization of the transfer input before or after the given block + */ + protected String visualizeBlockTransferInputHelper( + VisualizeWhere where, Block bb, Analysis analysis, String separator) { + if (analysis == null) { + throw new BugInCF( + "analysis must be non-null when visualizing the transfer input of a block."); + } + + Direction analysisDirection = analysis.getDirection(); + + S regularStore; + S thenStore = null; + S elseStore = null; + boolean isTwoStores = false; + + UniqueId storesFrom; + + if (analysisDirection == Direction.FORWARD && where == VisualizeWhere.AFTER) { + regularStore = analysis.getResult().getStoreAfter(bb); + storesFrom = analysis.getResult(); + } else if (analysisDirection == Direction.BACKWARD && where == VisualizeWhere.BEFORE) { + regularStore = analysis.getResult().getStoreBefore(bb); + storesFrom = analysis.getResult(); + } else { + TransferInput input = analysis.getInput(bb); + assert input != null : "@AssumeAssertion(nullness): invariant"; + storesFrom = input; + isTwoStores = input.containsTwoStores(); + regularStore = input.getRegularStore(); + thenStore = input.getThenStore(); + elseStore = input.getElseStore(); + } + + StringBuilder sbStore = new StringBuilder(); + if (verbose) { + sbStore.append(storesFrom.getClassAndUid() + separator); + } + sbStore.append(where == VisualizeWhere.BEFORE ? "Before: " : "After: "); + + if (!isTwoStores) { + sbStore.append(visualizeStore(regularStore)); + } else { + assert thenStore != null : "@AssumeAssertion(nullness): invariant"; + assert elseStore != null : "@AssumeAssertion(nullness): invariant"; + sbStore.append("then="); + sbStore.append(visualizeStore(thenStore)); + sbStore.append(","); + sbStore.append(separator); + sbStore.append("else="); + sbStore.append(visualizeStore(elseStore)); + } + if (where == VisualizeWhere.BEFORE) { + sbStore.append(separator + "~~~~~~~~~"); + } else { + sbStore.insert(0, "~~~~~~~~~" + separator); + } + return sbStore.toString(); + } + + /** + * Visualize a special block. + * + * @param sbb the special block + * @return the String representation of the special block + */ + protected String visualizeSpecialBlockHelper(SpecialBlock sbb) { + switch (sbb.getSpecialType()) { + case ENTRY: + return ""; + case EXIT: + return ""; + case EXCEPTIONAL_EXIT: + return ""; + default: + throw new BugInCF("Unrecognized special block type: " + sbb.getType()); + } + } + + /** + * Generate the order of processing blocks. Because a block may appear more than once in {@link + * ControlFlowGraph#getDepthFirstOrderedBlocks()}, the orders of each block are stored in a + * separate array list. + * + * @param cfg the current control flow graph + * @return an IdentityHashMap that maps from blocks to their orders + */ + protected IdentityHashMap> getProcessOrder(ControlFlowGraph cfg) { + IdentityHashMap> depthFirstOrder = new IdentityHashMap<>(); + int count = 1; + for (Block b : cfg.getDepthFirstOrderedBlocks()) { + depthFirstOrder.computeIfAbsent(b, k -> new ArrayList<>()); + @SuppressWarnings( + "nullness:assignment.type.incompatible") // computeIfAbsent's function doesn't + // return null + @NonNull List blockIds = depthFirstOrder.get(b); + blockIds.add(count++); + } + return depthFirstOrder; + } + + @Override + public String visualizeStore(S store) { + return store.visualize(this); + } + + /** + * Generate the String representation of the nodes of a control flow graph. + * + * @param blocks the set of all the blocks in a control flow graph + * @param cfg the control flow graph + * @param analysis the current analysis + * @return the String representation of the nodes + */ + protected abstract String visualizeNodes( + Set blocks, ControlFlowGraph cfg, @Nullable Analysis analysis); + + /** + * Generate the String representation of an edge. + * + * @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 visualizeEdge(Object sId, Object eId, String flowRule); + + /** + * Return the header of the generated graph. + * + * @return the String representation of the header of the control flow graph + */ + protected abstract String visualizeGraphHeader(); + + /** + * Return the footer of the generated graph. + * + * @return the String representation of the footer of the control flow graph + */ + protected abstract String visualizeGraphFooter(); + + /** + * Given a list of process orders (integers), returns a string representation. + * + *

      Examples: "Process order: 23", "Process order: 23,25". + * + * @param order a list of process orders + * @return a String representation of the given process orders + */ + protected String getProcessOrderSimpleString(List order) { + return "Process order: " + StringsPlume.join(",", order); + } + + /** + * Get the simple name of a node. + * + * @param t a node + * @return the node's simple name, without "Node" + */ + protected String getNodeSimpleName(Node t) { + String name = t.getClass().getSimpleName(); + return name.replace("Node", ""); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizeLauncher.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizeLauncher.java new file mode 100644 index 000000000000..77502f942caa --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizeLauncher.java @@ -0,0 +1,372 @@ +package org.checkerframework.dataflow.cfg.visualize; + +import com.sun.tools.javac.file.JavacFileManager; +import com.sun.tools.javac.main.JavaCompiler; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.List; +import com.sun.tools.javac.util.Options; + +import org.checkerframework.checker.mustcall.qual.MustCall; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.AbstractValue; +import org.checkerframework.dataflow.analysis.Analysis; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.analysis.TransferFunction; +import org.checkerframework.dataflow.cfg.CFGProcessor; +import org.checkerframework.dataflow.cfg.CFGProcessor.CFGProcessResult; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.plumelib.util.ArrayMap; + +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Collections; +import java.util.Map; + +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; + +/** + * Launcher to generate the DOT or String representation of the control flow graph of a given method + * in a given class. + * + *

      Usage: Directly run it as the main class to generate the DOT representation of the control + * flow graph of a given method in a given class. See {@link + * org.checkerframework.dataflow.cfg.playground.ConstantPropagationPlayground} for another way to + * use it. + */ +public final class CFGVisualizeLauncher { + + /** Class cannot be instantiated. */ + private CFGVisualizeLauncher() { + throw new AssertionError("Class CFGVisualizeLauncher cannot be instantiated."); + } + + /** + * The main entry point of CFGVisualizeLauncher. + * + * @param args command-line arguments + */ + public static void main(String[] args) { + CFGVisualizeOptions config = CFGVisualizeOptions.parseArgs(args); + + performAnalysis(config, null); + } + + /** + * Generate a visualization of the CFG of a method, with an optional analysis. + * + * @param the abstract value type of the analysis + * @param the store type of the analysis + * @param the transfer function type of the analysis + * @param config CFGVisualizeOptions that includes input file, output directory, method name, + * and class name + * @param analysis analysis to perform before the visualization (or {@code null} if no analysis + * is to be performed) + */ + public static , S extends Store, T extends TransferFunction> + void performAnalysis(CFGVisualizeOptions config, @Nullable Analysis analysis) { + if (!config.isString()) { + if (analysis == null) { + generateDOTofCFGWithoutAnalysis( + config.getInputFile(), + config.getOutputDirectory(), + config.getMethodName(), + config.getClassName(), + config.isPDF(), + config.isVerbose()); + } else { + generateDOTofCFG( + config.getInputFile(), + config.getOutputDirectory(), + config.getMethodName(), + config.getClassName(), + config.isPDF(), + config.isVerbose(), + analysis); + } + } else { + if (analysis == null) { + String stringGraph = + generateStringOfCFGWithoutAnalysis( + config.getInputFile(), + config.getMethodName(), + config.getClassName(), + config.isVerbose()); + System.out.println(stringGraph); + } else { + Map res = + generateStringOfCFG( + config.getInputFile(), + config.getMethodName(), + config.getClassName(), + config.isVerbose(), + analysis); + if (res != null) { + String stringGraph = (String) res.get("stringGraph"); + if (stringGraph == null) { + System.err.println( + "Unexpected output from generating string control flow graph, shouldn't be" + + " null. Result map: " + + res); + return; + } + System.out.println(stringGraph); + } else { + System.err.println( + "Unexpected output from generating string control flow graph, shouldn't be" + + " null."); + } + } + } + } + + /** + * Generate the DOT representation of the CFG for a method, only. Does no dataflow analysis. + * + * @param inputFile a Java source file, used as input + * @param outputDir output directory + * @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 pdf also generate a PDF + * @param verbose show verbose information in CFG + */ + private static void generateDOTofCFGWithoutAnalysis( + String inputFile, + String outputDir, + String method, + String clas, + boolean pdf, + boolean verbose) { + 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 a Java source file, used as input + * @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 + */ + private static String generateStringOfCFGWithoutAnalysis( + String inputFile, String method, String clas, boolean verbose) { + 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. + * + * @param the abstract value type to be tracked by the analysis + * @param the store type used in the analysis + * @param the transfer function type that is used to approximated runtime behavior + * @param inputFile a Java source file, used as input + * @param outputDir source output directory + * @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 pdf also generate a PDF + * @param verbose show verbose information in CFG + * @param analysis analysis to perform before the visualization (or {@code null} if no analysis + * is to be performed) + */ + private static < + V extends AbstractValue, + S extends Store, + T extends TransferFunction> + void generateDOTofCFG( + String inputFile, + String outputDir, + String method, + String clas, + boolean pdf, + boolean verbose, + @Nullable Analysis analysis) { + ControlFlowGraph cfg = generateMethodCFG(inputFile, clas, method); + if (analysis != null) { + analysis.performAnalysis(cfg); + } + + Map args = new ArrayMap<>(2); + args.put("outdir", outputDir); + args.put("verbose", verbose); + + CFGVisualizer viz = new DOTCFGVisualizer<>(); + viz.init(args); + Map res = viz.visualizeWithAction(cfg, cfg.getEntryBlock(), analysis); + viz.shutdown(); + + if (pdf && res != null) { + assert res.get("dotFileName") != null : "@AssumeAssertion(nullness): specification"; + producePDF((String) res.get("dotFileName")); + } + } + + /** + * Generate the control flow graph of a method in a class. + * + * @param file a Java source file, used as input + * @param clas name of the class which includes the method to generate the CFG for + * @param method name of the method to generate the CFG for + * @return control flow graph of the specified method + */ + public static ControlFlowGraph generateMethodCFG(String file, String clas, String method) { + CFGProcessor cfgProcessor = new CFGProcessor(clas, method); + + Context context = new Context(); + Options.instance(context).put("compilePolicy", "ATTR_ONLY"); + JavaCompiler javac = new JavaCompiler(context); + + JavaFileObject l; + try (@SuppressWarnings( + "mustcall:type.argument") // Context isn't annotated for the Must Call + // Checker. + JavacFileManager fileManager = + (JavacFileManager) context.get(JavaFileManager.class)) { + l = fileManager.getJavaFileObjectsFromStrings(List.of(file)).iterator().next(); + } catch (IOException e) { + throw new Error(e); + } + + PrintStream err = System.err; + try { + // Redirect syserr to nothing (and prevent the compiler from issuing + // warnings about our exception). + @SuppressWarnings({ + "builder:required.method.not.called", + "mustcall:assignment" + }) // Won't be needed in JDK 11+ with use of "OutputStream.nullOutputStream()". + @MustCall() OutputStream nullOS = + // In JDK 11+, this can be just "OutputStream.nullOutputStream()". + new OutputStream() { + @Override + public void write(int b) throws IOException {} + }; + System.setErr(new PrintStream(nullOS)); + javac.compile(List.of(l), List.of(clas), List.of(cfgProcessor), List.nil()); + } catch (Throwable e) { + // ok + } finally { + System.setErr(err); + } + + CFGProcessResult res = cfgProcessor.getCFGProcessResult(); + + if (res == null) { + printError( + "internal error in type processor! method typeProcessOver() doesn't get" + + " called."); + System.exit(1); + } + + if (!res.isSuccess()) { + printError(res.getErrMsg()); + System.exit(1); + } + + return res.getCFG(); + } + + /** + * Write generated String representation of the CFG for a method to a file. + * + * @param inputFile a Java source file, used as input + * @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 outputFile source output file + * @param analysis instance of forward or backward analysis from specific dataflow test case + */ + @SuppressWarnings("CatchAndPrintStackTrace") // we want to use e.printStackTrace here. + public static void writeStringOfCFG( + String inputFile, + String method, + String clas, + String outputFile, + Analysis analysis) { + Map res = generateStringOfCFG(inputFile, method, clas, true, analysis); + try (FileWriter out = new FileWriter(outputFile)) { + if (res != null && res.get("stringGraph") != null) { + out.write(res.get("stringGraph").toString()); + } + out.write("\n"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Invoke "dot" command to generate a PDF. + * + * @param file name of the dot file + */ + private static void producePDF(String file) { + try { + String command = "dot -Tpdf \"" + file + "\" -o \"" + file + ".pdf\""; + Process child = Runtime.getRuntime().exec(new String[] {"/bin/sh", "-c", command}); + child.waitFor(); + } catch (InterruptedException | IOException e) { + e.printStackTrace(); + System.exit(1); + } + } + + /** + * Generate the String representation of the CFG for a method. + * + * @param the abstract value type to be tracked by the analysis + * @param the store type used in the analysis + * @param the transfer function type that is used to approximated runtime behavior + * @param inputFile a Java source file, used as input + * @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 + * @param analysis analysis to perform before the visualization (or {@code null} if no analysis + * is to be performed) + * @return a map which includes a key "stringGraph" and the String representation of CFG as the + * value + */ + private static < + V extends AbstractValue, + S extends Store, + T extends TransferFunction> + @Nullable Map generateStringOfCFG( + String inputFile, + String method, + String clas, + boolean verbose, + @Nullable Analysis analysis) { + ControlFlowGraph cfg = generateMethodCFG(inputFile, clas, method); + if (analysis != null) { + analysis.performAnalysis(cfg); + } + + Map args = Collections.singletonMap("verbose", verbose); + + CFGVisualizer viz = new StringCFGVisualizer<>(); + viz.init(args); + Map res = viz.visualize(cfg, cfg.getEntryBlock(), analysis); + viz.shutdown(); + return res; + } + + /** + * Print error message. + * + * @param string error message + */ + private static void printError(@Nullable String string) { + System.err.println("ERROR: " + string); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizeOptions.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizeOptions.java new file mode 100644 index 000000000000..39ba302c5adf --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizeOptions.java @@ -0,0 +1,255 @@ +package org.checkerframework.dataflow.cfg.visualize; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.io.File; + +/** + * Options for running analysis on files. + * + *

      Usage: An instance of this class is created by calling {@link #parseArgs(String[])} with the + * command line arguments. The arguments are parsed and the options are stored in the instance. They + * can be retrieved by calling the appropriate getter method. See {@link + * org.checkerframework.dataflow.cfg.visualize.CFGVisualizeLauncher} for an example. + */ +public class CFGVisualizeOptions { + + /** Default method name. */ + private static final String DEFAULT_METHOD = "test"; + + /** Default class name. */ + private static final String DEFAULT_CLASS = "Test"; + + /** Default output directory. */ + private static final String DEFAULT_OUTPUT_DIR = "."; + + /** The input file. */ + private final String input; + + /** The output directory. */ + private final String output; + + /** The method name. */ + private final String method; + + /** The class name. */ + private final String clas; + + /** True if the PDF should be generated. */ + private final boolean pdf; + + /** True if the verbose output should be generated. */ + private final boolean verbose; + + /** True if the string representation should be generated. */ + private final boolean string; + + /** + * Private constructor. + * + *

      This constructor is private to ensure that the object is only created by calling {@link + * #parseArgs(String[])}. + * + * @param input the input file + * @param output the output directory + * @param method the method name + * @param clas the class name + * @param pdf true if the PDF should be generated + * @param verbose true if the verbose output should be generated + * @param string true if the string representation should be generated + */ + private CFGVisualizeOptions( + String input, + String output, + String method, + String clas, + boolean pdf, + boolean verbose, + boolean string) { + this.input = input; + this.output = output; + this.method = method; + this.clas = clas; + this.pdf = pdf; + this.verbose = verbose; + this.string = string; + } + + /** + * Parse the command line arguments. + * + *

      This method calls System.exit(1) if there are no arguments or if the input file cannot be + * read. + * + * @param args command-line arguments, see {@link #printUsage()} + * @return CFGVisualizeOptions object containing the parsed options + */ + public static CFGVisualizeOptions parseArgs(String[] args) { + if (args.length == 0) { + printUsage(); + System.exit(1); + } + String input = args[0]; + File file = new File(input); + if (!file.canRead()) { + printError("Cannot read input file: " + file.getAbsolutePath()); + printUsage(); + System.exit(1); + } + + String method = DEFAULT_METHOD; + String clas = DEFAULT_CLASS; + String output = DEFAULT_OUTPUT_DIR; + boolean pdf = false; + boolean error = false; + boolean verbose = false; + boolean string = false; + + for (int i = 1; i < args.length; i++) { + switch (args[i]) { + case "--outputdir": + if (i >= args.length - 1) { + printError("Did not find after --outputdir."); + continue; + } + i++; + output = args[i]; + break; + case "--pdf": + pdf = true; + break; + case "--method": + if (i >= args.length - 1) { + printError("Did not find after --method."); + continue; + } + i++; + method = args[i]; + break; + case "--class": + if (i >= args.length - 1) { + printError("Did not find after --class."); + continue; + } + i++; + clas = args[i]; + break; + case "--verbose": + verbose = true; + break; + case "--string": + string = true; + break; + default: + printError("Unknown command line argument: " + args[i]); + error = true; + break; + } + } + + if (error) { + System.exit(1); + } + + return new CFGVisualizeOptions(input, output, method, clas, pdf, verbose, string); + } + + /** + * Getter for the input file. + * + * @return the input file + */ + public String getInputFile() { + return input; + } + + /** + * Getter for the output directory. + * + * @return the output directory + */ + public String getOutputDirectory() { + return output; + } + + /** + * Getter for the method name. + * + * @return the method name + */ + public String getMethodName() { + return method; + } + + /** + * Getter for the class name. + * + * @return the class name + */ + public String getClassName() { + return clas; + } + + /** + * Getter for the PDF flag. + * + * @return true if the PDF should be generated + */ + public boolean isPDF() { + return pdf; + } + + /** + * Getter for the verbose flag. + * + * @return true if the verbose output should be generated + */ + public boolean isVerbose() { + return verbose; + } + + /** + * Getter for the string flag. + * + * @return true if the string representation should be generated + */ + public boolean isString() { + return string; + } + + /** + * Print usage information. + * + *

      Sends the usage information to System.out. + */ + private static void printUsage() { + System.out.println( + "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( + " --method: The method to generate the CFG for (defaults to 'test')."); + System.out.println( + " --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( + " --string: Print the string representation of the control flow graph" + + " (defaults to 'false')."); + } + + /** + * Print error message. + * + *

      Sends the error message to System.err. + * + * @param string error message + */ + private static void printError(@Nullable String string) { + System.err.println("ERROR: " + string); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizer.java new file mode 100644 index 000000000000..d23a6eafbb9a --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/CFGVisualizer.java @@ -0,0 +1,222 @@ +package org.checkerframework.dataflow.cfg.visualize; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.AbstractValue; +import org.checkerframework.dataflow.analysis.Analysis; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.analysis.TransferFunction; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ConditionalBlock; +import org.checkerframework.dataflow.cfg.block.SpecialBlock; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.expression.ArrayAccess; +import org.checkerframework.dataflow.expression.ClassName; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.LocalVariable; +import org.checkerframework.dataflow.expression.MethodCall; + +import java.util.Map; + +/** + * Perform some visualization on a control flow graph. The particular operations depend on the + * implementation. + * + * @param the abstract value type to be tracked by the analysis + * @param the store type used in the analysis + * @param the transfer function type that is used to approximate runtime behavior + */ +public interface CFGVisualizer< + V extends AbstractValue, S extends Store, T extends TransferFunction> { + /** + * Initialization method guaranteed to be called once before the first invocation of {@link + * #visualize} or {@link #visualizeWithAction}. + * + * @param args implementation-dependent options + */ + void init(Map args); + + /** + * Returns the separator for lines within a node's representation. + * + * @return the separator for lines within a node's representation + */ + public abstract String getSeparator(); + + /** + * Creates a visualization representing the control flow graph starting at {@code entry}. The + * keys and values in the returned map are implementation dependent. The method should not + * perform any actions. + * + *

      An invocation {@code visualize(cfg, entry, null);} does not output stores at the beginning + * of basic blocks. + * + * @param cfg the CFG to visualize + * @param entry the entry node of the control flow graph to be represented + * @param analysis an analysis containing information about the program represented by the CFG. + * The information includes {@link Store}s that are valid at the beginning of basic blocks + * reachable from {@code entry} and per-node information for value producing {@link Node}s. + * Can also be {@code null} to indicate that this information should not be output. + * @return visualization results, e.g. generated file names ({@link DOTCFGVisualizer}) or a + * String representation of the CFG ({@link StringCFGVisualizer}) + * @see #visualizeWithAction(ControlFlowGraph, Block, Analysis) + */ + @Nullable Map visualize( + ControlFlowGraph cfg, Block entry, @Nullable Analysis analysis); + + /** + * Output a visualization representing the control flow graph starting at {@code entry}. The + * keys and values in the returned map are implementation dependent. The concrete actions are + * implementation dependent, and can include outputting information and producing files. + * + *

      An invocation {@code visualizeWithAction(cfg, entry, null);} does not output stores at the + * beginning of basic blocks. + * + * @param cfg the CFG to visualize + * @param entry the entry node of the control flow graph to be represented + * @param analysis an analysis containing information about the program represented by the CFG. + * The information includes {@link Store}s that are valid at the beginning of basic blocks + * reachable from {@code entry} and per-node information for value producing {@link Node}s. + * Can also be {@code null} to indicate that this information should not be output. + * @return visualization results, e.g. generated file names ({@link DOTCFGVisualizer}) or a + * String representation of the CFG ({@link StringCFGVisualizer}) + * @see #visualize(ControlFlowGraph, Block, Analysis) + */ + @Nullable Map visualizeWithAction( + ControlFlowGraph cfg, Block entry, @Nullable Analysis analysis); + + /** + * Delegate the visualization responsibility to the passed {@link Store} instance, which will + * call back to this visualizer instance for sub-components. + * + * @param store the store to visualize + * @return the String representation of the given store + */ + String visualizeStore(S store); + + /** + * Called by {@code CFAbstractStore#internalVisualize()} to visualize a local variable. + * + * @param localVar the local variable + * @param value the value of the local variable + * @return the String representation of the local variable + */ + String visualizeStoreLocalVar(LocalVariable localVar, V value); + + /** + * Called by {@code CFAbstractStore#internalVisualize()} to visualize the value of the current + * object {@code this} in this Store. + * + * @param value the value of the current object {@code this} + * @return the String representation of {@code this} + */ + String visualizeStoreThisVal(V value); + + /** + * Called by {@code CFAbstractStore#internalVisualize()} to visualize the value of one field + * collected by this Store. + * + * @param fieldAccess the field + * @param value the value of the field + * @return the String representation of the field + */ + String visualizeStoreFieldVal(FieldAccess fieldAccess, V value); + + /** + * Called by {@code CFAbstractStore#internalVisualize()} to visualize the value of one array + * collected by this Store. + * + * @param arrayValue the array + * @param value the value of the array + * @return the String representation of the array + */ + String visualizeStoreArrayVal(ArrayAccess arrayValue, V value); + + /** + * Called by {@code CFAbstractStore#internalVisualize()} to visualize the value of pure method + * calls collected by this Store. + * + * @param methodCall the pure method call + * @param value the value of the pure method call + * @return the String representation of the pure method call + */ + String visualizeStoreMethodVals(MethodCall methodCall, V value); + + /** + * Called by {@code CFAbstractStore#internalVisualize()} to visualize the value of class names + * collected by this Store. + * + * @param className the class name + * @param value the value of the class name + * @return the String representation of the class name + */ + String visualizeStoreClassVals(ClassName className, V value); + + /** + * Called by {@code CFAbstractStore#internalVisualize()} to visualize the specific information + * collected according to the specific kind of Store. Currently, these Stores call this method: + * {@code LockStore}, {@code NullnessStore}, and {@code InitializationStore} to visualize + * additional information. + * + * @param keyName the name of the specific information to be visualized + * @param value the value of the specific information to be visualized + * @return the String representation of the specific information + */ + String visualizeStoreKeyVal(String keyName, Object value); + + /** + * Visualize a block based on the analysis. + * + * @param bb the block + * @param analysis the current analysis + * @return the String representation of the given block + */ + String visualizeBlock(Block bb, @Nullable Analysis analysis); + + /** + * Visualize a SpecialBlock. + * + * @param sbb the special block + * @return the String representation of the type of the special block {@code sbb}: entry, exit, + * or exceptional-exit + */ + String visualizeSpecialBlock(SpecialBlock sbb); + + /** + * Visualize a ConditionalBlock. + * + * @param cbb the conditional block + * @return the String representation of the conditional block + */ + String visualizeConditionalBlock(ConditionalBlock cbb); + + /** + * Visualize the transferInput before a Block based on the analysis. + * + * @param bb the block + * @param analysis the current analysis + * @return the String representation of the transferInput before the given block + */ + String visualizeBlockTransferInputBefore(Block bb, Analysis analysis); + + /** + * Visualize the transferInput after a Block based on the analysis. + * + * @param bb the block + * @param analysis the current analysis + * @return the String representation of the transferInput after the given block + */ + String visualizeBlockTransferInputAfter(Block bb, Analysis analysis); + + /** + * Visualize a Node based on the analysis. + * + * @param t the node + * @param analysis the current analysis + * @return the String representation of the given node + */ + String visualizeBlockNode(Node t, @Nullable Analysis analysis); + + /** Shutdown method called once from the shutdown hook of the {@code BaseTypeChecker}. */ + void shutdown(); +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/DOTCFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/DOTCFGVisualizer.java new file mode 100644 index 000000000000..88d26e95566b --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/DOTCFGVisualizer.java @@ -0,0 +1,374 @@ +package org.checkerframework.dataflow.cfg.visualize; + +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.VariableTree; +import com.sun.tools.javac.tree.JCTree; + +import org.checkerframework.checker.nullness.qual.KeyFor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.AbstractValue; +import org.checkerframework.dataflow.analysis.Analysis; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.analysis.TransferFunction; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.UnderlyingAST; +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; +import org.checkerframework.dataflow.cfg.block.Block.BlockType; +import org.checkerframework.dataflow.cfg.block.ConditionalBlock; +import org.checkerframework.dataflow.cfg.block.SpecialBlock; +import org.checkerframework.dataflow.cfg.visualize.AbstractCFGVisualizer.VisualizeWhere; +import org.checkerframework.dataflow.expression.ArrayAccess; +import org.checkerframework.dataflow.expression.ClassName; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.LocalVariable; +import org.checkerframework.dataflow.expression.MethodCall; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.UserError; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; + +/** Generate a graph description in the DOT language of a control graph. */ +public class DOTCFGVisualizer< + V extends AbstractValue, S extends Store, T extends TransferFunction> + extends AbstractCFGVisualizer { + + /** The output directory. */ + @SuppressWarnings("nullness:initialization.field.uninitialized") // uses init method + protected String outDir; + + /** The (optional) checker name. Used as a part of the name of the output dot file. */ + protected @Nullable String checkerName; + + /** Mapping from class/method representation to generated dot file. */ + @SuppressWarnings("nullness:initialization.field.uninitialized") // uses init method + protected Map generated; + + /** Terminator for lines that are left-justified. */ + protected static final String leftJustifiedTerminator = "\\l"; + + @Override + @SuppressWarnings("nullness") // assume arguments are set correctly + public void init(Map args) { + super.init(args); + this.outDir = (String) args.get("outdir"); + if (this.outDir == null) { + throw new BugInCF( + "outDir should never be null," + + " provide it in args when calling DOTCFGVisualizer.init(args)."); + } + this.checkerName = (String) args.get("checkerName"); + this.generated = new HashMap<>(); + } + + @Override + public String getSeparator() { + return leftJustifiedTerminator; + } + + @Override + public Map visualize( + ControlFlowGraph cfg, Block entry, @Nullable Analysis analysis) { + String dotGraph = visualizeGraph(cfg, entry, analysis); + + Map vis = new HashMap<>(2); + vis.put("dotGraph", dotGraph); + return vis; + } + + @Override + public Map visualizeWithAction( + ControlFlowGraph cfg, Block entry, @Nullable Analysis analysis) { + Map vis = visualize(cfg, entry, analysis); + String dotGraph = (String) vis.get("dotGraph"); + if (dotGraph == null) { + throw new BugInCF("dotGraph key missing in visualize result!"); + } + String dotFileName = dotOutputFileName(cfg.underlyingAST); + + try (BufferedWriter out = new BufferedWriter(new FileWriter(dotFileName))) { + out.write(dotGraph); + } catch (IOException e) { + throw new UserError("Error creating dot file (is the path valid?): " + dotFileName, e); + } + vis.put("dotFileName", dotFileName); + return vis; + } + + @SuppressWarnings("keyfor:enhancedfor.type.incompatible") + @Override + public String visualizeNodes( + Set blocks, ControlFlowGraph cfg, @Nullable Analysis analysis) { + + StringBuilder sbDotNodes = new StringBuilder(); + + IdentityHashMap> processOrder = getProcessOrder(cfg); + + // Definition of all nodes including their labels. + for (@KeyFor("processOrder") Block v : blocks) { + sbDotNodes.append(" ").append(v.getUid()).append(" ["); + if (v.getType() == BlockType.CONDITIONAL_BLOCK) { + sbDotNodes.append("shape=polygon sides=8 "); + } else if (v.getType() == BlockType.SPECIAL_BLOCK) { + sbDotNodes.append("shape=oval "); + } else { + sbDotNodes.append("shape=rectangle "); + } + sbDotNodes.append("label=\""); + if (verbose) { + sbDotNodes + .append(getProcessOrderSimpleString(processOrder.get(v))) + .append(getSeparator()); + } + String strBlock = visualizeBlock(v, analysis); + if (strBlock.length() == 0) { + if (v.getType() == BlockType.CONDITIONAL_BLOCK) { + // The footer of the conditional block. + sbDotNodes.append("\"];"); + } else { + // The footer of the block which has no content and is not a special or + // conditional block. + sbDotNodes.append("?? empty ??\"];"); + } + } else { + sbDotNodes.append(strBlock).append("\"];"); + } + sbDotNodes.append(System.lineSeparator()); + } + return sbDotNodes.toString(); + } + + @Override + protected String visualizeEdge(Object sId, Object eId, String flowRule) { + return " " + format(sId) + " -> " + format(eId) + " [label=\"" + flowRule + "\"];"; + } + + @Override + public String visualizeBlock(Block bb, @Nullable Analysis analysis) { + return super.visualizeBlockHelper(bb, analysis, getSeparator()); + } + + @Override + public String visualizeSpecialBlock(SpecialBlock sbb) { + return super.visualizeSpecialBlockHelper(sbb); + } + + @Override + public String visualizeConditionalBlock(ConditionalBlock cbb) { + // No extra content in DOT output. + return ""; + } + + @Override + public String visualizeBlockTransferInputBefore(Block bb, Analysis analysis) { + return super.visualizeBlockTransferInputHelper( + VisualizeWhere.BEFORE, bb, analysis, getSeparator()); + } + + @Override + public String visualizeBlockTransferInputAfter(Block bb, Analysis analysis) { + return super.visualizeBlockTransferInputHelper( + VisualizeWhere.AFTER, bb, analysis, getSeparator()); + } + + /** + * Create a dot file and return its name. + * + * @param ast an abstract syntax tree + * @return the file name used for DOT output + */ + protected String dotOutputFileName(UnderlyingAST ast) { + StringBuilder srcLoc = new StringBuilder(); + StringBuilder outFile = new StringBuilder(); + + if (ast.getKind() == UnderlyingAST.Kind.ARBITRARY_CODE) { + CFGStatement cfgStatement = (CFGStatement) ast; + String clsName = cfgStatement.getSimpleClassName(); + outFile.append(clsName); + outFile.append("-initializer-"); + outFile.append(ast.getUid()); + + srcLoc.append("<"); + srcLoc.append(clsName); + srcLoc.append("::initializer::"); + srcLoc.append(((JCTree) cfgStatement.getCode()).pos); + srcLoc.append(">"); + } else if (ast.getKind() == UnderlyingAST.Kind.METHOD) { + CFGMethod cfgMethod = (CFGMethod) ast; + String clsName = cfgMethod.getSimpleClassName(); + String methodName = cfgMethod.getMethodName(); + 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(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.getSimpleClassName(); + String enclosingMethodName = cfgLambda.getEnclosingMethodName(); + long uid = TreeUtils.treeUids.get(cfgLambda.getCode()); + outFile.append(clsName); + outFile.append("-"); + if (enclosingMethodName != null) { + outFile.append(enclosingMethodName); + outFile.append("-"); + } + outFile.append(uid); + + srcLoc.append("<"); + srcLoc.append(clsName); + if (enclosingMethodName != null) { + srcLoc.append("::"); + srcLoc.append(enclosingMethodName); + srcLoc.append("("); + @SuppressWarnings( + "nullness") // enclosingMethodName != null => getEnclosingMethod() != null + @NonNull MethodTree method = cfgLambda.getEnclosingMethod(); + srcLoc.append(method.getParameters()); + srcLoc.append(")"); + } + srcLoc.append("::"); + srcLoc.append(((JCTree) cfgLambda.getCode()).pos); + srcLoc.append(">"); + } else { + throw new BugInCF("Unexpected AST kind: " + ast.getKind() + " value: " + ast); + } + if (checkerName != null && !checkerName.isEmpty()) { + outFile.append('-'); + outFile.append(checkerName); + } + outFile.append(".dot"); + + // make path safe for Linux + if (outFile.length() > 255) { + outFile.setLength(255); + } + // make path safe for Windows + String outFileBaseName = outFile.toString().replace("<", "_").replace(">", ""); + String outFileName = outDir + "/" + outFileBaseName; + + generated.put(srcLoc.toString(), outFileName); + + return outFileName; + } + + @Override + protected String format(Object obj) { + return escapeString(obj); + } + + @Override + public String visualizeStoreThisVal(V value) { + return storeEntryIndent + "this > " + escapeString(value); + } + + @Override + public String visualizeStoreLocalVar(LocalVariable localVar, V value) { + return storeEntryIndent + localVar + " > " + escapeString(value); + } + + @Override + public String visualizeStoreFieldVal(FieldAccess fieldAccess, V value) { + return storeEntryIndent + fieldAccess + " > " + escapeString(value); + } + + @Override + public String visualizeStoreArrayVal(ArrayAccess arrayValue, V value) { + return storeEntryIndent + arrayValue + " > " + escapeString(value); + } + + @Override + public String visualizeStoreMethodVals(MethodCall methodCall, V value) { + return storeEntryIndent + escapeString(methodCall) + " > " + escapeString(value); + } + + @Override + public String visualizeStoreClassVals(ClassName className, V value) { + return storeEntryIndent + className + " > " + escapeString(value); + } + + @Override + public String visualizeStoreKeyVal(String keyName, Object value) { + return storeEntryIndent + keyName + " = " + value; + } + + /** + * Escape the input String. + * + * @param str the string to be escaped + * @return the escaped version of the string + */ + private static String escapeString(String str) { + return str.replace("\"", "\\\"").replace("\r", "\\\\r").replace("\n", "\\\\n"); + } + + /** + * Escape the double quotes from the string representation of the given object. + * + * @param obj an object + * @return an escaped version of the string representation of the object + */ + private static String escapeString(Object obj) { + return escapeString(String.valueOf(obj)); + } + + /** + * Write a file {@code methods.txt} that contains a mapping from source code location to + * generated dot file. + */ + @Override + public void shutdown() { + // Open for append, in case of multiple sub-checkers. + try (FileWriter fstream = new FileWriter(outDir + "/methods.txt", true); + BufferedWriter out = new BufferedWriter(fstream)) { + for (Map.Entry kv : generated.entrySet()) { + out.write(kv.getKey()); + out.append("\t"); + out.write(kv.getValue()); + out.append(lineSeparator); + } + } catch (IOException e) { + throw new UserError( + "Error creating methods.txt file in: " + outDir + "; ensure the path is valid", + e); + } + } + + @Override + protected String visualizeGraphHeader() { + return "digraph {" + lineSeparator; + } + + @Override + protected String visualizeGraphFooter() { + return "}"; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/StringCFGVisualizer.java b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/StringCFGVisualizer.java new file mode 100644 index 000000000000..196e243759a2 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/cfg/visualize/StringCFGVisualizer.java @@ -0,0 +1,196 @@ +package org.checkerframework.dataflow.cfg.visualize; + +import org.checkerframework.checker.nullness.qual.KeyFor; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.AbstractValue; +import org.checkerframework.dataflow.analysis.Analysis; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.analysis.TransferFunction; +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.block.Block; +import org.checkerframework.dataflow.cfg.block.ConditionalBlock; +import org.checkerframework.dataflow.cfg.block.SpecialBlock; +import org.checkerframework.dataflow.cfg.visualize.AbstractCFGVisualizer.VisualizeWhere; +import org.checkerframework.dataflow.expression.ArrayAccess; +import org.checkerframework.dataflow.expression.ClassName; +import org.checkerframework.dataflow.expression.FieldAccess; +import org.checkerframework.dataflow.expression.LocalVariable; +import org.checkerframework.dataflow.expression.MethodCall; + +import java.io.PrintStream; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; + +/** Generate the String representation of a control flow graph. */ +public class StringCFGVisualizer< + V extends AbstractValue, S extends Store, T extends TransferFunction> + extends AbstractCFGVisualizer { + + /** Stream to output String representation to. */ + protected PrintStream out; + + /** Create a StringCFGVisualizer. */ + public StringCFGVisualizer() { + out = System.out; + } + + @Override + public void init(Map args) { + super.init(args); + PrintStream argout = (PrintStream) args.get("output"); + if (argout != null) { + out = argout; + } + } + + @Override + public String getSeparator() { + return "\n"; + } + + @Override + public Map visualize( + ControlFlowGraph cfg, Block entry, @Nullable Analysis analysis) { + String stringGraph = visualizeGraph(cfg, entry, analysis); + return Collections.singletonMap("stringGraph", stringGraph); + } + + @Override + public Map visualizeWithAction( + ControlFlowGraph cfg, Block entry, @Nullable Analysis analysis) { + Map vis = visualize(cfg, entry, analysis); + String stringGraph = (String) vis.get("stringGraph"); + out.println(stringGraph); + return vis; + } + + @SuppressWarnings("keyfor:enhancedfor.type.incompatible") + @Override + public String visualizeNodes( + Set blocks, ControlFlowGraph cfg, @Nullable Analysis analysis) { + StringJoiner sjStringNodes = new StringJoiner(lineSeparator); + IdentityHashMap> processOrder = getProcessOrder(cfg); + + // Generate all the Nodes. + for (@KeyFor("processOrder") Block v : blocks) { + sjStringNodes.add(v.getUid() + ":"); + if (verbose) { + sjStringNodes.add(getProcessOrderSimpleString(processOrder.get(v))); + } + sjStringNodes.add(visualizeBlock(v, analysis)); + sjStringNodes.add(""); + } + + return sjStringNodes.toString().trim(); + } + + @Override + protected String visualizeEdge(Object sId, Object eId, String flowRule) { + if (this.verbose) { + return sId + " -> " + eId + " " + flowRule; + } + return sId + " -> " + eId; + } + + @Override + public String visualizeBlock(Block bb, @Nullable Analysis analysis) { + return super.visualizeBlockHelper(bb, analysis, lineSeparator).trim(); + } + + @Override + public String visualizeSpecialBlock(SpecialBlock sbb) { + return super.visualizeSpecialBlockHelper(sbb); + } + + @Override + public String visualizeConditionalBlock(ConditionalBlock cbb) { + return "ConditionalBlock: then: " + + cbb.getThenSuccessor().getUid() + + ", else: " + + cbb.getElseSuccessor().getUid(); + } + + @Override + public String visualizeBlockTransferInputBefore(Block bb, Analysis analysis) { + return super.visualizeBlockTransferInputHelper( + VisualizeWhere.BEFORE, bb, analysis, lineSeparator); + } + + @Override + public String visualizeBlockTransferInputAfter(Block bb, Analysis analysis) { + return super.visualizeBlockTransferInputHelper( + VisualizeWhere.AFTER, bb, analysis, lineSeparator); + } + + @Override + protected String format(Object obj) { + return obj.toString(); + } + + @Override + public String visualizeStoreThisVal(V value) { + return storeEntryIndent + "this > " + value; + } + + @Override + public String visualizeStoreLocalVar(LocalVariable localVar, V value) { + return storeEntryIndent + localVar + " > " + value; + } + + @Override + public String visualizeStoreFieldVal(FieldAccess fieldAccess, V value) { + return storeEntryIndent + fieldAccess + " > " + value; + } + + @Override + public String visualizeStoreArrayVal(ArrayAccess arrayValue, V value) { + return storeEntryIndent + arrayValue + " > " + value; + } + + @Override + public String visualizeStoreMethodVals(MethodCall methodCall, V value) { + return storeEntryIndent + methodCall + " > " + value; + } + + @Override + public String visualizeStoreClassVals(ClassName className, V value) { + return storeEntryIndent + className + " > " + value; + } + + @Override + public String visualizeStoreKeyVal(String keyName, Object value) { + return storeEntryIndent + keyName + " = " + value; + } + + /** + * {@inheritDoc} + * + *

      StringCFGVisualizer does not write into file, so left intentionally blank. + */ + @Override + public void shutdown() {} + + /** + * {@inheritDoc} + * + *

      StringCFGVisualizer does not need a specific header, so just return an empty string. + */ + @Override + protected String visualizeGraphHeader() { + return ""; + } + + /** + * {@inheritDoc} + * + *

      StringCFGVisualizer does not need a specific footer, so just return an empty string. + */ + @Override + protected String visualizeGraphFooter() { + return ""; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/Constant.java b/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/Constant.java index 6dceb06294d4..02b5e969ec9d 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/Constant.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/Constant.java @@ -1,14 +1,16 @@ package org.checkerframework.dataflow.constantpropagation; -import java.util.Objects; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.AbstractValue; +import org.checkerframework.javacutil.BugInCF; + +import java.util.Objects; public class Constant implements AbstractValue { /** What kind of abstract value is this? */ - protected Type type; + protected final Type type; /** The value of this abstract value (or null). */ protected @Nullable Integer value; @@ -117,8 +119,8 @@ public String toString() { case CONSTANT: assert isConstant() : "@AssumeAssertion(nullness)"; return value.toString(); + default: + throw new BugInCF("Unexpected type: " + type); } - assert false; - return "???"; } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/ConstantPropagationStore.java b/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/ConstantPropagationStore.java index 2bec0c692d5f..8930dddfe733 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/ConstantPropagationStore.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/ConstantPropagationStore.java @@ -1,22 +1,27 @@ package org.checkerframework.dataflow.constantpropagation; -import java.util.HashMap; -import java.util.Map; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.analysis.FlowExpressions; import org.checkerframework.dataflow.analysis.Store; -import org.checkerframework.dataflow.cfg.CFGVisualizer; import org.checkerframework.dataflow.cfg.node.IntegerLiteralNode; import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizer; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.plumelib.util.ArrayMap; +import org.plumelib.util.CollectionsPlume; + +import java.util.LinkedHashMap; +import java.util.Map; +/** A store that records information about constant values. */ public class ConstantPropagationStore implements Store { /** Information about variables gathered so far. */ - Map contents; + private final Map contents; + /** Creates a new ConstantPropagationStore. */ public ConstantPropagationStore() { - contents = new HashMap<>(); + contents = new LinkedHashMap<>(); } protected ConstantPropagationStore(Map contents) { @@ -50,12 +55,13 @@ public void setInformation(Node n, Constant val) { @Override public ConstantPropagationStore copy() { - return new ConstantPropagationStore(new HashMap<>(contents)); + return new ConstantPropagationStore(new LinkedHashMap<>(contents)); } @Override public ConstantPropagationStore leastUpperBound(ConstantPropagationStore other) { - Map newContents = new HashMap<>(); + Map newContents = + ArrayMap.newArrayMapOrLinkedHashMap(contents.size() + other.contents.size()); // go through all of the information of the other class for (Map.Entry e : other.contents.entrySet()) { @@ -140,30 +146,25 @@ public int hashCode() { @Override public String toString() { - // only output local variable information - Map smallerContents = new HashMap<>(); + // Only output local variable information. + // This output is very terse, so a CFG containing it fits well in the manual. + Map contentsLocalVars = + new LinkedHashMap<>(CollectionsPlume.mapCapacity(contents)); for (Map.Entry e : contents.entrySet()) { if (e.getKey() instanceof LocalVariableNode) { - smallerContents.put(e.getKey(), e.getValue()); + contentsLocalVars.put(e.getKey(), e.getValue()); } } - return smallerContents.toString(); + return contentsLocalVars.toString(); } @Override - public boolean canAlias(FlowExpressions.Receiver a, FlowExpressions.Receiver b) { + public boolean canAlias(JavaExpression a, JavaExpression b) { return true; } - /** - * {@inheritDoc} - * - *

      {@code value} is {@code null} because {@link ConstantPropagationStore} doesn't support - * visualization. - */ @Override - @SuppressWarnings("nullness") public String visualize(CFGVisualizer viz) { - return viz.visualizeStoreKeyVal("constant propagation", null); + return viz.visualizeStoreKeyVal("constant propagation", toString()); } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/ConstantPropagationTransfer.java b/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/ConstantPropagationTransfer.java index 5dc1ad85b903..2d75ee65bbb6 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/ConstantPropagationTransfer.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/constantpropagation/ConstantPropagationTransfer.java @@ -1,7 +1,5 @@ package org.checkerframework.dataflow.constantpropagation; -import java.util.List; -import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.ConditionalTransferResult; import org.checkerframework.dataflow.analysis.ForwardTransferFunction; import org.checkerframework.dataflow.analysis.RegularTransferResult; @@ -15,6 +13,8 @@ import org.checkerframework.dataflow.cfg.node.LocalVariableNode; import org.checkerframework.dataflow.cfg.node.Node; +import java.util.List; + public class ConstantPropagationTransfer extends AbstractNodeVisitor< TransferResult, @@ -23,7 +23,7 @@ public class ConstantPropagationTransfer @Override public ConstantPropagationStore initialStore( - UnderlyingAST underlyingAST, @Nullable List parameters) { + UnderlyingAST underlyingAST, List parameters) { ConstantPropagationStore store = new ConstantPropagationStore(); return store; } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/ArrayAccess.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/ArrayAccess.java new file mode 100644 index 000000000000..ab245472636c --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/ArrayAccess.java @@ -0,0 +1,124 @@ +package org.checkerframework.dataflow.expression; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.javacutil.AnnotationProvider; + +import java.util.Objects; + +import javax.lang.model.type.TypeMirror; + +/** An array access. */ +public class ArrayAccess extends JavaExpression { + + /** The array being accessed. */ + protected final JavaExpression array; + + /** The index; an expression of type int. */ + protected final JavaExpression index; + + /** + * Create a new ArrayAccess. + * + * @param type the type of the array access + * @param array the array being accessed + * @param index the index; an expression of type int + */ + public ArrayAccess(TypeMirror type, JavaExpression array, JavaExpression index) { + super(type); + this.array = array; + this.index = index; + } + + @Override + public boolean containsOfClass(Class clazz) { + if (getClass() == clazz) { + return true; + } + if (array.containsOfClass(clazz)) { + return true; + } + return index.containsOfClass(clazz); + } + + @Override + public boolean isDeterministic(AnnotationProvider provider) { + return array.isDeterministic(provider) && index.isDeterministic(provider); + } + + /** + * Returns the array being accessed. + * + * @return the array being accessed + */ + public JavaExpression getArray() { + return array; + } + + public JavaExpression getIndex() { + return index; + } + + @Override + public boolean isUnassignableByOtherCode() { + return false; + } + + @Override + public boolean isUnmodifiableByOtherCode() { + return false; + } + + @Override + public boolean syntacticEquals(JavaExpression je) { + if (!(je instanceof ArrayAccess)) { + return false; + } + ArrayAccess other = (ArrayAccess) je; + return array.syntacticEquals(other.array) && index.syntacticEquals(other.index); + } + + @Override + public boolean containsSyntacticEqualJavaExpression(JavaExpression other) { + return syntacticEquals(other) + || array.containsSyntacticEqualJavaExpression(other) + || index.containsSyntacticEqualJavaExpression(other); + } + + @Override + public boolean containsModifiableAliasOf(Store store, JavaExpression other) { + if (array.containsModifiableAliasOf(store, other)) { + return true; + } + return index.containsModifiableAliasOf(store, other); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof ArrayAccess)) { + return false; + } + ArrayAccess other = (ArrayAccess) obj; + return array.equals(other.array) && index.equals(other.index); + } + + @Override + public int hashCode() { + return Objects.hash(array, index); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append(array.toString()); + result.append("["); + result.append(index.toString()); + result.append("]"); + return result.toString(); + } + + @Override + public R accept(JavaExpressionVisitor visitor, P p) { + return visitor.visitArrayAccess(this, p); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/ArrayCreation.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/ArrayCreation.java new file mode 100644 index 000000000000..bda0d3a406f2 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/ArrayCreation.java @@ -0,0 +1,164 @@ +package org.checkerframework.dataflow.expression; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.TypesUtils; +import org.plumelib.util.StringsPlume; + +import java.util.List; +import java.util.Objects; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** JavaExpression for array creations. {@code new String[]()}. */ +public class ArrayCreation extends JavaExpression { + + /** + * List of dimensions expressions. A {code null} element means that there is no dimension + * expression for the given array level. + */ + protected final List<@Nullable JavaExpression> dimensions; + + /** List of initializers. */ + protected final List initializers; + + /** + * Creates an ArrayCreation object. + * + * @param type array type + * @param dimensions list of dimension expressions; a {@code null} element means that there is + * no dimension expression for the given array level + * @param initializers list of initializer expressions + */ + public ArrayCreation( + TypeMirror type, + List<@Nullable JavaExpression> dimensions, + List initializers) { + super(type); + assert type.getKind() == TypeKind.ARRAY; + this.dimensions = dimensions; + this.initializers = initializers; + } + + /** + * Returns a list representing the dimensions of this array creation. A {code null} element + * means that there is no dimension expression for the given array level. + * + * @return a list representing the dimensions of this array creation + */ + public List<@Nullable JavaExpression> getDimensions() { + return dimensions; + } + + public List getInitializers() { + return initializers; + } + + @Override + public boolean containsOfClass(Class clazz) { + for (JavaExpression n : dimensions) { + if (n != null && n.getClass() == clazz) { + return true; + } + } + for (JavaExpression n : initializers) { + if (n.getClass() == clazz) { + return true; + } + } + return false; + } + + @Override + public boolean isDeterministic(AnnotationProvider provider) { + return listIsDeterministic(dimensions, provider) + && listIsDeterministic(initializers, provider); + } + + @Override + public boolean isUnassignableByOtherCode() { + return false; + } + + @Override + public boolean isUnmodifiableByOtherCode() { + return false; + } + + @Override + public int hashCode() { + return Objects.hash(dimensions, initializers, getType().toString()); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof ArrayCreation)) { + return false; + } + ArrayCreation other = (ArrayCreation) obj; + return this.dimensions.equals(other.getDimensions()) + && this.initializers.equals(other.getInitializers()) + // It might be better to use Types.isSameType(getType(), other.getType()), but I + // don't have a Types object. + && getType().toString().equals(other.getType().toString()); + } + + @Override + public boolean syntacticEquals(JavaExpression je) { + if (!(je instanceof ArrayCreation)) { + return false; + } + ArrayCreation other = (ArrayCreation) je; + return JavaExpression.syntacticEqualsList(this.dimensions, other.dimensions) + && JavaExpression.syntacticEqualsList(this.initializers, other.initializers) + && getType().toString().equals(other.getType().toString()); + } + + @Override + public boolean containsSyntacticEqualJavaExpression(JavaExpression other) { + return syntacticEquals(other) + || JavaExpression.listContainsSyntacticEqualJavaExpression(dimensions, other) + || JavaExpression.listContainsSyntacticEqualJavaExpression(initializers, other); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + if (dimensions.isEmpty()) { + sb.append("new " + type); + } else { + sb.append("new " + TypesUtils.getInnermostComponentType((ArrayType) type)); + for (JavaExpression dim : dimensions) { + sb.append("["); + sb.append(dim == null ? "" : dim); + sb.append("]"); + } + } + if (!initializers.isEmpty()) { + sb.append(" {"); + sb.append(StringsPlume.join(", ", initializers)); + sb.append("}"); + } + return sb.toString(); + } + + @Override + public String toStringDebug() { + return "\"" + + super.toStringDebug() + + "\"" + + " type=" + + type + + " dimensions=" + + dimensions + + " initializers=" + + initializers; + } + + @Override + public R accept(JavaExpressionVisitor visitor, P p) { + return visitor.visitArrayCreation(this, p); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/BinaryOperation.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/BinaryOperation.java new file mode 100644 index 000000000000..50b9018ba8d7 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/BinaryOperation.java @@ -0,0 +1,235 @@ +package org.checkerframework.dataflow.expression; + +import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.cfg.node.BinaryOperationNode; +import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.BugInCF; + +import java.util.Objects; + +import javax.lang.model.type.TypeMirror; + +/** JavaExpression for binary operations. */ +public class BinaryOperation extends JavaExpression { + + /** The binary operation kind. */ + protected final Tree.Kind operationKind; + + /** The left operand. */ + protected final JavaExpression left; + + /** The right operand. */ + protected final JavaExpression right; + + /** + * Create a binary operation. + * + * @param type the result type + * @param operationKind the operator + * @param left the left operand + * @param right the right operand + */ + public BinaryOperation( + TypeMirror type, Tree.Kind operationKind, JavaExpression left, JavaExpression right) { + super(type); + this.operationKind = operationKind; + this.left = left; + this.right = right; + } + + /** + * Create a binary operation. + * + * @param node the binary operation node + * @param left the left operand + * @param right the right operand + */ + public BinaryOperation(BinaryOperationNode node, JavaExpression left, JavaExpression right) { + this(node.getType(), node.getTree().getKind(), left, right); + } + + /** + * Returns the operator of this binary operation. + * + * @return the binary operation kind + */ + public Tree.Kind getOperationKind() { + return operationKind; + } + + /** + * Returns the left operand of this binary operation. + * + * @return the left operand + */ + public JavaExpression getLeft() { + return left; + } + + /** + * Returns the right operand of this binary operation. + * + * @return the right operand + */ + public JavaExpression getRight() { + return right; + } + + @Override + public boolean containsOfClass(Class clazz) { + if (getClass() == clazz) { + return true; + } + return left.containsOfClass(clazz) || right.containsOfClass(clazz); + } + + @Override + public boolean isDeterministic(AnnotationProvider provider) { + return left.isDeterministic(provider) && right.isDeterministic(provider); + } + + @Override + public boolean isUnassignableByOtherCode() { + return left.isUnassignableByOtherCode() && right.isUnassignableByOtherCode(); + } + + @Override + public boolean isUnmodifiableByOtherCode() { + return left.isUnmodifiableByOtherCode() && right.isUnmodifiableByOtherCode(); + } + + @Override + public boolean syntacticEquals(JavaExpression je) { + if (!(je instanceof BinaryOperation)) { + return false; + } + BinaryOperation other = (BinaryOperation) je; + return operationKind == other.getOperationKind() + && left.syntacticEquals(other.left) + && right.syntacticEquals(other.right); + } + + @Override + public boolean containsSyntacticEqualJavaExpression(JavaExpression other) { + return this.syntacticEquals(other) + || left.containsSyntacticEqualJavaExpression(other) + || right.containsSyntacticEqualJavaExpression(other); + } + + @Override + public boolean containsModifiableAliasOf(Store store, JavaExpression other) { + return left.containsModifiableAliasOf(store, other) + || right.containsModifiableAliasOf(store, other); + } + + @Override + public int hashCode() { + return Objects.hash(operationKind, left, right); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof BinaryOperation)) { + return false; + } + BinaryOperation biOp = (BinaryOperation) other; + if (!(operationKind == biOp.getOperationKind())) { + return false; + } + if (isCommutative()) { + return (left.equals(biOp.left) && right.equals(biOp.right)) + || (left.equals(biOp.right) && right.equals(biOp.left)); + } + return left.equals(biOp.left) && right.equals(biOp.right); + } + + /** + * Returns true if the binary operation is commutative, e.g., x + y == y + x. + * + * @return true if the binary operation is commutative + */ + private boolean isCommutative() { + switch (operationKind) { + case PLUS: + case MULTIPLY: + case AND: + case OR: + case XOR: + case EQUAL_TO: + case NOT_EQUAL_TO: + case CONDITIONAL_AND: + case CONDITIONAL_OR: + return true; + default: + return false; + } + } + + @Override + public String toString() { + return left.toString() + + " " + + operationKindToString(operationKind) + + " " + + right.toString(); + } + + /** + * Return the Java source code representation of the given operation. + * + * @param operationKind an unary operation kind + * @return the Java source code representation of the given operation + */ + private String operationKindToString(Tree.Kind operationKind) { + switch (operationKind) { + case CONDITIONAL_AND: + return "&&"; + case AND: + return "&"; + case OR: + return "|"; + case DIVIDE: + return "/"; + case EQUAL_TO: + return "=="; + case GREATER_THAN: + return ">"; + case GREATER_THAN_EQUAL: + return ">="; + case LEFT_SHIFT: + return "<<"; + case LESS_THAN: + return "<"; + case LESS_THAN_EQUAL: + return "<="; + case MINUS: + return "-"; + case MULTIPLY: + return "*"; + case NOT_EQUAL_TO: + return "!="; + case CONDITIONAL_OR: + return "||"; + case PLUS: + return "+"; + case REMAINDER: + return "%"; + case RIGHT_SHIFT: + return ">>"; + case UNSIGNED_RIGHT_SHIFT: + return ">>>"; + case XOR: + return "^"; + default: + throw new BugInCF("unhandled " + operationKind); + } + } + + @Override + public R accept(JavaExpressionVisitor visitor, P p) { + return visitor.visitBinaryOperation(this, p); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/ClassName.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/ClassName.java new file mode 100644 index 000000000000..708bdaa6ee44 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/ClassName.java @@ -0,0 +1,98 @@ +package org.checkerframework.dataflow.expression; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.javacutil.AnnotationProvider; + +import java.util.Objects; + +import javax.lang.model.type.TypeMirror; + +/** + * A ClassName represents either a class literal or the occurrence of a class as part of a static + * field access or static method invocation. + */ +public class ClassName extends JavaExpression { + /** The string representation of the raw type of this. */ + private final String typeString; + + /** + * Creates a new ClassName object for the given type. + * + * @param type the type for the new ClassName. If it will represent a class literal, the type is + * declared primitive, void, or array of one of them. If it represents part of a static + * field access or static method invocation, the type is declared, type variable, or array + * (including array of primitive). + */ + public ClassName(TypeMirror type) { + super(type); + String typeString = type.toString(); + if (typeString.endsWith(">")) { + typeString = typeString.substring(0, typeString.indexOf("<")); + } + this.typeString = typeString; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof ClassName)) { + return false; + } + ClassName other = (ClassName) obj; + return typeString.equals(other.typeString); + } + + @Override + public int hashCode() { + return Objects.hash(typeString); + } + + @Override + public String toString() { + return typeString + ".class"; + } + + @Override + public boolean containsOfClass(Class clazz) { + return getClass() == clazz; + } + + @Override + public boolean isDeterministic(AnnotationProvider provider) { + return true; + } + + @Override + public boolean isUnassignableByOtherCode() { + return true; + } + + @Override + public boolean isUnmodifiableByOtherCode() { + return true; + } + + @Override + public boolean syntacticEquals(JavaExpression je) { + if (!(je instanceof ClassName)) { + return false; + } + ClassName other = (ClassName) je; + return typeString.equals(other.typeString); + } + + @Override + public boolean containsSyntacticEqualJavaExpression(JavaExpression other) { + return this.syntacticEquals(other); + } + + @Override + public boolean containsModifiableAliasOf(Store store, JavaExpression other) { + return false; // not modifiable + } + + @Override + public R accept(JavaExpressionVisitor visitor, P p) { + return visitor.visitClassName(this, p); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/FieldAccess.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/FieldAccess.java new file mode 100644 index 000000000000..9af3750d15f3 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/FieldAccess.java @@ -0,0 +1,178 @@ +package org.checkerframework.dataflow.expression; + +import com.sun.tools.javac.code.Symbol; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.cfg.node.FieldAccessNode; +import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TypesUtils; + +import java.util.Objects; + +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; + +/** + * A FieldAccess represents a field access. It does not represent a class literal such as {@code + * SomeClass.class} or {@code int[].class}. + */ +public class FieldAccess extends JavaExpression { + /** The receiver of the field access. */ + protected final JavaExpression receiver; + + /** The field being accessed. */ + protected final VariableElement field; + + /** + * Returns the receiver. + * + * @return the receiver + */ + public JavaExpression getReceiver() { + return receiver; + } + + /** + * Returns the field. + * + * @return the field + */ + public VariableElement getField() { + return field; + } + + /** + * Create a {@code FieldAccess}. + * + * @param receiver receiver of the field access + * @param node the FieldAccessNode + */ + public FieldAccess(JavaExpression receiver, FieldAccessNode node) { + this(receiver, node.getType(), node.getElement()); + } + + /** + * Create a {@code FieldAccess}. + * + * @param receiver receiver of the field access + * @param fieldElement element of the field + */ + public FieldAccess(JavaExpression receiver, VariableElement fieldElement) { + this(receiver, fieldElement.asType(), fieldElement); + } + + /** + * Create a {@code FieldAccess}. + * + * @param receiver receiver of the field access + * @param type type of the field + * @param fieldElement element of the field + */ + public FieldAccess(JavaExpression receiver, TypeMirror type, VariableElement fieldElement) { + super(type); + this.receiver = receiver; + this.field = fieldElement; + String fieldName = fieldElement.toString(); + if (fieldName.equals("class") || fieldName.equals("this")) { + BugInCF e = + new BugInCF( + String.format( + "bad field name \"%s\" in new FieldAccess(%s, %s, %s)%n", + fieldName, receiver, type, fieldElement)); + e.printStackTrace(System.out); + e.printStackTrace(System.err); + throw e; + } + } + + public boolean isFinal() { + return ElementUtils.isFinal(field); + } + + public boolean isStatic() { + return ElementUtils.isStatic(field); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof FieldAccess)) { + return false; + } + FieldAccess fa = (FieldAccess) obj; + return fa.getField().equals(getField()) && fa.getReceiver().equals(getReceiver()); + } + + @Override + public int hashCode() { + return Objects.hash(getField(), getReceiver()); + } + + @Override + public boolean syntacticEquals(JavaExpression je) { + if (!(je instanceof FieldAccess)) { + return false; + } + FieldAccess other = (FieldAccess) je; + return this.receiver.syntacticEquals(other.receiver) && this.field.equals(other.field); + } + + @Override + public boolean containsSyntacticEqualJavaExpression(JavaExpression other) { + return syntacticEquals(other) || receiver.containsSyntacticEqualJavaExpression(other); + } + + @Override + public boolean containsModifiableAliasOf(Store store, JavaExpression other) { + return super.containsModifiableAliasOf(store, other) + || receiver.containsModifiableAliasOf(store, other); + } + + @Override + public String toString() { + if (receiver instanceof ClassName) { + return receiver.getType() + "." + field; + } else { + return receiver + "." + field; + } + } + + @Override + public String toStringDebug() { + return String.format( + "FieldAccess(type=%s, receiver=%s, field=%s [%s] [%s] owner=%s)", + type, + receiver.toStringDebug(), + field, + field.getClass().getSimpleName(), + System.identityHashCode(field), + ((Symbol) field).owner); + } + + @Override + public boolean containsOfClass(Class clazz) { + return getClass() == clazz || receiver.containsOfClass(clazz); + } + + @Override + public boolean isDeterministic(AnnotationProvider provider) { + return receiver.isDeterministic(provider); + } + + @Override + public boolean isUnassignableByOtherCode() { + return isFinal() && getReceiver().isUnassignableByOtherCode(); + } + + @Override + public boolean isUnmodifiableByOtherCode() { + return isUnassignableByOtherCode() && TypesUtils.isImmutableTypeInJdk(getReceiver().type); + } + + @Override + public R accept(JavaExpressionVisitor visitor, P p) { + return visitor.visitFieldAccess(this, p); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/FormalParameter.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/FormalParameter.java new file mode 100644 index 000000000000..1b5694925b37 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/FormalParameter.java @@ -0,0 +1,130 @@ +package org.checkerframework.dataflow.expression; + +import com.sun.tools.javac.code.Symbol.VarSymbol; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TypeAnnotationUtils; + +import java.util.Objects; + +import javax.lang.model.element.VariableElement; + +/** + * A formal parameter, represented by its 1-based index. + * + *

      {@link LocalVariable} represents a formal parameter expressed using its name. + */ +public class FormalParameter extends JavaExpression { + + /** The 1-based index. */ + protected final int index; + + /** The element for this formal parameter. */ + protected final VariableElement element; + + /** + * Creates a FormalParameter. + * + * @param index the 1-based index + * @param element the element for the formal parameter + */ + public FormalParameter(int index, VariableElement element) { + super(ElementUtils.getType(element)); + this.index = index; + this.element = element; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof FormalParameter)) { + return false; + } + + FormalParameter other = (FormalParameter) obj; + return this.index == other.index && LocalVariable.sameElement(this.element, other.element); + } + + /** + * Returns the 1-based index of this formal parameter. + * + * @return the 1-based index of this formal parameter + */ + public int getIndex() { + return index; + } + + /** + * Returns the element for this variable. + * + * @return the element for this variable + */ + public VariableElement getElement() { + return element; + } + + @Override + public int hashCode() { + VarSymbol vs = (VarSymbol) element; + return Objects.hash( + index, + vs.name.toString(), + TypeAnnotationUtils.unannotatedType(vs.type).toString(), + vs.owner.toString()); + } + + @Override + public String toString() { + return "#" + index; + } + + @Override + public String toStringDebug() { + return super.toStringDebug() + + " [element=" + + element + + ", owner=" + + ((VarSymbol) element).owner + + "]"; + } + + @Override + public boolean containsOfClass(Class clazz) { + return getClass() == clazz; + } + + @Override + public boolean syntacticEquals(JavaExpression je) { + if (!(je instanceof FormalParameter)) { + return false; + } + FormalParameter other = (FormalParameter) je; + return index == other.index; + } + + @Override + public boolean containsSyntacticEqualJavaExpression(JavaExpression other) { + return syntacticEquals(other); + } + + @Override + public boolean isUnassignableByOtherCode() { + return true; + } + + @Override + public boolean isUnmodifiableByOtherCode() { + return true; + } + + @Override + public boolean isDeterministic(AnnotationProvider provider) { + return true; + } + + @Override + public R accept(JavaExpressionVisitor visitor, P p) { + return visitor.visitFormalParameter(this, p); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java new file mode 100644 index 000000000000..3ac4d6d82b4e --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpression.java @@ -0,0 +1,821 @@ +package org.checkerframework.dataflow.expression; + +import com.sun.source.tree.ArrayAccessTree; +import com.sun.source.tree.BinaryTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LiteralTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.MethodTree; +import com.sun.source.tree.NewArrayTree; +import com.sun.source.tree.NewClassTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.UnaryTree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePath; + +import org.checkerframework.checker.interning.qual.EqualsMethod; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.cfg.node.ArrayAccessNode; +import org.checkerframework.dataflow.cfg.node.ArrayCreationNode; +import org.checkerframework.dataflow.cfg.node.BinaryOperationNode; +import org.checkerframework.dataflow.cfg.node.ClassNameNode; +import org.checkerframework.dataflow.cfg.node.FieldAccessNode; +import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; +import org.checkerframework.dataflow.cfg.node.NarrowingConversionNode; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.node.StringConversionNode; +import org.checkerframework.dataflow.cfg.node.SuperNode; +import org.checkerframework.dataflow.cfg.node.ThisNode; +import org.checkerframework.dataflow.cfg.node.UnaryOperationNode; +import org.checkerframework.dataflow.cfg.node.ValueLiteralNode; +import org.checkerframework.dataflow.cfg.node.WideningConversionNode; +import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreePathUtil; +import org.checkerframework.javacutil.TreeUtils; +import org.checkerframework.javacutil.TypesUtils; +import org.plumelib.util.CollectionsPlume; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +// The Lock Checker also supports "" as a JavaExpression, but that is implemented in the Lock +// Checker. +// There are no special subclasses (AST nodes) for "". +/** + * 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 conditional expression {@code ?:}; use + * {@link org.checkerframework.dataflow.expression.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 + * org.checkerframework.dataflow.expression.MethodCall}, {@link + * org.checkerframework.dataflow.expression.ArrayAccess}, {@link + * org.checkerframework.dataflow.expression.LocalVariable}, etc. + * + * @see the syntax of + * Java expressions supported by the Checker Framework + */ +public abstract class JavaExpression { + /** The type of this expression. */ + protected final TypeMirror type; + + /** + * Create a JavaExpression. + * + * @param type the type of the expression + */ + protected JavaExpression(TypeMirror type) { + assert type != null; + this.type = type; + } + + public TypeMirror getType() { + return type; + } + + public abstract boolean containsOfClass(Class clazz); + + public boolean containsUnknown() { + return containsOfClass(Unknown.class); + } + + /** + * Returns true if the expression is deterministic. + * + * @param provider an annotation provider (a type factory) + * @return true if this expression is deterministic + */ + public abstract boolean isDeterministic(AnnotationProvider provider); + + /** + * Returns true if all the expressions in the list are deterministic. + * + * @param list the list whose elements to test + * @param provider an annotation provider (a type factory) + * @return true if all the expressions in the list are deterministic + */ + public static boolean listIsDeterministic( + List list, AnnotationProvider provider) { + return list.stream().allMatch(je -> je == null || je.isDeterministic(provider)); + } + + /** + * Returns true if and only if the value this expression stands for cannot be changed (with + * respect to ==) by a method call. This is the case for local variables, the self reference, + * final field accesses whose receiver is {@link #isUnassignableByOtherCode}, and operations + * whose operands are all {@link #isUnmodifiableByOtherCode}. + * + * @see #isUnmodifiableByOtherCode + */ + public abstract boolean isUnassignableByOtherCode(); + + /** + * Returns true if and only if the value this expression stands for cannot be changed by a + * method call, including changes to any of its fields. + * + *

      Approximately, this returns true if the expression is {@link #isUnassignableByOtherCode} + * and its type is immutable. + * + * @see #isUnassignableByOtherCode + */ + public abstract boolean isUnmodifiableByOtherCode(); + + /** + * Returns true if and only if the two Java expressions are syntactically identical. + * + *

      This exists for use by {@link #containsSyntacticEqualJavaExpression}. + * + * @param je the other Java expression to compare to this one + * @return true if and only if the two Java expressions are syntactically identical + */ + @EqualsMethod + public abstract boolean syntacticEquals(JavaExpression je); + + /** + * Returns true if the corresponding list elements satisfy {@link #syntacticEquals}. + * + * @param lst1 the first list to compare + * @param lst2 the second list to compare + * @return true if the corresponding list elements satisfy {@link #syntacticEquals} + */ + public static boolean syntacticEqualsList( + List lst1, + List lst2) { + if (lst1.size() != lst2.size()) { + return false; + } + for (int i = 0; i < lst1.size(); i++) { + JavaExpression dim1 = lst1.get(i); + JavaExpression dim2 = lst2.get(i); + if (dim1 == null && dim2 == null) { + continue; + } else if (dim1 == null || dim2 == null) { + return false; + } else { + if (!dim1.syntacticEquals(dim2)) { + return false; + } + } + } + return true; + } + + /** + * Returns true if and only if this contains a JavaExpression that is syntactically equal to + * {@code other}. + * + * @param other the JavaExpression to search for + * @return true if and only if this contains a JavaExpression that is syntactically equal to + * {@code other} + */ + public abstract boolean containsSyntacticEqualJavaExpression(JavaExpression other); + + /** + * Returns true if the given list contains a JavaExpression that is syntactically equal to + * {@code other}. + * + * @param list the list in which to search for a match + * @param other the JavaExpression to search for + * @return true if and only if the list contains a JavaExpression that is syntactically equal to + * {@code other} + */ + public static boolean listContainsSyntacticEqualJavaExpression( + List list, JavaExpression other) { + return list.stream() + .anyMatch(je -> je != null && je.containsSyntacticEqualJavaExpression(other)); + } + + /** + * Returns true if and only if {@code other} appears anywhere in this or an expression appears + * in this such that {@code other} might alias this expression, and that expression is + * modifiable. + * + *

      This is always true, except for cases where the Java type information prevents aliasing + * and none of the subexpressions can alias 'other'. + */ + public boolean containsModifiableAliasOf(Store store, JavaExpression other) { + return this.equals(other) || store.canAlias(this, other); + } + + /** + * Format this verbosely, for debugging. + * + * @return a verbose string representation of this + */ + public String toStringDebug() { + return String.format("%s(%s): %s", getClass().getSimpleName(), type, toString()); + } + + /// + /// Static methods + /// + + /** + * Returns the Java expression for a {@link FieldAccessNode}. The result may contain {@link + * Unknown} as receiver. + * + * @param node the FieldAccessNode to convert to a JavaExpression + * @return the {@link FieldAccess} or {@link ClassName} that corresponds to {@code node} + */ + public static JavaExpression fromNodeFieldAccess(FieldAccessNode node) { + Node receiverNode = node.getReceiver(); + String fieldName = node.getFieldName(); + if (fieldName.equals("this")) { + // The CFG represents "className.this" as a FieldAccessNode, but it isn't a field + // access. + return new ThisReference(receiverNode.getType()); + } else if (fieldName.equals("class")) { + // The CFG represents "className.class" as a FieldAccessNode; bit it is a class literal. + return new ClassName(receiverNode.getType()); + } + JavaExpression receiver; + if (node.isStatic()) { + receiver = new ClassName(receiverNode.getType()); + } else { + receiver = fromNode(receiverNode); + } + return new FieldAccess(receiver, node); + } + + /** + * Returns the internal representation (as {@link FieldAccess}) of a {@link FieldAccessNode}. + * The result may contain {@link Unknown} as receiver. + * + * @param node the ArrayAccessNode to convert to a JavaExpression + * @return the internal representation (as {@link FieldAccess}) of a {@link FieldAccessNode}. + * Can contain {@link Unknown} as receiver. + */ + public static ArrayAccess fromArrayAccess(ArrayAccessNode node) { + JavaExpression array = fromNode(node.getArray()); + JavaExpression index = fromNode(node.getIndex()); + return new ArrayAccess(node.getType(), array, index); + } + + /** + * We ignore operations such as widening and narrowing when computing the internal + * representation. + * + * @param receiverNode a node to convert to a JavaExpression + * @return the internal representation of the given node. Might contain {@link Unknown}. + */ + public static JavaExpression fromNode(Node receiverNode) { + JavaExpression result = null; + if (receiverNode instanceof FieldAccessNode) { + result = fromNodeFieldAccess((FieldAccessNode) receiverNode); + } else if (receiverNode instanceof ThisNode) { + result = new ThisReference(receiverNode.getType()); + } else if (receiverNode instanceof SuperNode) { + result = new ThisReference(receiverNode.getType()); + } else if (receiverNode instanceof LocalVariableNode) { + LocalVariableNode lv = (LocalVariableNode) receiverNode; + result = new LocalVariable(lv); + } else if (receiverNode instanceof ArrayAccessNode) { + ArrayAccessNode a = (ArrayAccessNode) receiverNode; + result = fromArrayAccess(a); + } else if (receiverNode instanceof StringConversionNode) { + // ignore string conversion + return fromNode(((StringConversionNode) receiverNode).getOperand()); + } else if (receiverNode instanceof WideningConversionNode) { + // ignore widening + return fromNode(((WideningConversionNode) receiverNode).getOperand()); + } else if (receiverNode instanceof NarrowingConversionNode) { + // ignore narrowing + return fromNode(((NarrowingConversionNode) receiverNode).getOperand()); + } else if (receiverNode instanceof UnaryOperationNode) { + UnaryOperationNode uopn = (UnaryOperationNode) receiverNode; + return new UnaryOperation(uopn, fromNode(uopn.getOperand())); + } else if (receiverNode instanceof BinaryOperationNode) { + BinaryOperationNode bopn = (BinaryOperationNode) receiverNode; + return new BinaryOperation( + bopn, fromNode(bopn.getLeftOperand()), fromNode(bopn.getRightOperand())); + } else if (receiverNode instanceof ClassNameNode) { + ClassNameNode cn = (ClassNameNode) receiverNode; + result = new ClassName(cn.getType()); + } else if (receiverNode instanceof ValueLiteralNode) { + ValueLiteralNode vn = (ValueLiteralNode) receiverNode; + result = new ValueLiteral(vn.getType(), vn); + } else if (receiverNode instanceof ArrayCreationNode) { + ArrayCreationNode an = (ArrayCreationNode) receiverNode; + List<@Nullable JavaExpression> dimensions = + CollectionsPlume.mapList(JavaExpression::fromNode, an.getDimensions()); + List initializers = + CollectionsPlume.mapList(JavaExpression::fromNode, an.getInitializers()); + result = new ArrayCreation(an.getType(), dimensions, initializers); + } else if (receiverNode instanceof MethodInvocationNode) { + MethodInvocationNode mn = (MethodInvocationNode) receiverNode; + MethodInvocationTree t = mn.getTree(); + if (t == null) { + throw new BugInCF("Unexpected null tree for node: " + mn); + } + assert TreeUtils.isUseOfElement(t) : "@AssumeAssertion(nullness): tree kind"; + ExecutableElement invokedMethod = TreeUtils.elementFromUse(t); + + // Note that the method might be nondeterministic. + List parameters = + CollectionsPlume.mapList(JavaExpression::fromNode, mn.getArguments()); + JavaExpression methodReceiver; + if (ElementUtils.isStatic(invokedMethod)) { + methodReceiver = new ClassName(mn.getTarget().getReceiver().getType()); + } else { + methodReceiver = fromNode(mn.getTarget().getReceiver()); + } + result = new MethodCall(mn.getType(), invokedMethod, methodReceiver, parameters); + } + + if (result == null) { + result = new Unknown(receiverNode); + } + return result; + } + + /** + * Converts a javac {@link ExpressionTree} to a CF JavaExpression. The result might contain + * {@link Unknown}. + * + *

      We ignore operations such as widening and narrowing when computing the JavaExpression. + * + * @param tree a javac tree + * @return a JavaExpression for the given javac tree + */ + public static JavaExpression fromTree(ExpressionTree tree) { + JavaExpression result; + switch (tree.getKind()) { + case ARRAY_ACCESS: + ArrayAccessTree a = (ArrayAccessTree) tree; + JavaExpression arrayAccessExpression = fromTree(a.getExpression()); + JavaExpression index = fromTree(a.getIndex()); + result = new ArrayAccess(TreeUtils.typeOf(a), arrayAccessExpression, index); + break; + + case BOOLEAN_LITERAL: + case CHAR_LITERAL: + case DOUBLE_LITERAL: + case FLOAT_LITERAL: + case INT_LITERAL: + case LONG_LITERAL: + case NULL_LITERAL: + case STRING_LITERAL: + LiteralTree vn = (LiteralTree) tree; + result = new ValueLiteral(TreeUtils.typeOf(tree), vn.getValue()); + break; + + case NEW_ARRAY: + NewArrayTree newArrayTree = (NewArrayTree) tree; + List<@Nullable JavaExpression> dimensions; + if (newArrayTree.getDimensions() == null) { + dimensions = Collections.emptyList(); + } else { + dimensions = new ArrayList<>(newArrayTree.getDimensions().size()); + for (ExpressionTree dimension : newArrayTree.getDimensions()) { + dimensions.add(fromTree(dimension)); + } + } + List initializers; + if (newArrayTree.getInitializers() == null) { + initializers = Collections.emptyList(); + } else { + initializers = new ArrayList<>(newArrayTree.getInitializers().size()); + for (ExpressionTree initializer : newArrayTree.getInitializers()) { + initializers.add(fromTree(initializer)); + } + } + + result = new ArrayCreation(TreeUtils.typeOf(tree), dimensions, initializers); + break; + + case METHOD_INVOCATION: + MethodInvocationTree mn = (MethodInvocationTree) tree; + assert TreeUtils.isUseOfElement(mn) : "@AssumeAssertion(nullness): tree kind"; + ExecutableElement invokedMethod = TreeUtils.elementFromUse(mn); + + // Note that the method might be nondeterministic. + List parameters = + CollectionsPlume.mapList(JavaExpression::fromTree, mn.getArguments()); + JavaExpression methodReceiver; + if (ElementUtils.isStatic(invokedMethod)) { + @SuppressWarnings( + "nullness:assignment" // enclosingTypeElement(ExecutableElement): + // @NonNull + ) + @NonNull TypeElement methodType = + ElementUtils.enclosingTypeElement(invokedMethod); + methodReceiver = new ClassName(methodType.asType()); + } else { + methodReceiver = getReceiver(mn); + } + TypeMirror resultType = TreeUtils.typeOf(mn); + result = new MethodCall(resultType, invokedMethod, methodReceiver, parameters); + break; + + case MEMBER_SELECT: + result = fromMemberSelect((MemberSelectTree) tree); + break; + + case IDENTIFIER: + IdentifierTree identifierTree = (IdentifierTree) tree; + TypeMirror typeOfId = TreeUtils.typeOf(identifierTree); + Name identifierName = identifierTree.getName(); + if (identifierName.contentEquals("this") || identifierName.contentEquals("super")) { + result = new ThisReference(typeOfId); + break; + } + assert TreeUtils.isUseOfElement(identifierTree) + : "@AssumeAssertion(nullness): tree kind"; + Element ele = TreeUtils.elementFromUse(identifierTree); + if (ele == null) { + result = null; + } else if (ElementUtils.isTypeElement(ele)) { + result = new ClassName(ele.asType()); + } else { + result = fromVariableElement(typeOfId, (VariableElement) ele, identifierTree); + } + break; + + case UNARY_PLUS: + return fromTree(((UnaryTree) tree).getExpression()); + case BITWISE_COMPLEMENT: + case LOGICAL_COMPLEMENT: + case POSTFIX_DECREMENT: + case POSTFIX_INCREMENT: + case PREFIX_DECREMENT: + case PREFIX_INCREMENT: + case UNARY_MINUS: + JavaExpression operand = fromTree(((UnaryTree) tree).getExpression()); + return new UnaryOperation(TreeUtils.typeOf(tree), tree.getKind(), operand); + + case CONDITIONAL_AND: + case CONDITIONAL_OR: + case DIVIDE: + case EQUAL_TO: + case GREATER_THAN: + case GREATER_THAN_EQUAL: + case LEFT_SHIFT: + case LESS_THAN: + case LESS_THAN_EQUAL: + case MINUS: + case MULTIPLY: + case NOT_EQUAL_TO: + case OR: + case PLUS: + case REMAINDER: + case RIGHT_SHIFT: + case UNSIGNED_RIGHT_SHIFT: + case XOR: + BinaryTree binaryTree = (BinaryTree) tree; + JavaExpression left = fromTree(binaryTree.getLeftOperand()); + JavaExpression right = fromTree(binaryTree.getRightOperand()); + return new BinaryOperation(TreeUtils.typeOf(tree), tree.getKind(), left, right); + + default: + result = null; + } + + if (result == null) { + result = new Unknown(tree); + } + return result; + } + + /** + * Returns the Java expression corresponding to the given variable tree {@code tree}. + * + * @param tree a variable tree + * @return a JavaExpression for {@code tree} + */ + public static JavaExpression fromVariableTree(VariableTree tree) { + return fromVariableElement( + TreeUtils.typeOf(tree), TreeUtils.elementFromDeclaration(tree), tree); + } + + /** + * Returns the Java expression corresponding to the given variable element {@code ele}. + * + * @param typeOfEle the type of {@code ele} + * @param ele element whose JavaExpression is returned + * @param tree the tree for the variable + * @return the Java expression corresponding to the given variable element {@code ele} + */ + private static JavaExpression fromVariableElement( + TypeMirror typeOfEle, @Nullable VariableElement ele, Tree tree) { + if (ele == null) { + return new Unknown(tree); + } + switch (ele.getKind()) { + case LOCAL_VARIABLE: + case RESOURCE_VARIABLE: + case EXCEPTION_PARAMETER: + case PARAMETER: + return new LocalVariable(ele); + case FIELD: + case ENUM_CONSTANT: + // Implicit access expression, such as "this" or a class name + JavaExpression fieldAccessExpression; + @SuppressWarnings("nullness:dereference.of.nullable") // a field has enclosing class + TypeMirror enclosingTypeElement = ElementUtils.enclosingTypeElement(ele).asType(); + if (ElementUtils.isStatic(ele)) { + fieldAccessExpression = new ClassName(enclosingTypeElement); + } else { + fieldAccessExpression = new ThisReference(enclosingTypeElement); + } + return new FieldAccess(fieldAccessExpression, typeOfEle, ele); + default: + if (ElementUtils.isBindingVariable(ele)) { + return new LocalVariable(ele); + } + throw new BugInCF( + "Unexpected kind of VariableTree: kind: %s element: %s", + ele.getKind(), ele); + } + } + + /** + * Creates a JavaExpression from the {@code memberSelectTree}. + * + * @param memberSelectTree tree + * @return a JavaExpression for {@code memberSelectTree} + */ + private static JavaExpression fromMemberSelect(MemberSelectTree memberSelectTree) { + TypeMirror expressionType = TreeUtils.typeOf(memberSelectTree.getExpression()); + if (TreeUtils.isClassLiteral(memberSelectTree)) { + // the identifier is "class" + return new ClassName(expressionType); + } + if (TreeUtils.isExplicitThisDereference(memberSelectTree)) { + // the identifier is "class" + return new ThisReference(expressionType); + } + + assert TreeUtils.isUseOfElement(memberSelectTree) : "@AssumeAssertion(nullness): tree kind"; + Element ele = TreeUtils.elementFromUse(memberSelectTree); + if (ElementUtils.isTypeElement(ele)) { + // o instanceof MyClass.InnerClass + // o instanceof MyClass.InnerInterface + TypeMirror selectType = TreeUtils.typeOf(memberSelectTree); + return new ClassName(selectType); + } + switch (ele.getKind()) { + case METHOD: + case CONSTRUCTOR: + return fromTree(memberSelectTree.getExpression()); + case ENUM_CONSTANT: + case FIELD: + TypeMirror fieldType = TreeUtils.typeOf(memberSelectTree); + JavaExpression je = fromTree(memberSelectTree.getExpression()); + return new FieldAccess(je, fieldType, (VariableElement) ele); + default: + throw new BugInCF("Unexpected element kind: %s element: %s", ele.getKind(), ele); + } + } + + /** + * Returns the parameters of {@code methodEle} as {@link LocalVariable}s. + * + * @param methodEle the method element + * @return list of parameters as {@link LocalVariable}s + */ + public static List getParametersAsLocalVariables(ExecutableElement methodEle) { + return CollectionsPlume.mapList(LocalVariable::new, methodEle.getParameters()); + } + + /** + * Returns the parameters of {@code methodEle} as {@link FormalParameter}s. + * + * @param methodEle the method element + * @return list of parameters as {@link FormalParameter}s + */ + public static List getFormalParameters(ExecutableElement methodEle) { + List parameters = new ArrayList<>(methodEle.getParameters().size()); + int oneBasedIndex = 1; + for (VariableElement variableElement : methodEle.getParameters()) { + parameters.add(new FormalParameter(oneBasedIndex, variableElement)); + oneBasedIndex++; + } + return parameters; + } + + /// + /// Obtaining the receiver + /// + + /** + * Returns the receiver of the given invocation. + * + * @param accessTree a method or constructor invocation + * @return the receiver of the given invocation + */ + public static JavaExpression getReceiver(ExpressionTree accessTree) { + // TODO: Handle field accesses too? + assert accessTree instanceof MethodInvocationTree || accessTree instanceof NewClassTree; + ExpressionTree receiverTree = TreeUtils.getReceiverTree(accessTree); + if (receiverTree != null) { + return fromTree(receiverTree); + } else { + Element ele = TreeUtils.elementFromUse(accessTree); + if (ele == null) { + throw new BugInCF("TreeUtils.elementFromUse(" + accessTree + ") => null"); + } + return getImplicitReceiver(ele); + } + } + + /** + * Returns the implicit receiver of ele. + * + *

      Returns either a new ClassName or a new ThisReference depending on whether ele is static + * or not. The passed element must be a field, method, or class. + * + *

      When this returns a ThisReference, its type is the class that declares {@code ele}, which + * is not necessarily the type of {@code this} at the invocation site. + * + * @param ele a field, method, or class + * @return either a new ClassName or a new ThisReference depending on whether ele is static or + * not + */ + public static JavaExpression getImplicitReceiver(Element ele) { + TypeElement enclosingTypeElement = ElementUtils.enclosingTypeElement(ele); + if (enclosingTypeElement == null) { + throw new BugInCF("getImplicitReceiver's arg has no enclosing type: " + ele); + } + TypeMirror enclosingType = enclosingTypeElement.asType(); + if (ElementUtils.isStatic(ele)) { + return new ClassName(enclosingType); + } else { + return new ThisReference(enclosingType); + } + } + + /** + * Returns either a new ClassName or ThisReference JavaExpression object for the enclosingType. + * + *

      The Tree should be an expression or a statement that does not have a receiver or an + * implicit receiver. For example, a local variable declaration. + * + * @param path a tree path + * @param enclosingType type of the enclosing type + * @return a new {@link ClassName} or {@link ThisReference} that is a JavaExpression object for + * the enclosingType + */ + public static JavaExpression getPseudoReceiver(TreePath path, TypeMirror enclosingType) { + if (TreePathUtil.isTreeInStaticScope(path)) { + return new ClassName(enclosingType); + } else { + return new ThisReference(enclosingType); + } + } + + /** + * Accept method of the visitor pattern. + * + * @param visitor the visitor to be applied to this JavaExpression + * @param p the parameter for this operation + * @param result type of the operation + * @param

      parameter type + * @return the result of visiting this + */ + public abstract R accept(JavaExpressionVisitor visitor, P p); + + /** + * Viewpoint-adapts {@code this} to a field access with receiver {@code receiver}. + * + * @param receiver receiver of the field access + * @return viewpoint-adapted version of this + */ + public JavaExpression atFieldAccess(JavaExpression receiver) { + return ViewpointAdaptJavaExpression.viewpointAdapt(this, receiver); + } + + /** + * Viewpoint-adapts {@code this} to the {@code methodTree} by converting any {@code + * FormalParameter} into {@code LocalVariable}s. + * + * @param methodTree method declaration tree + * @return viewpoint-adapted version of this + */ + public final JavaExpression atMethodBody(MethodTree methodTree) { + List parametersJe = + CollectionsPlume.mapList( + (VariableTree param) -> + new LocalVariable(TreeUtils.elementFromDeclaration(param)), + methodTree.getParameters()); + return ViewpointAdaptJavaExpression.viewpointAdapt(this, parametersJe); + } + + /** + * Viewpoint-adapts {@code this} to the {@code methodInvocationTree}. + * + * @param methodInvocationTree method invocation + * @return viewpoint-adapted version of this + */ + public final JavaExpression atMethodInvocation(MethodInvocationTree methodInvocationTree) { + JavaExpression receiverJe = JavaExpression.getReceiver(methodInvocationTree); + List argumentsJe = + argumentTreesToJavaExpressions( + TreeUtils.elementFromUse(methodInvocationTree), + methodInvocationTree.getArguments()); + return ViewpointAdaptJavaExpression.viewpointAdapt(this, receiverJe, argumentsJe); + } + + /** + * Viewpoint-adapts {@code this} to the {@code invocationNode}. + * + * @param invocationNode method invocation + * @return viewpoint-adapted version of this + */ + public final JavaExpression atMethodInvocation(MethodInvocationNode invocationNode) { + JavaExpression receiverJe = + JavaExpression.fromNode(invocationNode.getTarget().getReceiver()); + List argumentsJe = + CollectionsPlume.mapList(JavaExpression::fromNode, invocationNode.getArguments()); + return ViewpointAdaptJavaExpression.viewpointAdapt(this, receiverJe, argumentsJe); + } + + /** + * Viewpoint-adapts {@code this} to the {@code newClassTree}. + * + * @param newClassTree constructor invocation + * @return viewpoint-adapted version of this + */ + public JavaExpression atConstructorInvocation(NewClassTree newClassTree) { + JavaExpression receiverJe = JavaExpression.getReceiver(newClassTree); + List argumentsJe = + argumentTreesToJavaExpressions( + TreeUtils.elementFromUse(newClassTree), newClassTree.getArguments()); + return ViewpointAdaptJavaExpression.viewpointAdapt(this, receiverJe, argumentsJe); + } + + /** + * Converts method or constructor arguments from Trees to JavaExpressions, accounting for + * varargs. + * + * @param method the method or constructor being invoked + * @param argTrees the arguments to the method or constructor + * @return the arguments, as JavaExpressions + */ + private static List argumentTreesToJavaExpressions( + ExecutableElement method, List argTrees) { + if (isVarArgsInvocation(method, argTrees)) { + List result = new ArrayList<>(method.getParameters().size()); + for (int i = 0; i < method.getParameters().size() - 1; i++) { + result.add(JavaExpression.fromTree(argTrees.get(i))); + } + + List varargArgs = + new ArrayList<>(argTrees.size() - method.getParameters().size() + 1); + for (int i = method.getParameters().size() - 1; i < argTrees.size(); i++) { + varargArgs.add(JavaExpression.fromTree(argTrees.get(i))); + } + Element varargsElement = method.getParameters().get(method.getParameters().size() - 1); + TypeMirror tm = ElementUtils.getType(varargsElement); + result.add(new ArrayCreation(tm, Collections.emptyList(), varargArgs)); + + return result; + } + + return CollectionsPlume.mapList(JavaExpression::fromTree, argTrees); + } + + /** + * Returns true if method is a varargs method or constructor and its varargs arguments are not + * passed in an array. + * + * @param method the method or constructor + * @param args the arguments at the call site + * @return true if method is a varargs method and its varargs arguments are not passed in an + * array + */ + private static boolean isVarArgsInvocation( + ExecutableElement method, List args) { + if (!method.isVarArgs()) { + return false; + } + if (method.getParameters().size() != args.size()) { + return true; + } + TypeMirror lastArgType = TreeUtils.typeOf(args.get(args.size() - 1)); + if (lastArgType.getKind() != TypeKind.ARRAY) { + return true; + } + List paramElts = method.getParameters(); + VariableElement lastParamElt = paramElts.get(paramElts.size() - 1); + return TypesUtils.getArrayDepth(ElementUtils.getType(lastParamElt)) + != TypesUtils.getArrayDepth(lastArgType); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionConverter.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionConverter.java new file mode 100644 index 000000000000..7055aa24153f --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionConverter.java @@ -0,0 +1,121 @@ +package org.checkerframework.dataflow.expression; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.checker.nullness.qual.PolyNull; +import org.plumelib.util.CollectionsPlume; + +import java.util.List; + +/** + * This class calls {@link #convert(JavaExpression)} on each subexpression of the {@link + * JavaExpression} and returns a new {@code JavaExpression} built from the result of calling {@code + * convert} on each subexpression. (If an expression has no subexpression, then the expression + * itself is returned.) + * + *

      This class makes it easy to implement a subclass that converts subexpressions of a {@link + * JavaExpression} based on which kind of {@code JavaExpression} the subexpression is. Subclasses + * should override the visit method of kinds of JavaExpressions to convert. + */ +public abstract class JavaExpressionConverter extends JavaExpressionVisitor { + + /** + * Converts {@code javaExpr} and returns the resulting {@code JavaExpression}. + * + * @param javaExpr the expression to convert + * @return the converted expression + */ + public JavaExpression convert(JavaExpression javaExpr) { + return super.visit(javaExpr, null); + } + + /** + * Converts all the expressions in {@code list} and returns the resulting list. + * + * @param list the list of expressions to convert + * @return the list of converted expressions + */ + public List<@PolyNull JavaExpression> convert(List<@PolyNull JavaExpression> list) { + return CollectionsPlume.mapList( + (@PolyNull JavaExpression expression) -> { + // Can't use a ternary operator because of: + // https://github.com/typetools/checker-framework/issues/1170 + if (expression == null) { + return null; + } + return convert(expression); + }, + list); + } + + @Override + protected JavaExpression visitArrayAccess(ArrayAccess arrayAccessExpr, Void unused) { + JavaExpression array = convert(arrayAccessExpr.getArray()); + JavaExpression index = convert(arrayAccessExpr.getIndex()); + return new ArrayAccess(arrayAccessExpr.type, array, index); + } + + @Override + protected JavaExpression visitArrayCreation(ArrayCreation arrayCreationExpr, Void unused) { + List<@Nullable JavaExpression> dims = convert(arrayCreationExpr.getDimensions()); + List inits = convert(arrayCreationExpr.getInitializers()); + return new ArrayCreation(arrayCreationExpr.getType(), dims, inits); + } + + @Override + protected JavaExpression visitBinaryOperation(BinaryOperation binaryOpExpr, Void unused) { + JavaExpression left = convert(binaryOpExpr.getLeft()); + JavaExpression right = convert(binaryOpExpr.getRight()); + return new BinaryOperation( + binaryOpExpr.getType(), binaryOpExpr.getOperationKind(), left, right); + } + + @Override + protected JavaExpression visitClassName(ClassName classNameExpr, Void unused) { + return classNameExpr; + } + + @Override + protected JavaExpression visitFieldAccess(FieldAccess fieldAccessExpr, Void unused) { + JavaExpression receiver = convert(fieldAccessExpr.getReceiver()); + return new FieldAccess(receiver, fieldAccessExpr.getType(), fieldAccessExpr.getField()); + } + + @Override + protected JavaExpression visitFormalParameter(FormalParameter parameterExpr, Void unused) { + return parameterExpr; + } + + @Override + protected JavaExpression visitLocalVariable(LocalVariable localVarExpr, Void unused) { + return localVarExpr; + } + + @Override + protected JavaExpression visitMethodCall(MethodCall methodCallExpr, Void unused) { + JavaExpression receiver = convert(methodCallExpr.getReceiver()); + List args = convert(methodCallExpr.getArguments()); + return new MethodCall( + methodCallExpr.getType(), methodCallExpr.getElement(), receiver, args); + } + + @Override + protected JavaExpression visitThisReference(ThisReference thisExpr, Void unused) { + return thisExpr; + } + + @Override + protected JavaExpression visitUnaryOperation(UnaryOperation unaryOpExpr, Void unused) { + JavaExpression operand = convert(unaryOpExpr.getOperand()); + return new UnaryOperation(unaryOpExpr.getType(), unaryOpExpr.getOperationKind(), operand); + } + + @Override + protected JavaExpression visitUnknown(Unknown unknownExpr, Void unused) { + return unknownExpr; + } + + @Override + protected JavaExpression visitValueLiteral(ValueLiteral literalExpr, Void unused) { + return literalExpr; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionScanner.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionScanner.java new file mode 100644 index 000000000000..11414a13bbaf --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionScanner.java @@ -0,0 +1,107 @@ +package org.checkerframework.dataflow.expression; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.List; + +/** + * A simple scanner for {@link JavaExpression}. + * + * @param

      the parameter passed to the scan methods + */ +public abstract class JavaExpressionScanner

      extends JavaExpressionVisitor { + + /** + * Scans the JavaExpression. + * + * @param javaExpression the expression to scan + * @param p parameter to pass + */ + public void scan(JavaExpression javaExpression, P p) { + visit(javaExpression, p); + } + + /** + * Scans each JavaExpression in {@code expressions}. + * + * @param expressions a list of JavaExpressions to scan + * @param p pameter to pass + */ + public void scan(List expressions, P p) { + for (JavaExpression expression : expressions) { + if (expression != null) { + visit(expression, p); + } + } + } + + @Override + protected Void visitArrayAccess(ArrayAccess arrayAccessExpr, P p) { + visit(arrayAccessExpr.getArray(), p); + visit(arrayAccessExpr.getIndex(), p); + return null; + } + + @Override + protected Void visitArrayCreation(ArrayCreation arrayCreationExpr, P p) { + scan(arrayCreationExpr.getDimensions(), p); + scan(arrayCreationExpr.getInitializers(), p); + return null; + } + + @Override + protected Void visitBinaryOperation(BinaryOperation binaryOpExpr, P p) { + visit(binaryOpExpr.getLeft(), p); + visit(binaryOpExpr.getRight(), p); + return null; + } + + @Override + protected Void visitClassName(ClassName classNameExpr, P p) { + return null; + } + + @Override + protected Void visitFormalParameter(FormalParameter parameterExpr, P p) { + return null; + } + + @Override + protected Void visitFieldAccess(FieldAccess fieldAccessExpr, P p) { + visit(fieldAccessExpr.getReceiver(), p); + return null; + } + + @Override + protected Void visitLocalVariable(LocalVariable localVarExpr, P p) { + return null; + } + + @Override + protected Void visitMethodCall(MethodCall methodCallExpr, P p) { + visit(methodCallExpr.getReceiver(), p); + scan(methodCallExpr.getArguments(), p); + return null; + } + + @Override + protected Void visitThisReference(ThisReference thisExpr, P p) { + return null; + } + + @Override + protected Void visitUnaryOperation(UnaryOperation unaryOpExpr, P p) { + visit(unaryOpExpr.getOperand(), p); + return null; + } + + @Override + protected Void visitUnknown(Unknown unknownExpr, P p) { + return null; + } + + @Override + protected Void visitValueLiteral(ValueLiteral literalExpr, P p) { + return null; + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionVisitor.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionVisitor.java new file mode 100644 index 000000000000..024ec632ce1f --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/JavaExpressionVisitor.java @@ -0,0 +1,129 @@ +package org.checkerframework.dataflow.expression; + +/** + * A simple visitor for {@link JavaExpression}. + * + * @param the return type of the visit methods + * @param

      the parameter passed to the visit methods + */ +public abstract class JavaExpressionVisitor { + + /** + * Visits the given {@code javaExpr}. + * + * @param javaExpr the expression to visit + * @param p the parameter to pass to the visit method + * @return the result of visiting the expression + */ + public R visit(JavaExpression javaExpr, P p) { + return javaExpr.accept(this, p); + } + + /** + * Visit an {@link ArrayAccess}. + * + * @param arrayAccessExpr the JavaExpression to visit + * @param p the parameter to pass to the visit method + * @return the result of visiting the {@code arrayAccessExpr} + */ + protected abstract R visitArrayAccess(ArrayAccess arrayAccessExpr, P p); + + /** + * Visit an {@link ArrayCreation}. + * + * @param arrayCreationExpr the JavaExpression to visit + * @param p the parameter to pass to the visit method + * @return the result of visiting the {@code arrayCreationExpr} + */ + protected abstract R visitArrayCreation(ArrayCreation arrayCreationExpr, P p); + + /** + * Visit a {@link BinaryOperation}. + * + * @param binaryOpExpr the JavaExpression to visit + * @param p the parameter to pass to the visit method + * @return the result of visiting the {@code binaryOpExpr} + */ + protected abstract R visitBinaryOperation(BinaryOperation binaryOpExpr, P p); + + /** + * Visit a {@link ClassName}. + * + * @param classNameExpr the JavaExpression to visit + * @param p the parameter to pass to the visit method + * @return the result of visiting the {@code classNameExpr} + */ + protected abstract R visitClassName(ClassName classNameExpr, P p); + + /** + * Visit a {@link FieldAccess}. + * + * @param fieldAccessExpr the JavaExpression to visit + * @param p the parameter to pass to the visit method + * @return the result of visiting the {@code fieldAccessExpr} + */ + protected abstract R visitFieldAccess(FieldAccess fieldAccessExpr, P p); + + /** + * Visit a {@link FormalParameter}. + * + * @param parameterExpr the JavaExpression to visit + * @param p the parameter to pass to the visit method + * @return the result of visiting the {@code parameterExpr} + */ + protected abstract R visitFormalParameter(FormalParameter parameterExpr, P p); + + /** + * Visit a {@link LocalVariable}. + * + * @param localVarExpr the JavaExpression to visit + * @param p the parameter to pass to the visit method + * @return the result of visiting the {@code localVarExpr} + */ + protected abstract R visitLocalVariable(LocalVariable localVarExpr, P p); + + /** + * Visit a {@link MethodCall}. + * + * @param methodCallExpr the JavaExpression to visit + * @param p the parameter to pass to the visit method + * @return the result of visiting the {@code methodCallExpr} + */ + protected abstract R visitMethodCall(MethodCall methodCallExpr, P p); + + /** + * Visit a {@link ThisReference}. + * + * @param thisExpr the JavaExpression to visit + * @param p the parameter to pass to the visit method + * @return the result of visiting the {@code thisExpr} + */ + protected abstract R visitThisReference(ThisReference thisExpr, P p); + + /** + * Visit an {@link UnaryOperation}. + * + * @param unaryOpExpr the JavaExpression to visit + * @param p the parameter to pass to the visit method + * @return the result of visiting the {@code unaryOpExpr} + */ + protected abstract R visitUnaryOperation(UnaryOperation unaryOpExpr, P p); + + /** + * Visit an {@link Unknown}. + * + * @param unknownExpr the JavaExpression to visit + * @param p the parameter to pass to the visit method + * @return the result of visiting the {@code unknownExpr} + */ + protected abstract R visitUnknown(Unknown unknownExpr, P p); + + /** + * Visit a {@link ValueLiteral}. + * + * @param literalExpr the JavaExpression to visit + * @param p the parameter to pass to the visit method + * @return the result of visiting the {@code literalExpr} + */ + protected abstract R visitValueLiteral(ValueLiteral literalExpr, P p); +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/LocalVariable.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/LocalVariable.java new file mode 100644 index 000000000000..0930d6bedc2f --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/LocalVariable.java @@ -0,0 +1,136 @@ +package org.checkerframework.dataflow.expression; + +import com.sun.tools.javac.code.Symbol.VarSymbol; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TypesUtils; + +import java.util.Objects; + +import javax.lang.model.element.VariableElement; + +/** + * A local variable. + * + *

      This class also represents a formal parameter expressed using its name. Class {@link + * FormalParameter} represents a formal parameter expressed using the "#2" notation. + */ +public class LocalVariable extends JavaExpression { + /** The element for this local variable. */ + protected final VariableElement element; + + /** + * Creates a new LocalVariable. + * + * @param localVar a CFG local variable + */ + public LocalVariable(LocalVariableNode localVar) { + super(localVar.getType()); + this.element = localVar.getElement(); + } + + /** + * Creates a new LocalVariable. + * + * @param element the element for the local variable + */ + public LocalVariable(VariableElement element) { + super(ElementUtils.getType(element)); + this.element = element; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof LocalVariable)) { + return false; + } + LocalVariable other = (LocalVariable) obj; + + return sameElement(element, other.element); + } + + /** + * Returns true if the two elements are the same. + * + * @param element1 the first element to compare + * @param element2 the second element to compare + * @return true if the two elements are the same + */ + protected static boolean sameElement(VariableElement element1, VariableElement element2) { + VarSymbol vs1 = (VarSymbol) element1; + VarSymbol vs2 = (VarSymbol) element2; + // If a LocalVariable is created via JavaExpressionParseUtil#parse, then `vs1.equals(vs2)` + // will not return true even if the elements represent the same local variable. + // The owner of a lambda parameter is the enclosing method, so a local variable and a lambda + // parameter might have the same name and the same owner. Use pos to differentiate this + // case. + return vs1.pos == vs2.pos && vs1.name == vs2.name && vs1.owner.equals(vs2.owner); + } + + /** + * Returns the element for this variable. + * + * @return the element for this variable + */ + public VariableElement getElement() { + return element; + } + + @Override + public int hashCode() { + VarSymbol vs = (VarSymbol) element; + return Objects.hash(vs.pos, vs.name, vs.owner); + } + + @Override + public String toString() { + return element.toString(); + } + + @Override + public String toStringDebug() { + return super.toStringDebug() + " [owner=" + ((VarSymbol) element).owner + "]"; + } + + @Override + public boolean containsOfClass(Class clazz) { + return getClass() == clazz; + } + + @Override + public boolean isDeterministic(AnnotationProvider provider) { + return true; + } + + @Override + public boolean syntacticEquals(JavaExpression je) { + if (!(je instanceof LocalVariable)) { + return false; + } + LocalVariable other = (LocalVariable) je; + return this.equals(other); + } + + @Override + public boolean containsSyntacticEqualJavaExpression(JavaExpression other) { + return syntacticEquals(other); + } + + @Override + public boolean isUnassignableByOtherCode() { + return true; + } + + @Override + public boolean isUnmodifiableByOtherCode() { + return TypesUtils.isImmutableTypeInJdk(((VarSymbol) element).type); + } + + @Override + public R accept(JavaExpressionVisitor visitor, P p) { + return visitor.visitLocalVariable(this, p); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/MethodCall.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/MethodCall.java new file mode 100644 index 000000000000..4c50db53d224 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/MethodCall.java @@ -0,0 +1,191 @@ +package org.checkerframework.dataflow.expression; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.util.PurityUtils; +import org.checkerframework.javacutil.AnnotationProvider; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; + +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeMirror; + +/** A call to a @Deterministic method. */ +public class MethodCall extends JavaExpression { + + /** The method being called. */ + protected final ExecutableElement method; + + /** The receiver argument. */ + protected final JavaExpression receiver; + + /** The arguments. */ + protected final List arguments; + + /** + * Creates a new MethodCall. + * + * @param type the type of the method call + * @param method the method being called + * @param receiver the receiver argument + * @param arguments the arguments + */ + public MethodCall( + TypeMirror type, + ExecutableElement method, + JavaExpression receiver, + List arguments) { + super(type); + this.receiver = receiver; + this.arguments = arguments; + this.method = method; + } + + /** + * Returns the ExecutableElement for the method call. + * + * @return the ExecutableElement for the method call + */ + public ExecutableElement getElement() { + return method; + } + + /** + * Returns the method call receiver (for inspection only - do not modify). + * + * @return the method call receiver (for inspection only - do not modify) + */ + public JavaExpression getReceiver() { + return receiver; + } + + /** + * Returns the method call arguments (for inspection only - do not modify any of the arguments). + * + * @return the method call arguments (for inspection only - do not modify any of the arguments) + */ + public List getArguments() { + return Collections.unmodifiableList(arguments); + } + + @Override + public boolean containsOfClass(Class clazz) { + if (getClass() == clazz) { + return true; + } + if (receiver.containsOfClass(clazz)) { + return true; + } + for (JavaExpression p : arguments) { + if (p.containsOfClass(clazz)) { + return true; + } + } + return false; + } + + @Override + public boolean isDeterministic(AnnotationProvider provider) { + return (PurityUtils.isDeterministic(provider, method) || provider.isDeterministic(method)) + && listIsDeterministic(arguments, provider); + } + + @Override + public boolean isUnassignableByOtherCode() { + // There is no need to check that the method is deterministic, because a MethodCall is + // only created for deterministic methods. + return receiver.isUnmodifiableByOtherCode() + && arguments.stream().allMatch(JavaExpression::isUnmodifiableByOtherCode); + } + + @Override + public boolean isUnmodifiableByOtherCode() { + return isUnassignableByOtherCode(); + } + + @Override + public boolean syntacticEquals(JavaExpression je) { + if (!(je instanceof MethodCall)) { + return false; + } + MethodCall other = (MethodCall) je; + return method.equals(other.method) + && this.receiver.syntacticEquals(other.receiver) + && JavaExpression.syntacticEqualsList(this.arguments, other.arguments); + } + + @Override + public boolean containsSyntacticEqualJavaExpression(JavaExpression other) { + return syntacticEquals(other) + || receiver.containsSyntacticEqualJavaExpression(other) + || JavaExpression.listContainsSyntacticEqualJavaExpression(arguments, other); + } + + @Override + public boolean containsModifiableAliasOf(Store store, JavaExpression other) { + if (receiver.containsModifiableAliasOf(store, other)) { + return true; + } + for (JavaExpression p : arguments) { + if (p.containsModifiableAliasOf(store, other)) { + return true; + } + } + return false; // the method call itself is not modifiable + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof MethodCall)) { + return false; + } + if (method.getKind() == ElementKind.CONSTRUCTOR) { + // No two constructor instances are equal. + return false; + } + MethodCall other = (MethodCall) obj; + return method.equals(other.method) + && receiver.equals(other.receiver) + && arguments.equals(other.arguments); + } + + @Override + public int hashCode() { + if (method.getKind() == ElementKind.CONSTRUCTOR) { + // No two constructor instances have the same hashcode. + return System.identityHashCode(this); + } + return Objects.hash(method, receiver, arguments); + } + + @Override + public String toString() { + StringBuilder preParen = new StringBuilder(); + if (receiver instanceof ClassName) { + preParen.append(receiver.getType()); + } else { + preParen.append(receiver); + } + preParen.append("."); + String methodName = method.getSimpleName().toString(); + preParen.append(methodName); + preParen.append("("); + StringJoiner result = new StringJoiner(", ", preParen, ")"); + for (JavaExpression argument : arguments) { + result.add(argument.toString()); + } + return result.toString(); + } + + @Override + public R accept(JavaExpressionVisitor visitor, P p) { + return visitor.visitMethodCall(this, p); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/ThisReference.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/ThisReference.java new file mode 100644 index 000000000000..4ca54f224c0c --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/ThisReference.java @@ -0,0 +1,75 @@ +package org.checkerframework.dataflow.expression; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.TypesUtils; + +import javax.lang.model.type.TypeMirror; + +/** A use of {@code this}. */ +public class ThisReference extends JavaExpression { + /** + * Create a new ThisReference. + * + * @param type the type of the {@code this} reference + */ + public ThisReference(TypeMirror type) { + super(type); + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj instanceof ThisReference; + } + + @Override + public int hashCode() { + return 0; + } + + @Override + public String toString() { + return "this"; + } + + @Override + public boolean containsOfClass(Class clazz) { + return getClass() == clazz; + } + + @Override + public boolean isDeterministic(AnnotationProvider provider) { + return true; + } + + @Override + public boolean isUnassignableByOtherCode() { + return true; + } + + @Override + public boolean isUnmodifiableByOtherCode() { + return TypesUtils.isImmutableTypeInJdk(type); + } + + @Override + public boolean syntacticEquals(JavaExpression je) { + return je instanceof ThisReference; + } + + @Override + public boolean containsSyntacticEqualJavaExpression(JavaExpression other) { + return this.syntacticEquals(other); + } + + @Override + public boolean containsModifiableAliasOf(Store store, JavaExpression other) { + return false; // 'this' is not modifiable + } + + @Override + public R accept(JavaExpressionVisitor visitor, P p) { + return visitor.visitThisReference(this, p); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/UnaryOperation.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/UnaryOperation.java new file mode 100644 index 000000000000..7b61243c2b2f --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/UnaryOperation.java @@ -0,0 +1,150 @@ +package org.checkerframework.dataflow.expression; + +import com.sun.source.tree.Tree; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.cfg.node.UnaryOperationNode; +import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.BugInCF; + +import java.util.Objects; + +import javax.lang.model.type.TypeMirror; + +/** JavaExpression for unary operations. */ +public class UnaryOperation extends JavaExpression { + + /** The unary operation kind. */ + protected final Tree.Kind operationKind; + + /** The operand. */ + protected final JavaExpression operand; + + /** + * Create a unary operation. + * + * @param type the type of the result + * @param operationKind the operator + * @param operand the operand + */ + public UnaryOperation(TypeMirror type, Tree.Kind operationKind, JavaExpression operand) { + super(operand.type); + this.operationKind = operationKind; + this.operand = operand; + } + + /** + * Create a unary operation. + * + * @param node the unary operation node + * @param operand the operand + */ + public UnaryOperation(UnaryOperationNode node, JavaExpression operand) { + this(node.getType(), node.getTree().getKind(), operand); + } + + /** + * Returns the operator of this unary operation. + * + * @return the unary operation kind + */ + public Tree.Kind getOperationKind() { + return operationKind; + } + + /** + * Returns the operand of this unary operation. + * + * @return the operand + */ + public JavaExpression getOperand() { + return operand; + } + + @Override + public boolean containsOfClass(Class clazz) { + if (getClass() == clazz) { + return true; + } + return operand.containsOfClass(clazz); + } + + @Override + public boolean isDeterministic(AnnotationProvider provider) { + return operand.isDeterministic(provider); + } + + @Override + public boolean isUnassignableByOtherCode() { + return operand.isUnassignableByOtherCode(); + } + + @Override + public boolean isUnmodifiableByOtherCode() { + return operand.isUnmodifiableByOtherCode(); + } + + @Override + public boolean syntacticEquals(JavaExpression je) { + if (!(je instanceof UnaryOperation)) { + return false; + } + UnaryOperation other = (UnaryOperation) je; + return operationKind == other.getOperationKind() && operand.syntacticEquals(other.operand); + } + + @Override + public boolean containsSyntacticEqualJavaExpression(JavaExpression other) { + return this.syntacticEquals(other) || operand.containsSyntacticEqualJavaExpression(other); + } + + @Override + public boolean containsModifiableAliasOf(Store store, JavaExpression other) { + return operand.containsModifiableAliasOf(store, other); + } + + @Override + public int hashCode() { + return Objects.hash(operationKind, operand); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof UnaryOperation)) { + return false; + } + UnaryOperation unOp = (UnaryOperation) other; + return operationKind == unOp.getOperationKind() && operand.equals(unOp.operand); + } + + @Override + public String toString() { + String operandString = operand.toString(); + switch (operationKind) { + case BITWISE_COMPLEMENT: + return "~" + operandString; + case LOGICAL_COMPLEMENT: + return "!" + operandString; + case POSTFIX_DECREMENT: + return operandString + "--"; + case POSTFIX_INCREMENT: + return operandString + "++"; + case PREFIX_DECREMENT: + return "--" + operandString; + case PREFIX_INCREMENT: + return "++" + operandString; + case UNARY_MINUS: + return "-" + operandString; + case UNARY_PLUS: + return "+" + operandString; + default: + throw new BugInCF("Unrecognized unary operation kind " + operationKind); + } + } + + @Override + public R accept(JavaExpressionVisitor visitor, P p) { + return visitor.visitUnaryOperation(this, p); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/Unknown.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/Unknown.java new file mode 100644 index 000000000000..50962765e264 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/Unknown.java @@ -0,0 +1,115 @@ +package org.checkerframework.dataflow.expression; + +import com.sun.source.tree.Tree; + +import org.checkerframework.checker.interning.qual.UsesObjectEquals; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.TreeUtils; + +import javax.lang.model.type.TypeMirror; + +/** Stands for any expression that the Dataflow Framework lacks explicit support for. */ +@UsesObjectEquals +public class Unknown extends JavaExpression { + + /** String representation of the expression that has no corresponding {@code JavaExpression}. */ + private final String originalExpression; + + /** + * Create a new Unknown JavaExpression. + * + * @param type the Java type of this + */ + public Unknown(TypeMirror type) { + this(type, "?"); + } + + /** + * Create a new Unknown JavaExpression. + * + * @param type the Java type of this + * @param originalExpression a String representation of the expression that has no corresponding + * {@code JavaExpression} + */ + public Unknown(TypeMirror type, String originalExpression) { + super(type); + this.originalExpression = originalExpression; + } + + /** + * Create a new Unknown JavaExpression. + * + * @param tree a tree that does not have a corresponding {@code JavaExpression} + */ + public Unknown(Tree tree) { + this(TreeUtils.typeOf(tree), TreeUtils.toStringTruncated(tree, 40)); + } + + /** + * Create a new Unknown JavaExpression. + * + * @param node a node that does not have a corresponding {@code JavaExpression} + */ + public Unknown(Node node) { + this(node.getType(), node.toString()); + } + + @Override + public boolean equals(@Nullable Object obj) { + return obj == this; + } + + // Overridden to avoid an error "overrides equals, but does not override hashCode" + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + @Override + public String toString() { + return originalExpression; + } + + @Override + public boolean containsOfClass(Class clazz) { + return getClass() == clazz; + } + + @Override + public boolean isDeterministic(AnnotationProvider provider) { + return false; + } + + @Override + public boolean isUnassignableByOtherCode() { + return false; + } + + @Override + public boolean isUnmodifiableByOtherCode() { + return false; + } + + @Override + public boolean syntacticEquals(JavaExpression je) { + return this == je; + } + + @Override + public boolean containsSyntacticEqualJavaExpression(JavaExpression other) { + return this.syntacticEquals(other); + } + + @Override + public boolean containsModifiableAliasOf(Store store, JavaExpression other) { + return true; + } + + @Override + public R accept(JavaExpressionVisitor visitor, P p) { + return visitor.visitUnknown(this, p); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/ValueLiteral.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/ValueLiteral.java new file mode 100644 index 000000000000..65c90bdb4ad2 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/ValueLiteral.java @@ -0,0 +1,174 @@ +package org.checkerframework.dataflow.expression; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.cfg.node.ValueLiteralNode; +import org.checkerframework.javacutil.AnnotationProvider; +import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.TypesUtils; + +import java.math.BigInteger; +import java.util.Objects; + +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** JavaExpression for literals. */ +public class ValueLiteral extends JavaExpression { + + /** The value of the literal. */ + protected final @Nullable Object value; + + /** The negative of Long.MIN_VALUE, which does not fit in a long. */ + private static final BigInteger NEGATIVE_LONG_MIN_VALUE = new BigInteger("9223372036854775808"); + + /** + * Creates a ValueLiteral from the node with the given type. + * + * @param type type of the literal + * @param node the literal represents by this {@link + * org.checkerframework.dataflow.expression.ValueLiteral} + */ + public ValueLiteral(TypeMirror type, ValueLiteralNode node) { + super(type); + value = node.getValue(); + } + + /** + * Creates a ValueLiteral where the value is {@code value} that has the given type. + * + * @param type type of the literal + * @param value the literal value + */ + public ValueLiteral(TypeMirror type, @Nullable Object value) { + super(type); + this.value = value; + } + + /** + * Returns the negation of this literal. Throws an exception if negation is not possible. + * + * @return the negation of this literal + */ + public ValueLiteral negate() { + if (TypesUtils.isIntegralPrimitive(type)) { + if (value == null) { + throw new BugInCF("null value of integral type " + type); + } + return new ValueLiteral(type, negateBoxedPrimitive(value)); + } + throw new BugInCF(String.format("cannot negate: %s type=%s", this, type)); + } + + /** + * Negate a boxed primitive. + * + * @param o a boxed primitive + * @return a boxed primitive that is the negation of the argument + */ + private Object negateBoxedPrimitive(Object o) { + if (value instanceof Byte) { + return (byte) -(Byte) value; + } + if (value instanceof Short) { + return (short) -(Short) value; + } + if (value instanceof Integer) { + return -(Integer) value; + } + if (value instanceof Long) { + return -(Long) value; + } + if (value instanceof Float) { + return -(Float) value; + } + if (value instanceof Double) { + return -(Double) value; + } + if (value instanceof BigInteger) { + assert value.equals(NEGATIVE_LONG_MIN_VALUE); + return Long.MIN_VALUE; + } + throw new BugInCF("Cannot be negated: " + o + " " + o.getClass()); + } + + /** + * Returns the value of this literal. + * + * @return the value of this literal + */ + public @Nullable Object getValue() { + return value; + } + + @Override + public boolean containsOfClass(Class clazz) { + return getClass() == clazz; + } + + @Override + public boolean isDeterministic(AnnotationProvider provider) { + return true; + } + + @Override + public boolean isUnassignableByOtherCode() { + return true; + } + + @Override + public boolean isUnmodifiableByOtherCode() { + return true; + } + + @Override + public boolean syntacticEquals(JavaExpression je) { + return this.equals(je); + } + + @Override + public boolean containsSyntacticEqualJavaExpression(JavaExpression other) { + return this.syntacticEquals(other); + } + + @Override + public boolean containsModifiableAliasOf(Store store, JavaExpression other) { + return false; // not modifiable + } + + /// java.lang.Object methods + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof ValueLiteral)) { + return false; + } + ValueLiteral other = (ValueLiteral) obj; + // TODO: Can this string comparison be cleaned up? + // Cannot use Types.isSameType(type, other.type) because we don't have a Types object. + return type.toString().equals(other.type.toString()) && Objects.equals(value, other.value); + } + + @Override + public String toString() { + if (TypesUtils.isString(type)) { + return "\"" + value + "\""; + } else if (type.getKind() == TypeKind.LONG) { + assert value != null : "@AssumeAssertion(nullness): invariant"; + return value.toString() + "L"; + } else if (type.getKind() == TypeKind.CHAR) { + return "\'" + value + "\'"; + } + return value == null ? "null" : value.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(value, type.toString()); + } + + @Override + public R accept(JavaExpressionVisitor visitor, P p) { + return visitor.visitValueLiteral(this, p); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/expression/ViewpointAdaptJavaExpression.java b/dataflow/src/main/java/org/checkerframework/dataflow/expression/ViewpointAdaptJavaExpression.java new file mode 100644 index 000000000000..26f129e5948e --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/expression/ViewpointAdaptJavaExpression.java @@ -0,0 +1,104 @@ +package org.checkerframework.dataflow.expression; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.List; + +/** + * This class has methods to viewpoint-adapt {@link JavaExpression} by replacing {@link + * ThisReference} and {@link FormalParameter} expressions with the given {@link JavaExpression}s. + */ +public class ViewpointAdaptJavaExpression extends JavaExpressionConverter { + + // Public static methods + + /** + * Replace {@link FormalParameter}s by {@code args} in {@code javaExpr}. ({@link ThisReference}s + * are not converted.) + * + * @param javaExpr the expression to viewpoint-adapt + * @param args the expressions that replace {@link FormalParameter}s; if null, {@link + * FormalParameter}s are not replaced + * @return the viewpoint-adapted expression + */ + public static JavaExpression viewpointAdapt( + JavaExpression javaExpr, @Nullable List args) { + return viewpointAdapt(javaExpr, null, args); + } + + /** + * Replace {@link ThisReference} with {@code thisReference} in {@code javaExpr}. ({@link + * FormalParameter} are not replaced. + * + * @param javaExpr the expression to viewpoint-adapt + * @param thisReference the expression that replaces occurrences of {@link ThisReference}; if + * null, {@link ThisReference}s are not replaced + * @return the viewpoint-adapted expression + */ + public static JavaExpression viewpointAdapt( + JavaExpression javaExpr, @Nullable JavaExpression thisReference) { + return viewpointAdapt(javaExpr, thisReference, null); + } + + /** + * Replace {@link FormalParameter}s with {@code args} and {@link ThisReference} with {@code + * thisReference} in {@code javaExpr}. + * + * @param javaExpr the expression to viewpoint-adapt + * @param thisReference the expression that replaces occurrences of {@link ThisReference}; if + * null, {@link ThisReference}s are not replaced + * @param args the expressions that replaces {@link FormalParameter}s; if null, {@link + * FormalParameter}s are not replaced + * @return the viewpoint-adapted expression + */ + public static JavaExpression viewpointAdapt( + JavaExpression javaExpr, + @Nullable JavaExpression thisReference, + @Nullable List args) { + return new ViewpointAdaptJavaExpression(thisReference, args).convert(javaExpr); + } + + // Fields + + /** List of arguments used to replace occurrences {@link FormalParameter}s. */ + private final @Nullable List args; + + /** The expression to replace occurrences of {@link ThisReference}s. */ + private final @Nullable JavaExpression thisReference; + + // Instance methods + + /** + * Creates a {@link JavaExpressionConverter} that viewpoint-adapts using the given {@code + * thisReference} and {@code args}. + * + * @param thisReference the expression that replaces occurrences of {@link ThisReference}; + * {@code null} means don't replace + * @param args list of arguments that replaces occurrences {@link FormalParameter}s; {@code + * null} means don't replace + */ + private ViewpointAdaptJavaExpression( + @Nullable JavaExpression thisReference, @Nullable List args) { + this.args = args; + this.thisReference = thisReference; + } + + @Override + protected JavaExpression visitThisReference(ThisReference thisExpr, Void unused) { + if (thisReference != null) { + return thisReference; + } + return super.visitThisReference(thisExpr, unused); + } + + @Override + protected JavaExpression visitFormalParameter(FormalParameter parameterExpr, Void unused) { + if (args != null) { + int index = parameterExpr.getIndex() - 1; + if (index < args.size()) { + return args.get(index); + } + } + return super.visitFormalParameter(parameterExpr, unused); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/livevariable/LiveVarNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/livevariable/LiveVarNode.java new file mode 100644 index 000000000000..c40558fa3121 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/livevariable/LiveVarNode.java @@ -0,0 +1,53 @@ +package org.checkerframework.dataflow.livevariable; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.node.FieldAccessNode; +import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import org.checkerframework.dataflow.cfg.node.Node; + +/** + * A LiveVarNode contains a CFG node, which can only be a LocalVariableNode or FieldAccessNode. It + * is used to represent the estimate of live variables at certain CFG block during dataflow + * analysis. We override `.equals` in this class to compare nodes by value equality rather than + * reference equality. We want two different nodes with the same value (that is, two nodes refer to + * the same live variable in the program) to be regarded as the same. + */ +public class LiveVarNode { + + /** + * A live variable is represented by a node, which can be a {@link + * org.checkerframework.dataflow.cfg.node.LocalVariableNode} or {@link + * org.checkerframework.dataflow.cfg.node.FieldAccessNode}. + */ + protected final Node liveVariable; + + /** + * Create a new live variable. + * + * @param n a node + */ + public LiveVarNode(Node n) { + assert n instanceof FieldAccessNode || n instanceof LocalVariableNode; + this.liveVariable = n; + } + + @Override + public int hashCode() { + return this.liveVariable.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof LiveVarNode)) { + return false; + } + LiveVarNode other = (LiveVarNode) obj; + // We use `.equals` instead of `==` here to compare value equality. + return this.liveVariable.equals(other.liveVariable); + } + + @Override + public String toString() { + return this.liveVariable.toString(); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/livevariable/LiveVarStore.java b/dataflow/src/main/java/org/checkerframework/dataflow/livevariable/LiveVarStore.java index 976ac38271c5..a85f111c0ec5 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/livevariable/LiveVarStore.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/livevariable/LiveVarStore.java @@ -1,12 +1,7 @@ package org.checkerframework.dataflow.livevariable; -import java.util.HashSet; -import java.util.Set; -import java.util.StringJoiner; import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.analysis.FlowExpressions.Receiver; import org.checkerframework.dataflow.analysis.Store; -import org.checkerframework.dataflow.cfg.CFGVisualizer; import org.checkerframework.dataflow.cfg.node.BinaryOperationNode; import org.checkerframework.dataflow.cfg.node.FieldAccessNode; import org.checkerframework.dataflow.cfg.node.InstanceOfNode; @@ -15,26 +10,34 @@ import org.checkerframework.dataflow.cfg.node.TernaryExpressionNode; import org.checkerframework.dataflow.cfg.node.TypeCastNode; import org.checkerframework.dataflow.cfg.node.UnaryOperationNode; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizer; +import org.checkerframework.dataflow.expression.JavaExpression; import org.checkerframework.javacutil.BugInCF; +import org.plumelib.util.ArraySet; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.StringJoiner; /** A live variable store contains a set of live variables represented by nodes. */ public class LiveVarStore implements Store { - /** A set of live variable abstract values. */ - private final Set liveVarValueSet; + /** The set of live variables in this store */ + private final Set liveVarNodeSet; /** Create a new LiveVarStore. */ public LiveVarStore() { - liveVarValueSet = new HashSet<>(); + liveVarNodeSet = new LinkedHashSet<>(); } /** * Create a new LiveVarStore. * - * @param liveVarValueSet a set of live variable abstract values + * @param liveVarNodeSet the set of live variable nodes. The parameter is captured and the + * caller should not retain an alias. */ - public LiveVarStore(Set liveVarValueSet) { - this.liveVarValueSet = liveVarValueSet; + public LiveVarStore(Set liveVarNodeSet) { + this.liveVarNodeSet = liveVarNodeSet; } /** @@ -42,8 +45,8 @@ public LiveVarStore(Set liveVarValueSet) { * * @param variable a live variable */ - public void putLiveVar(LiveVarValue variable) { - liveVarValueSet.add(variable); + public void putLiveVar(LiveVarNode variable) { + liveVarNodeSet.add(variable); } /** @@ -51,8 +54,8 @@ public void putLiveVar(LiveVarValue variable) { * * @param variable a live variable */ - public void killLiveVar(LiveVarValue variable) { - liveVarValueSet.remove(variable); + public void killLiveVar(LiveVarNode variable) { + liveVarNodeSet.remove(variable); } /** @@ -63,7 +66,7 @@ public void killLiveVar(LiveVarValue variable) { public void addUseInExpression(Node expression) { // TODO Do we need a AbstractNodeScanner to do the following job? if (expression instanceof LocalVariableNode || expression instanceof FieldAccessNode) { - LiveVarValue liveVarValue = new LiveVarValue(expression); + LiveVarNode liveVarValue = new LiveVarNode(expression); putLiveVar(liveVarValue); } else if (expression instanceof UnaryOperationNode) { UnaryOperationNode unaryNode = (UnaryOperationNode) expression; @@ -92,25 +95,27 @@ public boolean equals(@Nullable Object obj) { return false; } LiveVarStore other = (LiveVarStore) obj; - return other.liveVarValueSet.equals(this.liveVarValueSet); + return other.liveVarNodeSet.equals(this.liveVarNodeSet); } @Override public int hashCode() { - return this.liveVarValueSet.hashCode(); + return this.liveVarNodeSet.hashCode(); } @Override public LiveVarStore copy() { - return new LiveVarStore(new HashSet<>(liveVarValueSet)); + return new LiveVarStore(new LinkedHashSet<>(liveVarNodeSet)); } @Override public LiveVarStore leastUpperBound(LiveVarStore other) { - Set liveVarValueSetLub = new HashSet<>(); - liveVarValueSetLub.addAll(this.liveVarValueSet); - liveVarValueSetLub.addAll(other.liveVarValueSet); - return new LiveVarStore(liveVarValueSetLub); + Set liveVarNodeSetLub = + ArraySet.newArraySetOrLinkedHashSet( + this.liveVarNodeSet.size() + other.liveVarNodeSet.size()); + liveVarNodeSetLub.addAll(this.liveVarNodeSet); + liveVarNodeSetLub.addAll(other.liveVarNodeSet); + return new LiveVarStore(liveVarNodeSetLub); } /** It should not be called since it is not used by the backward analysis. */ @@ -120,25 +125,25 @@ public LiveVarStore widenedUpperBound(LiveVarStore previous) { } @Override - public boolean canAlias(Receiver a, Receiver b) { + public boolean canAlias(JavaExpression a, JavaExpression b) { return true; } @Override public String visualize(CFGVisualizer viz) { String key = "live variables"; - if (liveVarValueSet.isEmpty()) { + if (liveVarNodeSet.isEmpty()) { return viz.visualizeStoreKeyVal(key, "none"); } StringJoiner sjStoreVal = new StringJoiner(", "); - for (LiveVarValue liveVarValue : liveVarValueSet) { - sjStoreVal.add(liveVarValue.toString()); + for (LiveVarNode liveVar : liveVarNodeSet) { + sjStoreVal.add(liveVar.toString()); } return viz.visualizeStoreKeyVal(key, sjStoreVal.toString()); } @Override public String toString() { - return liveVarValueSet.toString(); + return liveVarNodeSet.toString(); } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/livevariable/LiveVarTransfer.java b/dataflow/src/main/java/org/checkerframework/dataflow/livevariable/LiveVarTransfer.java index a80514358515..dcc9557fda5b 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/livevariable/LiveVarTransfer.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/livevariable/LiveVarTransfer.java @@ -1,11 +1,10 @@ package org.checkerframework.dataflow.livevariable; -import java.util.List; -import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.analysis.BackwardTransferFunction; import org.checkerframework.dataflow.analysis.RegularTransferResult; import org.checkerframework.dataflow.analysis.TransferInput; import org.checkerframework.dataflow.analysis.TransferResult; +import org.checkerframework.dataflow.analysis.UnusedAbstractValue; import org.checkerframework.dataflow.cfg.UnderlyingAST; import org.checkerframework.dataflow.cfg.node.AbstractNodeVisitor; import org.checkerframework.dataflow.cfg.node.AssignmentNode; @@ -13,18 +12,24 @@ import org.checkerframework.dataflow.cfg.node.Node; import org.checkerframework.dataflow.cfg.node.ObjectCreationNode; import org.checkerframework.dataflow.cfg.node.ReturnNode; -import org.checkerframework.dataflow.cfg.node.StringConcatenateAssignmentNode; +import org.checkerframework.dataflow.qual.SideEffectFree; + +import java.util.List; /** A live variable transfer function. */ public class LiveVarTransfer extends AbstractNodeVisitor< - TransferResult, - TransferInput> - implements BackwardTransferFunction { + TransferResult, + TransferInput> + implements BackwardTransferFunction { + + /** Creates a new LiveVarTransfer. */ + public LiveVarTransfer() {} @Override + @SideEffectFree public LiveVarStore initialNormalExitStore( - UnderlyingAST underlyingAST, @Nullable List returnNodes) { + UnderlyingAST underlyingAST, List returnNodes) { return new LiveVarStore(); } @@ -34,37 +39,27 @@ public LiveVarStore initialExceptionalExitStore(UnderlyingAST underlyingAST) { } @Override - public RegularTransferResult visitNode( - Node n, TransferInput p) { + public RegularTransferResult visitNode( + Node n, TransferInput p) { return new RegularTransferResult<>(null, p.getRegularStore()); } @Override - public RegularTransferResult visitAssignment( - AssignmentNode n, TransferInput p) { - RegularTransferResult transferResult = - (RegularTransferResult) super.visitAssignment(n, p); + public RegularTransferResult visitAssignment( + AssignmentNode n, TransferInput p) { + RegularTransferResult transferResult = + (RegularTransferResult) + super.visitAssignment(n, p); processLiveVarInAssignment( n.getTarget(), n.getExpression(), transferResult.getRegularStore()); return transferResult; } @Override - public RegularTransferResult visitStringConcatenateAssignment( - StringConcatenateAssignmentNode n, TransferInput p) { - RegularTransferResult transferResult = - (RegularTransferResult) - super.visitStringConcatenateAssignment(n, p); - processLiveVarInAssignment( - n.getLeftOperand(), n.getRightOperand(), transferResult.getRegularStore()); - return transferResult; - } - - @Override - public RegularTransferResult visitMethodInvocation( - MethodInvocationNode n, TransferInput p) { - RegularTransferResult transferResult = - (RegularTransferResult) + public RegularTransferResult visitMethodInvocation( + MethodInvocationNode n, TransferInput p) { + RegularTransferResult transferResult = + (RegularTransferResult) super.visitMethodInvocation(n, p); LiveVarStore store = transferResult.getRegularStore(); for (Node arg : n.getArguments()) { @@ -74,10 +69,11 @@ public RegularTransferResult visitMethodInvocation( } @Override - public RegularTransferResult visitObjectCreation( - ObjectCreationNode n, TransferInput p) { - RegularTransferResult transferResult = - (RegularTransferResult) super.visitObjectCreation(n, p); + public RegularTransferResult visitObjectCreation( + ObjectCreationNode n, TransferInput p) { + RegularTransferResult transferResult = + (RegularTransferResult) + super.visitObjectCreation(n, p); LiveVarStore store = transferResult.getRegularStore(); for (Node arg : n.getArguments()) { store.addUseInExpression(arg); @@ -85,6 +81,19 @@ public RegularTransferResult visitObjectCreation( return transferResult; } + @Override + public RegularTransferResult visitReturn( + ReturnNode n, TransferInput p) { + RegularTransferResult transferResult = + (RegularTransferResult) super.visitReturn(n, p); + Node result = n.getResult(); + if (result != null) { + LiveVarStore store = transferResult.getRegularStore(); + store.addUseInExpression(result); + } + return transferResult; + } + /** * Update the information of live variables from an assignment statement. * @@ -93,7 +102,7 @@ public RegularTransferResult visitObjectCreation( * @param store the live variable store */ private void processLiveVarInAssignment(Node variable, Node expression, LiveVarStore store) { - store.killLiveVar(new LiveVarValue(variable)); + store.killLiveVar(new LiveVarNode(variable)); store.addUseInExpression(expression); } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/livevariable/LiveVarValue.java b/dataflow/src/main/java/org/checkerframework/dataflow/livevariable/LiveVarValue.java deleted file mode 100644 index c986a93818cc..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/livevariable/LiveVarValue.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.checkerframework.dataflow.livevariable; - -import org.checkerframework.checker.nullness.qual.Nullable; -import org.checkerframework.dataflow.analysis.AbstractValue; -import org.checkerframework.dataflow.cfg.node.Node; -import org.checkerframework.javacutil.BugInCF; - -/** A live variable (which is represented by a node) wrapper turning node into abstract value. */ -public class LiveVarValue implements AbstractValue { - - /** - * A live variable is represented by a node, which can be a {@link - * org.checkerframework.dataflow.cfg.node.LocalVariableNode} or {@link - * org.checkerframework.dataflow.cfg.node.FieldAccessNode}. - */ - protected final Node liveVariable; - - @Override - public LiveVarValue leastUpperBound(LiveVarValue other) { - throw new BugInCF("lub of LiveVar get called!"); - } - - /** - * Create a new live variable. - * - * @param n a node - */ - public LiveVarValue(Node n) { - this.liveVariable = n; - } - - @Override - public int hashCode() { - return this.liveVariable.hashCode(); - } - - @Override - public boolean equals(@Nullable Object obj) { - if (!(obj instanceof LiveVarValue)) { - return false; - } - LiveVarValue other = (LiveVarValue) obj; - return this.liveVariable.equals(other.liveVariable); - } - - @Override - public String toString() { - return this.liveVariable.toString(); - } -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/qual/Pure.java b/dataflow/src/main/java/org/checkerframework/dataflow/qual/Pure.java deleted file mode 100644 index d6c309b73a12..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/qual/Pure.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.checkerframework.dataflow.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; - -/** - * {@code Pure} is a method annotation that means both {@link SideEffectFree} and {@link - * Deterministic}. The more important of these, when performing pluggable type-checking, is usually - * {@link SideEffectFree}. - * - *

      This annotation is inherited by subtypes, just as if it were meta-annotated with - * {@code @InheritedAnnotation}. - * - * @checker_framework.manual #type-refinement-purity Side effects, determinism, purity, and - * flow-sensitive analysis - */ -// @InheritedAnnotation cannot be written here, because "dataflow" project cannot depend on -// "framework" project. -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) -public @interface Pure { - /** The type of purity. */ - public static enum Kind { - /** The method has no visible side effects. */ - SIDE_EFFECT_FREE, - - /** The method returns exactly the same value when called in the same environment. */ - DETERMINISTIC - } -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/reachingdef/ReachingDefinitionNode.java b/dataflow/src/main/java/org/checkerframework/dataflow/reachingdef/ReachingDefinitionNode.java new file mode 100644 index 000000000000..4488f47ef4fe --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/reachingdef/ReachingDefinitionNode.java @@ -0,0 +1,49 @@ +package org.checkerframework.dataflow.reachingdef; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.cfg.node.AssignmentNode; + +/** + * A ReachingDefinitionNode contains a CFG node, which can only be a AssignmentNode. It is used to + * represent the estimate of a reaching definition at certain CFG block during dataflow analysis. We + * override `.equals` in this class to compare Nodes by value equality rather than reference + * equality. We want two different nodes with the same values (that is, the two nodes refer to the + * same reaching definition in the program) to be regarded as the same here. + */ +public class ReachingDefinitionNode { + + /** + * A reaching definition is represented by a node, which can only be a {@link + * org.checkerframework.dataflow.cfg.node.AssignmentNode}. + */ + protected final AssignmentNode def; + + /** + * Create a new reaching definition. + * + * @param n an assignment node + */ + public ReachingDefinitionNode(AssignmentNode n) { + this.def = n; + } + + @Override + public int hashCode() { + return this.def.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof ReachingDefinitionNode)) { + return false; + } + ReachingDefinitionNode other = (ReachingDefinitionNode) obj; + // We use `.equals` instead of `==` here to compare value equality. + return this.def.equals(other.def); + } + + @Override + public String toString() { + return this.def.toString(); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/reachingdef/ReachingDefinitionStore.java b/dataflow/src/main/java/org/checkerframework/dataflow/reachingdef/ReachingDefinitionStore.java new file mode 100644 index 000000000000..d56d7fc46db2 --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/reachingdef/ReachingDefinitionStore.java @@ -0,0 +1,122 @@ +package org.checkerframework.dataflow.reachingdef; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.Store; +import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizer; +import org.checkerframework.dataflow.expression.JavaExpression; +import org.checkerframework.javacutil.BugInCF; + +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.StringJoiner; + +/** + * A reaching definition store contains a set of reaching definitions represented by + * ReachingDefinitionNode + */ +public class ReachingDefinitionStore implements Store { + + /** The set of reaching definitions in this store */ + private final Set reachingDefSet; + + /** Create a new ReachDefinitionStore. */ + public ReachingDefinitionStore() { + reachingDefSet = new LinkedHashSet<>(); + } + + /** + * Create a new ReachDefinitionStore. + * + * @param reachingDefSet a set of reaching definition nodes. The parameter is captured and the + * caller should not retain an alias. + */ + public ReachingDefinitionStore(Set reachingDefSet) { + this.reachingDefSet = reachingDefSet; + } + + /** + * Remove the information of a reaching definition from the reaching definition set. + * + * @param defTarget target of a reaching definition + */ + public void killDef(Node defTarget) { + Iterator it = reachingDefSet.iterator(); + while (it.hasNext()) { + // We use `.equals` instead of `==` here to compare value equality + // rather than reference equality, because if two left-hand side node + // have same values, we need to kill the old one and replace with the + // new one. + ReachingDefinitionNode generatedDefNode = it.next(); + if (generatedDefNode.def.getTarget().equals(defTarget)) { + it.remove(); + } + } + } + + /** + * Add a reaching definition to the reaching definition set. + * + * @param def a reaching definition + */ + public void putDef(ReachingDefinitionNode def) { + reachingDefSet.add(def); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof ReachingDefinitionStore)) { + return false; + } + ReachingDefinitionStore other = (ReachingDefinitionStore) obj; + return other.reachingDefSet.equals(this.reachingDefSet); + } + + @Override + public int hashCode() { + return this.reachingDefSet.hashCode(); + } + + @Override + public ReachingDefinitionStore copy() { + return new ReachingDefinitionStore(new LinkedHashSet<>(reachingDefSet)); + } + + @Override + public ReachingDefinitionStore leastUpperBound(ReachingDefinitionStore other) { + LinkedHashSet reachingDefSetLub = + new LinkedHashSet<>(this.reachingDefSet.size() + other.reachingDefSet.size()); + reachingDefSetLub.addAll(this.reachingDefSet); + reachingDefSetLub.addAll(other.reachingDefSet); + return new ReachingDefinitionStore(reachingDefSetLub); + } + + @Override + public ReachingDefinitionStore widenedUpperBound(ReachingDefinitionStore previous) { + throw new BugInCF("ReachingDefinitionStore.widenedUpperBound was called!"); + } + + @Override + public boolean canAlias(JavaExpression a, JavaExpression b) { + return true; + } + + @Override + public String visualize(CFGVisualizer viz) { + String key = "reaching definitions"; + if (reachingDefSet.isEmpty()) { + return viz.visualizeStoreKeyVal(key, "none"); + } + StringJoiner sjStoreVal = new StringJoiner(", ", "{ ", " }"); + for (ReachingDefinitionNode reachDefNode : reachingDefSet) { + sjStoreVal.add(reachDefNode.toString()); + } + return viz.visualizeStoreKeyVal(key, sjStoreVal.toString()); + } + + @Override + public String toString() { + return "ReachingDefinitionStore: " + reachingDefSet.toString(); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/reachingdef/ReachingDefinitionTransfer.java b/dataflow/src/main/java/org/checkerframework/dataflow/reachingdef/ReachingDefinitionTransfer.java new file mode 100644 index 000000000000..2ed3b9e0b4ff --- /dev/null +++ b/dataflow/src/main/java/org/checkerframework/dataflow/reachingdef/ReachingDefinitionTransfer.java @@ -0,0 +1,63 @@ +package org.checkerframework.dataflow.reachingdef; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.dataflow.analysis.ForwardTransferFunction; +import org.checkerframework.dataflow.analysis.RegularTransferResult; +import org.checkerframework.dataflow.analysis.TransferInput; +import org.checkerframework.dataflow.analysis.TransferResult; +import org.checkerframework.dataflow.analysis.UnusedAbstractValue; +import org.checkerframework.dataflow.cfg.UnderlyingAST; +import org.checkerframework.dataflow.cfg.node.AbstractNodeVisitor; +import org.checkerframework.dataflow.cfg.node.AssignmentNode; +import org.checkerframework.dataflow.cfg.node.LocalVariableNode; +import org.checkerframework.dataflow.cfg.node.Node; + +import java.util.List; + +/** + * The reaching definition transfer function. The transfer function processes the + * ReachingDefinitionNode in ReachingDefinitionStore, killing the node with same LHS and putting new + * generated node into the store. See dataflow manual for more details. + */ +public class ReachingDefinitionTransfer + extends AbstractNodeVisitor< + TransferResult, + TransferInput> + implements ForwardTransferFunction { + + /** Create a new ReachingDefinitionTransfer. */ + public ReachingDefinitionTransfer() {} + + @Override + public ReachingDefinitionStore initialStore( + UnderlyingAST underlyingAST, @Nullable List parameters) { + return new ReachingDefinitionStore(); + } + + @Override + public RegularTransferResult visitNode( + Node n, TransferInput p) { + return new RegularTransferResult<>(null, p.getRegularStore()); + } + + @Override + public RegularTransferResult visitAssignment( + AssignmentNode n, TransferInput p) { + RegularTransferResult transferResult = + (RegularTransferResult) + super.visitAssignment(n, p); + processDefinition(n, transferResult.getRegularStore()); + return transferResult; + } + + /** + * Update a reaching definition node in the store from an assignment statement. + * + * @param def the definition that should be put into the store + * @param store the reaching definition store + */ + private void processDefinition(AssignmentNode def, ReachingDefinitionStore store) { + store.killDef(def.getTarget()); + store.putDef(new ReachingDefinitionNode(def)); + } +} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/util/AbstractMostlySingleton.java b/dataflow/src/main/java/org/checkerframework/dataflow/util/AbstractMostlySingleton.java deleted file mode 100644 index 6c689d1d50f4..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/util/AbstractMostlySingleton.java +++ /dev/null @@ -1,160 +0,0 @@ -package org.checkerframework.dataflow.util; - -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.Set; -import org.checkerframework.checker.nullness.qual.Nullable; -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. */ -public abstract class AbstractMostlySingleton implements Set { - - /** The possible states of the collection. */ - public enum State { - /** An empty set. */ - EMPTY, - /** A singleton set. */ - SINGLETON, - /** A set of arbitrary size. */ - ANY - } - - /** The current state. */ - protected State state; - /** The current value, non-null when the state is SINGLETON. */ - protected @Nullable T value; - /** The wrapped collection, non-null when the state is ANY. */ - protected @Nullable Collection set; - - /** Create an AbstractMostlySingleton. */ - protected AbstractMostlySingleton(State s) { - this.state = s; - this.value = null; - } - - /** Create an AbstractMostlySingleton. */ - protected AbstractMostlySingleton(State s, T v) { - this.state = s; - this.value = v; - } - - @Override - public int size() { - switch (state) { - case EMPTY: - return 0; - case SINGLETON: - return 1; - case ANY: - assert set != null : "@AssumeAssertion(nullness): set initialized before"; - return set.size(); - default: - throw new BugInCF("Unhandled state " + state); - } - } - - @Override - public boolean isEmpty() { - return size() == 0; - } - - @Override - public Iterator iterator() { - switch (state) { - case EMPTY: - return Collections.emptyIterator(); - case SINGLETON: - return new Iterator() { - private boolean hasNext = true; - - @Override - public boolean hasNext() { - return hasNext; - } - - @Override - public T next() { - if (hasNext) { - hasNext = false; - assert value != null - : "@AssumeAssertion(nullness): previous add is non-null"; - return value; - } - throw new NoSuchElementException(); - } - - @Override - public void remove() { - throw new UnsupportedOperationException(); - } - }; - case ANY: - assert set != null : "@AssumeAssertion(nullness): set initialized before"; - return set.iterator(); - default: - throw new BugInCF("Unhandled state " + state); - } - } - - @Override - public String toString() { - switch (state) { - case EMPTY: - return "[]"; - case SINGLETON: - return "[" + value + "]"; - case ANY: - assert set != null : "@AssumeAssertion(nullness): set initialized before"; - return set.toString(); - default: - throw new BugInCF("Unhandled state " + state); - } - } - - @Override - public boolean addAll(Collection c) { - boolean res = false; - for (T elem : c) { - res |= add(elem); - } - return res; - } - - @Override - public Object[] toArray() { - throw new UnsupportedOperationException(); - } - - @Override - public @Nullable S @PolyNull [] toArray(S @PolyNull [] a) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean remove(@Nullable Object o) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean containsAll(Collection c) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean retainAll(Collection c) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeAll(Collection c) { - throw new UnsupportedOperationException(); - } - - @Override - public void clear() { - throw new UnsupportedOperationException(); - } -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/util/IdentityMostlySingleton.java b/dataflow/src/main/java/org/checkerframework/dataflow/util/IdentityMostlySingleton.java deleted file mode 100644 index fa3734f4c29f..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/util/IdentityMostlySingleton.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.checkerframework.dataflow.util; - -import java.util.ArrayList; -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. - */ -public final class IdentityMostlySingleton extends AbstractMostlySingleton { - - /** Create an IdentityMostlySingleton. */ - public IdentityMostlySingleton() { - super(State.EMPTY); - } - - /** Create an IdentityMostlySingleton. */ - public IdentityMostlySingleton(T value) { - super(State.SINGLETON, value); - } - - @Override - @SuppressWarnings("fallthrough") - public boolean add(T e) { - switch (state) { - case EMPTY: - state = State.SINGLETON; - value = e; - return true; - case SINGLETON: - state = State.ANY; - set = new ArrayList<>(); - assert value != null : "@AssumeAssertion(nullness): previous add is non-null"; - set.add(value); - value = null; - // fallthrough - case ANY: - assert set != null : "@AssumeAssertion(nullness): set initialized before"; - return set.add(e); - default: - throw new BugInCF("Unhandled state " + state); - } - } - - @Override - public boolean contains(Object o) { - switch (state) { - case EMPTY: - return false; - case SINGLETON: - return o == value; - case ANY: - assert set != null : "@AssumeAssertion(nullness): set initialized before"; - return set.contains(o); - default: - throw new BugInCF("Unhandled state " + state); - } - } -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/util/MostlySingleton.java b/dataflow/src/main/java/org/checkerframework/dataflow/util/MostlySingleton.java deleted file mode 100644 index 86e782a2a9ac..000000000000 --- a/dataflow/src/main/java/org/checkerframework/dataflow/util/MostlySingleton.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.checkerframework.dataflow.util; - -import java.util.HashSet; -import java.util.Objects; -import org.checkerframework.javacutil.BugInCF; - -/** - * A set that is more efficient than HashSet for 0 and 1 elements. Uses {@code Objects.equals} for - * object comparison and a {@link HashSet} for backing storage. - */ -public final class MostlySingleton extends AbstractMostlySingleton { - - /** Create a MostlySingleton. */ - public MostlySingleton() { - super(State.EMPTY); - } - - /** Create a MostlySingleton. */ - public MostlySingleton(T value) { - super(State.SINGLETON, value); - } - - @Override - @SuppressWarnings("fallthrough") - public boolean add(T e) { - switch (state) { - case EMPTY: - state = State.SINGLETON; - value = e; - return true; - case SINGLETON: - state = State.ANY; - set = new HashSet<>(); - assert value != null : "@AssumeAssertion(nullness): previous add is non-null"; - set.add(value); - value = null; - // fallthrough - case ANY: - assert set != null : "@AssumeAssertion(nullness): set initialized before"; - return set.add(e); - default: - throw new BugInCF("Unhandled state " + state); - } - } - - @Override - public boolean contains(Object o) { - switch (state) { - case EMPTY: - return false; - case SINGLETON: - return Objects.equals(o, value); - case ANY: - assert set != null : "@AssumeAssertion(nullness): set initialized before"; - return set.contains(o); - default: - throw new BugInCF("Unhandled state " + state); - } - } -} diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/util/NodeUtils.java b/dataflow/src/main/java/org/checkerframework/dataflow/util/NodeUtils.java index 446f623e0557..81ca9f9970d0 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/util/NodeUtils.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/util/NodeUtils.java @@ -3,16 +3,21 @@ import com.sun.source.tree.Tree; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.tree.JCTree; -import javax.annotation.processing.ProcessingEnvironment; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.type.TypeKind; + +import org.checkerframework.dataflow.cfg.node.BooleanLiteralNode; +import org.checkerframework.dataflow.cfg.node.ConditionalNotNode; import org.checkerframework.dataflow.cfg.node.ConditionalOrNode; import org.checkerframework.dataflow.cfg.node.FieldAccessNode; import org.checkerframework.dataflow.cfg.node.MethodInvocationNode; import org.checkerframework.dataflow.cfg.node.Node; +import org.checkerframework.dataflow.cfg.node.TypeCastNode; import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TypesUtils; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.type.TypeKind; + /** A utility class to operate on a given {@link Node}. */ public class NodeUtils { @@ -29,8 +34,7 @@ public static boolean isBooleanTypeNode(Node node) { return true; } - // not all nodes have an associated tree, but those are all not of a - // boolean type. + // not all nodes have an associated tree, but those are all not of a boolean type. Tree tree = node.getTree(); if (tree == null) { return false; @@ -69,4 +73,35 @@ public static boolean isMethodInvocation( ExecutableElement invoked = ((MethodInvocationNode) node).getTarget().getMethod(); return ElementUtils.isMethod(invoked, method, env); } + + /** + * Returns true if the given node statically evaluates to {@code value} and has no side effects. + * + * @param n a node + * @param value the boolean value that the node is tested against + * @return true if the node is equivalent to a literal with value {@code value} + */ + public static boolean isConstantBoolean(Node n, boolean value) { + if (n instanceof BooleanLiteralNode) { + return ((BooleanLiteralNode) n).getValue() == value; + } else if (n instanceof ConditionalNotNode) { + return isConstantBoolean(((ConditionalNotNode) n).getOperand(), !value); + } else { + return false; + } + } + + /** + * Remove any {@link TypeCastNode}s wrapping a node, returning the operand nested within the + * type casts. + * + * @param node a node + * @return node, but with any surrounding typecasts removed + */ + public static Node removeCasts(Node node) { + while (node instanceof TypeCastNode) { + node = ((TypeCastNode) node).getOperand(); + } + return node; + } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/util/PurityChecker.java b/dataflow/src/main/java/org/checkerframework/dataflow/util/PurityChecker.java index bd756bb08a63..9933fa0e7f74 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/util/PurityChecker.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/util/PurityChecker.java @@ -3,25 +3,33 @@ import com.sun.source.tree.ArrayAccessTree; import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.CatchTree; +import com.sun.source.tree.ClassTree; import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.NewClassTree; import com.sun.source.tree.Tree; +import com.sun.source.tree.UnaryTree; import com.sun.source.util.TreePath; import com.sun.source.util.TreePathScanner; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; -import javax.lang.model.element.Element; + import org.checkerframework.dataflow.qual.Deterministic; import org.checkerframework.dataflow.qual.Pure; -import org.checkerframework.dataflow.qual.Pure.Kind; import org.checkerframework.dataflow.qual.SideEffectFree; import org.checkerframework.javacutil.AnnotationProvider; -import org.checkerframework.javacutil.Pair; +import org.checkerframework.javacutil.ElementUtils; +import org.checkerframework.javacutil.TreePathUtil; import org.checkerframework.javacutil.TreeUtils; +import org.plumelib.util.IPair; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; /** * A visitor that determines the purity (as defined by {@link @@ -44,6 +52,7 @@ public class PurityChecker { * @param annoProvider the annotation provider * @param assumeSideEffectFree true if all methods should be assumed to be @SideEffectFree * @param assumeDeterministic true if all methods should be assumed to be @Deterministic + * @param assumePureGetters true if all getter methods should be assumed to be @Pure * @return information about whether the given statement is side-effect-free, deterministic, or * both */ @@ -51,9 +60,11 @@ public static PurityResult checkPurity( TreePath statement, AnnotationProvider annoProvider, boolean assumeSideEffectFree, - boolean assumeDeterministic) { + boolean assumeDeterministic, + boolean assumePureGetters) { PurityCheckerHelper helper = - new PurityCheckerHelper(annoProvider, assumeSideEffectFree, assumeDeterministic); + new PurityCheckerHelper( + annoProvider, assumeSideEffectFree, assumeDeterministic, assumePureGetters); helper.scan(statement, null); return helper.purityResult; } @@ -65,13 +76,13 @@ public static PurityResult checkPurity( public static class PurityResult { /** Reasons that the referenced method is not side-effect-free. */ - protected final List> notSEFreeReasons = new ArrayList<>(1); + protected final List> notSEFreeReasons = new ArrayList<>(1); /** Reasons that the referenced method is not deterministic. */ - protected final List> notDetReasons = new ArrayList<>(1); + protected final List> notDetReasons = new ArrayList<>(1); /** Reasons that the referenced method is not side-effect-free and deterministic. */ - protected final List> notBothReasons = new ArrayList<>(1); + protected final List> notBothReasons = new ArrayList<>(1); /** * Contains all the varieties of purity that the expression has. Starts out with all @@ -103,7 +114,7 @@ public boolean isPure(EnumSet otherKinds) { * * @return the reasons why the method is not side-effect-free */ - public List> getNotSEFreeReasons() { + public List> getNotSEFreeReasons() { return notSEFreeReasons; } @@ -114,8 +125,8 @@ public List> getNotSEFreeReasons() { * @param msgId why the tree is not side-effect-free */ public void addNotSEFreeReason(Tree t, String msgId) { - notSEFreeReasons.add(Pair.of(t, msgId)); - kinds.remove(Kind.SIDE_EFFECT_FREE); + notSEFreeReasons.add(IPair.of(t, msgId)); + kinds.remove(Pure.Kind.SIDE_EFFECT_FREE); } /** @@ -123,7 +134,7 @@ public void addNotSEFreeReason(Tree t, String msgId) { * * @return the reasons why the method is not deterministic */ - public List> getNotDetReasons() { + public List> getNotDetReasons() { return notDetReasons; } @@ -134,8 +145,8 @@ public List> getNotDetReasons() { * @param msgId why the tree is not deterministic */ public void addNotDetReason(Tree t, String msgId) { - notDetReasons.add(Pair.of(t, msgId)); - kinds.remove(Kind.DETERMINISTIC); + notDetReasons.add(IPair.of(t, msgId)); + kinds.remove(Pure.Kind.DETERMINISTIC); } /** @@ -143,7 +154,7 @@ public void addNotDetReason(Tree t, String msgId) { * * @return the reasons why the method is not both side-effect-free and deterministic */ - public List> getNotBothReasons() { + public List> getNotBothReasons() { return notBothReasons; } @@ -154,20 +165,37 @@ public List> getNotBothReasons() { * @param msgId why the tree is not deterministic and side-effect-free */ public void addNotBothReason(Tree t, String msgId) { - notBothReasons.add(Pair.of(t, msgId)); - kinds.remove(Kind.DETERMINISTIC); - kinds.remove(Kind.SIDE_EFFECT_FREE); + notBothReasons.add(IPair.of(t, msgId)); + kinds.remove(Pure.Kind.DETERMINISTIC); + kinds.remove(Pure.Kind.SIDE_EFFECT_FREE); + } + + @Override + public String toString() { + return String.join( + System.lineSeparator(), + "PurityResult{", + " notSEF: " + notSEFreeReasons, + " notDet: " + notDetReasons, + " notBoth: " + notBothReasons, + "}"); } } // TODO: It would be possible to improve efficiency by visiting fewer nodes. This would require // overriding more visit* methods. I'm not sure whether such an optimization would be worth it. - /** Helper class to keep {@link PurityChecker}'s interface clean. */ + /** + * Helper class to keep {@link PurityChecker}'s interface clean. + * + *

      The scanner is run on a single statement, not on a class or method. + */ protected static class PurityCheckerHelper extends TreePathScanner { + /** The purity result. */ PurityResult purityResult = new PurityResult(); + /** The annotation provider (typically an AnnotatedTypeFactory). */ protected final AnnotationProvider annoProvider; /** @@ -182,56 +210,69 @@ protected static class PurityCheckerHelper extends TreePathScanner { */ private final boolean assumeDeterministic; + /** + * True if all getter methods should be assumed to be @SideEffectFree and @Deterministic, + * for the purposes of org.checkerframework.dataflow analysis. + */ + private final boolean assumePureGetters; + /** * Create a PurityCheckerHelper. * * @param annoProvider the annotation provider * @param assumeSideEffectFree true if all methods should be assumed to be @SideEffectFree * @param assumeDeterministic true if all methods should be assumed to be @Deterministic + * @param assumePureGetters true if getter methods should be assumed to be @Pure */ public PurityCheckerHelper( AnnotationProvider annoProvider, boolean assumeSideEffectFree, - boolean assumeDeterministic) { + boolean assumeDeterministic, + boolean assumePureGetters) { this.annoProvider = annoProvider; this.assumeSideEffectFree = assumeSideEffectFree; this.assumeDeterministic = assumeDeterministic; + this.assumePureGetters = assumePureGetters; } @Override - public Void visitCatch(CatchTree node, Void ignore) { - purityResult.addNotDetReason(node, "catch"); - return super.visitCatch(node, ignore); + public Void visitCatch(CatchTree tree, Void ignore) { + purityResult.addNotDetReason(tree, "catch"); + return super.visitCatch(tree, ignore); } + /** Represents a method that is both deterministic and side-effect free. */ + private static final EnumSet detAndSeFree = + EnumSet.of(Pure.Kind.DETERMINISTIC, Pure.Kind.SIDE_EFFECT_FREE); + @Override - public Void visitMethodInvocation(MethodInvocationTree node, Void ignore) { - assert TreeUtils.isUseOfElement(node) : "@AssumeAssertion(nullness): tree kind"; - Element elt = TreeUtils.elementFromUse(node); + public Void visitMethodInvocation(MethodInvocationTree tree, Void ignore) { + ExecutableElement elt = TreeUtils.elementFromUse(tree); if (!PurityUtils.hasPurityAnnotation(annoProvider, elt)) { - purityResult.addNotBothReason(node, "call.method"); + purityResult.addNotBothReason(tree, "call"); } else { EnumSet purityKinds = - (assumeDeterministic && assumeSideEffectFree) + ((assumeDeterministic && assumeSideEffectFree) + || (assumePureGetters && ElementUtils.isGetter(elt))) // Avoid computation if not necessary - ? EnumSet.of(Kind.DETERMINISTIC, Kind.SIDE_EFFECT_FREE) + ? detAndSeFree : PurityUtils.getPurityKinds(annoProvider, elt); - boolean det = assumeDeterministic || purityKinds.contains(Kind.DETERMINISTIC); + boolean det = assumeDeterministic || purityKinds.contains(Pure.Kind.DETERMINISTIC); boolean seFree = - assumeSideEffectFree || purityKinds.contains(Kind.SIDE_EFFECT_FREE); + assumeSideEffectFree || purityKinds.contains(Pure.Kind.SIDE_EFFECT_FREE); if (!det && !seFree) { - purityResult.addNotBothReason(node, "call.method"); + purityResult.addNotBothReason(tree, "call"); } else if (!det) { - purityResult.addNotDetReason(node, "call.method"); + purityResult.addNotDetReason(tree, "call"); } else if (!seFree) { - purityResult.addNotSEFreeReason(node, "call.method"); + purityResult.addNotSEFreeReason(tree, "call"); } } - return super.visitMethodInvocation(node, ignore); + return super.visitMethodInvocation(tree, ignore); } @Override - public Void visitNewClass(NewClassTree node, Void ignore) { + public Void visitNewClass(NewClassTree tree, Void ignore) { // Ordinarily, "new MyClass()" is forbidden. It is permitted, however, when it is the // expression in "throw EXPR;". (In the future, more expressions could be permitted.) // @@ -259,39 +300,60 @@ public Void visitNewClass(NewClassTree node, Void ignore) { // * need to check every containing try statement, not just the nearest enclosing // one. - // Object creation is usually prohibited, but permit "throw new SomeException();" - // if it is not contained within any try statement that has a catch clause. - // (There is no need to check the latter condition, because the Purity Checker - // forbids all catch statements.) + // Object creation is usually prohibited, but permit "throw new SomeException();" if it + // is not contained within any try statement that has a catch clause. (There is no need + // to check the latter condition, because the Purity Checker forbids all catch + // statements.) Tree parent = getCurrentPath().getParentPath().getLeaf(); boolean okThrowDeterministic = parent.getKind() == Tree.Kind.THROW; - assert TreeUtils.isUseOfElement(node) : "@AssumeAssertion(nullness): tree kind"; - Element ctorElement = TreeUtils.elementFromUse(node); - boolean deterministic = assumeDeterministic || okThrowDeterministic; + ExecutableElement ctorElement = TreeUtils.elementFromUse(tree); + boolean deterministic = + assumeDeterministic + || okThrowDeterministic + // No need to check assumePureGetters because a constructor is never a + // getter. + || PurityUtils.isDeterministic(annoProvider, ctorElement); boolean sideEffectFree = assumeSideEffectFree || PurityUtils.isSideEffectFree(annoProvider, ctorElement); // This does not use "addNotBothReason" because the reasons are different: one is - // because the constructor is called at all, and the other is because the constuctor - // is not side-effect-free. + // because the constructor is called at all, and the other is because the constuctor is + // not side-effect-free. if (!deterministic) { - purityResult.addNotDetReason(node, "object.creation"); + purityResult.addNotDetReason(tree, "object.creation"); } if (!sideEffectFree) { - purityResult.addNotSEFreeReason(node, "call.constructor"); + purityResult.addNotSEFreeReason(tree, "call"); } // TODO: if okThrowDeterministic, permit arguments to the newClass to be // non-deterministic (don't add those to purityResult), but still don't permit them to // have side effects. This should probably wait until a rewrite of the Purity Checker. - return super.visitNewClass(node, ignore); + return super.visitNewClass(tree, ignore); } @Override - public Void visitAssignment(AssignmentTree node, Void ignore) { - ExpressionTree variable = node.getVariable(); + public Void visitAssignment(AssignmentTree tree, Void ignore) { + ExpressionTree variable = tree.getVariable(); assignmentCheck(variable); - return super.visitAssignment(node, ignore); + return super.visitAssignment(tree, ignore); + } + + @Override + public Void visitUnary(UnaryTree tree, Void ignore) { + switch (tree.getKind()) { + case POSTFIX_DECREMENT: + case POSTFIX_INCREMENT: + case PREFIX_DECREMENT: + case PREFIX_INCREMENT: + ExpressionTree expression = tree.getExpression(); + assignmentCheck(expression); + break; + default: + // Nothing to do + break; + } + return super.visitUnary(tree, ignore); } /** @@ -300,6 +362,15 @@ public Void visitAssignment(AssignmentTree node, Void ignore) { * @param variable the lhs to check */ protected void assignmentCheck(ExpressionTree variable) { + variable = TreeUtils.withoutParens(variable); + VariableElement fieldElt = TreeUtils.asFieldAccess(variable); + if (fieldElt != null + && isFieldInCurrentClass(fieldElt) + && TreePathUtil.inConstructor(getCurrentPath())) { + // assigning a field in a constructor + // TODO: add a check for ArrayAccessTree too. + return; + } if (TreeUtils.isFieldAccess(variable)) { // lhs is a field access purityResult.addNotBothReason(variable, "assign.field"); @@ -312,6 +383,22 @@ protected void assignmentCheck(ExpressionTree variable) { } } + /** + * Returns true if the given field is defined by the current class. + * + * @param fieldElt a field + * @return true if the given field is defined by the current class + */ + private boolean isFieldInCurrentClass(VariableElement fieldElt) { + ClassTree currentTypeTree = TreePathUtil.enclosingClass(getCurrentPath()); + assert currentTypeTree != null : "@AssumeAssertion(nullness)"; + TypeElement currentType = TreeUtils.elementFromDeclaration(currentTypeTree); + assert currentType != null : "@AssumeAssertion(nullness)"; + TypeElement definesField = ElementUtils.enclosingTypeElement(fieldElt); + assert definesField != null : "@AssumeAssertion(nullness)"; + return currentType.equals(definesField); + } + /** * Checks if the argument is a local variable. * @@ -323,10 +410,10 @@ protected boolean isLocalVariable(ExpressionTree variable) { } @Override - public Void visitCompoundAssignment(CompoundAssignmentTree node, Void ignore) { - ExpressionTree variable = node.getVariable(); + public Void visitCompoundAssignment(CompoundAssignmentTree tree, Void ignore) { + ExpressionTree variable = tree.getVariable(); assignmentCheck(variable); - return super.visitCompoundAssignment(node, ignore); + return super.visitCompoundAssignment(tree, ignore); } } } diff --git a/dataflow/src/main/java/org/checkerframework/dataflow/util/PurityUtils.java b/dataflow/src/main/java/org/checkerframework/dataflow/util/PurityUtils.java index 782256af229e..ce61c39c76c5 100644 --- a/dataflow/src/main/java/org/checkerframework/dataflow/util/PurityUtils.java +++ b/dataflow/src/main/java/org/checkerframework/dataflow/util/PurityUtils.java @@ -1,17 +1,19 @@ package org.checkerframework.dataflow.util; import com.sun.source.tree.MethodTree; -import java.util.EnumSet; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Element; + import org.checkerframework.dataflow.qual.Deterministic; import org.checkerframework.dataflow.qual.Pure; -import org.checkerframework.dataflow.qual.Pure.Kind; import org.checkerframework.dataflow.qual.SideEffectFree; import org.checkerframework.javacutil.AnnotationProvider; -import org.checkerframework.javacutil.BugInCF; +import org.checkerframework.javacutil.ElementUtils; import org.checkerframework.javacutil.TreeUtils; +import java.util.EnumSet; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ExecutableElement; + /** * A utility class for working with the {@link SideEffectFree}, {@link Deterministic}, and {@link * Pure} annotations. @@ -22,6 +24,15 @@ */ public class PurityUtils { + /** Do not instantiate. */ + private PurityUtils() { + throw new Error("Do not instantiate PurityUtils."); + } + + /** Represents a method that is both deterministic and side-effect free. */ + private static final EnumSet detAndSeFree = + EnumSet.of(Pure.Kind.DETERMINISTIC, Pure.Kind.SIDE_EFFECT_FREE); + /** * Does the method {@code methodTree} have any purity annotation? * @@ -40,7 +51,8 @@ public static boolean hasPurityAnnotation(AnnotationProvider provider, MethodTre * @param methodElement a method to test * @return whether the method has any purity annotations */ - public static boolean hasPurityAnnotation(AnnotationProvider provider, Element methodElement) { + public static boolean hasPurityAnnotation( + AnnotationProvider provider, ExecutableElement methodElement) { return !getPurityKinds(provider, methodElement).isEmpty(); } @@ -52,10 +64,7 @@ public static boolean hasPurityAnnotation(AnnotationProvider provider, Element m * @return whether the method is deterministic */ public static boolean isDeterministic(AnnotationProvider provider, MethodTree methodTree) { - Element methodElement = TreeUtils.elementFromTree(methodTree); - if (methodElement == null) { - throw new BugInCF("Could not find element for tree: " + methodTree); - } + ExecutableElement methodElement = TreeUtils.elementFromDeclaration(methodTree); return isDeterministic(provider, methodElement); } @@ -66,64 +75,59 @@ public static boolean isDeterministic(AnnotationProvider provider, MethodTree me * @param methodElement a method to test * @return whether the method is deterministic */ - public static boolean isDeterministic(AnnotationProvider provider, Element methodElement) { + public static boolean isDeterministic( + AnnotationProvider provider, ExecutableElement methodElement) { EnumSet kinds = getPurityKinds(provider, methodElement); - return kinds.contains(Kind.DETERMINISTIC); - } - - /** - * Is the method {@code methodTree} side-effect-free? - * - * @param provider how to get annotations - * @param methodTree a method to test - * @return whether the method is side-effect-free - */ - public static boolean isSideEffectFree(AnnotationProvider provider, MethodTree methodTree) { - Element methodElement = TreeUtils.elementFromTree(methodTree); - if (methodElement == null) { - throw new BugInCF("Could not find element for tree: " + methodTree); - } - return isSideEffectFree(provider, methodElement); + return kinds.contains(Pure.Kind.DETERMINISTIC); } /** * Is the method {@code methodElement} side-effect-free? * + *

      This method does not use, and has different semantics than, {@link + * AnnotationProvider#isSideEffectFree}. This method is concerned only with standard purity + * annotations. + * * @param provider how to get annotations * @param methodElement a method to test * @return whether the method is side-effect-free */ - public static boolean isSideEffectFree(AnnotationProvider provider, Element methodElement) { + public static boolean isSideEffectFree( + AnnotationProvider provider, ExecutableElement methodElement) { EnumSet kinds = getPurityKinds(provider, methodElement); - return kinds.contains(Kind.SIDE_EFFECT_FREE); + return kinds.contains(Pure.Kind.SIDE_EFFECT_FREE); } /** - * Returns the types of purity of the method {@code methodTree}. + * Returns the purity annotations on the method {@code methodTree}. * - * @param provider how to get annotations + * @param provider how to get annotations. Its {@link AnnotationProvider#isSideEffectFree} and + * {@link AnnotationProvider#isDeterministic} methods are not used. * @param methodTree a method to test * @return the types of purity of the method {@code methodTree} */ public static EnumSet getPurityKinds( AnnotationProvider provider, MethodTree methodTree) { - Element methodElement = TreeUtils.elementFromTree(methodTree); - if (methodElement == null) { - throw new BugInCF("Could not find element for tree: " + methodTree); - } + ExecutableElement methodElement = TreeUtils.elementFromDeclaration(methodTree); return getPurityKinds(provider, methodElement); } /** - * Returns the types of purity of the method {@code methodElement}. + * Returns the purity annotations on the method {@code methodElement}. * - * @param provider how to get annotations + * @param provider how to get annotations. Its {@link AnnotationProvider#isSideEffectFree} and + * {@link AnnotationProvider#isDeterministic} methods are not used. * @param methodElement a method to test * @return the types of purity of the method {@code methodElement} */ - // TODO: should the return type be an EnumSet? public static EnumSet getPurityKinds( - AnnotationProvider provider, Element methodElement) { + AnnotationProvider provider, ExecutableElement methodElement) { + // Special case for record accessors + if (ElementUtils.isRecordAccessor(methodElement) + && ElementUtils.isAutoGeneratedRecordMember(methodElement)) { + return detAndSeFree; + } + AnnotationMirror pureAnnotation = provider.getDeclAnnotation(methodElement, Pure.class); AnnotationMirror sefAnnotation = provider.getDeclAnnotation(methodElement, SideEffectFree.class); @@ -131,14 +135,14 @@ public static EnumSet getPurityKinds( provider.getDeclAnnotation(methodElement, Deterministic.class); if (pureAnnotation != null) { - return EnumSet.of(Kind.DETERMINISTIC, Kind.SIDE_EFFECT_FREE); + return detAndSeFree; } EnumSet result = EnumSet.noneOf(Pure.Kind.class); if (sefAnnotation != null) { - result.add(Kind.SIDE_EFFECT_FREE); + result.add(Pure.Kind.SIDE_EFFECT_FREE); } if (detAnnotation != null) { - result.add(Kind.DETERMINISTIC); + result.add(Pure.Kind.DETERMINISTIC); } return result; } diff --git a/dataflow/src/test/java/busyexpr/BusyExpression.java b/dataflow/src/test/java/busyexpr/BusyExpression.java new file mode 100644 index 000000000000..00587aec969c --- /dev/null +++ b/dataflow/src/test/java/busyexpr/BusyExpression.java @@ -0,0 +1,30 @@ +package busyexpr; + +import org.checkerframework.dataflow.analysis.BackwardAnalysis; +import org.checkerframework.dataflow.analysis.BackwardAnalysisImpl; +import org.checkerframework.dataflow.analysis.UnusedAbstractValue; +import org.checkerframework.dataflow.busyexpr.BusyExprStore; +import org.checkerframework.dataflow.busyexpr.BusyExprTransfer; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeLauncher; + +/** Used in busyExpressionTest Gradle task to test the BusyExpression analysis. */ +public class BusyExpression { + /** + * The main method expects to be run in dataflow/tests/busy-expression directory. + * + * @param args not used + */ + public static void main(String[] args) { + + String inputFile = "Test.java"; // input file name; + String method = "test"; + String clazz = "Test"; + String outputFile = "Out.txt"; + + BusyExprTransfer transfer = new BusyExprTransfer(); + BackwardAnalysis backwardAnalysis = + new BackwardAnalysisImpl<>(transfer); + CFGVisualizeLauncher.writeStringOfCFG( + inputFile, method, clazz, outputFile, backwardAnalysis); + } +} diff --git a/dataflow/src/test/java/cfgconstruction/CFGConstruction.java b/dataflow/src/test/java/cfgconstruction/CFGConstruction.java new file mode 100644 index 000000000000..e59df6a13396 --- /dev/null +++ b/dataflow/src/test/java/cfgconstruction/CFGConstruction.java @@ -0,0 +1,16 @@ +package cfgconstruction; + +import org.checkerframework.dataflow.cfg.ControlFlowGraph; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeLauncher; + +public class CFGConstruction { + + public static void main(String[] args) { + String inputFile = "Test.java"; + String clazz = "Test"; + String method = "manyNestedTryFinallyBlocks"; + + ControlFlowGraph cfg = CFGVisualizeLauncher.generateMethodCFG(inputFile, clazz, method); + cfg.checkInvariants(); + } +} diff --git a/dataflow/src/test/java/constantpropagation/ConstantPropagation.java b/dataflow/src/test/java/constantpropagation/ConstantPropagation.java new file mode 100644 index 000000000000..a83e8c374742 --- /dev/null +++ b/dataflow/src/test/java/constantpropagation/ConstantPropagation.java @@ -0,0 +1,29 @@ +package constantpropagation; + +import org.checkerframework.dataflow.analysis.ForwardAnalysis; +import org.checkerframework.dataflow.analysis.ForwardAnalysisImpl; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeLauncher; +import org.checkerframework.dataflow.constantpropagation.Constant; +import org.checkerframework.dataflow.constantpropagation.ConstantPropagationStore; +import org.checkerframework.dataflow.constantpropagation.ConstantPropagationTransfer; + +public class ConstantPropagation { + + /** + * The main method expects to be run in dataflow/tests/constant-propagation directory. + * + * @param args not used + */ + public static void main(String[] args) { + + String inputFile = "Test.java"; + String method = "test"; + String clas = "Test"; + String outputFile = "Out.txt"; + + ConstantPropagationTransfer transfer = new ConstantPropagationTransfer(); + ForwardAnalysis + forwardAnalysis = new ForwardAnalysisImpl<>(transfer); + CFGVisualizeLauncher.writeStringOfCFG(inputFile, method, clas, outputFile, forwardAnalysis); + } +} diff --git a/dataflow/src/test/java/livevar/LiveVariable.java b/dataflow/src/test/java/livevar/LiveVariable.java index 177b6bb5a031..e30939fa2138 100644 --- a/dataflow/src/test/java/livevar/LiveVariable.java +++ b/dataflow/src/test/java/livevar/LiveVariable.java @@ -1,14 +1,11 @@ package livevar; -import java.io.FileWriter; -import java.io.IOException; -import java.util.Map; import org.checkerframework.dataflow.analysis.BackwardAnalysis; import org.checkerframework.dataflow.analysis.BackwardAnalysisImpl; -import org.checkerframework.dataflow.cfg.CFGVisualizeLauncher; +import org.checkerframework.dataflow.analysis.UnusedAbstractValue; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeLauncher; import org.checkerframework.dataflow.livevariable.LiveVarStore; import org.checkerframework.dataflow.livevariable.LiveVarTransfer; -import org.checkerframework.dataflow.livevariable.LiveVarValue; /** Used in liveVariableTest Gradle task to test the LiveVariable analysis. */ public class LiveVariable { @@ -19,24 +16,15 @@ public class LiveVariable { * @param args not used */ public static void main(String[] args) { - String inputFile = "Test.java"; String method = "test"; - String clazz = "Test"; + String clas = "Test"; String outputFile = "Out.txt"; LiveVarTransfer transfer = new LiveVarTransfer(); - BackwardAnalysis backwardAnalysis = + BackwardAnalysis backwardAnalysis = new BackwardAnalysisImpl<>(transfer); - CFGVisualizeLauncher cfgVisualizeLauncher = new CFGVisualizeLauncher(); - Map res = - cfgVisualizeLauncher.generateStringOfCFG( - inputFile, method, clazz, true, backwardAnalysis); - try (FileWriter out = new FileWriter(outputFile)) { - out.write(res.get("stringGraph").toString()); - out.write("\n"); - } catch (IOException e) { - e.printStackTrace(); - } + CFGVisualizeLauncher.writeStringOfCFG( + inputFile, method, clas, outputFile, backwardAnalysis); } } diff --git a/dataflow/src/test/java/reachingdef/ReachingDefinition.java b/dataflow/src/test/java/reachingdef/ReachingDefinition.java new file mode 100644 index 000000000000..d55ce717c649 --- /dev/null +++ b/dataflow/src/test/java/reachingdef/ReachingDefinition.java @@ -0,0 +1,30 @@ +package reachingdef; + +import org.checkerframework.dataflow.analysis.ForwardAnalysis; +import org.checkerframework.dataflow.analysis.ForwardAnalysisImpl; +import org.checkerframework.dataflow.analysis.UnusedAbstractValue; +import org.checkerframework.dataflow.cfg.visualize.CFGVisualizeLauncher; +import org.checkerframework.dataflow.reachingdef.ReachingDefinitionStore; +import org.checkerframework.dataflow.reachingdef.ReachingDefinitionTransfer; + +/** Used in reachingDefinitionsTest Gradle task to test the ReachingDefinition analysis. */ +public class ReachingDefinition { + + /** + * The main method expects to be run in dataflow/tests/reaching-definitions directory. + * + * @param args not used + */ + public static void main(String[] args) { + + String inputFile = "Test.java"; + String method = "test"; + String clas = "Test"; + String outputFile = "Out.txt"; + + ReachingDefinitionTransfer transfer = new ReachingDefinitionTransfer(); + ForwardAnalysis + forwardAnalysis = new ForwardAnalysisImpl<>(transfer); + CFGVisualizeLauncher.writeStringOfCFG(inputFile, method, clas, outputFile, forwardAnalysis); + } +} diff --git a/dataflow/tests/busyexpr/Expected.txt b/dataflow/tests/busyexpr/Expected.txt new file mode 100644 index 000000000000..50e4af804b13 --- /dev/null +++ b/dataflow/tests/busyexpr/Expected.txt @@ -0,0 +1,238 @@ +2 -> 3 EACH_TO_EACH +3 -> 4 EACH_TO_EACH +4 -> 8 THEN_TO_BOTH +4 -> 14 ELSE_TO_BOTH +8 -> 9 EACH_TO_EACH +14 -> 15 EACH_TO_EACH +9 -> 11 EACH_TO_EACH +9 -> 1 ClassCircularityError +9 -> 1 ClassFormatError +9 -> 1 NoClassDefFoundError +9 -> 1 OutOfMemoryError +15 -> 16 EACH_TO_EACH +15 -> 1 RuntimeException +15 -> 1 Error +11 -> 12 EACH_TO_EACH +11 -> 1 RuntimeException +11 -> 1 Error +16 -> 17 EACH_TO_EACH +12 -> 17 EACH_TO_EACH +17 -> 18 EACH_TO_EACH +18 -> 19 EACH_TO_EACH +18 -> 21 ArithmeticException +19 -> 23 EACH_TO_EACH +21 -> 23 EACH_TO_EACH +23 -> 0 EACH_TO_EACH + +2: +Process order: 1 +AnalysisResult#1 +Before: busy expressions = none +~~~~~~~~~ + + +3: +Process order: 2 +AnalysisResult#3 +Before: busy expressions = none +~~~~~~~~~ +a [ VariableDeclaration ] +2 [ IntegerLiteral ] +a = 2 [ Assignment ] +b [ VariableDeclaration ] +3 [ IntegerLiteral ] +b = 3 [ Assignment ] +x [ VariableDeclaration ] +1 [ IntegerLiteral ] +x = 1 [ Assignment ] +y [ VariableDeclaration ] +a [ LocalVariable ] +b [ LocalVariable ] +(a != b) [ NotEqual ] +~~~~~~~~~ +TransferInput#80 +After: busy expressions = (b >> a) + +4: +Process order: 3 +AnalysisResult#5 +Before: busy expressions = (b >> a) +~~~~~~~~~ +ConditionalBlock: then: 8, else: 14 + +8: +Process order: 4 +AnalysisResult#7 +Before: busy expressions = (b >> a) +~~~~~~~~~ +x [ LocalVariable ] +b [ LocalVariable ] +a [ LocalVariable ] +(b >> a) [ SignedRightShift ] +x = (b >> a) [ Assignment ] +expression statement x = b >> a [ ExpressionStatement ] +a [ LocalVariable ] +b [ LocalVariable ] +(a - b) [ NumericalSubtraction ] +~~~~~~~~~ +TransferInput#68 +After: busy expressions = none + +14: +Process order: 8 +AnalysisResult#9 +Before: busy expressions = (b >> a) +~~~~~~~~~ +y [ LocalVariable ] +b [ LocalVariable ] +a [ LocalVariable ] +(b >> a) [ SignedRightShift ] +y = (b >> a) [ Assignment ] +expression statement y = b >> a [ ExpressionStatement ] +a [ LocalVariable ] +0 [ IntegerLiteral ] +a = 0 [ Assignment ] +expression statement a = 0 [ ExpressionStatement ] +(this) [ ImplicitThis ] +(this).test [ MethodAccess ] +a [ LocalVariable ] +b [ LocalVariable ] +(a - b) [ NumericalSubtraction ] +~~~~~~~~~ +TransferInput#40 +After: busy expressions = (a - b) + +9: +Process order: 5 +AnalysisResult#11 +Before: busy expressions = none +~~~~~~~~~ +Test [ ClassName ] +~~~~~~~~~ +TransferInput#2 +After: busy expressions = none + +15: +Process order: 9 +AnalysisResult#13 +Before: busy expressions = (a - b) +~~~~~~~~~ +(this).test((a - b)) [ MethodInvocation ] +~~~~~~~~~ +TransferInput#4 +After: busy expressions = none + +11: +Process order: 6 +AnalysisResult#15 +Before: busy expressions = (a - b) +~~~~~~~~~ +new Test((a - b)) [ ObjectCreation ] +~~~~~~~~~ +TransferInput#3 +After: busy expressions = none + +1: +Process order: 17 +AnalysisResult#17 +Before: busy expressions = none +~~~~~~~~~ + + +16: +Process order: 10 +AnalysisResult#19 +Before: busy expressions = none +~~~~~~~~~ +expression statement test(a - b) [ ExpressionStatement ] +~~~~~~~~~ +TransferInput#35 +After: busy expressions = none + +12: +Process order: 7 +AnalysisResult#21 +Before: busy expressions = (a + b) +~~~~~~~~~ +expression statement new Test(a - b) [ ExpressionStatement ] +y [ LocalVariable ] +a [ LocalVariable ] +b [ LocalVariable ] +(a + b) [ NumericalAddition ] +y = (a + b) [ Assignment ] +expression statement y = a + b [ ExpressionStatement ] +~~~~~~~~~ +TransferInput#36 +After: busy expressions = none + +17: +Process order: 11 +AnalysisResult#23 +Before: busy expressions = none +~~~~~~~~~ +d [ VariableDeclaration ] +marker (start of try statement #0) [ Marker ] +marker (start of try block #0) [ Marker ] +d [ LocalVariable ] +y [ LocalVariable ] +x [ LocalVariable ] +~~~~~~~~~ +TransferInput#27 +After: busy expressions = none + +18: +Process order: 12 +AnalysisResult#25 +Before: busy expressions = (y / x) +~~~~~~~~~ +(y / x) [ IntegerDivision ] +~~~~~~~~~ +TransferInput#25 +After: busy expressions = (y / x) + +19: +Process order: 13 +AnalysisResult#27 +Before: busy expressions = (y / x) +~~~~~~~~~ +(y / x) [ IntegerDivision ] +d = (y / x) [ Assignment ] +expression statement d = y / x [ ExpressionStatement ] +marker (end of try block #0) [ Marker ] +~~~~~~~~~ +TransferInput#9 +After: busy expressions = none + +21: +Process order: 14 +AnalysisResult#29 +Before: busy expressions = none +~~~~~~~~~ +marker (start of catch block for ArithmeticException #0) [ CatchMarker ] +e [ VariableDeclaration ] +d [ LocalVariable ] +10000000 [ IntegerLiteral ] +d = 10000000 [ Assignment ] +expression statement d = 10000000 [ ExpressionStatement ] +marker (end of catch block for ArithmeticException #0) [ CatchMarker ] +~~~~~~~~~ +TransferInput#10 +After: busy expressions = none + +23: +Process order: 15 +AnalysisResult#31 +Before: busy expressions = none +~~~~~~~~~ +d [ LocalVariable ] +return d [ Return ] +~~~~~~~~~ +TransferInput#5 +After: busy expressions = none + +0: +Process order: 16 +AnalysisResult#33 +Before: busy expressions = none +~~~~~~~~~ + diff --git a/dataflow/tests/busyexpr/Test.java b/dataflow/tests/busyexpr/Test.java new file mode 100644 index 000000000000..4e93b45962a3 --- /dev/null +++ b/dataflow/tests/busyexpr/Test.java @@ -0,0 +1,25 @@ +class Test { + Test(int x) {} + + public int test(int m) { + int a = 2, b = 3, x = 1, y; + if (a != b) { + x = b >> a; + new Test(a - b); // test object creation + y = a + b; + } else { + y = b >> a; + a = 0; + test(a - b); // test method invocation + } + + // test exceptional exit block + int d; + try { + d = y / x; + } catch (ArithmeticException e) { + d = 10000000; + } + return d; + } +} diff --git a/dataflow/tests/cfgconstruction/Test.java b/dataflow/tests/cfgconstruction/Test.java new file mode 100644 index 000000000000..998fa64de4d8 --- /dev/null +++ b/dataflow/tests/cfgconstruction/Test.java @@ -0,0 +1,26 @@ +public class Test { + + public void manyNestedTryFinallyBlocks() { + try { + System.out.println("!"); + } finally { + try { + System.out.println("!"); + } finally { + try { + System.out.println("!"); + } finally { + try { + System.out.println("!"); + } finally { + try { + System.out.println("!"); + } finally { + System.out.println("!"); + } + } + } + } + } + } +} diff --git a/dataflow/tests/constant-propagation/Expected.txt b/dataflow/tests/constant-propagation/Expected.txt new file mode 100644 index 000000000000..7d227a4b9a3a --- /dev/null +++ b/dataflow/tests/constant-propagation/Expected.txt @@ -0,0 +1,81 @@ +2 -> 3 EACH_TO_EACH +3 -> 4 EACH_TO_EACH +4 -> 8 THEN_TO_BOTH +4 -> 10 ELSE_TO_BOTH +8 -> 11 EACH_TO_EACH +10 -> 11 EACH_TO_EACH +11 -> 0 EACH_TO_EACH + +2: +Process order: 1 +TransferInput#0 +Before: constant propagation = {} +~~~~~~~~~ + + +3: +Process order: 2 +TransferInput#1 +Before: constant propagation = {} +~~~~~~~~~ +a [ VariableDeclaration ] +0 [ IntegerLiteral ] > 0 +a = 0 [ Assignment ] > 0 +b [ VariableDeclaration ] +a [ LocalVariable ] > 0 +5 [ IntegerLiteral ] > 5 +(a > 5) [ GreaterThan ] +~~~~~~~~~ +AnalysisResult#1 +After: constant propagation = {a=0} + +4: +Process order: 3 +TransferInput#10 +Before: constant propagation = {a=0} +~~~~~~~~~ +ConditionalBlock: then: 8, else: 10 + +8: +Process order: 4 +TransferInput#12 +Before: constant propagation = {a=0} +~~~~~~~~~ +b [ LocalVariable ] +a [ LocalVariable ] +b = a [ Assignment ] +expression statement b = a [ ExpressionStatement ] +~~~~~~~~~ +AnalysisResult#3 +After: constant propagation = {a=0, b=0} + +10: +Process order: 5 +TransferInput#13 +Before: constant propagation = {a=0} +~~~~~~~~~ +b [ LocalVariable ] +4 [ IntegerLiteral ] +b = 4 [ Assignment ] +expression statement b = 4 [ ExpressionStatement ] +~~~~~~~~~ +AnalysisResult#5 +After: constant propagation = {a=0, b=4} + +11: +Process order: 6 +TransferInput#25 +Before: constant propagation = {a=0, b=T} +~~~~~~~~~ +b [ LocalVariable ] +return b [ Return ] +~~~~~~~~~ +AnalysisResult#7 +After: constant propagation = {a=0, b=T} + +0: +Process order: 7 +TransferInput#29 +Before: constant propagation = {a=0, b=T} +~~~~~~~~~ + diff --git a/dataflow/tests/constant-propagation/Test.java b/dataflow/tests/constant-propagation/Test.java new file mode 100644 index 000000000000..62cb612e315e --- /dev/null +++ b/dataflow/tests/constant-propagation/Test.java @@ -0,0 +1,11 @@ +public class Test { + public int test() { + int a = 0, b; + if (a > 5) { + b = a; + } else { + b = 4; + } + return b; + } +} diff --git a/dataflow/tests/issue3447/Test.java b/dataflow/tests/issue3447/Test.java new file mode 100644 index 000000000000..563d01a1d625 --- /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/dataflow/tests/live-variable/Expected.txt b/dataflow/tests/live-variable/Expected.txt index 079d9f0ce499..0652beb051b6 100644 --- a/dataflow/tests/live-variable/Expected.txt +++ b/dataflow/tests/live-variable/Expected.txt @@ -2,17 +2,25 @@ 3 -> 4 EACH_TO_EACH 4 -> 8 THEN_TO_BOTH 4 -> 10 ELSE_TO_BOTH -8 -> 0 EACH_TO_EACH -10 -> 0 EACH_TO_EACH +8 -> 11 EACH_TO_EACH +10 -> 11 EACH_TO_EACH +11 -> 12 EACH_TO_EACH +12 -> 13 EACH_TO_EACH +12 -> 15 ArithmeticException +13 -> 17 EACH_TO_EACH +15 -> 17 EACH_TO_EACH +17 -> 0 EACH_TO_EACH 2: Process order: 1 +AnalysisResult#1 Before: live variables = none ~~~~~~~~~ 3: Process order: 2 +AnalysisResult#3 Before: live variables = none ~~~~~~~~~ a [ VariableDeclaration ] @@ -28,16 +36,19 @@ a [ LocalVariable ] 0 [ IntegerLiteral ] (a > 0) [ GreaterThan ] ~~~~~~~~~ +TransferInput#52 After: live variables = a, b, c 4: Process order: 3 +AnalysisResult#5 Before: live variables = a, b, c ~~~~~~~~~ ConditionalBlock: then: 8, else: 10 8: Process order: 4 +AnalysisResult#7 Before: live variables = a, c ~~~~~~~~~ d [ VariableDeclaration ] @@ -46,10 +57,12 @@ c [ LocalVariable ] (a + c) [ NumericalAddition ] d = (a + c) [ Assignment ] ~~~~~~~~~ -After: live variables = none +TransferInput#36 +After: live variables = a 10: Process order: 5 +AnalysisResult#9 Before: live variables = a, b ~~~~~~~~~ e [ VariableDeclaration ] @@ -58,10 +71,81 @@ b [ LocalVariable ] (a + b) [ NumericalAddition ] e = (a + b) [ Assignment ] ~~~~~~~~~ +TransferInput#35 +After: live variables = a + +11: +Process order: 6 +AnalysisResult#11 +Before: live variables = a +~~~~~~~~~ +f [ VariableDeclaration ] +b [ LocalVariable ] +0 [ IntegerLiteral ] +b = 0 [ Assignment ] +expression statement b = 0 [ ExpressionStatement ] +marker (start of try statement #0) [ Marker ] +marker (start of try block #0) [ Marker ] +f [ LocalVariable ] +1 [ IntegerLiteral ] +a [ LocalVariable ] +~~~~~~~~~ +TransferInput#23 +After: live variables = a, b + +12: +Process order: 7 +AnalysisResult#13 +Before: live variables = a +~~~~~~~~~ +(1 / a) [ IntegerDivision ] +~~~~~~~~~ +TransferInput#21 +After: live variables = a + +13: +Process order: 8 +AnalysisResult#15 +Before: live variables = a +~~~~~~~~~ +(1 / a) [ IntegerDivision ] +f = (1 / a) [ Assignment ] +expression statement f = 1 / a [ ExpressionStatement ] +marker (end of try block #0) [ Marker ] +~~~~~~~~~ +TransferInput#5 +After: live variables = a + +15: +Process order: 9 +AnalysisResult#17 +Before: live variables = a, b +~~~~~~~~~ +marker (start of catch block for ArithmeticException #0) [ CatchMarker ] +e [ VariableDeclaration ] +f [ LocalVariable ] +b [ LocalVariable ] +f = b [ Assignment ] +expression statement f = b [ ExpressionStatement ] +marker (end of catch block for ArithmeticException #0) [ CatchMarker ] +~~~~~~~~~ +TransferInput#6 +After: live variables = a + +17: +Process order: 10 +AnalysisResult#19 +Before: live variables = a +~~~~~~~~~ +a [ LocalVariable ] +return a [ Return ] +~~~~~~~~~ +TransferInput#1 After: live variables = none 0: -Process order: 6 +Process order: 11 +AnalysisResult#21 Before: live variables = none ~~~~~~~~~ diff --git a/dataflow/tests/live-variable/Test.java b/dataflow/tests/live-variable/Test.java index 891241c18fe4..db2c8e4b9f3f 100644 --- a/dataflow/tests/live-variable/Test.java +++ b/dataflow/tests/live-variable/Test.java @@ -1,10 +1,19 @@ +// This file may not be renamed; it has to have the same filename as ../issue3447/Test.java . public class Test { - public void test() { + public int test() { int a = 1, b = 2, c = 3; if (a > 0) { int d = a + c; } else { int e = a + b; } + int f; + b = 0; + try { + f = 1 / a; + } catch (ArithmeticException e) { + f = b; + } + return a; } } diff --git a/dataflow/tests/reachingdef/Expected.txt b/dataflow/tests/reachingdef/Expected.txt new file mode 100644 index 000000000000..471be89feb9f --- /dev/null +++ b/dataflow/tests/reachingdef/Expected.txt @@ -0,0 +1,107 @@ +2 -> 3 EACH_TO_EACH +3 -> 4 EACH_TO_EACH +4 -> 8 THEN_TO_BOTH +4 -> 10 ELSE_TO_BOTH +8 -> 11 EACH_TO_EACH +10 -> 11 EACH_TO_EACH +11 -> 0 EACH_TO_EACH + +2: +Process order: 1 +TransferInput#0 +Before: reaching definitions = none +~~~~~~~~~ + + +3: +Process order: 2 +TransferInput#1 +Before: reaching definitions = none +~~~~~~~~~ +a [ VariableDeclaration ] +1 [ IntegerLiteral ] +a = 1 [ Assignment ] +b [ VariableDeclaration ] +2 [ IntegerLiteral ] +b = 2 [ Assignment ] +c [ VariableDeclaration ] +3 [ IntegerLiteral ] +c = 3 [ Assignment ] +x [ VariableDeclaration ] +"a" [ StringLiteral ] +x = "a" [ Assignment ] +y [ VariableDeclaration ] +"b" [ StringLiteral ] +y = "b" [ Assignment ] +a [ LocalVariable ] +0 [ IntegerLiteral ] +(a > 0) [ GreaterThan ] +~~~~~~~~~ +AnalysisResult#1 +After: reaching definitions = { a = 1, b = 2, c = 3, x = "a", y = "b" } + +4: +Process order: 3 +TransferInput#21 +Before: reaching definitions = { a = 1, b = 2, c = 3, x = "a", y = "b" } +~~~~~~~~~ +ConditionalBlock: then: 8, else: 10 + +8: +Process order: 4 +TransferInput#23 +Before: reaching definitions = { a = 1, b = 2, c = 3, x = "a", y = "b" } +~~~~~~~~~ +d [ VariableDeclaration ] +a [ LocalVariable ] +c [ LocalVariable ] +(a + c) [ NumericalAddition ] +d = (a + c) [ Assignment ] +~~~~~~~~~ +AnalysisResult#3 +After: reaching definitions = { a = 1, b = 2, c = 3, x = "a", y = "b", d = (a + c) } + +10: +Process order: 5 +TransferInput#24 +Before: reaching definitions = { a = 1, b = 2, c = 3, x = "a", y = "b" } +~~~~~~~~~ +e [ VariableDeclaration ] +a [ LocalVariable ] +b [ LocalVariable ] +(a + b) [ NumericalAddition ] +e = (a + b) [ Assignment ] +~~~~~~~~~ +AnalysisResult#5 +After: reaching definitions = { a = 1, b = 2, c = 3, x = "a", y = "b", e = (a + b) } + +11: +Process order: 6 +TransferInput#38 +Before: reaching definitions = { a = 1, b = 2, c = 3, x = "a", y = "b", e = (a + b), d = (a + c) } +~~~~~~~~~ +b [ LocalVariable ] +0 [ IntegerLiteral ] +b = 0 [ Assignment ] +expression statement b = 0 [ ExpressionStatement ] +a [ LocalVariable ] +b [ LocalVariable ] +a = b [ Assignment ] +expression statement a = b [ ExpressionStatement ] +x [ LocalVariable ] +y [ LocalVariable ] +(x + y) [ StringConcatenate ] +x = (x + y) [ Assignment ] +expression statement x += y [ ExpressionStatement ] +a [ LocalVariable ] +return a [ Return ] +~~~~~~~~~ +AnalysisResult#7 +After: reaching definitions = { c = 3, y = "b", e = (a + b), d = (a + c), b = 0, a = b, x = (x + y) } + +0: +Process order: 7 +TransferInput#55 +Before: reaching definitions = { c = 3, y = "b", e = (a + b), d = (a + c), b = 0, a = b, x = (x + y) } +~~~~~~~~~ + diff --git a/dataflow/tests/reachingdef/Test.java b/dataflow/tests/reachingdef/Test.java new file mode 100644 index 000000000000..ea71b2fc492b --- /dev/null +++ b/dataflow/tests/reachingdef/Test.java @@ -0,0 +1,15 @@ +public class Test { + public int test() { + int a = 1, b = 2, c = 3; + String x = "a", y = "b"; + if (a > 0) { + int d = a + c; + } else { + int e = a + b; + } + b = 0; + a = b; + x += y; + return a; + } +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md new file mode 100644 index 000000000000..984963fe7df5 --- /dev/null +++ b/docs/CHANGELOG.md @@ -0,0 +1,5382 @@ +Version 3.42.0-eisop3 (March 1, 2024) +------------------------------------- + +**User-visible changes:** + +Performance improvements in the Nullness Checker. + +**Implementation details:** + +Support separate defaults for wildcard and type variable upper bounds. +Add support for defaults for type variable uses. +See changes in `TypeUseLocation`, `QualiferDefaults`, and `QualifierHierarchy`, +as well as the new `ParametricTypeVariableUseQualifier` meta-annotation. + +Refactored the `TypeInformationPresenter` into several classes in the new +`org.checkerframework.framework.util.visualize` package. + +**Closed issues:** + +eisop#703, typetools#6433, typetools#6438. + + +Version 3.42.0-eisop2 (January 9, 2024) +--------------------------------------- + +**Implementation details:** + +Moved `ErrorTypeKindException` from `org.checkerframework.framework.util.element.ElementAnnotationUtil` to +`org.checkerframework.framework.type.AnnotatedTypeMirror`. Properly raise these errors in more cases. + +Deprecated `AnnotationUtils#isDeclarationAnnotation` and added the clearer `AnnotationUtils#isTypeUseAnnotation`. + +Removed the dependency on the classgraph library, which added over 500kB to `checker.jar`. +It is easy to add the dependency for debugging. + +**Closed issues:** + +eisop#666, eisop#673. + + +Version 3.42.0-eisop1 (January 2, 2024) +--------------------------------------- + +**Closed issues:** + +typetools#6373, typetools#6374. + + +Version 3.42.0 (December 15, 2023) +---------------------------------- + +**User-visible changes:** + +Method annotation `@AssertMethod` indicates that a method checks a value and +possibly throws an assertion. Using it can make flow-sensitive type refinement +more effective. + +In `org.checkerframework.common.util.debug`, renamed `EmptyProcessor` to `DoNothingProcessor`. +Removed `org.checkerframework.common.util.report.DoNothingChecker`. +Moved `ReportChecker` from `org.checkerframework.common.util.report` to `org.checkerframework.common.util.count.report`. +(EISOP note: we did not follow this renaming - if anything, `counting` could be a special case of `reporting`, not +the other way around.) + + +Version 3.41.0-eisop1 (December 5, 2023) +---------------------------------------- + +**User-visible changes:** + +The Nullness Checker now warns about redundant null cases in switch statements and expressions when +using the `-Alint=redundantNullComparison` command-line argument. + +**Closed issues:** + +eisop#628, eisop#635, eisop#640, eisop#641. + + +Version 3.41.0 (December 4, 2023) +--------------------------------- + +**User-visible changes:** + +New command-line options: +* `-AassumePureGetters`: Unsoundly assume that every getter method is pure. + +**Implementation details:** + +Added method `isDeterministic()` to the `AnnotationProvider` interface. + +`CFAbstractValue#leastUpperBound` and `CFAbstractValue#widenUpperBound` are now +final. Subclasses should override method `CFAbstractValue#upperBound(V, +TypeMirror, boolean)` instead. + +(EISOP note: typetools added the new method annotation `org.checkerframework.dataflow.qual.AssertMethod` +to treat such methods like assert statements. EISOP might change the implementation of this feature +in a future release.) + +**Closed issues:** + +#1497, #3345, #6037, #6204, #6276, #6282, #6290, #6296, #6319, #6327. + + +Version 3.40.0-eisop2 (November 24, 2023) +----------------------------------------- + +**Implementation details:** + +Always use reflective access for `TreeMaker#Select`, to allow artifacts built with +Java 21+ to be executed on Java <21. + + +Version 3.40.0-eisop1 (November 24, 2023) +----------------------------------------- + +**User-visible changes:** + +Improvements to initialization type frames in the Initialization Checker. + +**Implementation details:** + +New method `TreeUtils#isEnhancedSwitchStatement` to determine if a switch statement tree +is an enhanced switch statement. + +**Closed issues:** + +eisop#609, eisop#610, eisop#612. + + +Version 3.40.0 (November 1, 2023) +--------------------------------- + +**User-visible changes:** + +Optional Checker: `checker-util.jar` defines `OptionalUtil.castPresent()` for +suppressing false positive warnings from the Optional Checker. + +**Closed issues:** + +#4947, #6179, #6215, #6218, #6222, #6247, #6259, #6260. + + +Version 3.39.0-eisop1 (October 22, 2023) +---------------------------------------- + +**User-visible changes:** + +The Initialization Checker is now separated from the Nullness Checker. +To unsoundly use the Nullness Checker without initialization checking, use the new `-AassumeInitialized` +command-line argument. +Error messages will now be either from the Initialization Checker or the Nullness Checker, which +simplifies the types in error messages. +`@SuppressWarnings("initialization")` should be used to suppress initialization warnings. +In this release, `nullness` continues to suppress warnings from the Initialization Checker, while +`nullnessnoinit` may be used to suppress warnings from the Nullness Checker only. A future release +will make suppression behavior consistent with other checkers. + +The Initialization Checker supports the new qualifier `@PolyInitialized` to express qualifier polymorphism. + +Fixed a bug in the Nullness Checker where an instance receiver is incorrectly marked non-null after +a static method or field access. This could lead to new nullness errors. The static access should be +changed to be through a class name. + +Checkers now enforce `@TargetLocations` meta-annotations: if a qualifier is declared with the +meta-annotation `@TargetLocations({TypeUseLocation...})`, the qualifier should only be applied to +these type use locations. +The new command-line argument `-AignoreTargetLocations` disables validating the target locations +of qualifiers. This option is not enabled by default. With this flag, the checker ignores all +`@TargetLocations` meta-annotations and allows all qualifiers to be applied to every type use. + +**Implementation details:** + +Corrected the arguments to an `ObjectCreationNode` when the node refers to an +anonymous constructor invocation with an explicit enclosing expression in Java 11+. +Now the first argument is not treated as an enclosing expression if it is not. + +Deprecated `ObjectCreationNode#getConstructor` in favor of new `ObjectCreationNode#getTypeToInstantiate()`. + +Removed class `StringConcatenateAssignmentNode` and its last usages. +The class was deprecated in release 3.21.3-eisop1 (March 23, 2022) and no longer used in CFGs. + +Changed the return types of +- `BaseTypeChecker#getImmediateSubcheckerClasses()` and overrides to + `Set>`, +- `AnalysisResult#getFinalLocalValues()` to `Map`, and +- `GenericAnnotatedTypeFactory#getFinalLocalValues()` to `Map`. + +**Closed issues:** + +eisop#297, eisop#376, eisop#400, eisop#519, eisop#532, eisop#533, typetools#1590, typetools#1919. + + +Version 3.39.0 (October 2, 2023) +-------------------------------- + +**User-visible changes:** + +The Checker Framework runs on a version 21 JVM. +It does not yet soundly check all new Java 21 language features, but it does not +crash when compiling them. + +**Implementation details:** + +Dataflow supports all the new Java 21 language features. + * A new node, `DeconstructorPatternNode`, was added, so any implementation of + `NodeVisitor` must be updated. + * Method `InstanceOfNode.getBindingVariable()` is deprecated; use + `getPatternNode()` or `getBindingVariables()` instead. + +WPI uses 1-based indexing for formal parameters and arguments. + +**Closed issues:** + +#5911, #5967, #6155, #6173, #6201. + + +Version 3.38.0 (September 1, 2023) +---------------------------------- + +**User-visible changes:** + +Eliminated the `@SignedPositiveFromUnsigned` annotation, which users were +advised against using. + +**Implementation details:** + +Renamed `SourceChecker.processArg()` to `processErrorMessageArg()`. + +**Closed issues:** + +#2156, #5672, #6110, #6111, #6116, #6125, #6129, #6136. + + +Version 3.37.0 (August 1, 2023) +------------------------------- + +**User-visible changes:** + +Removed support for deprecated option `-AuseDefaultsForUncheckedCode`. + +The Signedness Checker no longer allows (nor needs) `@UnknownSignedness` +to be written on a non-integral type. + +**Implementation details:** + +`QualifierHierarchy`: + * The constructor takes an `AnnotatedTypeFactory`. + * Changes to `isSubtype()`: + * `isSubtype()` has been renamed to `isSubypeQualifiers()` and made protected. + Clients that are not in a qualifier hierarchy should call `isSubtypeShallow()` + or, rarely, new method `isSubtypeQualifiersOnly()`. + * New public method `isSubtypeShallow()` that takes two more arguments than + `isSubypeQualifiers()`. + * Similar changes to `greatestLowerBound()` and `leastUpperBound()`. + +**Closed issues:** + +#6076, #6077, #6078, #6098, #6100, #6104, #6113. + + +Version 3.36.0 (July 3, 2023) +----------------------------- + +**User-visible changes:** + +The Initialization Checker issues a `cast.unsafe` warning instead of an +`initialization.cast` error. + +The Resource Leak Checker now issues a `required.method.not.known` error +when an expression with type `@MustCallUnknown` has a must-call obligation +(e.g., because it is a parameter annotated as `@Owning`). + +The Resource Leak Checker's default MustCall type for type variables has been +changed from `@MustCallUnknown` to `@MustCall({})`. This change reduces the +number of false positive warnings in code that uses type variables but not +resources. However, it makes some code that uses type variables and resources +unverifiable with any annotation. + +**Implementation details:** + +Deprecated `ElementUtils.getSimpleNameOrDescription()` in favor of `getSimpleDescription()`. + +Renamed methods in `AnnotatedTypeMirror`. +The old versions are deprecated. Because the `*PrimaryAnnotation*` methods +might not return an annotation of a type variable or wildcard, it is better to +call `getEffectiveAnnotation*` or `hasEffectiveAnnotation*` instead. + * `clearAnnotations*()` => `clearPrimaryAnnotations()` + * `getAnnotation*()` => `getPrimaryAnnotation*()`. + * `hasAnnotation*()` => `hasPrimaryAnnotation()`. + * `removeAnnotation*()` => `removePrimaryAnnotation*()`. + * `isAnnotatedInHierarchy()` => `hasPrimaryAnnotationInHierarchy()` + * `removeNonTopAnnotationInHierarchy()` should not be used. +(EISOP note: these renamings break javac convention and are inconsistently applied. +Only the last two changes are retained.) + +Dataflow Framework: + * New `ExpressionStatementNode` marks an expression that is used as a statement. + * Removed class `StringConcatenateAssignmentNode`, which is now desugared. +(EISOP note: these were performed in 3.21.2-eisop1 and 3.21.3-eisop1, respectively.) + +`GenericAnnotatedTypeFactory`: + * Renamed `getTypeFactoryOfSubchecker()` to `getTypeFactoryOfSubcheckerOrNull`. + * Added new `getTypeFactoryOfSubchecker()` that never returns null. + +Return types changed: + * `GenericAnnotatedTypeFactory.getFinalLocalValues()` return type changed to + `Map`, though the returned value is still a `HashMap`. + * `BaseTypeChecker.getImmediateSubcheckerClasses()` return type changed to + `Set`, though the returned value is still a `LinkedHashSet`. + +Renamed methods in `CFAbstractValue`: + * `combineOneAnnotation()` => `combineAnnotationWithTypeVar()` + * `combineNoAnnotations()` => `combineTwoTypeVars()` + +**Closed issues:** +#5908, #5936, #5971, #6019, #6025, #6028, #6030, #6039, #6053, #6060, #6069. + + +Version 3.35.0 (June 1, 2023) +----------------------------- + +**User-visible changes:** + +The Checker Framework no longer issues `type.checking.not.run` errors. +This reduces clutter in the output. + +Signedness Checker: + * The receiver type of `Object.hashCode()` is now `@UnknownSignedness`. + +**Implementation details:** + +Instead of overriding `isRelevant()`, a type factory implementation should +override `isRelevantImpl()`. Clients should continue to call `isRelevant()`; +never call `isRelevantImpl()` except as `super.isRelevantImpl()`. + +Methods that now return a `boolean` rather than `void`: + * `commonAssignmentCheck()` + * `checkArrayInitialization()` + * `checkLock()` + * `checkLockOfThisOrTree()` + * `ensureExpressionIsEffectivelyFinal()` + +Methods that now return `AnnotationMirrorSet` instead of `Set`: + * `getTopAnnotations()` + * `getBottomAnnotations()` + * `getDefaultTypeDeclarationBounds()` + * `getExceptionParameterLowerBoundAnnotations()` + +Renamed `BaseTypeVisitor.checkExtendsImplements()` to `checkExtendsAndImplements()`. + +Class `FieldInvariants`: + * constructor now takes an `AnnotatedTypeFactory` + * `isSuperInvariant()` has been renamed to `isStrongerThan()` and + no longer takes an `AnnotatedTypeFactory` + +`CFAbstractValue.validateSet()` takes a type factory rather than a `QualifierHierarchy`. + +Removed methods that have been deprecated for over two years. + +**Closed issues:** + +#4170, #5722, #5777, #5807, #5821, #5826, #5829, #5837, #5930. + + +Version 3.34.0-eisop1 (May 9, 2023) +----------------------------------- + +**User-visible changes:** + +There is now a dedicated website for the EISOP Framework at https://eisop.github.io/ . + +The new command-line arguments `-AaliasedTypeAnnos={aliases}` and `-AaliasedDeclAnnos={aliases}` +define custom type and declaration annotation aliases for the canonical annotations of a checker. +`aliases` is in the format +`FQN.canonical.Qualifier1:FQN.alias1.Qual1,FQN.alias2.Qual1;FQN.canonical.Qualifier2:FQN.alias1.Qual2`. + +**Implementation details:** + +The EISOP Framework continues to build and run on JDK 8. + +Improvements to `-AwarnRedundantAnnotations` with type variables and the Interning Checker. + +Refactored handling of test options and fixed the interaction between the `detailedmsgtext` and +`nomsgtext` options. + +New `CFGVisualizeOptions` class for handling command-line arguments, making the +dataflow demo `Playground` applications much easier to use. + + +Version 3.34.0 (May 2, 2023) +---------------------------- + +**User-visible changes:** + +The Checker Framework runs under JDK 20 -- that is, it runs on a version 20 JVM. + +Explicit lambda parameters are defaulted the same as method parameters. For +example, in `(String s) -> {...}` the type of `s` is `@NonNull String`. + +**Implementation details:** + +Renamings in `AnnotatedTypeFactory`: + * `prepareCompilationUnitForWriting()` => `wpiPrepareCompilationUnitForWriting()` + * `prepareClassForWriting()` => `wpiPrepareClassForWriting()` + * `prepareMethodForWriting()` => `wpiPrepareMethodForWriting()` + and changed its signature by adding two formal parameters + +**Closed issues:** + +#803, #5739, #5749, #5767, #5781, #5787. + + +Version 3.33.0 (April 3, 2023) +------------------------------ + +**User-visible changes:** + +The new command-line argument `-AwarnRedundantAnnotations` warns about redundant +annotations. With this flag, a warning is issued if an explicitly written +annotation on a type is the same as the default annotation. This feature does +not warn about all redundant annotations, only some. +(EISOP note: this was implemented in Version 3.27.0-eisop1.) + +The Value Checker is cognizant of signedness annotations. This eliminates some +false positive warnings. + +**Implementation details:** + +The Checker Framework no longer builds under JDK 8. +However, you can still run the Checker Framework under JDK 8. +(EISOP note: the EISOP Framework continues to build and run on JDK 8.) + +**Closed issues:** + +#3785, #5436, #5708, #5717, #5720, #5721, #5727, #5732. + + +Version 3.32.0-eisop1 (March 9, 2023) +------------------------------------- + +**User-visible changes:** + +The new command-line argument `-AcheckEnclosingExpr` enables type checking for +enclosing expression types of inner class instantiations. This fixes an +unsoundness, in particular for the Nullness Initialization Checker, which did +not detect the use of an uninitialized outer class for an inner class +instantiation. +The option is off by default to avoid many false-positive errors. + +**Implementation details:** + +Added method `AnnotatedExecutableType.getVarargType` to access the vararg type +of a method/constructor. +This allows us to remove usages of `AnnotatedTypes.adaptParameters()`. + +A `VariableDeclarationNode` is now correctly added to the CFG for the binding +variable in a `BindingPatternTree`. + +Remove the `fastAssemble` task which is subsumed by `assembleForJavac`. + +Successfully compiles with Java 20 and 21. + +**Closed issues:** + +eisop#282, eisop#310, eisop#312, typetools#5672. + + +Version 3.32.0 (March 2, 2023) +------------------------------ + +**User-visible changes:** + +Fixed a bug in the Nullness Checker where a call to a side-effecting method did +not make some formal parameters possibly-null. The Nullness Checker is likely +to issue more warnings for your code. For ways to eliminate the new warnings, +see . + +If you supply the `-AinvocationPreservesArgumentNullness` command-line +option, the Nullness Checker unsoundly assumes that arguments passed to +non-null parameters in an invocation remain non-null after the invocation. +This assumption is unsound in general, but it holds for most code. + +(EISOP note: contrary to this description, one needs to use +`-AinvocationPreservesArgumentNullness=false` to get the unsound behavior. +EISOP keeps only the `-AconservativeArgumentNullnessAfterInvocation` option, +introduced in version 3.25.0-eisop1, which this typetools option is based on.) + +**Implementation details:** + +Moved `TreeUtils.isAutoGeneratedRecordMember(Element)` to `ElementUtils`. +(EISOP note: originally introduced the method in the correct location in Version 3.27.0-eisop1.) + +Renamed `TreeUtils.instanceOfGetPattern()` to `TreeUtils.instanceOfTreeGetPattern()`. +(EISOP note: EISOP performed this renaming in Version 3.21.2-eisop1.) + +Deprecated `AnnotatedTypes#isExplicitlySuperBounded` and `AnnotatedTypes#isExplicitlyExtendsBounded` +because they are duplicates of `#hasExplicitSuperBound` and `#hasExplicitExtendsBound`. + + +Version 3.31.0 (February 17, 2023) +---------------------------------- + +**User-visible changes:** + +Command-line argument `-AshowPrefixInWarningMessages` puts the checker name +on the first line of each warning and error message. + +Signedness Checker changes: + * Cast expressions are not subject to type refinement. When a programmer + writes a cast such as `(@Signed int) 2`, it is not refined to + `@SignednessGlb` and cannot be used in an unsigned context. + * When incompatible arguments are passed to `@PolySigned` formal parameters, + the error is expressed in terms of `@SignednessBottom` rather than the + greatest lower bound of the argument types. + +**Implementation details:** + +Moved `AnnotationMirrorSet` and `AnnotationMirrorMap` from +`org.checkerframework.framework.util` to `org.checkerframework.javacutil`. +Changed uses of `Set` to `AnnotationMirrorSet` including in APIs. +Removed methods from AnnotationUtils that are no longer useful: +`createAnnotationMap`, `createAnnotationSet`, `createUnmodifiableAnnotationSet`. + +**Closed issues:** + +#5597. + + +Version 3.30.0 (February 2, 2023) +--------------------------------- + +**Implementation details:** + +`getQualifierKind()` throws an exception rather than returning null. +(EISOP note: this method is in `ElementQualifierHierarchy` and `QualifierKindHierarchy`.) + +Renamed Gradle task `copyJarsToDist` to `assembleForJavac`. + +**Closed issues:** + +#5402, #5486, #5489, #5519, #5524, #5526. + + +Version 3.29.0 (January 5, 2023) +-------------------------------- + +**User-visible changes:** + +Dropped support for `-ApermitUnsupportedJdkVersion` command-line argument. +You can now run the Checker Framework under any JDK version, without a warning. +(EISOP note: a note is however still issued. Use the EISOP option +`-AnoJreVersionCheck` to also suppress the note.) + +Pass `-Astubs=permit-nullness-assertion-exception.astub` to not be warned about null +pointer exceptions within nullness assertion methods like `Objects.requireNonNull`. + +Pass `-Astubs=sometimes-nullable.astub` to unsoundly permit passing null to +calls if null is sometimes but not always permitted. + +**Closed issues:** + +#5412, #5431, #5435, #5438, #5447, #5450, #5453, #5471, #5472, #5487. + + +Version 3.28.0-eisop1 (December 7, 2022) +---------------------------------------- + +**User-visible changes:** + +Support JSpecify annotations in the `org.jspecify.annotations` package. + +**Implementation details:** + +Remove duplicate code in `AnnotatedTypeFactory` and `javacutil`. + + +Version 3.28.0 (December 1, 2022) +--------------------------------- + +**User-visible changes:** + +The Checker Framework runs under JDK 19 -- that is, it runs on a version 19 JVM. + +**Implementation details:** + +Renamed `TryFinallyScopeCell` to `LabelCell`. + +Renamed `TreeUtils.isEnumSuper` to `isEnumSuperCall`. + +**Closed issues:** + +#5390, #5399, #5390. + + +Version 3.27.0-eisop1 (November 6, 2022) +---------------------------------------- + +**User-visible changes:** + +The new command-line argument `-AwarnRedundantAnnotations` warns about redundant annotations. +With this flag, a warning is issued if an explicitly written annotation on a type is the same +as the default annotation for this type and location. + +Support additional Nullness Checker annotation aliases from: +- `io.micronaut.core.annotation` +- `io.vertx.codegen.annotations` +- `jakarta.annotation` +- `net.bytebuddy[.agent].utility.nullability` + +**Implementation details:** + +When reporting issues on an artificial tree (generated by the compiler), always +try to find the closest non-artificial parent in the AST path to provide position +information. + +Formatting rules for `*.ajava` files are now consistent with the ones for `*.java` files. +Imports are now ignored when parsing `ajava` files. + +Moved method `isAutoGeneratedRecordMember(Element e)`, which was added in 3.27.0, +from `TreeUtils` to the more appropriate `ElementUtils`. + +Refined the return types of several `TreeUtils` `elementFromDeclaration` methods +to be `@NonNull`. + +**Closed issues:** + +eisop#244, eisop#360. + + +Version 3.27.0 (November 1, 2022) +--------------------------------- + +**User-visible changes:** + +The Constant Value Checker supports new annotation `@DoesNotMatchRegex`. + +**Closed issues:** + +#5238, #5360, #5362, #5387. + + +Version 3.26.0-eisop1 (October 13, 2022) +---------------------------------------- + +**Implementation details:** + +Documentation improvements and various code fixes. + +**Closed issues:** + +eisop#333, eisop#348. + + +Version 3.26.0 (October 3, 2022) +-------------------------------- + +**User-visible changes:** + +The Checker Framework runs under JDK 18 -- that is, it runs on a version 18 JVM. +(It worked before, but gave a warning that it was not tested.) + +Annotations are available for some new JDK 17 APIs (some of those +introduced since JDK 11). + +Added `-AnoWarnMemoryConstraints` to change the "Memory constraints are impeding +performance; please increase max heap size" message from a warning to a note. + +'unneeded.suppression' warnings can now themeselves be suppressed. + +**Implementation details:** + +Deprecated `TreeUtils.constructor()` in favor of `TreeUtils.elementFromUse()`. + +Added method `isSideEffectFree()` to the `AnnotationProvider` interface. + +Deprecated `CFAbstractStore.isSideEffectFree()` in favor of new method +`AnnotationProvider.isSideEffectFree()`. Note the different contracts of +`PurityUtils.isSideEffectFree()` and `AnnotationProvider.isSideEffectFree()`. + +Use `TreeUtils.elementFromDeclaration` and `TreeUtils.elementFromUse` in +preference to `TreeUtils.elementFromTree`, when possible. + +For code formatting, use `./gradlew spotlessCheck` and `./gradlew spotlessApply`. +The `checkFormat` and `reformat` Gradle tasks have been removed. + +Removed variable `BaseTypeVisitor.inferPurity`. + +**Closed issues:** + +#5081, #5159, #5245, #5302, #5319, #5323. + + +Version 3.25.0-eisop1 (September 3, 2022) +----------------------------------------- + +**User-visible changes:** + +The new command-line argument `-AconservativeArgumentNullnessAfterInvocation` improves +the soundness of the Nullness Checker. In previous versions and without supplying the +new flag, the receiver and arguments that are passed to non-null parameters in a method call +or constructor invocation are assumed to be non-null after the invocation. +This assumption is unsound in general, but holds for most code. +Use the new flag to soundly handle the nullness of the receiver and arguments in an invocation. +In a future version, we might change the default for this option. + +Support the JSpecify NonNull annotation as an alias in the Nullness Checker. + +Fixed ordering of command-line and JDK stubs. + +**Closed issues:** + +eisop#300, eisop#321. + + +Version 3.25.0 (September 1, 2022) +---------------------------------- + +**User-visible changes:** + +Make `mustcall.not.inheritable` a warning rather than an error. + +The Property File Checker, Internationalization Checker, and Compiler +Message Checker use `File.pathSeparator` to separate property file paths in +`-Apropfiles`, rather than ':'. + +Added `DoNothingChecker` that does nothing. + +**Closed issues:** + +#5216, #5240, #5256, #5273. + + +Version 3.24.0-eisop1 (August 5, 2022) +-------------------------------------- + +**User-visible changes:** + +Postconditions on the parameters of a constructor are now used at new object creations. + + +Version 3.24.0 (August 3, 2022) +------------------------------- + +**User-visible changes:** + +Performance improvements. + +Minor bug fixes and enhancements. + +**Implementation details:** + +Prefer `SystemUtil.jreVersion` to `SystemUtil.getJreVersion()`. + +**Closed issues:** + +#5200, #5216. + + +Version 3.23.0-eisop2 (July 22, 2022) +------------------------------------- + +**Implementation details:** + +Improved defaulting in stub files: +As an extension to the fix for eisop#270, we now allow internally parsing +multiple stub files at the same time. This should make `AnnotatedTypeFactory.getDeclAnnotations` +return the expected declaration annotations for all kinds of elements, +even if it is parsing a different stub file. + +**Closed issues:** + +eisop#308. + + +Version 3.23.0-eisop1 (July 14, 2022) +------------------------------------- + +**Implementation details:** + +Added support for viewpoint adaptation of types via the added +ViewpointAdapter interface. This support is experimental and the API +will change, in particular if the feature is fully integrated with +the DependentTypesHelper. + +Improved defaulting in stub files: +Method `AnnotatedTypeFactory.getDeclAnnotations` now returns the +annotations for a package element. Previously, it returned an empty set +when parsing another file. (eisop#270) + +Method `CFAbstractTransfer.visitMethodInvocation` now only creates a +`ConditionalTransferResult` when the method return type is boolean or +Boolean. This avoids unnecessary duplication of many stores, reducing +memory consumption. + +Improved the CFG type of implicit this receivers. (typetools#5174) + +**Closed issues:** + +eisop#270, eisop#281, typetools#5174, typetools#5189. + + +Version 3.23.0 (July 11, 2022) +------------------------------ + +**User-visible changes:** + +By default, command-line argument `-AstubWarnIfNotFound` is treated as true +for stub files provided on the command line and false for built-in stub +files. Use `-AstubWarnIfNotFound` to enable it for all stub files, and use +new `-AstubNoWarnIfNotFound` to disable it for all stub files. + +New command-line argument `-ApermitStaticOwning` suppresses Resource Leak +Checker warnings related to static owning fields. + +New command-line argument `-ApermitInitializationLeak` suppresses Resource Leak +Checker warnings related to field initialization. + +**Closed issues:** + +#4855, #5151, #5166, #5172, #5175, #5181, #5189. + + +Version 3.22.2 (June 14, 2022) +------------------------------ + +**Implementation details:** + +Expose CFG APIs to allow inserting jumps and throws. + + +Version 3.22.1-eisop1 (June 3, 2022) +------------------------------------ + +**User-visible changes:** + +Type parameters with explicit j.l.Object upper bounds and +unannotated, unbounded wildcards now behave the same in .astub +files and in .java files. + +**Implementation details:** + +In `PropagationTreeAnnotator.visitBinary`, we now consider the two cases where +the resulting Java type of a binary operation can be different from the operands' +types: string concatenation and binary comparison. We apply the declaration +bounds of the resulting Java type to ensure annotations in the ATM are valid. + +Deprecated `AnnotatedTypeFactory.binaryTreeArgTypes(AnnotatedTypeMirror, AnnotatedTypeMirror)` in favor of +`AnnotatedTypeFactory.binaryTreeArgTypes(BinaryTree)` and +`AnnotatedTypeFactory.compoundAssignmentTreeArgTypes(CompoundAssignmentTree)`. + +**Closed issues:** + +typetools#3025, typetools#3030, typetools#3236. + +Test cases for issues that already pass: +typetools#2722, typetools#2995, typetools#3015, typetools#3027. + +typetools#58 was closed in error. See +https://github.com/eisop/checker-framework/issues/242 +for follow-up discussions. + + +Version 3.22.1 (June 1, 2022) +----------------------------- + +**Closed issues:** +#58, #5136, #5138, #5142, #5143. + + +Version 3.22.0-eisop1 (May 6, 2022) +----------------------------------- + +**User-visible changes:** + +Added reaching definitions and very busy expressions analysis demos. + +**Implementation details:** + +Fixed the types of `MethodInvocationNode#arguments` and +`ObjectCreationNode#arguments` in CFGs. Previously, argument nodes are created +using the types from the method declaration, which means some nodes are using +type variables that are not substituted by type arguments at the call site. +For example, we used to observe `new T[]{"a", "b"}` instead of +`new String[]{"a", "b"}`, while the second one makes more sense. + +Added a new gradle task `fastAssemble` to quickly rebuild the Checker +Framework for local development. This command will assemble the jar +files without generating any Javadoc or sources.jar files, thus it is +faster than the gradle assemble task. + +Type system test drivers no longer need to pass `-Anomsgtext`. +The Checker Framework test driver (in `TypecheckExecutor.compile`) now always +passes the `-Anomsgtext` option. + +Moved the `-AajavaChecks` option from `CheckerFrameworkPerDirectoryTest` to +`TypecheckExecutor.compile` to ensure the option is used for all tests. + +**Closed issues:** +eisop#210, eisop#215. + + +Version 3.22.0 (May 2, 2022) +---------------------------- + +**User-visible changes:** + +The Signedness Checker now checks calls to `equals()` as well as to `==`. When +two formal parameter types are annotated with @PolySigned, the two arguments at +a call site must have the same signedness type annotation. (This differs from +the standard rule for polymorphic qualifiers.) + +**Implementation details:** + +When passed a NewClassTree that creates an anonymous constructor, +AnnotatedTypeFactory#constructorFormUse now returns the type of the anonymous +constructor rather than the type of the super constructor invoked in the +anonymous classes constructor. If the super constructor has explicit +annotations, they are copied to the anonymous classes constructor. + +**Closed issues:** +#5113. + + +Version 3.21.4-eisop1 (April 4, 2022) +------------------------------------- + +**Closed issues:** +eisop#199, eisop#204. + + +Version 3.21.4 (April 1, 2022) +------------------------------ + +**Closed issues:** +#5086. + + +Version 3.21.3-eisop1 (March 23, 2022) +-------------------------------------- + +**User-visible changes:** + +If you supply the new `-AjspecifyNullMarkedAlias=false` command-line +option, then the Nullness Checker will not treat +`org.jspecify.nullness.NullMarked` as a defaulting annotation. +By default the `NullMarked` annotation continues to be recognized. + +**Implementation details:** + +Changed `AnnotatedTypeFactory.initializeAtm` from public to package +private visibility. Nobody outside the package should call this method. + +Changed `CFAbstractTransfer.insertIntoStores` from public to protected +visibility. It is only meant as a utility method for use within a +transfer function. + +Deprecated class `StringConcatenateAssignmentNode` and its usages. +String concatenate assignments are now desugared to an assignment and +a concatenation node instead. +This avoids error prone duplication of logic. + +**Closed issues:** +typetools#5075. + + +Version 3.21.3 (March 1, 2022) +------------------------------ + +**Closed issues:** +#2847, #4965, #5039, #5042, #5047. + + +Version 3.21.2-eisop1 (February 2, 2022) +---------------------------------------- + +**User-visible changes:** + +Improved support for `NullMarked` default annotation. + +`DefaultQualifier` supports the new `applyToSubpackages` annotation attribute +to decide whether a default should also apply to subpackages. To preserve the +current behavior the default is `true`. + +**Implementation details:** + +Moved files AnnotationFormatter.java and DefaultAnnotationFormatter.java from +javacutil/src/main/java/org/checkerframework/javacutil/ to +framework/src/main/java/org/checkerframework/framework/util/. +typetools PR 3821 incorrectly moved these files, without adapting their +packages, leading to framework classes in javacutil. +The AnnotationFormatter depends on the InvisibleQualifier framework +annotation, so should be in that project. +Added additional toStringSimple methods to AnnotationUtils to format +AnnotationMirrors without depending on the framework project. + +AnnotatedTypeFactory: removed field `artificialTreeToEnclosingElementMap` and +final methods `getEnclosingElementForArtificialTree` and +`setEnclosingElementForArtificialTree`. The new final method +`setPathForArtificialTree` is used by `CFCFGBuilder` to update the mapping. Now +all trees, including artificial trees, have a correct path and enclosing +element. + +Dataflow Framework: new `ExpressionStatementNode` marks an expression that is +used as a statement. + +To correctly handle ternary expressions, support synthetic AssignmentNodes that +do not merge stores. These synthetic assignments are used for the assignments +to the synthetic variables in a ternary expression. +(typetools PR #5000 48f2652bc8bf4801b2e750cd92325583939f2f52 added synthetic +variables for ternary expressions to the CFG. This broke how the Nullness +Checker handles ternary expressions, leading to false positives.) + +**Closed issues:** +typetools#3281. + + +Version 3.21.2 (February 1, 2022) +--------------------------------- + +**User-visible changes:** + +The `wpi.sh` script supports non-standard names for build system compile targets +via the new `-c` command-line option. + +The Checker Framework now more precisely computes and checks the type of the +pattern variable in a pattern match instanceof. + +**Implementation details:** + +Deprecated CFGLambda.getMethod{Name} in favor of getEnclosingMethod{Name}. + +**Closed issues:** +#4615, #4993, #5006, #5007, #5008, #5013, #5016, #5021. + + +Version 3.21.1 (January 7, 2022) +-------------------------------- + +**User-visible changes:** + +The Checker Framework Gradle Plugin now works incrementally: if you change just +one source file, then Gradle will recompile just that file rather than all +files. + +**Closed issues:** +#2401, #4994, #4995, #4996. + + +Version 3.21.0 (December 17, 2021) +---------------------------------- + +**User-visible changes:** + +The Checker Framework now more precisely computes the type of a switch expression. + +**Implementation details:** + +The Dataflow Framework now analyzes switch expressions and switch statements +that use the new `->` case syntax. To do so, a new node, SwitchExpressionNode, +was added. + +**Closed issues:** +#2373, #4934, #4977, #4979, #4987. + + +Version 3.20.0 (December 6, 2021) +--------------------------------- + +**User-visible changes:** + +The Checker Framework now runs on code that contains switch expressions and +switch statements that use the new `->` case syntax, but treats them +conservatively. A future version will improve precision. + +**Implementation details:** + +The Dataflow Framework can be run on code that contains switch expressions and +switch statements that use the new `->` case syntax, but it does not yet +analyze the cases in a switch expression and it treats `->` as `:`. A future +version will do so. + +Removed methods and classes that have been deprecated for more than one year: + * Old way of constructing qualifier hierarchies + * `@SuppressWarningsKeys` + * `RegularBlock.getContents()` + * `TestUtilities.testBooleanProperty()` + * `CFAbstractTransfer.getValueWithSameAnnotations()` + +**Closed issues:** +#4911, #4948, #4965. + + +Version 3.19.0-eisop1 (November 4, 2021) +---------------------------------------- + +**User-visible changes:** + +Avoid shading of string literals which broke some annotation aliasing. +Add more nullness annotation aliases. + +**Implementation details:** + +Remove the unsound "BOTH-TO-THEN", "BOTH-TO-ELSE" logic from the Dataflow +Framework. + +Small improvements and code-style clean-ups in the Dataflow Framework and +in the core Checker Framework "framework" package. + +**Closed issues:** +eisop#121, typetools#4923. + + +Version 3.19.0 (November 1, 2021) +--------------------------------- + +**User-visible changes:** + +The Checker Framework runs under JDK 17 -- that is, it runs on a version 17 JVM. +The Checker Framework also continues to run under JDK 8 and JDK 11. New +command-line argument `-ApermitUnsupportedJdkVersion` lets you run the Checker +Framework on any JDK (version 8 or greater) without a warning about an +unsupported JDK version. The Checker Framework does not yet run on code that +contains switch expressions. + +**Implementation details:** + +Removed `org.checkerframework.framework.type.VisitorState` +Removed `AnnotatedTypeFactory#postTypeVarSubstitution` + +Deprecated methods in AnnotatedTypeFactory: +* `getCurrentClassTree` +* `getCurrentMethodReceiver` + +**Closed issues:** +#4932, #4924, #4908, #3014. + + +Version 3.18.1-eisop1 (October 7, 2021) +--------------------------------------- + +**User-visible changes:** + +Add more aliases for nullness annotations; fix manual formatting (#105). + + +Version 3.18.1 (October 4, 2021) +-------------------------------- + +**Closed issues:** +#4902 and #4903. + + +Version 3.18.0-eisop1 (September 23, 2021) +------------------------------------------ + +The new `-AnoJreVersionCheck` command-line argument can be used to not get +a warning about running the Checker Framework on an unsupported JRE version. + +JAR files are minimized to only include required classes. + +Temporarily remove support for "Whole Program Inference" - the -Ainfer option and +related scripts. + +**Implementation details:** + +Changes to `AnnotatedTypeMirror`: + * Rename `clearPrimaryAnnotations()` back to `clearAnnotations()` to be consistent + with other method names. Undoes change in typetools 3.16.0. + * Remove `getAnnotation()` method. `getAnnotationInHierarchy` should be used instead. + Undoes change in typetools #3691. + + +Version 3.18.0 (September 1, 2021) +---------------------------------- + +**User-visible changes:** + +Java records are type-checked. Thanks to Neil Brown. + +**Closed issues:** +#4838, #4843, #4852, #4853, #4861, #4876, #4877, #4878, #4878, #4889, #4889. + + +Version 3.17.0 (August 3, 2021) +------------------------------- + +**User-visible changes:** + +`-Ainfer` can now infer postcondition annotations that reference formal parameters +(e.g. `"#1"`, `"#2"`) and the receiver (`"this"`). + +**Implementation details:** + +Method renamings and signature changes (old methods are removed) in `GenericAnnotatedTypeFactory`: +* `getPreconditionAnnotation(VariableElement, AnnotatedTypeMirror)` => `getPreconditionAnnotations(String, AnnotatedTypeMirror, AnnotatedTypeMirror)` +* `getPostconditionAnnotation(VariableElement, AnnotatedTypeMirror, List)` => `getPostconditionAnnotations(String, AnnotatedTypeMirror, AnnotatedTypeMirror, List)` +* `getPreOrPostconditionAnnotation(VariableElement, AnnotatedTypeMirror, Analysis.BeforeOrAfter, List)` => `getPreOrPostconditionAnnotations(String, AnnotatedTypeMirror, AnnotatedTypeMirror, Analysis.BeforeOrAfter, List)` +* `requiresOrEnsuresQualifierAnno(VariableElement, AnnotationMirror, Analysis.BeforeOrAfter)` => `createRequiresOrEnsuresQualifier(String, AnnotationMirror, AnnotatedTypeMirror, Analysis.BeforeOrAfter, List)` + +Method renamings and signature changes (old method is removed) in `WholeProgramInferenceStorage`: +* `getPreOrPostconditionsForField(Analysis.BeforeOrAfter, ExecutableElement, VariableElement, AnnotatedTypeFactory)` => `getPreOrPostconditions(Analysis.BeforeOrAfter, ExecutableElement, String, AnnotatedTypeMirror, AnnotatedTypeFactory)` + +Method renamings: + * `CFAbstractAnalysis.getFieldValues` => `getFieldInitialValues` + +The following methods no longer take a `fieldValues` parameter: + * `GenericAnnotatedTypeFactory#createFlowAnalysis` + * `CFAnalysis` construtor + * `CFAbstractAnalysis#performAnalysis` + * `CFAbstractAnalysis` constructors + +**Closed issues:** +#4685, #4689, #4785, #4805, #4806, #4815, #4829, #4849. + + +Version 3.16.0 (July 13, 2021) +------------------------------ + +**User-visible changes:** + +You can run the Checker Framework on a JDK 16 JVM. You can pass the `--release +16` command-line argument to the compiler. You may need to add additional +command-line options, such as `--add-opens`; see the Checker Framework manual. +New syntax, such as records and switch expressions, is not yet supported or +type-checked; that will be added in a future release. Thanks to Neil Brown for +the JDK 16 support. + +The Lock Checker supports a new type, `@NewObject`, for the result of a +constructor invocation. + +The `-Ainfer` command-line argument now outputs purity annotations even if +neither `-AsuggestPureMethods` nor `-AcheckPurityAnnotations` is supplied +on the command line. + +**Implementation details:** + +Method renamings (the old methods remain but are deprecated): + * `AnnotationFileElementTypes.getDeclAnnotation` => `getDeclAnnotations` + +Method renamings (the old methods were removed): + * `AnnotatedTypeMirror.clearAnnotations => `clearPrimaryAnnotations` + +Method renamings in `DefaultTypeHierarchy` (the old methods were removed): + * `visitIntersectionSupertype` => `visitIntersectionSupertype` + * `visitIntersectionSubtype` => `visitIntersection_Type` + * `visitUnionSubtype` => `visitUnion_Type` + * `visitTypevarSubtype` => `visitTypevar_Type` + * `visitTypevarSupertype` => `visitType_Typevar` + * `visitWildcardSubtype` => `visitWildcard_Type` + * `visitWildcardSupertype` => `visitType_Wildcard` + +Method renamings in `AnnotatedTypes` (the old methods were removed): + * `expandVarArgs` => `expandVarArgsParameters` + * `expandVarArgsFromTypes` => `expandVarArgsParametersFromTypes` + +**Closed issues:** +#3013, #3754, #3791, #3845, #4523, #4767. + +Version 3.15.0 (June 18, 2021) +---------------------------- + +**User-visible changes:** + +The Resource Leak Checker ensures that certain methods are called on an +object before it is de-allocated. By default, it enforces that `close()` is +called on any expression whose compile-time type implements `java.io.Closeable`. + +**Implementation details:** + +Method renamings (the old methods remain but are deprecated): + * `AnnotatedDeclaredType#wasRaw` => `isUnderlyingTypeRaw` + * `AnnotatedDeclaredType#setWasRaw` => `setIsUnderlyingTypeRaw` + +**Closed issues:** +#4549, #4646, #4684, and #4699. + + +Version 3.14.0 (June 1, 2021) +---------------------------- + +**User-visible changes:** + +The Units Checker supports new qualifiers (thanks to Rene Kraneis): + * `@Volume`, `@m3`, `@mm3`, `@km3` + * `@Force`, `@N`, `@kN` + * `@t` (metric ton, a unit of mass) + +Stub files can now override declaration annotations in the annotated JDK. +Previously, stub files only overrode type annotations in the annotated JDK. + +Command-line argument `-AstubWarnIfNotFound` is treated as true for stub +files provided on the command line. + +**Implementation details:** + +Method `SourceChecker.getProperties` takes a third formal parameter `permitNonExisting`. + +Method `TreeUtils.getMethodName()` returns a `String` rather than a `Name`. + +Removed CheckerDevelMain. + +**Closed issues:** +#3993, #4116, #4586, #4598, #4612, #4614. + + +Version 3.13.0 (May 3, 2021) +---------------------------- + +**Survey:** + +If you use the Checker Framework, please answer a 3-question survey about what +version of Java you use. It will take less than 1 minute to complete. Please +answer it at +. +Thanks! + +**User-visible changes:** + +Command-line argument -AassumeKeyFor makes the Nullness Checker and Map Key +Checker unsoundly assume that the argument to `Map.get` is a key for the +receiver map. + +Not included in eisop: +Warning message keys are shorter. This reduces clutter in error messages and in +`@SuppressWarnings` annotations. Most ".type.invalid", ".type.incompatible", +".invalid", and ".not.satisfied" suffixes and "type.invalid." prefixes have been +removed, and most ".invalid." substrings have been changed to ".". + +The Checker Framework no longer crashes on code that contains binding +variables (introduced in Java 14 for `instanceof` pattern matching), and +such variables are reflected in the control flow graph (CFG). Thanks to +Chris Day for this change. However, note that the Checker Framework only +has full support for Java 8 and Java 11. + +New command-line argument `-AstubWarnNote` makes stub file warnings notes +rather than warnings. + +Removed the StubGenerator section from the manual, because changes in JDK 11 +have broken the StubGenerator program. + +**Implementation details:** + +Method renamings: + * `DependentTypesHelper.atReturnType` => `atMethodBody` + +**Closed issues:** +#1268, #3039, #4410, #4550, #4558, #4563, #4566, #4567, #4571, #4584, #4591, +#4594, #4600. + + +Version 3.12.0 (April 1, 2021) +------------------------------ + +**User-visible changes:** + +New FAQ item "How should I annotate code that uses generics?" gives +examples of annotations on type variables, together with their meaning. + +`-Ainfer=ajava` uses ajava files (rather than jaif files or stub files) +internally during whole-program inference. + +The Optional Checker supports a new annotation `@OptionalBottom` that +stands for (only) the `null` value. + +The `value` element/argument to `@EnumVal` is now required. Previously it +defaulted to an empty array. + +**Implementation details:** + +A precondition or normal postcondition annotation's `value` element must have +type `String[]`, not `String`. A conditional postcondition annotation's +`expression` element must have type `String[]`, not `String`. These changes +will not affect users (any programmer-written annotation that was legal before +will still be legal), but it may affect checker implementations. + +`JavaExpressionParseUtil`: +`JavaExpressionParseUtil#parse` no longer viewpoint-adapts Java expressions. It +just converts the expression `String` to a `JavaExpression`. To that end, +`JavaExpressionParseUtil.JavaExpressionContext` has been removed and +`JavaExpressionParseUtil#parse` no longer takes a context object. Most calls to +`JavaExpressionParseUtil#parse` should be replaced with a call to one of the +methods in `StringToJavaExpressions`. + +Renamed `AnnotatedTypeComparer` to `DoubleAnnotatedTypeScanner`. In the new +class, the method `compare` was renamed `defaultAction`. The method `combineRs` +was replaced by `reduce`. + +Removed methods: + * `AnnotationUtils.getElementValueArrayOrSingleton` + * `DependentTypesHelper.standardizeNewClassTree`: use `atExpression` instead + * `DependentTypesHelper.standardizeString`: override one of the methods + explained in the Javadoc of `convertAnnotationMirror` + +Method renamings: + * `DefaultQualifierForUseTypeAnnotator.getSupportAnnosFromDefaultQualifierForUses` => `getDefaultQualifierForUses` + * In `DependentTypesHelper`: + * `check*` => `check*ForErrorExpressions` + * `viewpointAdaptConstructor` => `atConstructorInvocation` + * `viewpointAdaptMethod` => `atMethodInvocation` + * `viewpointAdaptTypeVariableBounds` => `atParameterizedTypeUse` + * `standardizeClass` => `atTypeDecl` + * `standardizeExpression` => `atExpression` + * `standardizeFieldAccess` => `atFieldAccess` + * `standardizeReturnType` => `atReturnType` + * `standardizeVariable` => `atVariableDeclaration` + +Deprecated some overloads in `AnnotationUtils` that take a `CharSequence` +(use an overload that takes an `ExecutablElement`): + * `getElementValueArray` + * `getElementValueClassName` + * `getElementValueClassNames` + * `getElementValueEnumArray` + * `getElementValueEnum` + * `getElementValue` + * `getElementValuesWithDefaults` + +Deprecated methods in `AnnotationUtils`: + * `areSameByClass`: use `areSameByName` + * `getElementValuesWithDefaults`: use a `getElementValue*` method + +Removed deprecated `PluginUtil` class. + +**Closed issues:** +#1376, #3740, #3970, #4041, #4254, #4346, #4355, #4358, #4372, #4381, #4384, +#4417, #4449, #4452, #4480. + + +Version 3.11.0 (March 1, 2021) +------------------------------ + +**User-visible changes:** + +In a stub file for a class C, you may write a declaration for a method that is +inherited by C but not defined by it. Previously, such stub declarations were +ignored. For more information, see the manual's documentation of "fake +overrides". + +Nullness Checker error message key changes: + * `known.nonnull` => `nulltest.redundant` + * `initialization.static.fields.uninitialized` => `initialization.static.field.uninitialized`, + and it is now issued on the field rather than on the class + * new `initialization.field.uninitialized` is issued on the field instead of + `initialization.fields.uninitialized` on the class, if there is no + explicitly-written constructor. + +Signature Checker supports two new type qualifiers: + * `@CanonicalNameAndBinaryName` + * `@CanonicalNameOrPrimitiveType` + +**Implementation details:** + +You can make a variable's default type depend on its name, or a method +return type default depend on the method's name. To support this feature, +`@DefaultFor` has new elements `names` and `namesExceptions`. + +Changes to protected fields in `OverrideChecker`: + * Removed `overriderMeth`, `overriderTyp`, `overriddenMeth`, `overriddenTyp` + * Renamed `methodReference` => `isMethodReference` + * Renamed `overridingType` => `overriderType` + * Renamed `overridingReturnType` => `overriderReturnType` + +Changes to JavaExpression parsing: + * The signatures of these methods changed; see Javadoc. + * `JavaExpressionParseUtil#parse` + * `DependentTypesHelper#standardizeString` + * These methods moved: + * `GenericAnnotatedTypeFactory#standardizeAnnotationFromContract` => `DependentTypesHelper` + * `JavaExpressionParseUtil#fromVariableTree` => `JavaExpression` + +Changes to JavaExpressionContext: + * New method JavaExpressionContext#buildContextForMethodDeclaration(MethodTree, SourceChecker) + replaces all overloads of buildContextForMethodDeclaration. + +Parsing a Java expression no longer requires the formal parameters +`AnnotationProvider provider` or `boolean allowNonDeterministic`. Methods +in `JavaExpression` with simplified signatures include + * `fromArrayAccess` + * `fromNodeFieldAccess` + * `fromNode` + * `fromTree` + * `getParametersOfEnclosingMethod` + * `getReceiver` + +`CFAbstractStore.insertValue` does nothing if passed a nondeterministic +expression. Use new method `CFAbstractStore.insertValuePermitNondeterministic` +to map a nondeterministic expression to a value. + +**Closed issues:** +#862, #3631, #3991, #4031, #4206, #4207, #4226, #4231, #4248, #4263, #4265, +#4279, #4286, #4289. + + +Version 3.10.0 (February 1, 2021) +--------------------------------- + +**User-visible changes:** + +Moved utility classes from `checker-qual.jar` to the new `checker-util.jar`. +Also, added `util` to the end of all the packages of the utility classes. + +In Maven Central, `checker.jar` no longer contains duplicates of qualifiers in +`checker-qual.jar`, but rather uses a Maven dependency. A fat jar file with all +the dependencies (like the old `checker.jar`) is available in Maven Central with +the classifier "all". + +When supplying the `-Ainfer=...` command-line argument, you must also supply `-Awarns`. + +Replaced several error message keys: + * `contracts.precondition.expression.parameter.name` + * `contracts.postcondition.expression.parameter.name` + * `contracts.conditional.postcondition.expression.parameter.name` + * `method.declaration.expression.parameter.name` +by new message keys: + * `expression.parameter.name.invalid` + * `expression.parameter.name.shadows.field` + +**Implementation details:** + +Deprecated `ElementUtils.enclosingClass`; use `ElementUtils.enclosingTypeElement`. + +Removed classes (use `SourceChecker` instead): + * `BaseTypeContext` + * `CFContext` + * `BaseContext` + +Removed methods: + * `SourceChecker.getContext()`: it returned the receiver + * `SourceChecker.getChecker()`: it returned the receiver + * `AnnotatedTypeFactory.getContext()`: use `getChecker()` + * methods on `TreePath`s from class 'TreeUtils`; use the versions in `TreePathUtil`. + +Moved class: + * org.checkerframework.framework.util.PurityUnqualified to + org.checkerframework.framework.qual.PurityUnqualified + +Renamed methods: + * `AnnotatedTypeMirror.directSuperTypes` => `directSupertypes` (note + capitalization) for consistency with `javax.lang.model.util.Types` + * `AnnotatedTypeMirror.removeAnnotation(Class)` => `removeAnnotationByClass` + * `MethodCall.getParameters` => `getArguments` + * `MethodCall.containsSyntacticEqualParameter` => `containsSyntacticEqualArgument` + * `ArrayAccess.getReceiver` => `getArray` + +**Closed issues:** +#3325 , #3474. + + +Version 3.9.1 (January 13, 2021) +-------------------------------- + +**Implementation details:** + +Copied methods on `TreePath`s from class 'TreeUtils` to new class `TreePathUtil`. +(The methods in TreePath will be deleted in the next release.) + * `TreeUtils.enclosingClass` => `TreePathUtil.enclosingClass` + * `TreeUtils.enclosingMethod` => `TreePathUtil.enclosingMethod` + * `TreeUtils.enclosingMethodOrLambda` => `TreePathUtil.enclosingMethodOrLambda` + * `TreeUtils.enclosingNonParen` => `TreePathUtil.enclosingNonParen` + * `TreeUtils.enclosingOfClass` => `TreePathUtil.enclosingOfClass` + * `TreeUtils.enclosingOfKind` => `TreePathUtil.enclosingOfKind` + * `TreeUtils.enclosingTopLevelBlock` => `TreePathUtil.enclosingTopLevelBlock` + * `TreeUtils.enclosingVariable` => `TreePathUtil.enclosingVariable` + * `TreeUtils.getAssignmentContext` => `TreePathUtil.getAssignmentContext` + * `TreeUtils.inConstructor` => `TreePathUtil.inConstructor` + * `TreeUtils.isTreeInStaticScope` => `TreePathUtil.isTreeInStaticScope` + * `TreeUtils.pathTillClass` => `TreePathUtil.pathTillClass` + * `TreeUtils.pathTillOfKind` => `TreePathUtil.pathTillOfKind` + +**Closed issues:** +#789, #3202, #4071, #4083, #4114, #4115. + + +Version 3.9.0 (January 4, 2021) +------------------------------- + +**User-visible changes:** + +New scripts `checker/bin/wpi.sh` and `checker/bin/wpi-many.sh` run whole-program +inference, without modifying the source code of the target programs. + +The `-Ainfer` command-line argument now infers + * method preconditions (`@RequiresQualifiers`, `@RequiresNonNull`) + * method postconditions (`@EnsuresQualifiers`, `@EnsuresNonNull`) + * `@MonotonicNonNull` + +The Called Methods Checker supports the -AdisableReturnsReceiver command-line option. + +The Format String Checker recognizes Error Prone's `@FormatMethod` annotation. + +Use of `@SuppressWarnings("fbc")` to suppress initialization warnings is deprecated. + +**Implementation details:** + +Class renamings: + * `StubParser` => `AnnotationFileParser` + * `Receiver` => `JavaExpression` + Also related class and method renamings, such as + * `FlowExpressions.internalReprOf` => `JavaExpressions.fromNode` + * In the Dataflow Framework: + * `ThisLiteralNode` => `ThisNode` + * `ExplicitThisLiteralNode` => `ExplicitThisNode` + * `ImplicitThisLiteralNode` => `ImplicitThisNode` + +Method deprecations: + * Deprecated `AnnotatedTypeFactory.addAliasedAnnotation`; use `addAliasedTypeAnnotation` + +**Closed issues:** +#765, #2452, #2953, #3377, #3496, #3499, #3826, #3956, #3971, #3974, #3994, +#4004, #4005, #4018, #4032, #4068, #4070. + + +Version 3.8.0 (December 1, 2020) +-------------------------------- + +**User-visible changes:** + +The Initialized Fields Checker warns when a field is not initialized by a +constructor. This is more general than the Initialization Checker, which +only checks that `@NonNull` fields are initialized. + +The manual describes how to modify an sbt build file to run the Checker +Framework. + +The -AwarnUnneededSuppressions command-line option warns only about +suppression strings that contain a checker name. + +The -AwarnUnneededSuppressionsExceptions=REGEX command-line option +partially disables -AwarnUnneededSuppressions. Most users don't need this. + +**Implementation details:** + +Added classes `SubtypeIsSubsetQualifierHierarchy` and +`SubtypeIsSupersetQualifierHierarchy`. + +Moved the `contractsUtils` field from the visitor to the type factory. + +Class renamings: + * `ContractsUtils` => `ContractsFromMethod` + +Method renamings: + * `ElementUtils.getVerboseName` => `ElementUtils.getQualifiedName` + * `ElementUtils.getSimpleName` => `ElementUtils.getSimpleSignature` + +Field renamings: + * `AnnotatedTypeMirror.actualType` => `AnnotatedTypeMirror.underlyingType` + +Added a formal parameter to methods in `MostlyNoElementQualifierHierarchy`: + * `leastUpperBoundWithElements` + * `greatestLowerBoundWithElements` + +Removed a formal parameter from methods in `BaseTypeVisitor`: + * `checkPostcondition` + * `checkConditionalPostcondition` + +In `Analysis.runAnalysisFor()`, changed `boolean` parameter to enum `BeforeOrAfter`. + +Removed `org.checkerframework.framework.util.AnnotatedTypes#getIteratedType`; use +`AnnotatedTypeFactory#getIterableElementType(ExpressionTree)` instead. + +**Closed issues:** +#3287, #3390, #3681, #3839, #3850, #3851, #3862, #3871, #3884, #3888, #3908, +#3929, #3932, #3935. + + +Version 3.7.1 (November 2, 2020) +-------------------------------- + +**User-visible changes:** + +The Constant Value Checker supports two new annotations: @EnumVal and @MatchesRegex. + +The Nullness Checker supports annotation org.jspecify.annotations.NullnessUnspecified. + +**Implementation details:** + +AnnotatedIntersectionType#directSuperTypes now returns +List. + +The @RelevantJavaTypes annotation is now enforced: a checker issues a warning +if the programmer writes a type annotation on a type that is not listed. + +Deprecated CFAbstractTransfer.getValueWithSameAnnotations(), which is no +longer used. Added new methods getWidenedValue() and getNarrowedValue(). + +Renamed TestUtilities.assertResultsAreValid() to TestUtilities.assertTestDidNotFail(). + +Renamed BaseTypeValidator.isValidType() to BaseTypeValidator.isValidStructurally(). + +New method BaseTypeVisitor#visitAnnotatedType(List, Tree) centralizes checking +of user-written type annotations, even when parsed in declaration locations. + +**Closed issues:** +#868, #1908, #2075, #3349, #3362, #3569, #3614, #3637, #3709, #3710, #3711, +#3720, #3730, #3742, #3760, #3770, #3775, #3776, #3792, #3793, #3794, #3819, +#3831. + + +Version 3.7.0 (October 1, 2020) +------------------------------- + +**User-visible changes:** + +The new Called Methods Checker tracks methods that have definitely been +called on an object. It automatically supports detecting mis-uses of the +builder pattern in code that uses Lombok or AutoValue. + +Accumulation analysis is now supported via a generic Accumulation Checker. +An accumulation analysis is a restricted form of typestate analysis that does +not require a precise alias analysis for soundness. The Called Methods Checker +is an accumulation analysis. + +The Nullness Checker supports annotations +org.codehaus.commons.nullanalysis.NotNull, +org.codehaus.commons.nullanalysis.Nullable, and +org.jspecify.annotations.Nullable. + +The Signature Checker supports annotations @CanonicalName and @CanonicalNameOrEmpty. +The Signature Checker treats jdk.jfr.Unsigned as an alias for its own @Unsigned annotation. + +The shorthand syntax for the -processor command-line argument applies to +utility checkers, such as the Constant Value Checker. + +**Implementation details:** + +A checker implementation may override AnnotatedTypeFactory.getWidenedAnnotations +to provide special behavior for primitive widening conversions. + +Deprecated org.checkerframework.framework.util.MultiGraphQualifierHierarchy and +org.checkerframework.framework.util.GraphQualifierHierarchy. Removed +AnnotatedTypeFactory#createQualifierHierarchy(MultiGraphFactory) and +AnnotatedTypeFactory#createQualifierHierarchyFactory. See Javadoc of +MultiGraphQualifierHierarchy for instructions on how to use the new classes and +methods. + +Renamed methods: + * NumberUtils.isFloatingPoint => TypesUtils.isFloatingPoint + * NumberUtils.isIntegral => TypesUtils.isIntegralPrimitiveOrBoxed + * NumberUtils.isPrimitiveFloatingPoint => TypeKindUtils.isFloatingPoint + * NumberUtils.isPrimitiveIntegral => TypeKindUtils.isIntegral + * NumberUtils.unboxPrimitive => TypeKindUtils.primitiveOrBoxedToTypeKind + * TypeKindUtils.widenedNumericType => TypeKindUtils.widenedNumericType + * TypesUtils.isFloating => TypesUtils.isFloatingPrimitive + * TypesUtils.isIntegral => TypesUtils.isIntegralPrimitive + +The CFStore copy constructor now takes only one argument. + +**Closed issues:** +#352, #354, #553, #722, #762, #2208, #2239, #3033, #3105, #3266, #3275, #3408, +#3561, #3616, #3619, #3622, #3625, #3630, #3632, #3648, #3650, #3667, #3668, +#3669, #3700, #3701. + + +Version 3.6.1 (September 2, 2020) +--------------------------------- + +Documented that the Checker Framework can issue false positive warnings in +dead code. + +Documented when the Signedness Checker permits right shift operations. + +**Closed issues:** +#3484, #3562, #3565, #3566, #3570, #3584, #3594, #3597, #3598. + + +Version 3.6.0 (August 3, 2020) +------------------------------ + +**User-visible changes:** + +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 methods: + * GenericAnnotatedTypeFactory#addAnnotationsFromDefaultQualifierForUse => #addAnnotationsFromDefaultForType and + * BaseTypeValidator#shouldCheckTopLevelDeclaredType => #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) +---------------------------- + +**User-visible changes:** + +Use "allcheckers:" instead of "all:" as a prefix in a warning suppression string. +Writing `@SuppressWarnings("allcheckers")` means the same thing as +`@SuppressWarnings("all")`, unless the `-ArequirePrefixInWarningSuppressions` +command-line argument is supplied. See the manual for details. + +It is no longer necessary to pass -Astubs=checker.jar/javadoc.astub when +compiling a program that uses Javadoc classes. + +Renamed command-line arguments: + * -AshowSuppressWarningKeys to -AshowSuppressWarningsStrings + +The Signature Checker no longer considers Java keywords to be identifiers. +Renamed Signature Checker annotations: + * @BinaryNameInUnnamedPackage => @BinaryNameWithoutPackage + * @FieldDescriptorForPrimitiveOrArrayInUnnamedPackage => @FieldDescriptorWithoutPackage + * @IdentifierOrArray => @ArrayWithoutPackage +Added new Signature Checker annotations: + * @BinaryNameOrPrimitiveType + * @DotSeparatedIdentifiersOrPrimitiveType + * @IdentifierOrPrimitiveType + +The Nullness Checker now treats `System.getProperty()` soundly. Use +`-Alint=permitClearProperty` to disable special treatment of +`System.getProperty()` and to permit undefining built-in system properties. + +Class qualifier parameters: When a generic class represents a collection, +a user can write a type qualifier on the type argument, as in +`List<@Tainted Character>` versus `List<@Untainted Character>`. When a +non-generic class represents a collection with a hard-coded type (as +`StringBuffer` hard-codes `Character`), you can use the new class qualifier +parameter feature to distinguish `StringBuffer`s that contain different +types of characters. + +The Dataflow Framework supports backward analysis. See its manual. + +**Implementation details:** + +Changed the types of some fields and methods from array to List: + * QualifierDefaults.validLocationsForUncheckedCodeDefaults() + * QualifierDefaults.STANDARD_CLIMB_DEFAULTS_TOP + * QualifierDefaults.STANDARD_CLIMB_DEFAULTS_BOTTOM + * QualifierDefaults.STANDARD_UNCHECKED_DEFAULTS_TOP + * QualifierDefaults.STANDARD_UNCHECKED_DEFAULTS_BOTTOM + +Dataflow Framework: Analysis is now an interface. Added AbstractAnalysis, +ForwardAnalysis, ForwardTransferFunction, ForwardAnalysisImpl, +BackwardAnalysis, BackwardTransferFunction, and BackwardAnalysisImpl. +To adapt existing code: + * `extends Analysis` => `extends ForwardAnalysisImpl` + * `implements TransferFunction` => `implements ForwardTransferFunction` + +In AbstractQualifierPolymorphism, use AnnotationMirrors instead of sets of +annotation mirrors. + +Renamed meta-annotation SuppressWarningsKeys to SuppressWarningsPrefix. +Renamed SourceChecker#getSuppressWarningsKeys(...) to getSuppressWarningsPrefixes. +Renamed SubtypingChecker#getSuppressWarningsKeys to getSuppressWarningsPrefixes. + +Added GenericAnnotatedTypeFactory#postAnalyze, changed signature of +GenericAnnotatedTypeFactory#handleCFGViz, and removed CFAbstractAnalysis#visualizeCFG. + +Removed methods and classes marked deprecated in release 3.3.0 or earlier. + +**Closed issues:** +#1362, #1727, #2632, #3249, #3296, #3300, #3356, #3357, #3358, #3359, #3380. + + +Version 3.4.1 (June 1, 2020) +---------------------------- + +-Ainfer now takes an argument: + * -Ainfer=jaifs uses .jaif files to store the results of whole-program inference. + * -Ainfer=stubs uses .astub files to store the results of whole-program inference. + * -Ainfer is deprecated but is the same as -Ainfer=jaifs, for backwards compatibility. + +New command-line option: + -AmergeStubsWithSource If both a stub file and a source file are available, use both. + +**Closed issues:** +#2893, #3021, #3128, #3160, #3232, #3277, #3285, #3289, #3295, #3302, #3305, +#3307, #3310, #3316, #3318, #3329. + + +Version 3.4.0 (May 3, 2020) +--------------------------- + +The annotated jdk8.jar is no longer used. You should remove any occurrence of + -Xbootclasspath/p:.../jdk8.jar +from your build scripts. Annotations for JDK 8 are included in checker.jar. + +The Returns Receiver Checker enables documenting and checking that a method +returns its receiver (i.e., the `this` parameter). + +**Closed issues:** +#3267, #3263, #3217, #3212, #3201, #3111, #3010, #2943, #2930. + + +Version 3.3.0 (April 1, 2020) +----------------------------- + +**User-visible changes:** + +New command-line options: + `-Alint=trustArrayLenZero` trust `@ArrayLen(0)` annotations when determining + the type of Collections.toArray. + +Renamings: + `-AuseDefaultsForUncheckedCode` to `-AuseConservativeDefaultsForUncheckedCode` + The old name works temporarily but will be removed in a future release. + +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=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 +substring worked, including the empty string which suppressed all warnings. +Use "all" to suppress all warnings.) + +All postcondition annotations are repeatable (e.g., `@EnsuresNonNull`, +`@EnsuresNonNullIf`, ...). + +Renamed wrapper annotations (which users should not write): + * `@DefaultQualifiers` => `@DefaultQualifier.List` + * `@EnsuresQualifiersIf` => `@EnsuresQualifierIf.List` + * `@EnsuresQualifiers` => `@EnsuresQualifier.List` + * `@RequiresQualifiers` => `@RequiresQualifier.List` + +**Implementation details:** + +Removed `@DefaultInUncheckedCodeFor` and +`@DefaultQualifierInHierarchyInUncheckedCode`. + +Renamings: + * applyUncheckedCodeDefaults() to applyConservativeDefaults() + * useUncheckedCodeDefault() to useConservativeDefault() + * AnnotatedTypeReplacer to AnnotatedTypeCopierWithReplacement + * AnnotatedTypeMerger to AnnotatedTypeReplacer + +Deprecated the `framework.source.Result` class; use `DiagMessage` or +`List` instead. If you were creating a `Result` just to +pass it to `report`, then call new methods `reportError` and +`reportWarning` instead. + +AbstractTypeProcessor#typeProcessingOver() always gets called. + +**Closed issues:** +#1307, #1881, #1929, #2432, #2793, #3040, #3046, #3050, #3056, #3083, #3124, +#3126, #3129, #3132, #3139, #3149, #3150, #3167, #3189. + + +Version 3.2.0 (March 2, 2020) +----------------------------- + +@SuppressWarnings("initialization") suppresses only warnings whose key +contains "initialization". Previously, it suppressed all warnings issued +by the Nullness Checker or the Initialization Checker. + +**Closed issues:** +#2719, #3001, #3020, #3069, #3093, #3120. + + +Version 3.1.1 (February 3, 2020) +-------------------------------- + +New command-line options: + -AassumeDeterministic Unsoundly assume that every method is deterministic + -AassumePure Unsoundly assume that every method is pure + +Renamed -Anocheckjdk to -ApermitMissingJdk. +The old version still works, for backward compatibility. + +Renamed -Alint=forbidnonnullarraycomponents to +-Alint=soundArrayCreationNullness. The old version still works, for +backward compatibility. + +Implementation details: + * Deprecated QualifierHierarchy#getTypeQualifiers. + * Deprecated Analysis#Analysis(ProcessingEnvironment) and Analysis#Analysis(T, + int, ProcessingEnvironment); use Analysis#Analysis(), Analysis#Analysis(int), + Analysis#Analysis(T), and Analysis#Analysis(T, int) instead. + * Renamed SourceChecker#getMessages to getMessagesProperties. + * Renamed one overload of SourceChecker.printMessages to printOrStoreMessage. + +**Closed issues:** +#2181, #2975, #3018, #3022, #3032, #3036, #3037, #3038, #3041, #3049, #3055, +#3076. + + +Version 3.1.0 (January 3, 2020) +------------------------------- + +Command-line option -AprintGitProperties prints information about the git +repository from which the Checker Framework was compiled. + +**Implementation details:** + * Removed static cache in AnnotationUtils#areSameByClass and added + AnnotatedTypeFactory#areSameByClass that uses an instance cache. + * Removed static cache in AnnotationBuilder#fromName and #fromClass. + * ContractsUtils#getPreconditions takes an ExecutableElement as an argument. + * ContractsUtils#getContracts returns a Set. + * Moved ContractUtils.Contract to outer level. + * Renamed ConditionalPostcondition#annoResult to ConditionalPostcondition#resultValue. + +**Closed issues:** +#2867, #2897, #2972. + + +Version 3.0.1 (December 2, 2019) +-------------------------------- + +New command-line option for the Constant Value Checker +`-AnoNullStringsConcatenation` unsoundly assumes that every operand of a String +concatenation is non-null. + +**Implementation details:** + * Moved AnnotatedTypes#hasTypeQualifierElementTypes to AnnotationUtils. + * Deprecated AnnotatedTypes#isTypeAnnotation and AnnotatedTypes#hasTypeQualifierElementTypes. + +**Closed issues:** +#945, #1224, #2024, #2744, #2809, #2815, #2818, #2830, #2840, #2853, #2854, +#2865, #2873, #2874, #2878, #2880, #2886, #2888, #2900, #2905, #2919, #2923. + + +Version 3.0.0 (November 1, 2019) +-------------------------------- + +The Checker Framework works on both JDK 8 and JDK 11. + * Type annotations for JDK 8 remain in jdk8.jar. + * Type annotations for JDK 11 appear in stub files in checker.jar. + +Removed the @PolyAll annotation. + +**Implementation details:** + * Removed all previously deprecated methods. + * AnnotatedTypeFactory#getFnInterfaceFromTree now returns an AnnotatedExecutableType. + * AnnotationUtils#areSame and #areSameByName now only accept non-null + AnnotationMirrors + +**Closed issues:** +#1169, #1654, #2081, #2703, #2739, #2749, #2779, #2781, #2798, #2820, #2824, +#2829, #2842, #2845, #2848. + + +Version 2.11.1 (October 1, 2019) +-------------------------------- + +The manual links to the Object Construction Checker. + +**Closed issues:** +#1635, #2718, #2767. + + +Version 2.11.0 (August 30, 2019) +-------------------------------- + +The Checker Framework now uses the Java 9 javac API. The manual describes +how to satisfy this dependency, in a way that works on a Java 8 JVM. +Running the Checker Framework on a Java 9 JVM is not yet supported. + + +Version 2.10.1 (August 22, 2019) +-------------------------------- + +**Closed issues:** +#1152, #1614, #2031, #2482, #2543, #2587, #2678, #2686, #2690, #2712, #2717, +#2713, #2721, #2725, #2729. + + +Version 2.10.0 (August 1, 2019) +------------------------------- + +Removed the NullnessRawnessChecker. Use the NullnessChecker instead. + +**Closed issues:** +#435, #939, #1430, #1687, #1771, #1902, #2173, #2345, #2470, #2534, #2606, +#2613, #2619, #2633, #2638. + + +Version 2.9.0 (July 3, 2019) +---------------------------- + +Renamed the Signedness Checker's @Constant annotation to @SignednessGlb. +Introduced an alias, @SignedPositive, for use by programmers. + +Annotated the first argument of Opt.get and Opt.orElseThrow as @NonNull. + +Removed meta-annotation @ImplicitFor: + * Use the new meta-annotation @QualifierForLiteral to replace + @ImplicitFor(literals, stringpatterns). + * Use the meta-annotation @DefaultFor to replace @ImplicitFor(typeKinds, + types). + * Use the new meta-annotation @UpperBoundFor to specify a qualifier upper + bound for certain types. + * You can completely remove + @ImplicitFor(typeNames = Void.class, literals = LiteralKind.NULL) + on bottom qualifiers. + @DefaultFor(types = Void.class) + and + @QualifierForLiterals(literals = LiteralKind.NULL) + are added to the bottom qualifier by default. + +Add @DefaultQualifierOnUse and @NoDefaultQualifierOnUse type declaration annotations + +New/changed error message keys: + * initialization.static.fields.uninitialized for uninitialized static fields + * unary.increment.type.incompatible and unary.decrement.type.incompatible + replace some occurrences of compound.assignment.type.incompatible + +**Implementation details:** + * Renamed QualifierPolymorphism#annotate methods to resolve + * Renamed ImplicitsTreeAnnotator to LiteralTreeAnnotator + * Renamed ImplicitsTypeAnnotator to DefaultForTypeAnnotator + * Removed TypeUseLocation.TYPE_DECLARATION + * Removed InheritedFromClassAnnotator, replace with DefaultQualifierForUseTypeAnnotator + * Rename TreeUtils.isSuperCall and TreeUtils.isThisCall to + isSuperConstructorCall and isThisConstructorCall + +**Closed issues:** +#2247, #2391, #2409, #2434, #2451, #2457, #2468, #2484, #2485, #2493, #2505, +#2536, #2537, #2540, #2541, #2564, #2565, #2585. + + +Version 2.8.2 (June 3, 2019) +---------------------------- + +The Signature Checker supports a new type, @FqBinaryName. + +Added a template for a repository that you can use to write a custom checker. + +Linked to the Checker Framework Gradle plugin, which makes it easy to run +a checker on a project that is built using the Gradle build tool. + +Implementation detail: deprecated TreeUtils.skipParens in favor of +TreeUtils.withoutParens which has the same specification. + +**Closed issues:** +#2291, #2406, #2469, #2477, #2479, #2480, #2494, #2499. + + +Version 3.0.0-b1 (May 1, 2019) +------------------------------ + +First release of artifacts suitable for Java 9--12. +There is no checker artifact, because no replacement for the +annotated JDK mechanism exists yet. + + +Version 2.8.1 (May 1, 2019) +--------------------------- + +Moved text about the Purity Checker into its own chapter in the manual. + +**Closed issues:** +#660, #2030, #2223, #2240, #2244, #2375, #2407, #2410, #2415, #2420, #2421, +#2446, #2447, #2460, #2462. + + +Version 2.8.0 (April 3, 2019) +----------------------------- + +Support `androidx.annotation.RecentlyNonNull` and `RecentlyNullable` (as of +2.6.0, but not previously documented). + +The following qualifiers are now repeatable: `@DefaultQualifier` +`@EnsuresQualifierIf` `@EnsuresQualifier` `@RequiresQualifier`. Therefore, +users generally do not need to write the following wrapper annotations: +`@DefaultQualifiers` `@EnsuresQualifiersIf` `@EnsuresQualifiers` +`@RequiresQualifiers`. + +New command-line option `-ArequirePrefixInWarningSuppressions` makes +`@SuppressWarnings` recognize warning keys of the form +"checkername:key.about.problem" but ignore warning keys of the form +"key.about.problem" without the checker name as a prefix. + +New CONSTRUCTOR_RESULT enum constant in TypeUseLocation makes it possible to +set default annotations for constructor results. + +Clarified the semantics of annotations on class and constructor declarations. +See Section 25.5 "Annotations on classes and constructors" in the manual. + +Interface changes: + * Added protected methods to BaseTypeVisitor so that checkers can change the + checks for annotations on classes, constructor declarations, and constructor + invocations. + * Removed BaseTypeVisitor#checkAssignability and BaseTypeVisitor#isAssignable + methods. + * Renamed AnnotatedTypeFactory#getEnclosingMethod to + AnnotatedTypeFactory#getEnclosingElementForArtificialTree + +**Closed issues:** +#2159, #2230, #2318, #2324, #2330, #2334, #2343, #2344, #2353, #2366, #2367, +#2370, #2371, #2385. + + +Version 2.7.0 (March 1, 2019) +----------------------------- + +The manual links to the AWS crypto policy compliance checker, which enforces +that no weak cipher algorithms are used with the Java crypto API. + +The Nullness Checker supports RxJava annotations +io.reactivex.annotations.NonNull and io.reactivex.annotations.Nullable. + +The checker-qual artifact (jar file) contains an OSGi manifest. + +New TYPE_DECLARATION enum constant in TypeUseLocation makes it possible to +(for example) set defaults annotations for class/interface definitions. + +Interface changes: + * Renamed the "value" element of the @HasSubsequence annotation to + "subsequence". + * Renamed @PolySignedness to @PolySigned. + * Renamed AnnotatedTypeFactory.ParameterizedMethodType to + ParameterizedExecutableType. + +Added missing checks regarding annotations on classes, constructor +declarations, and constructor invocations. You may see new warnings. + +**Closed issues:** +#788, #1751, #2147, #2163, #2186, #2235, #2243, #2263, #2264, #2286, #2302, +#2326, #2327. + + +Version 2.6.0 (February 3, 2019) +-------------------------------- + +The manual includes a section about how to use Lombok and the Checker +Framework simultaneously. + +Commons CSV has been added to the annotated libraries on Maven Central. + +Some error messages have been changed to improve comprehensibility, +such as by adjusting wording or adding additional information. + +Relevant to type system implementers: +Renamed method areSameIgnoringValues to areSameByName. + +**Closed issues:** +#2008, #2166, #2185, #2187, #2221, #2224, #2229, #2234, #2248. +Also fixed false negatives in handling of Map.get(). + + +Version 2.5.8 (December 5, 2018) +-------------------------------- + +The manual now links to the AWS KMS compliance checker, which enforces +that calls to AWS KMS only generate 256-bit keys. + +**Closed issues:** +#372, #1678, #2207, #2212, #2217. + + +Version 2.5.7 (November 4, 2018) +-------------------------------- + +New @EnsuresKeyFor and @EnsuresKeyForIf method annotations permit +specifying the postcondition that a method gives some value a @KeyFor type. + +The manual links to the Rx Thread & Effect Checker, which enforces +UI Thread safety properties for stream-based Android applications. + +**Closed issues:** +#1014, #2151, #2178, #2180, #2183, #2188, #2190, #2195, #2196, #2198, #2199. + + +Version 2.5.6 (October 3, 2018) +------------------------------- + +Introduce checker-qual-android artifact that is just like the checker-qual +artifact, but the qualifiers have classfile retention. This is useful for +Android projects. + +Removed the code for the checker-compat-qual artifact. It was only useful +for Java 7, which the Checker Framework no longer supports. The +checker-compat-qual artifact remains available on Maven Central, with +versions 2.5.5 and earlier. + +**Closed issues:** +#2135, #2157, #2158, #2164, #2171. + + +Version 2.5.5 (August 30, 2018) +------------------------------- + +Implicit imports (deprecated in November 2014) are no longer supported. + +Renamed the testlib Maven artifact to framework-test. + +Removed command-line option -AprintErrorStack, which is now the default. +Added -AnoPrintErrorStack to disable it (which should be rare). + +Replaced ErrorReporter class with BugInCF and UserError exceptions. + +**Closed issues:** +#1999, #2008, #2023, #2029, #2074, #2088, #2098, #2099, #2102, #2107. + + +Version 2.5.4 (August 1, 2018) +------------------------------ + +**Closed issues:** +#2030, #2048, #2052, #2059, #2065, #2067, #2073, #2082. + + +Version 2.5.3 (July 2, 2018) +---------------------------- + +**Closed issues:** +#266, #1248, #1678, #2010, #2011, #2018, #2020, #2046, #2047, #2054. + + +Version 2.5.2 (June 1, 2018) +---------------------------- + +In the Map Key Checker, null is now @UnknownKeyFor. See the "Map Key Checker" +chapter in the manual for more details. + +**Closed issues:** +#370, #469, #1701, #1916, #1922, #1959, #1976, #1978, #1981, #1983, #1984, #1991, #1992. + + +Version 2.5.1 (May 1, 2018) +--------------------------- + +Added a Maven artifact of the Checker Framework testing library, testlib. + +**Closed issues:** +#849, #1739, #1838, #1847, #1890, #1901, #1911, #1912, #1913, #1934, #1936, +#1941, #1942, #1945, #1946, #1948, #1949, #1952, #1953, #1956, #1958. + + +Version 2.5.0 (April 2, 2018) +----------------------------- + +Declaration annotations that are aliases for type annotations are now treated +as if they apply to the top-level type. See "Declaration annotations" section +in the "Warnings" chapter in the manual for more details. + +Ended support for annotations in comments. See "Migrating away from +annotations in comments" section in the "Handling legacy code" chapter in the +manual for instructions on how to remove annotations from comments. + +**Closed issues:** +#515, #1667, #1739, #1776, #1819, #1863, #1864, #1865, #1866, #1867, #1870, +#1876, #1879, #1882, #1898, #1903, #1905, #1906, #1910, #1914, #1915, #1920. + + +Version 2.4.0 (March 1, 2018) +----------------------------- + +Added the Index Checker, which eliminates ArrayIndexOutOfBoundsException. + +Added the Optional Checker, which verifies uses of Java 8's Optional class. + +Removed the Linear Checker, whose implementation was inconsistent with its +documentation. + +Added a @QualifierArgument annotation to be used on pre- and postcondition + annotations created by @PreconditionAnnotation, @PostconditionAnnotation, + and @ConditionalPostconditionAnnotation. This allows qualifiers with + arguments to be used in pre- and postconditions. + +Added new type @InternalFormForNonArray to the Signature Checker + +Moved annotated libraries from checker/lib/*.jar to the Maven Central Repository: + + +Moved the Javadoc stub file from checker/lib/javadoc.astub to +checker/resources/javadoc.astub. + +Simplified the instructions for running the Checker Framework with Gradle. + +The Checker Framework Eclipse plugin is no longer released nor supported. + +**Closed issues:** +#65, #66, #100, #108, #175, #184, #190, #194, #209, #239, #260, #270, #274, +#293, #302, #303, #306, #321, #325, #341, #356, #360, #361, #371, #383, #385, +#391, #397, #398, #410, #423, #424, #431, #430, #432, #548, #1131, #1148, +#1213, #1455, #1504, #1642, #1685, #1770, #1796, #1797, #1801, #1809, #1810, +#1815, #1817, #1818, #1823, #1831, #1837, #1839, #1850, #1851, #1852, #1861. + + +Version 2.3.2 (February 1, 2018) +-------------------------------- + +**Closed issues:** +#946, #1133, #1232, #1319, #1625, #1633, #1696, #1709, #1712, #1734, #1738, +#1749, #1754, #1760, #1761, #1768, #1769, #1781. + + +Version 2.3.1 (January 2, 2018) +------------------------------- + +**Closed issues:** +#1695, #1696, #1697, #1698, #1705, #1708, #1711, #1714, #1715, #1724. + + +Version 2.3.0 (December 1, 2017) +-------------------------------- + +Removed the deprecated @LazyNonNull type qualifier. +Deprecated most methods in InternalUtils and moved them to either +TreeUtils or TypesUtils. Adapted a few method names and parameter +orders for consistency. + +**Closed issues:** +#951, #1356, #1495, #1602, #1605, #1623, #1628, #1636, #1641, #1653, #1655, +#1664, #1665, #1681, #1684, #1688, #1690. + + +Version 2.2.2 (November 2, 2017) +-------------------------------- + +The Interning Checker supports a new annotation, @InternedDistinct, which +indicates that the value is not equals() to any other value. + +An annotated version of the Commons IO library appears in checker/lib/ . + +Closed issue #1586, which required re-opening issues 293 and 341 until +proper fixes for those are implemented. + +**Closed issues:** +#1386, #1389, #1423, #1520, #1529, #1530, #1531, #1546, #1553, #1555, #1565, +#1570, #1579, #1580, #1582, #1585, #1586, #1587, #1598, #1609, #1615, #1617. + + +Version 2.2.1 (September 29, 2017) +---------------------------------- + +Deprecated some methods in AnnotatedTypeMirror and AnnotationUtils, to +be removed after the 2.2.1 release. + +The qualifiers and utility classes in checker-qual.jar are compiled to Java 8 +byte code. A new jar, checker-qual7.jar, includes the qualifiers and utility +classes compiled to Java 7 byte code. + +**Closed issues:** +#724, #1431, #1442, #1459, #1464, #1482, #1496, #1499, #1500, #1506, #1507, +#1510, #1512, #1522, #1526, #1528, #1532, #1535, #1542, #1543. + + +Version 2.2.0 (September 5, 2017) +--------------------------------- + +A Java 8 JVM is required to run the Checker Framework. +You can still typecheck and compile Java 7 (or earlier) code. +With the "-target 7" flag, the resulting .class files still run with JDK 7. + +The stub file format has changed to be more similar to regular Java syntax. +Most notably, receiver annotations are written using standard Java 8 syntax +(a special first formal paramter named "this") and inner classes are written +using standard Java syntax (rather than at the top level using a name that +contains "$". You need to update your stub files to conform to the new syntax. + +**Closed issues:** +#220, #293, #297, #341, #375, #407, #536, #571, #798, #867, #1180, #1214, #1218, +#1371, #1411, #1427, #1428, #1435, #1438, #1450, #1456, #1460, #1466, #1473, +#1474. + + +Version 2.1.14 (3 August 2017) +------------------------------ + +Nullness Checker change to annotated JDK: The type argument to the Class, +Constructor, and Optional classes may now be annotated as @Nullable or +@NonNull. The nullness of the type argument doesn't matter, but this +enables easier integration with generic clients. + +Many crashes and false positives associated with uninferred method type +arguments have been correct. By default, uninferred method type arguments, +which can happen with Java 8 style target type contexts, are silently ignored. +Use the option -AconservativeUninferredTypeArguments to see warnings about +method calls where the Checker Framework fails to infer type arguments. + +**Closed issues:** +#753, #804, #961, #1032, #1062, #1066, #1098, #1209, #1280, #1316, #1329, #1355, +#1365, #1366, #1367, #1377, #1379, #1382, #1384, #1397, #1398, #1399, #1402, +#1404, #1406, #1407. + + +Version 2.1.13 (3 July 2017) +---------------------------- + +Verified that the Checker Framework builds from source on Windows Subsystem +for Linux, on Windows 10 Creators Edition. + +The manual explains how to configure Android projects that use Android Studio +3.0 and Android Gradle Plugin 3.0.0, which support type annotations. + +**Closed issues:** +#146, #1264, #1275, #1290, #1303, #1308, #1310, #1312, #1313, #1315, #1323, +#1324, #1331, #1332, #1333, #1334, #1347, #1357, #1372. + + +Version 2.1.12 (1 June 2017) +---------------------------- + +The manual links to Glacier, a class immutability checker. + +The stubparser license has been updated. You can now use stubparser under +either the LGPL or the Apache license, whichever you prefer. + +**Closed issues:** +#254, #1201, #1229, #1236, #1239, #1240, #1257, #1265, #1270, #1271, #1272, +#1274, #1288, #1291, #1299, #1304, #1305. + + +Version 2.1.11 (1 May 2017) +--------------------------- + +The manual contains new FAQ (frequently asked questions) sections about +false positive warnings and about inference for field types. + +**Closed issues:** +#989, #1096, #1136, #1228. + + +Version 2.1.10 (3 April 2017) +----------------------------- + +The Constant Value Checker, which performs constant propagation, has been +extended to perform interval analysis -- that is, it determines, for each +expression, a statically-known lower and upper bound. Use the new +@IntRange annotation to express this. Thanks to Jiasen (Jason) Xu for this +feature. + +**Closed issues:** +#134, #216, #227, #307, #334, #437, #445, #718, #1044, #1045, #1051, #1052, +#1054, #1055, #1059, #1077, #1087, #1102, #1108, #1110, #1111, #1120, #1124, +#1127, #1132. + + +Version 2.1.9 (1 March 2017) +---------------------------- + +By default, uninferred method type arguments, which can happen with Java 8 +style target type contexts, are silently ignored, removing many false +positives. The new option -AconservativeUninferredTypeArguments can be used to +get the conservative behavior. + +**Closed issues:** +#1006, #1011, #1015, #1027, #1035, #1036, #1037, #1039, #1043, #1046, #1049, +#1053, #1072, #1084. + + +Version 2.1.8 (20 January 2017) +------------------------------- + +The Checker Framework webpage has moved to . +Old URLs should redirect to the new one, but please update your links +and let us know if any old links are broken rather than redirecting. + +The documentation has been reorganized in the Checker Framework repository. +The manual, tutorial, and webpages now appear under checker-framework/docs/. + +**Closed issues:** +#770, #1003, #1012. + + +Version 2.1.7 (3 January 2017) +------------------------------ + +Manual improvements: + * Added a link to jOOQ's SQL checker. + * Documented the `-AprintVerboseGenerics` command-line option. + * Better explanation of relationship between Fake Enum and Subtyping Checkers. + +**Closed issues:** +#154, #322, #402, #404, #433, #531, #578, #720, #795, #916, #953, #973, #974, +#975, #976, #980, #988, #1000. + + +Version 2.1.6 (1 December 2016) +------------------------------- + +**Closed issues:** +#412, #475. + + +Version 2.1.5 (2 November 2016) +------------------------------- + +The new class org.checkerframework.checker.nullness.Opt provides every +method in Java 8's java.util.Optional class, but written for possibly-null +references rather than for the Optional type. This can shorten code that +manipulates possibly-null references. + +In bytecode, type variable upper bounds of type Object may or may not have +been explicitly written. The Checker Framework now assumes they were not +written explicitly in source code and defaults them as implicit upper bounds. + +The manual describes how to run a checker within the NetBeans IDE. + +The manual describes two approaches to creating a type alias or typedef. + +**Closed issues:** +#643, #775, #887, #906, #941. + + +Version 2.1.4 (3 October 2016) +------------------------------ + +**Closed issues:** +#885, #886, #919. + + +Version 2.1.3 (16 September 2016) +--------------------------------- + +**Closed issues:** +#122, #488, #495, #580, #618, #647, #713, #764, #818, #872, #893, #894, #901, +#902, #903, #905, #913. + + +Version 2.1.2 (1 September 2016) +-------------------------------- + +**Closed issues:** +#182, #367, #712, #811, #846, #857, #858, #863, #870, #871, #878, #883, #888. + + +Version 2.1.1 (1 August 2016) +----------------------------- + +The codebase conforms to a consistent coding style, which is enforced by +a git pre-commit hook. + +AnnotatedTypeFactory#createSupportedTypeQualifiers() must now return a mutable +list. Checkers that override this method will have to be changed. + +**Closed issues:** +#384, #590, #681, #790, #805, #809, #810, #820, #824, #826, #829, #838, #845, +#850, #856. + + +Version 2.1.0 (1 July 2016) +--------------------------- + +The new Signedness Checker prevents mixing of unsigned and signed +values and prevents meaningless operations on unsigned values. + +The Lock Checker expresses the annotated variable as ``; +previously it used `itself`, which may conflict with an identifier. + +**Closed issues:** +#166, #273, #358, #408, #471, #484, #594, #625, #692, #700, #701, #711, #717, +#752, #756, #759, #763, #767, #779, #783, #794, #807, #808. + + +Version 2.0.1 (1 June 2016) +--------------------------- + +We renamed method annotateImplicit to addComputedTypeAnnotations. If you +have implemented a checker, you need to change occurrences of +annotateImplicit to addComputedTypeAnnotations. + +The Checker Framework (checker.jar) is now placed on the processorpath +during compilation. Previously, it was placed on the classpath. The +qualifiers (checker-qual.jar) remain on the classpath. This change should +reduce conflicts between your code and the Checker Framework. If your code +depends on classes in the Checker Framework, then you should add those +classes to the classpath when you run the compiler. + +**Closed issues:** +#171, #250, #291, #523, #577, #672, #680, #688, #689, #690, #691, #695, #696, +#698, #702, #704, #705, #706, #707, #720, #721, #723, #728, #736, #738, #740. + + +Version 2.0.0 (2 May 2016) +-------------------------- + +Inference: + + * The infer-and-annotate.sh script infers annotations and inserts them in + your source code. This can reduce the burden of writing annotations and + let you get started using a type system more quickly. See the + "Whole-program inference" section in the manual for details. + +Type systems: + + * The Lock Checker has been replaced by a new implementation that provides + a stronger guarantee. The old Lock Checker prevented two threads from + simultaneously using a given variable, but race conditions were still + possible due to aliases. The new Lock Checker prevents two threads from + simultaneously dereferencing a given value, and thus prevents race + conditions. For details, see the "Lock Checker" chapter in the manual, + which has been rewritten to describe the new semantics. + + * The top type qualifier for the Signature String type system has been + renamed from @UnannotatedString to @SignatureUnknown. You shouldn't + ever write this annotation, but if you perform separate compilation (for + instance, if you do type-checking with the Signature String Checker + against a library that is annotated with Signature String annotations), + then you need to re-compile the library. + + * The IGJ, OIGJ, and Javari Checkers are no longer distributed with the + Checker Framework. If you wish to use them, install version 1.9.13 of + the Checker Framework. The implementations have been removed because + they were not being maintained. The type systems are valuable, but the + type-checkers should be rewritten from scratch. + +Documentation improvements: + + * New manual section "Tips for creating a checker" shows how to break down + the implementation of a type system into small, manageable pieces. + + * Improved instructions for using Maven and Gradle, including for Android + code. + +Tool changes: + + * The Checker Framework Live Demo webpage lets you try the Checker + Framework without installing it: + + * New command-line arguments -Acfgviz and -Averbosecfg enable better + debugging of the control-flow-graph generation step of type-checking. + + * New command-line argument -Ainfer is used by the infer-and-annotate.sh + script that performs type inference. + +**Closed issues:** +#69, #86, #199, #299, #329, #421, #428, #557, #564, #573, #579, #665, #668, #669, +#670, #671. + + +Version 1.9.13 (1 April 2016) +----------------------------- + +Documentation: + * Clarified Maven documentation about use of annotations in comments. + * Added FAQ about annotating fully-qualified type names. + +**Closed issues:** +#438, #572, #579, #607, #624, #631. + + +Version 1.9.12 (1 March 2016) +----------------------------- + +The Checker Framework distribution contains annotated versions +of libraries in directory checker-framework/checker/lib/. +During type-checking, you should put these versions first on your classpath, +to obtain more precise type-checking with fewer false positive warnings. + +tools.jar is no longer required to be on the classpath when using +checker-qual.jar + +The Signature String Checker supports two new string representations of a +Java type: @InternalForm and @ClassGetSimpleName. + +The manual documents how to run a pluggable type-checker in IntelliJ IDEA. + +The instructions on how to run a type-checker in Gradle have been updated to +use the artifacts in Maven Central. Examples using the instructions have been +added under checker-framework/docs/examples/GradleExamples/. + +Renamed enum DefaultLocation to TypeUseLocation. + +**Closed issues:** +#130, #263, #345, #458, #559, #559, #574, #582, #596. + + +Version 1.9.11 (1 February 2016) +-------------------------------- + +Renamed and merged -AuseSafeDefaultsForUnannotatedSourceCode and +-AsafeDefaultsForUnannotatedBytecode command-line options to +-AuseDefaultsForUncheckedCode that takes arguments source and bytecode. + +For type-system developers: + +* The previously deprecated + org.checkerframework.framework.qual.TypeQualifier{s} annotations + were removed. +* Every type system uses the CLIMB-to-top defaulting scheme, unless it + explicitly specifies a different one. Previously a type system needed + to explicitly request CLIMB-to-top, but now it is the default. + +**Closed issues:** +#524, #563, #568. + + +Version 1.9.10 (4 January 2016) +------------------------------- + +The Checker Framework distribution files now contain a version number: +for example, checker-framework-1.9.9.zip rather than checker-framework.zip. + +The Nullness Checker supports the org.eclipse.jgit.annotations.Nullable and +NonNull annotations. + +Buildfiles do less unnecessary recomputation. + +Documentation: + * Documented how to initialize circular data structures in the + Initialization type system. + * Linked to David Bürgin's Nullness Checker tutorial at + + * Acknowledged more contributors in the manual. + +For type-system developers: + * The org.checkerframework.framework.qual.TypeQualifier{s} annotations are + now deprecated. To indicate which annotations a checker supports, see + . + Support for TypeQualifier{s} will be removed in the next release. + * Renamed + `org.checkerframework.framework.qual.Default{,Qualifier}ForUnannotatedCode` to + `DefaultInUncheckedCodeFor and DefaultQualifierInHierarchyInUncheckedCode`. + +**Closed issues:** +#169, #363, #448, #478, #496, #516, #529. + + +Version 1.9.9 (1 December 2015) +------------------------------- + +Fixed issues: #511, #513, #514, #455, #527. + +Removed the javac_maven script and batch file, +which had been previously deprecated. + + +Version 1.9.8 (9 November 2015) +------------------------------- + +Field initialization warnings can now be suppressed for a single field at a +time, by placing @SuppressWarnings("initialization") on the field declaration. + +Updated Maven instructions to no longer require a script. +Added an example of how to use the instructions under +docs/examples/MavenExample. + +The javac_maven script (and batch file) are deprecated and will be +removed as of December 2015. + +Fixed issues: #487, #500, #502. + + +Version 1.9.7 (24 October 2015) +------------------------------- + +Fixed issues: #291, #474. + + +Version 1.9.6 (8 October 2015) +------------------------------ + +Fixed issue: #460. + + +Version 1.9.5 (1 September 2015) +-------------------------------- + +Test Framework Updates: + * The test framework has been refactored to improve extensibility. + * Tests that previously extended ParameterizedCheckerTest or + CheckerTest should extend either CheckerFrameworkTest or nothing. + * If a test used methods that were previously found on + CheckerTest, you may find them in TestUtilities. + +Fixed issues: #438, #457, #459. + + +Version 1.9.4 (4 August 2015) +----------------------------- + +Documented the notion of a compound checker, which depends on other checkers + and automatically runs them. + +Renamed -AuseConservativeDefaultsForUnannotatedSourceCode command-line + option to -AuseSafeDefaultsForUnannotatedSourceCode + +Moved the Checker Framework version control repository from Google Code to +GitHub, and from the Mercurial version control system to Git. If you have +cloned the old repository, then discard your old clone and create a new one +using this command: +``` + git clone https://github.com/typetools/checker-framework.git +``` + +Fixed issues: #427, #429, #434, #442, #450. + + +Version 1.9.3 (1 July 2015) +--------------------------- + +New command-line options: + * -AsafeDefaultsForUnannotatedBytecode causes a checker to use conservative + defaults for .class files that were compiled without running the given + checker. Without this option, type-checking is unsound (that is, there + might be errors at run time even though the checker issues no warnings). + * -AuseConservativeDefaultsForUnannotatedSourceCode uses conservative + annotations for unannotated type uses. Use this when compiling a library in + which some but not all classes are annotated. + +Various bug fixes and documentation improvements. + +Fixed issues: #436. + + +Version 1.9.2 (1 June 2015) +--------------------------- + +Internationalization Format String Checker: +This new type-checker prevents use of incorrect internationalization +format strings. + +Fixed issues: #434. + + +Version 1.9.1 (1 May 2015) +-------------------------- + +New FAQ entry: + "How does the Checker Framework compare with Eclipse's null analysis?" + + +Version 1.9.0 (17 April 2015) +----------------------------- + +Bug fixes for generics, especially type parameters: + * Manual chapter 21 "Generics and polymorphism" has been expanded, + and it gives more information on annotating type parameters. + * The qualifier on a type parameter (e.g. <@HERE T> ) only applies + to the lower bound of that type parameter. Previously it also + applied to the upper bound. + * Unannotated, unbounded wildcards are now qualified with the + annotations of the type parameter to which they are an argument. + See the new manual section 23.3.4 for more details. + * Warning "bound.type.incompatible" is issued if the lower bound of + a type parameter or wildcard is a supertype of its upper bound, + e.g. <@Nullable T extends @NonNull Object> + * Method type argument inference has been improved. Fewer warnings + should be issued when method invocations omit type arguments. + * Added command-line option -AprintVerboseGenerics to print more + information about type parameters and wildcards when they appear + in warning messages. + +Reflection resolution: +If you supply the -AresolveReflection command-line option, the Checker +Framework attempts to resolve reflection. This reduces the number of +false positive warnings caused by reflection. + +The documentation for the Map Key Checker has been moved into its own +chapter in the manual. + +Fixed issues: #221, #241, #313, #314, #328, #335, #337, #338, #339, #355, #369, + #376, #378, #386, #388, #389, #393, #403, #404, #413, #414, #415, + #417, #418, #420, #421, #422, #426. + + +Version 1.8.11 (2 March 2015) +----------------------------- + +Fixed issues: #396, #400, #401. + + +Version 1.8.10 (30 January 2015) +-------------------------------- + +Fixed issues: #37, #127, #350, #364, #365, #387, #392, #395. + + +Version 1.8.9 (19 December 2014) +-------------------------------- + +Aliasing Checker: +This new type-checker ensures that an expression has no aliases. + +Fixed issues: #362, #380, #382. + + +Version 1.8.8 (26 November 2014) +-------------------------------- + +@SuppressWarnings("all") suppresses all Checker Framework warnings. + +Implicit imports are deprecated, including the jsr308_imports environment +variable and the -jsr308_imports ... and -Djsr308.imports=... command-line +options. + +For checkers bundled with the Checker Framework, package names may now +be omitted when running from the command line. +E.g. + javac -processor NullnessChecker MyFile.java + +The Nullness checker supports Android annotations +android.support.annotation.NonNull and android.support.annotation.Nullable. + +Fixed issues: #366, #379. + + +Version 1.8.7 (30 October 2014) +------------------------------- + +Fix performance regression introduced in release 1.8.6. + +Nullness Checker: + * Updated Nullness annotations in the annotated JDK. + See issues: #336, #340, #374. + * String concatenations with null literals are now @NonNull + rather than @Nullable. See issue #357. + +Fixed issues: #200, #300, #332, #336, #340, #357, #359, #373, #374. + + +Version 1.8.6 (25 September 2014) +--------------------------------- + +Method Reference and Lambda Expression Support: +The Checker Framework now supports type-checking method references +and lambda expressions to ensure they are congruent with the +functional interface they are assigned to. The bodies of lambda expressions +are also now type-checked similarly to regular method bodies. + +Dataflow: + * Handling of the following language features has been improved: + boxed Booleans, finally blocks, switch statements, type casts, enhanced + for loops + * Performance improvements + +Annotations: +The checker-compat-qual.jar is now included with the Checker Framework +release. It can also be found in Maven Central at the coordinates: +org.checkerframework:checker-compat-qual +Annotations in checker-compat-qual.jar do not require Java 8 but +can only be placed in annotation locations valid in Java 7. + + +Version 1.8.5 (29 August 2014) +------------------------------ + +Eclipse Plugin: +All checkers in the Checker Framework manual now appear in the +Eclipse plugin by default. Users no longer have to include +checker.jar on their classpath to run any of the built-in checkers. + +Improved Java 7 compatibility and introduced Java 7 compliant +annotations for the Nullness Checker. Please see the section on +"Class-file compatibility with Java 7" in the manual for more details. + +Fixed issue #347. + + +Version 1.8.4 (1 August 2014) +----------------------------- + +The new Constant Value Checker is a constant propagation analysis: it +determines which variable values can be known at compile time. + +Overriding methods now inherit declaration annotations from methods they +override, if the declaration annotation is meta-annotate with +@InheritedAnnotation. In particular, the purity annotations @SideEffectFree, +@Deterministic, and @Pure are inherited. + +Command-line options: + * Renamed the -AenablePurity command-line flag to -AcheckPurityAnnotations. + * Added a command-line option -AoutputArgsToFile to output all command-line + options passed to the compiler to a file. This is especially useful when + debugging Maven compilation. + +Annotations: +These changes are relevant only to people who wish to use pluggable +type-checking with a standard Java 7 toolset. (If you are not having +trouble with your Java 7 JVM, then you don't care about them.) + * Made clean-room reimplementations of nullness-related annotations + compatible with Java 7 JVMs, by removing TYPE_USE as a target. + * Added a new set of Java 7 compatibility annotations for the Nullness Checker + in the org.checkerframework.checker.nullness.compatqual package. These + annotations do not require Java 8 but can only be placed in annotation + locations valid in Java 7. + +Java 8 support: +The Checker Framework no longer crashes when type-checking code with lambda +expressions, but it does issue a lambda.unsupported warning when +type-checking code containing lambda expressions. Full support for +type-checking lambda expressions will appear in a future release. + +Fixed issue #343. + + +Version 1.8.3 (1 July 2014) +--------------------------- + +Updated the Initialization Checker section in the manual with +a new introduction paragraph. + +Removed the Maven plugin section from the manual as the plugin is +no longer maintained and the final release was on June 2, #2014. +The javac_maven script (and batch file) are available to use +the Checker Framework from Maven. + +Fixed issue #331. + + +Version 1.8.2 (2 Jun 2014) +-------------------------- + +Converted from using rt.jar to ct.sym for creating the annotated jdk. +Using the annotated jdk on the bootclasspath of a VM will cause the +vm to crash immediately. + +The Lock Checker has been rewritten to support dataflow analysis. +It can now understand conditional expressions, for example, and +knows that "lock" is held in the body of statements like +"if (lock.tryLock()) { ... }" +The Lock Checker chapter in the manual has been updated accordingly +and describes the new Lock Checker features in detail. + +Provided a javac_maven script (and batch file) to make it simpler +to use the Checker Framework from Maven. The Maven plug-in is deprecated +and will be removed as of July 1, 2014. Added an explanation of how +to use the script in the Maven section of the manual. + +The Checker Framework installation instructions in the manual have +been updated. + +Fixed issues: #312, #315, #316, #318, #319, #324, #326, #327. + + +Version 1.8.1 (1 May 2014) +-------------------------- + +Support to directly use the Java 8 javac in addition to jsr308-langtools. +Added docs/examples directory to checker-framework.zip. +New section in the manual describing the contents of checker-framework.zip. + +Fixed issues: #204, #304, #320. + + +Version 1.8.0 (2 April 2014) +---------------------------- + +Added the GUI Effect Checker, which prevents "invalid thread access" errors +when a background thread in a GUI attempts to access the UI. + +Changed the Java package of all type-checkers and qualifiers. The package +"checkers" has been renamed to "org.checkerframeork.checker". This +requires you to change your import statements, such as from + import checkers.nullness.quals.*; +to + import org.checkerframework.checker.nullness.qual.*; +It also requires you to change command-line invocations of javac, such as from + javac -processor checkers.nullness.NullnessChecker ... +to + javac -processor org.checkerframework.checker.nullness.NullnessChecker ... + +Restructured the Checker Framework project and package layout, +using the org.checkerframework prefix. + + +Version 1.7.5 (5 March 2014) +---------------------------- + +Minor improvements to documentation and demos. +Support a few new units in the UnitsChecker. + + +Version 1.7.4 (19 February 2014) +-------------------------------- + +Error messages now display the error key that can be used in +SuppressWarnings annotations. Use -AshowSuppressWarningKeys to +show additional keys. + +Defaulted type qualifiers are now stored in the Element and written +to the final bytecode. + +Reduce special treatment of checkers.quals.Unqualified. + +Fixed issues: #170, #240, #265, #281. + + +Version 1.7.3 (4 February 2014) +------------------------------- + +Fixes for Issues #210, #253, #280, #288. + +Manual: + Improved discussion of checker guarantees. + +Maven Plugin: + Added option useJavacOutput to display exact compiler output. + +Eclipse Plugin: + Added the Format String Checker to the list of built-in checkers. + + +Version 1.7.2 (2 January 2014) +------------------------------ + +Fixed issues: #289, #292, #295, #296, #298. + + +Version 1.7.1 (9 December 2013) +------------------------------- + +Fixes for Issues #141, #145, #257, #261, #269, #267, #275, #278, #282, #283, #284, #285. + +**Implementation details:** + +Renamed AbstractBasicAnnotatedTypeFactory to GenericAnnotatedTypeFactory + + +Version 1.7.0 (23 October 2013) +------------------------------- + +Format String Checker: + This new type-checker ensures that format methods, such as + System.out.printf, are invoked with correct arguments. + +Renamed the Basic Checker to the Subtyping Checker. + +Reimplemented the dataflow analysis that performs flow-sensitive type + refinement. This fixes many bugs, improves precision, and adds features. + Many more Java expressions can be written as annotation arguments. + +Initialization Checker: + This new abstract type-checker verifies initialization properties. It + needs to be combined with another type system whose proper initialization + should be checked. This is the new default initialzation checker for the + Nullness Checker. It is based on the "Freedom Before Commitment" approach. + +Renamed method annotations used by the Nullness Checker: + @AssertNonNullAfter => @EnsuresNonNull + @NonNullOnEntry => @RequiresNonNull + @AssertNonNullIfTrue(...) => @IfMethodReturnsFalseEnsuresNonNull + @AssertNonNullIfFalse(...) => @IfMethodReturnsFalseEnsuresNonNull + @LazyNonNull => @MonotonicNonNull + @AssertParametersNonNull => [no replacement] +Removed annotations used by the Nullness Checker: + @AssertParametersNonNull +Renamed type annotations used by the Initialization Checker: + @NonRaw => @Initialized + @Raw => @UnknownInitialization + new annotation @UnderInitialization +The old Initialization Checker (that uses @Raw and @NonRaw) can be invoked + by invoking the NullnessRawnessChecker rather than the NullnessChecker. + +Purity (side effect) analysis uses new annotations @SideEffectFree, + @Deterministic, and @TerminatesExecution; @Pure means both @SideEffectFree + and @Deterministic. + +Pre- and postconditions about type qualifiers are available for any type system + through @RequiresQualifier, @EnsuresQualifier and @EnsuresQualifierIf. The + contract annotations for the Nullness Checker (e.g. @EnsuresNonNull) are now + only a special case of these general purpose annotations. + The meta-annotations @PreconditionAnnotation, @PostconditionAnnotation, and + @ConditionalPostconditionAnnotation can be used to create more special-case + annotations for other type systems. + +Renamed assertion comment string used by all checkers: + @SuppressWarnings => @AssumeAssertion + +To use an assert statement to suppress warnings, the assertion message must + include the string "@AssumeAssertion(warningkey)". Previously, just the + warning key sufficed, but the string @SuppressWarnings(warningkey) was + recommended. + +New command-line options: + -AonlyDefs and -AonlyUses complement existing -AskipDefs and -AskipUses + -AsuppressWarnings Suppress warnings matching the given key + -AassumeSideEffectFree Unsoundly assume that every method is side-effect-free + -AignoreRawTypeArguments Ignore subtype tests for type arguments that + were inferred for a raw type + -AenablePurity Check the bodies of methods marked as pure + (@SideEffectFree or @Deterministic) + -AsuggestPureMethods Suggest methods that could be marked as pure + -AassumeAssertionsAreEnabled, -AassumeAssertionsAreDisabled Whether to + assume that assertions are enabled or disabled + -AconcurrentSemantics Whether to assume concurrent semantics + -Anocheckjdk Don't err if no annotated JDK can be found + -Aflowdotdir Create an image of the control flow graph + -AinvariantArrays replaces -Alint=arrays:invariant + -AcheckCastElementType replaces -Alint=cast:strict + +Manual: + New manual section about array types. + New FAQ entries: "Which checker should I start with?", "How can I handle + typestate, or phases of my program with different data properties?", + "What is the meaning of a type qualifier at a class declaration?" + Reorganized FAQ chapter into sections. + Many other improvements. + + +Version 1.6.7 (28 August 2013) +------------------------------ + +User-visible framework improvements: + Improve the error message produced by -Adetailedmsgtext + +Bug fixes: + Fix issue #245: anonymous classes were skipped by default + + +Version 1.6.6 (01 August 2013) +------------------------------ + +Documentation: + The Checker Framework manual has been improved. Changes include: +more troubleshooting tips to the Checker Framework manual, an improved +discussion on qualifier bounds, more examples, improved formatting, and more. + An FAQ entry has been added to discuss JSR305. + Minor clarifications have been added to the Checker Framework tutorial. + + +Version 1.6.5 (01 July 2013) +---------------------------- + +User-visible framework improvements: + Stub files now support static imports. + +Maven plugin: + Maven plugin will now issue a warning rather than quit when zero checkers are specified in a project's pom.xml. + +Documentation: + Improved the Maven plugin instructions in the Checker Framework manual. + Added documentation for the -XDTA:noannotationsincomments compiler flag. + +Internal framework improvements: + Improved Maven-plugin developer documentation. + + +Version 1.6.4 (01 June 2013) +---------------------------- + +User-visible framework improvements: + StubGenerator now generates stubs that can be read by the StubParser. + +Maven plugin: + The Maven plugin no longer requires the Maven project's output directory to exist in order to run the Checker Framework. However, if you ask the Checker Framework to generate class files then the output directory will be created. + +Documentation: + Improved the Maven plugin instructions in the Checker Framework manual. + Improved the discussion of why to define both a bottom and a top qualifier in the Checker Framework manual. + Update FAQ to discuss that some other tools incorrectly interpret array declarations. + + +Version 1.6.3 (01 May 2013) +--------------------------- + +Eclipse plugin bug fixes: + The javac argument files used by the Eclipse plugin now properly escape file paths. Windows users should no longer encounter errors about missing built-in checkers. + +Documentation: + Add FAQ "What is the meaning of an annotation after a type?" + + +Version 1.6.2 (04 Apr 2013) +--------------------------- + +Eclipse plugin: + The "Additional compiler parameters" text field has now been replaced by a list. Parameters in this list may be activated/deactivated via checkbox. + +Eclipse plugin bug fixes: + Classpaths and source files should now be correctly quoted when they contain spaces. + +Internal framework improvements: + Update pom files to use the same update-version code as the Checker Framework "web" ant task. Remove pom specific update-version code. + Update build ant tasks to avoid re-running targets when executing tests from the release script. + + +Version 1.6.1 (01 Mar 2013) +--------------------------- + +User-visible framework improvements: + A number of error messages have been clarified. + Stub file now supports type annotations in front and after method type variable declarations. + You may now specify custom paths to javac.jar and jdk7.jar on the command line for non-standard installations. + +Internal framework improvements: + Add shouldBeApplied method to avoid unnecessary scans in DefaultApplier and avoid annotating void types. + Add createQualifierDefaults and createQualifierPolymorphism factory methods. + +Maven plugin: + Put Checker Framework jars at the beginning of classpath. + Added option to compile code in order to support checking for multi-module projects. + The plugin no longer copies the various Checker Framework maven artifacts to one location but instead takes advantage of the new custom path options for javac.jar and jdk7.jar. + The maven plugin no longer attempts to resolve jdk6.jar + +Eclipse plugin: + Put Checker Framework jars at the beginning of classpath. + All files selected from a single project can now be checked. The previous behavior only checked the entire project or one file depending on the type of the first file selected. + +Documentation: + Fixed broken links and incomplete URLs in the Checker Framework Manual. + Update FAQ to discuss that some other tools incorrectly interpret array declarations. + +Bug fixes + + +Version 1.6.0 (1 Feb 2013) +-------------------------- + +User-visible framework improvements: + It is possible to use enum constants in stub files without requiring the fully qualified name, as was previously necessary. + Support build on a stock Java 8 OpenJDK. + +Adapt to underlying jsr308-langtools changes. + The most visible change is syntax for fully-qualified types, from @A java.lang.Object to java.lang.@A Object. + JDK 7 is now required. The Checker Framework does not build or run on JDK 6. + +Documentation: + A new tutorial is available at . + + +Version 1.5.0 (14 Jan 2013) +--------------------------- + +User-visible framework improvements: + To invoke the Checker Framework, call the main method of class + CheckerMain, which is a drop-in replacement for javac. This replaces + all previous techniques for invoking the Checker Framework. Users + should no longer provide any Checker Framework jars on the classpath or + bootclasspath. jsr308-all.jar has been removed. + The Checker Framework now works with both JDK 6 and JDK 7, without need + for user customization. The Checker Framework determines the + appropriate annotated JDK to use. + All jar files now reside in checker-framework/checkers/binary/. + +Maven plugin: + Individual pom files (and artifacts in the Maven repository) for all + Checker Framework jar files. + Avoid too-long command lines on Windows. + See the Maven section of the manual for more details. + +Eclipse plugin: + Avoid too-long command lines on Windows. + Other bug fixes and interface improvements. + +Other framework improvements: + New -Adetailedmsgtext command-line option, intended for use by IDE plugins. + + +Version 1.4.4 (1 Dec 2012) +-------------------------- + +Internal framework improvements: + Add shutdown hook mechanism and use it for -AresourceStats resource + statistics flag. + Add -AstubWarnIfNotFound and -AstubDebug options to improve + warnings and debug information from the stub file parsing. + Ignore case when comparing error suppression keys. + Support the bottom type as subtype of any wildcard type. + +Tool Integration Changes + The Maven plugin id has been changed to reflect standard Maven + naming conventions. + Eclipse and Maven plugin version numbers will now + track the Checker Framework version numbers. + +Bug fixes. + + +Version 1.4.3 (1 Nov 2012) +-------------------------- + +Clarify license: + The Checker Framework is licensed under the GPL2. More permissive + licenses apply to annotations, tool plugins (Maven, Eclipse), + external libraries included with the Checker Framework, and examples in + the Checker Framework Manual. + Replaced all third-party annotations by cleanroom implementations, to + avoid any potential problems or confusion with licensing. + +Aliased annotations: + Clarified that there is no need to rewrite your program. The Checker + Framework recognizes dozens of annotations used by other tools. + +Improved documentation of Units Checker and Gradle Integration. +Improved developer documentation of Eclipse and Maven plugins. + +Bug fixes. + + +Version 1.4.2 (16 Oct 2012) +--------------------------- + +External tool support: + Eclipse plug-in now works properly, due to many fixes + +Regex Checker: + New CheckedPatternSyntaxException added to RegexUtil + +Support new foreign annotations: + org.eclipse.jdt.annotation.Nullable + org.eclipse.jdt.annotation.NonNull + +New FAQ: "What is a receiver?" + +Make annotations use 1-based numbering for formal parameters: + Previously, due to a bug the annotations used 0-based numbering. + This change means that you need to rewrite annotations in the following ways: + @KeyFor("#3") => @KeyFor("#4") + @AssertNonNullIfTrue("#0") => @AssertNonNullIfTrue("#1") + @AssertNonNullIfTrue({"#0", "#1"}) => @AssertNonNullIfTrue({"#1", "#2"}) + @AssertNonNullAfter("get(#2)") => @AssertNonNullAfter("get(#3)") + This command: + find . -type f -print | xargs perl -pi -e 's/("#)([0-9])(")/$1.($2+1).$3/eg' + handles the first two cases, which account for most uses. You would need + to handle any annotations like the last two cases in a different way, + such as by running + grep -r -n -E '\("[^"]+#[0-9][^A-Za-z]|\("#[0-9][^"]' . + and making manual changes to the matching lines. (It is possible to + provide a command that handles all cases, but it would be more likely to + make undesired changes.) + Whenever making automated changes, it is wise to save a copy of your + codebase, then compare it to the modified version so you can undo any + undesired changes. Also, avoid running the automated command over version + control files such as your .hg, .git, .svn, or CVS directory. + + +Version 1.4.1 (29 Sep 2012) +--------------------------- + +User-visible framework improvements: + Support stub files contained in .jar files. + Support aliasing for declaration annotations. + Updated the Maven plugin. + +Code refactoring: + Make AnnotationUtils and AnnotatedTypes into stateless utility classes. + Instead, provide the necessary parameters for particular methods. + Make class AnnotationBuilder independent of AnnotationUtils. + Remove the ProcessingEnvironment from AnnotatedTypeMirror, which was + hardly used and can be replaced easily. + Used more consistent naming for a few more fields. + Moved AnnotatedTypes from package checkers.types to checkers.utils. + this required making a few methods in AnnotatedTypeFactory public, + which might require changes in downstream code. + +Internal framework improvements: + Fixed Issues #136, #139, #142, #156. + Bug fixes and documentation improvements. + + +Version 1.4.0 (11 Sep 2012) +--------------------------- + +User-visible framework improvements: + Defaulting: + @DefaultQualifier annotations now use a Class instead of a String, + preventing simple typo errors. + @DefaultLocation extended with more constants. + TreeAnnotator propagates the least-upper-bound of the operands of + binary/compound operations, instead of taking the default qualifier. + Stub files now ignore the return type, allowing for files automatically + generated from other formats. + Type factories and type hierarchies: + Simplify AnnotatedTypeFactory constructors. + Add a GeneralAnnotatedTypeFactory that supports multiple type systems. + Improvements to QualifierHierarchy construction. + Type-checking improvements: + Propagate annotations from the sub-expression of a cast to its result. + Better handling of assignment context and improved inference of + array creation expressions. + Optional stricter checking of casts to array and generic types using + the new -Alint=cast:strict flag. + This will become the default in the future. + Code reorganization: + SourceChecker.initChecker no longer has a ProcessingEnvironment + parameter. The environment can now be accessed using the standard + processingEnv field (instead of the previous env field). + Classes com.sun.source.util.AbstractTypeProcessor and + checkers.util.AggregateChecker are now in package checkers.source. + Move isAssignable from the BaseTypeChecker to the BaseTypeVisitor; now + the Checker only consists of factories and logic is contained in the + Visitor. + Warning and error messages: + Issue a warning if an unsupported -Alint option is provided. + Improved error messages. + Maven plugin now works. + +Nullness Checker: + Only allow creation of (implicitly) non-null objects. + Optionally forbid creation of arrays with @NonNull component type, + when flag -Alint=arrays:forbidnonnullcomponents is supplied. + This will become the default in the future. + +Internal framework improvements: + Enable assertion checking. + Improve handling of annotated type variables. + Assignment context is now a type, not a tree. + Fix all compiler warnings. + + +Version 1.3.1 (21 Jul 2012) +--------------------------- + +Installation: + Clarify installation instructions for Windows. Remove javac.bat, which + worked for running distributed checkers but not for creating new checkers. + +User-visible framework improvements: + Implement @PolyAll qualifier to vary over multiple type systems. + The Checker Framework is unsound due to Java's covariant array subtyping. + You can enable invariant array subtyping (for qualifiers only, not for + base Java types) with the command-line option -Alint=arrays:invariant. + This will become the default in the future. + +Internal framework improvements: + Improve defaulting for multiple qualifier hierarchies. + Big refactoring of how qualifier hierarchies are built up. + Improvements to error handling output for unexpected exceptions. + Bug fixes and documentation improvements. + + +Version 1.3.0 (3 Jul 2012) +-------------------------- + +Annotation syntax changes, as mandated by the latest Type Annotations +(JSR 308) specification. The most important ones are: +- New receiver syntax, using "this" as a formal parameter name: + ReturnType methodname(@ReceiverAnnotation MyClass this, ...) { ... } +- Changed @Target default to be the Java 1.5 values +- UW extension: in addition to annotations in comments, support + special /*>>> */ comments to hide multiple tokens. + This is useful for the new receiver syntax and for import statements. + +Framework improvements: + Adapt to annotation storage changes in jsr308-langtools 1.3.0. + Move type validation methods from the BaseTypeChecker to BaseTypeVisitor. + + +Version 1.2.7 (14 May 2012) +--------------------------- + +Regex Checker: + Add basic support for the concatenation of two non-regular expressions + that produce a valid regular expression. + Support "isRegex" in flow inference. + +Framework improvements: + New @StubFiles annotation declaratively adds stub files to a checker. + +Internal bug fixes: + Respect skipDefs and skipUses in NullnessFlow. + Support package annotations in stub files. + Better support for enums in annotation attributes. + Cleanups to how implicit receivers are determined. + + +Version 1.2.6 (18 Mar 2012) +--------------------------- + +Nullness Checker: + Correctly handle unboxing in more contexts (if, switch (Issue 129), + while loops, ...) + +Regex Checker: + Add capturing groups parameter to Regex qualifier. + Count groups in String literals and String concatenation. + Verify group number to method calls that take a capturing group + number. + Update RegexUtil methods to take optional groups parameter. + Modify regex qualifier hierarchy to support groups parameter. + Add special case for Pattern.compile when called with Pattern.LITERAL flag. + +Internal bug fixes: + Improve flow's support of annotations with parameters. + Fix generics corner cases (Issues #131, #132, #133, #135). + Support type annotations in annotations and type-check annotations. + Improve reflective look-up of visitors and factories. + Small cleanups. + + +Version 1.2.5.1 (06 Feb 2012) +----------------------------- + +Nullness Checker: + Correct the annotations on ThreadLocal and InheritableThreadLocal. + +Internal bug fixes: + Expand release tests. + Compile release with JDK 6 to work on both JDK 6 and JDK 7. + + +Version 1.2.5 (3 Feb 2012) +-------------------------- + +Don't put classpath on the bootclasspath when invoking javac. This +prevents problems if, for example, android.jar is on the classpath. + +New -jsr308_imports ... and -Djsr308.imports=... command-line options, for +specifying implicit imports from the command line. This is needed by Maven. + +New -Aignorejdkastub option makes the checker not load the jdk.astub +file. Files from the "stubs" option are still loaded. + +Regex Checker: + Support concatenation of PolyRegex strings. + Improve examples of use of RegexUtil methods. + +Signature Checker: + Add new @ClassGetName annotation, for a 4th string representation of a + class that is used by the JDK. Add supporting annotations to make the + type hierarchy a complete lattice. + Add PolySignature annotation. + +Internal bug fixes: + Improve method type argument inference. + Handle type variables whose upper bound is a type variable. + Fix bug in least upper bound computation for anonymous classes. + Improve handling of annotations inherited from superclasses. + Fix design problem with Nullness Checker and primitive types. + Ensure that overriding methods respect pre- and postconditions. + Correctly resolve references to an enclosing this. + Improve handling of Java source that contains compilation errors. + + +Version 1.2.4 (15 Dec 2011) +--------------------------- + +All checkers: +- @Target(TYPE_USE) meta-annotation is properly handled. + +Nullness Checker: +- Do not allow nullness annotations on primitive types. +- Improvements to rawness (initialization) checks. +- Special-case known keys for System.getProperty. +- The -Alint=uninitialized command-line option now defaults to off, and + applies only to initialization of primitive and @Nullable fields. It is + not possible to disable, from the command line, the check that all + @NonNull fields are initialized. Such warnings must be suppressed + explicitly, for example by using @SuppressWarnings. + +Regex Checker: +- Improved RegexUtil class. + +Manual: +- Add FAQ item "Is the Checker Framework an official part of Java?" +- Trim down README.txt; users should read the manual instead. +- Improvements throughout, especially to Nullness and Regex Checker sections. + +**Implementation details:** +- Add a new @InvisibleQualifier meta-annotation for type qualifiers. + Instead of special-casing @Unqualified in the AnnotatedTypeMirror it + now looks for this meta-annotation. This also allows type systems to + hide type qualifiers it doesn't want visible, which we now use in the + Nullness Checker to hide the @Primitive annotation. +- Nullness Checker: Introduce a new internal qualifier @Primitive that is + used for primitive types. +- Be stricter about qualifiers being present on all types. If you get + errors about missing qualifiers, check your defaulting rules. + This helped in fixing small bugs in corner cases of the type + hierarchy and type factory. +- Unify decoding type annotations from trees and elements. +- Improve handling of annotations on type variables and upper bounds. +- Support checkers that use multiple, disjoint qualifier hierarchies. +- Many bug fixes. + + +Version 1.2.3 (1 Nov 2011) +-------------------------- + +Regex Checker: +- Add @PolyRegex polymorphic annotation +- Add more stub library annotations + +**Implementation details:** +- Do not use "null" for unqualified types. Explicitly use @Unqualified + and be strict about correct usage. If this causes trouble for you, + check your @ImplicitFor and @DefaultQualifierInHierarchy + meta-annotations and ensure correct defaulting in your + AnnotatedTypeFactory. + +Bug fixes: +- Correctly handle f-bounded polymorphism. AnnotatedTypeMirror now has + methods to query the "effective" annotations on a type, which + handles type variable and wildcard bounds correctly. Also, terminate + recursions by not doing lazy-initialization of bounds during defaulting. +- Many other small bug fixes and documentation updates. + + +Version 1.2.2 (1 Oct 2011) +-------------------------- + +Be less restrictive about when to start type processing when errors +already exist. +Add -AskipDefs command-line option to not type-check some class +definitions. +Documentation improvements. + + +Version 1.2.1 (20 Sep 2011) +--------------------------- + +Fix issues #109, #110, #111 and various other cleanups. +Improvements to the release process. +Documentation improvements. + + +Version 1.2.0.1 (4 Sep 2011) +---------------------------- + +New version number to stay in sync with JSR 308 compiler bugfix. +No significant changes. + + +Version 1.2.0 (2 Sep 2011) +-------------------------- + +Updated to JDK 8. Use -source 8 (the new default) for type annotations. +Documentation improvements +Bug fixes all over + +Nullness Checker: +- Correct the upper bounds of all Collection subtypes + + +Version 1.1.5 (22 Jul 2011) +--------------------------- + +**User-visible changes:** + +Units Checker: + Instead of conversion routines, provide unit constants, with which + to multiply unqualified values. This is easier to type and the + multiplication gets optimized away by the compiler. + +Fenum Checker: + Ensure that the switch statement expression is a supertype of all + the case expressions. + +**Implementation details:** + +- Parse declaration annotations in stub files + +- Output error messages instead of raising exceptions. This change + required us to introduce method "initChecker" in class + SourceChecker, which should be used instead of "init". This allows + us to handle the calls to initChecker within the framework. + Use method "errorAbort" to output an error message and abort + processing. + + +Version 1.1.4 (8 Jul 2011) +-------------------------- + +**User-visible changes:** + +Units Checker (new): + Ensures operations are performed on variables of correct units of + measurement (e.g., miles vs. kilometers vs. kilograms). + +Changed -AskipClasses command-line option to -AskipUses + +**Implementation details:** + +- Improve support for type qualifiers with enum attributes + + +Version 1.1.3 (17 Jun 2011) +--------------------------- + +**User-visible changes:** + +Interning: +- Add @UsesObjectEquals annotation + +Manual: +- Signature Checker is now documented +- Fenum Checker documentation improved +- Small improvements to other sections + +**Implementation details:** + +- Updates to the web-site build process + +- The BaseTypeVisitor used to provide the same two type parameters as + class SourceVisitor. However, all subtypes of BaseTypeVisitor were + instantiated as . We decided to directly instantiate the + SourceVisitor as and removed this complexity. + Instead, the BaseTypeVisitor is now parameterized by the subtype of + BaseTypeChecker that should be used. This gives a more concrete type + to field "checker" and is similar to BasicAnnotatedTypeFactory. + +- Added method AnnotatedTypeFactory.typeVariablesFromUse to allow + type-checkers to adapt the upper bounds of a type variable depending on + the type instantiation. + +- Method type argument inference: + Changed AnnotatedTypeFactory.methodFromUse to return a Pair consisting + of the method and the inferred or explicit method type arguments. + If you override this method, you will need to update your version. + See this change set for a simple example: + + +- Testing framework: + Support for multiple expected errors using the "// :: A :: B :: C" syntax. + +Many small updates and fixes. + + +Version 1.1.2 (12 Jan 2011) +--------------------------- + +Fake Enum Checker (new): + A "fake enumeration" is a set of integers rather than a proper Java enum. + They are used in legacy code and for efficiency (e.g., in Android). The + Fake Enum Checker gives them the same safety guarantees as a proper Java + enum. + +Property File Checker (new): + Ensures that valid keys are used for property files and resource bundles. + Also includes a checker that code is properly internationalized and a + checker for compiler message keys as used in the Checker Framework. + +Signature Checker (new): + Ensures that different string representations of a Java type (e.g., + `"pakkage.Outer.Inner"` vs. `"pakkage.Outer$Inner"` vs. `"Lpakkage/Outer$Inner;"`) + are not misused. + +Interning Checker enhancements: + Issues fewer false positives for code like "a==b || a.equals(b)" + +Foreign annotations: + The Checker Framework supports more non-Checker-Framework annotations. + This means that it can check already-annotated code without requiring you + to rewrite your annotations. + Add as an alias for checkers.interning.quals.Interned: + com.sun.istack.Interned + Add as aliases for checkers.nullness.quals.NonNull: + com.sun.istack.NotNull + org.netbeans.api.annotations.common.NonNull + Add as aliases for checkers.nullness.quals.Nullable: + com.sun.istack.Nullable + javax.validation.constraints.NotNull + org.netbeans.api.annotations.common.CheckForNull + org.netbeans.api.annotations.common.NullAllowed + org.netbeans.api.annotations.common.NullUnknown + +Manual improvements: + Improve installation instructions + Rewrite section on generics (thanks to Bert Fernandez and David Cok) + Also refactor the generics section into its own chapter + Rewrite section on @Unused and @Dependent + New manual section: Writing Java expressions as annotation arguments + Better explanation of warning suppression + JSR 308 is planned for Java 8, not Java 7 + +Stub files: + Support nested classes by expressing them at top level in binary form: A$B + Improved error reporting when parsing stub files + +Annotated JDK: + New way of generating annotated JDK + jdk.jar file no longer appears in repository + Warning if you are not using the annotated JDK. + +Miscellaneous: + Warn if -source command-line argument does not support type annotations + +Many bug fixes + There are too many to list, but some notable ones are to local type + inference, generics, pre- and post-conditions (e.g., @NonNullOnEntry, + @AssertNonNull*), and map keys (@KeyFor). In particular, preconditions + and map key annotations are now checked, and if they cannot be verified, + an error is raised; previously, they were not verified, just unsoundly + trusted. + + +Version 1.1.1 (18 Sep 2010) +--------------------------- + +Eclipse support: + Removed the obsolete Eclipse plug-in from repository. The new one uses a + different repository + (http://code.google.com/a/eclipselabs.org/p/checker-plugin/) but a user + obtains it from the same URL as before: + https://checkerframework.org/eclipse/ + +Property Key Checker: + The property key checker allows multiple resource bundles and the + simultaneous use of both resource bundles and property files. + +Javari Checker: + Added Javari stub classes for more JDK classes. + +Distribution: + Changed directory structure (top level is "checker-framework"; "checkers" + is a under that) for consistency with version control repository. + +Many documentation improvements and minor bugfixes. + + +Version 1.1.0b, 16 Jun 2010 +--------------------------- + +Fixed a bug related to running binary release in JDK 6 + + +Version 1.1.0 (13 Jun 2010) +--------------------------- + +Checkers + Introduced a new simple mechanism for running a checker + Added one annotated JDK for all checkers + +Nullness Checker + Fixed bugs related to map.get() and KeyFor annotation + Fixed bugs related to AssertNonNull* and parameters + Minor updates to the annotated JDK, especially to java.io.File + +Manual + Updated installation instructions + Clarified section regarding fields and type inference + + +Version 1.0.9 (25 May 2010) +--------------------------- + +Nullness Checker: + Improved Javadocs and manual documentation + Added two new annotations: AssertNonNullAfter, KeyFor + Fixed a bug related to AssertNonNullIfFalse and assert statements + Renamed NonNullVariable to NonNullOnEntry + +Checkers: + Interning: Skipping equality check, if either operands should be skipped + Fixed a bug related to annotations targeting array fields found in classfile + Fixed a bug related to method invocation generic type inference + in static methods + +Manual + Added a section on nullness method annotations + Revised the Nullness Checker section + Updated Ant usage instructions + + +Version 1.0.8 (15 May 2010) +--------------------------- + +Checkers + Changed behavior of flow type refinement when annotation is explicit + Handle array initializer trees (without explicit type) + Handle the case of Vector.copyInto + Include javax classes in the distributed jdk jar files + +Interning Checker + Handle interning inference of string concatenation + Add 20+ @Interned annotations to the JDK + Add an option, checkclass, to validate the interning + of specific classes only + +Bug fixes + Fix a bug related to array implicit types + Lock Checker: Treat null as a bottom type + +Manual + Added a new section about Flow inference and fields + + +Version 1.0.7 (12 Apr 2010) +--------------------------- + +Checkers + Distributed a Maven repository + Updated stub parser project to latest version (javaparser 1.0.8) + Fixed bugs related to iterable wildcards and type parameter types + + +Version 1.0.6 (24 Feb 2009) +--------------------------- + +Nullness Checker + Added support for new annotations: + Pure - indicates that the method, given the same parameters, return the + same values + AssertNonNullIfFalse - indicates that a field is NonNull if the method + returns false + Renamed AssertNonNull to AssertParametersNonNull + Updated the annotated jdk + +Javari Checker + Fixed many bugs: + handle implicit dereferencing of this (e.g. `field` in place of + `this.field`) + apply default annotations to method parameters + + +Version 1.0.5 (12 Jan 2009) +--------------------------- + +Checkers + Added support for annotated jdk jars + Improved readability of some failure messages + Added AssertNonNullIfTrue support for method parameter references + Fixed a bug related to LazyNonNull and array fields + Fixed a bug related to inference and compound assignments (e.g. +=) + nullness: permit the type of @NonNull Void + +Manual + Updated annotating-libraries chapter regarding annotated jdk + + +Version 1.0.4 (19 Dec 2009) +--------------------------- + +Bug Fixes + wildcards not recognized as subtypes of type variables + e.g. '? extends A' and 'A' + PolyNull methods not accepting null literal value arguments + spurious unexpected Raw warnings + +Manual + Clarified FAQ item regarding why List's type parameter is + "extends @NonNull Object" + + +Version 1.0.3 (5 Dec 2009) +-------------------------- + +Checkers + New location UPPER_BOUND for DefaultQualifier permits setting the default + for upper bounds, such as Object in "? extends Object". + @DefaultQualifier accepts simple names, like @DefaultQualifier("Nullable"), + rather than requiring @DefaultQualifier("checkers.nullness.quals.Nullable"). + Local variable type inference has improved support for array accesses. + The repository contains Eclipse project and launch configuration files. + This is helpful too people who want to build a checker, not to people + who merely want to run a checker. + Many bug fixes, including: + handling wildcard subtyping rules + stub files and vararg methods being ignored + nullness and spurious rawness errors + uses of array clone method (e.g. String[].clone()) + multibound type parameters (e.g. ) + +Manual + Documented the behavior of annotations on type parameter declarations. + New FAQ item: + How to collect warnings from multiple files + Why a qualifier shouldn't apply to both types and declarations + + +Version 1.0.2 (16 Nov 2009) +--------------------------- + +Checkers + Renamed Regex Checker's @ValidRegex annotation to @Regex + Improved Collection.toArray() heuristics to be more sound + +Bug fixes + Fixed the annotated JDK to match OpenJDK 6 + - Added missing methods and corrected class hierarchy + Fixed a crash related to intersection types + + +Version 1.0.1 (1 Nov 2009) +-------------------------- + +Checkers + Added new checkers: + RegEx checker to detect invalid regular expression use + Internationalization (I18n) checker to detect internationalization errors + +Functionality + Added more performance optimizations + nullness: Added support for netbeans nullness annotations + nullness: better semantics for redundant nullness tests + related to redundant tests in assertions + lock: Added support for JCIP annotation in the Lock Checker + tainting: Added support for polymorphism + Lock Checker supports the JCIP GuardedBy annotation + +Bug fixes + Fixed a crashing bug related to interaction between + generic types and wildcards + Fixed a bug in stub file parser related to vararg annotations + Fixed few bugs in skeleton file generators + +Manual + Tweak installation instructions + Reference Units Checker + Added new sections for new checkers + RegEx checker (S 10) + Internationalization Checker (S 11) + + +Version 1.0.0 (30 Sep 2009) +--------------------------- + +Functionality + Added Linear Checker to restrict aliasing + +Bug fixes + Fixed flow erros related to loop controls and break/continue + +Manual + Adopt new term, "Declaration Annotation" instead of non-type annotations + Added new sections: + Linear Checker (S 9) + Inexpressible types (S 14.3) + How to get started annotating legacy code (S 2.4.4) + Expanded Tainting Checker section + + +Version 0.9.9 (4 Sep 2009) +-------------------------- + +Functionality + Added more optional lint checks (cast:unsafe, all) + Nullness Checker supports @SuppressWarnings("nullness:generic.argument"), + for suppressing warnings related to misuse of generic type arguments. + This was already supported and documented, but had not been mentioned + in the changelog. + +Bug fixes + Fixed many bugs related to Stub files causing parser to ignore + bodiless constructors + annotated arrays annotations + type parameter and wildcard bounds annotations + +Manual + Rewrote 'javac implementation survival guide' (S 13.9) + Restructured 'Using a checker' (S 2) + Added 'Integration with external tools' (S 14) + Added new questions to the FAQ (S 15) + + +Version 0.9.8 (21 Aug 2009) +--------------------------- + +Functionality + Added a Tainting Checker + Added support for conditional nonnull checking + Added optional check for redundant nullness tests + Updated stub parser to latest libraries + +Bug fixes + Fixed a bug related to int[] treated as Object when passed to vararg T... + Fixed a crash related to intersection types + Fixed a bug related to -AskipClasses not being honored + Fixed a bug related to flow + +Manual + Added new sections + 8 Tainting Checker + 3.2.3 Conditional nullness + + +Version 0.9.7 (12 Aug 2009) +--------------------------- + +Functionality + Changed swNonNull to castNonNull + nullness: Improved flow to infer nullness based on method invocations + locking: Permitted @Holding to appear on constructors + +Bug fixes + Fixed a bug related to typevar and wildcard extends clauses + + +Version 0.9.6 (29 Jul 2009) +--------------------------- + +Functionality + Changed 'jsr308.skipClasses' property with '-AskipClasses' option + Locking checker + - Add subtype checking for Holding + - Treat constructors as synchronized methods + +Bug fixes + Added some missing nullness annotations in the jdk + Fixed some bugs related to reading stub files + +Manual + Added a new section + 2.10 Tips about writing annotations + Updated sections of + 2.6 Unused fields and dependent types + 3.1.1 Rawness annotation hierarchy + + +Version 0.9.5 (13 Jul 2009) +--------------------------- + +Functionality + Added support for Findbugs, JSR305, and IntelliJ nullness annotations + Added an Aggregate Checker base-class + Added support for a form of field access control + +Bug fixes + Added check for arguments in super() calls in constructors + +Manual + Added new sections: + Fields access control + Other tools for nullness checking + Bundling multiple checkers + + +Version 0.9.4 (30 Jun 2009) +--------------------------- + +Functionality + Added Lock Checker + +Bug fixes + Handle more patterns for determining Map.get() return type + +Manual Documentations + Improved installation instructions + Added the following sections + 2.6 Dependent types + 3.1 subsection for LazyNonNull + 10.9 When to use (and not to use) type qualifiers + + +Version 0.9.3 (23 Jun 2009) +--------------------------- + +Functionality + Added support DefaultQualifier on packages + Added support for Dependent qualifier types + see checkers.quals.Dependent + Added an option to treat checker errors as warnings + Improved flow handling of boolean logic + +Manual Documentations + Improved installation instructions + Improved discussion of effective and implicit qualifiers and defaults + Added a discussion about the need for bottom qualifiers + Added sections for how-to + . suppress Basic Checker warnings + . troubleshoot skeleton files + + +Version 0.9.2 (2 Jun 2009) +-------------------------- + +Functionality + Added pre-liminary support for lazy initialization in nullness + see LazyNonNull + +Bug fixes + Corrected method declarations in JDK skeleton files + - bug resulted in a runtime error + +Documentations + Updated qualifier javadoc documentations + Corrected a reference on passing qualifiers to javac + + +Version 0.9.1 (19 May 2009) +--------------------------- + +Bug fixes + Eliminated unexpected compiler errors when using checkers + Fixed bug related to reading annotations in skeleton files + +API Changes + Renamed SourceChecker.process() to .typeProcess() + +Manual + Updated troubleshooting info + info for annotations in skeleton files + + +Version 0.9b, 22 Apr 2009 +------------------------- + +No visible changes + + +Version 0.9 (16 Apr 2009) +------------------------- + +Framework + More space and performance optimizations + Handle raw type with multiple type var level + e.g. class Pair { ... } + +Manual + Improve installation instructions + Update references to command line arguments + + +Version 0.8.9 (28 Mar 2009) +--------------------------- + +Framework + Introduce Space (and minor performance) optimizations + Type-check constructor invocation receiver type + Fixed bug related to try-catch flow sensitivity analysis + Fixed bugs when type-checking annotations and enums + - bug results in null-pointer exception + + +Version 0.8.8 (13 Mar 2009) +--------------------------- + +Nullness Checker + Support for custom nullness assertion via @AssertNonNull + Support for meta-annotation AssertNonNull + Support for Collection.toArray() method + Infer the nullness of the returned type + Corrected some JDK Collection API annotations + +Framework + Fixed bugs related to assignments expressions in Flow + Fixed bugs related to enum and annotation type hierarchy + Fixed bugs related to default annotations on wildcard bounds + + +Version 0.8.7 (27 Feb 2009) +--------------------------- + +Framework + Support annotations on type parameters + Fixed bugs related to polymorphic types/annotations + Fixed bugs related to stub fixes + +Manual + Specify annotation defaults settings for IGJ + Update Known Problems section + +Version 0.8.6 (3 Feb 2009) +-------------------------- + +Framework + Fixed bugs related to flow sensitivity analysis related to + . for loop and do while loops + . multiple iterations of a loop + . complement of logical conditions + Declarative syntax for string literal type introduction rules + Support for specifying stub file directories + + +Version 0.8.5 (17 Jan 2009) +--------------------------- + +Framework + Fixed bugs related to flow sensitivity analysis + Fixed bugs related to annotations on type parameters + + +Version 0.8.4 (17 Dec 2008) +--------------------------- + +Distribution + Included checkers-quals.jar which contains the qualifiers only + +Framework + Fixed bugs related to inner classes + Fixed a bug related to resolving polymorphic qualifiers + within static methods + +Manual + Added 'Distributing your annotated project' + + +Version 0.8.3 (7 Dec 2008) +-------------------------- + +Framework + Fixed bugs related to inner classes + Changed cast semantics + Unqualified casts don't change cast away (or in) any qualifiers + Refactored AnnotationBuilder to ease building annotations + Added support for Object += String new behavior + Added a type validation check for method return types + +Nullness + Added inference of field initialization + Suppress false warnings due to method invocations within constructors + +IGJ + Added proper support for AssignsFields and inner classes interactions + +Manual + Updated 'Known Problems' section + + +Version 0.8.2 (14 Nov 2008) +--------------------------- + +Framework + Included a binary distribution in the releases + Added support for annotations on type parameters + Fixed bugs related to casts + +Nullness + Improved error messages readability + Added partial support for Map.get() detection + +Manual + Improved installation instructions + + +Version 0.8.1 (1 Nov 2008) +-------------------------- + +Framework + Added support for array initializers + Fixed many bugs related to generics and generic type inference + +Documentations + Added 'Getting Started' guide + + +Version 0.8 (27 Sep 2008) +------------------------- + +Framework + Added support for newly specified array syntax + Refactored code for annotating supertypes + Fixed AnnotationBuilder AnnotationMirror string representation + Fixed AnnotatedTypeMirror hashCode + +Manual + Reorganized 'Annotating Libraries' section + + +Version 0.7.9 (19 Sep 2008) +--------------------------- + +Framework + Added support for stub files/classes + Fixed bugs related to anonymous classes + Fixed bugs related to qualifier polymorphism + +Manual + Updated 'Annotating Libraries' section to describe stub files + +Tests + Added support for Windows + Fixed a bug causing IGJ tests to fail on Windows + + +Version 0.7.8 (12 Sep 2008) +--------------------------- + +Framework + Improved support for anonymous classes + Included refactorings to ease extensibility + Fixed some minor bugs + +Nullness + Fix some errors in annotated JDK + + +Version 0.7.7 (29 Aug 2008) +--------------------------- + +Framework + Fixed bugs related to polymorphic qualifiers + Fixed bugs related to elements array convention + Add implicit type arguments to raw types + +Interning + Suppress cast warnings for interned classes + +Manual + Removed discussion of non-standard array syntax alternatives + + +Version 0.7.6 (12 Aug 2008) +--------------------------- + +Framework + Changed default array syntax to ARRAYS-PRE, per the JSR 308 specification + Added an optional check for qualifier unsafe casts + Added support for running multiple checkers at once + Fixed bugs related array syntax + Fixed bugs related to accessing outer classes with-in inner classes + +Manual + Added a new subsection about Checker Auto-Discovery + 2.2.1 Checker Auto-discovery + + +Version 0.7.5 (2 Aug 2008) +-------------------------- + +Framework + Added support for ARRAYS-PRE and ELTS-PRE array syntax + Added a check for unsafe casts + Some improvements to the AnnotationBuilder API + +Nullness Checker + Added a check for synchronized objects + Added a check for (un)boxing conversions + +Javari Checker + Fixed some JDK annotated classes + + +Version 0.7.4 (11 July 2008) +---------------------------- + +Framework + Added support for annotations found in classfiles + Added support for the ARRAY-IN array syntax + Added AnnotationBuilder, to create AnotationMirrors with values + Improved the readability of recursive types string representation + +Nullness Checker + Added a check for thrown Throwable nullability + +IGJ Checker + Treat enums as mutable by default, like regular classes + +Manual + Added a new subsection about array syntax proposals: + 2.1.2 Annotating Arrays + + +Version 0.7.3 ( 4 July 2008) +---------------------------- + +Javari Checker + Converted JDK files into stubs + +Nullness Checker + Fixed java.lang.Number declaration in the annotated jdk + +Framework + Fixed a bug causing crashes related to primitive type boxing + Renamed DAGQualifierHierarchy to GraphQualifierHierarchy + + +Version 0.7.2 (26 June 2008) +---------------------------- + +IGJ Checker + Supports flow-sensitive type refinement + +Framework + Renamed Default annotation to DefaultQualifier + Added DefaultQualifiers annotation + Fixed bugs related to flow-sensitive type refinement + Fixed an error in the build script in Windows + +Manual + Added a new section + 9.2 javac implementation survival guide + Added hyperlinks to Javadocs of the referenced classes + + +Version 0.7.1 (20 June 2008) +---------------------------- + +Nullness Checker + Made NNEL the default qualifier scheme + +Basic Checker + Moved to its own checkers.basic package + +Framework + Enhanced type-checking within qualifier-polymorphic method bodies + Fixed a bug causing StackOverflowError when type-checking wildcards + Fixed a bug causing a NullPointerException when type-checking + compound assignments, in the form of += + +Class Skeleton Generator + Distributed in compiled form (no more special installation instructions) + Added required asmx.jar library to lib/ + +Manual + Added new sections + 2.2.1 Ant tasks + 2.2.2 Eclipse plugin + 2.6 The effective qualifier on a type + Rewrote section 8 on annotating libraries + Added reference to the new Eclipse plug-in + Deleted installation instructions + +Javari Checker + Fixed bugs causing a NullPointerException when type-checking + primitive arrays + +IGJ Checker + Fixed bugs related to uses of raw types + +API Changes + Moved AnnotationFactory functionality to AnnotationUtils + Removed .root and .inConflict from DAGQualifierHierarchy + + +Version 0.7 (14 June 2008) +-------------------------- + +Installation + New, very simple installation instructions for Linux. For other + operating systems, you should continue to use the old instructions. + +Nullness Checker + Renamed from "NonNull Checker" to "Nullness Checker". + Renamed package from checkers.nonnull to checkers.nullness. + The annotation names remain the same. + Added PolyNull, a polymorphic type qualifier for nullness. + +Interning Checker + Renamed from "Interned Checker" to "Interning Checker". + Renamed package from checkers.interned to checkers.interning. + The annotation names remain the same. + Added PolyInterned, a polymorphic type qualifier for Interning. + Added support for @Default annotation. + +Framework + Qualifiers + @PolymorphicQualifier was not previously documented in the manual. + Moved meta-qualifiers from checkers.metaquals package to checkers.quals. + Removed @VariableQualifier and @RootQualifier meta-qualifiers. + Added BasicAnnotatedTypeFactory, a factory that handles implicitFor, + defaults, flow-sensitive type inference. + Deprecated GraphQualifierHierarchy; DAGQualifierHierarchy replaces it. + Renamed methods in QualifierHierarchy. + +Manual + Rewrote several manual sections, most notably: + 2.1.1 Writing annotations in comments for backward compatibility + (note new -Xspacesincomments argument to javac) + 2.3 Checking partially-annotated programs: handling unannotated code + 2.6 Default qualifier for unannotated types + 2.7 Implicitly refined types (flow-sensitive type qualifier inference) + 8 Annotating libraries + 9 How to create a new checker plugin + Javadoc for the Checker Framework is included in its distribution and is + available online at . + + +Version 0.6.4 (9 June 2008) +--------------------------- + +All Framework + Updated the distributed JDK and examples to the new location of qualifiers + +Javari Checker + Improved documentation on polymorphism resolution + Removed redundant code now added to the framework from JavariVisitor, + JavariChecker and JavariAnnotatedTypeFactory + Refactored method polymorphism into JavariAnnotatedTypeFactory + Fixed bug on obtaining type from NewClassTree, annotations at constructor + invocation are not ignored now + Refactored polymorphism resolution, now all annotations on parameters and + receivers are replaced, not only on the return type + Refactored and renamed internal annotator classes in + JavariAnnotatedTypeFactory + Added more constructor tests + Moved Javari annotations to checkers.javari.quals package + + +Version 0.6.3 (6 June 2008) +--------------------------- + +Checker Framework + Improved documentation and manual + Treat qualifiers on extends clauses of type variables and wildcard types as + if present on type variable itself + Renamed AnnotationRelations to QualifierHierarchy + Renamed GraphAnnotationRelations to GraphQualifierHierarchy + Renamed TypeRelations to TypeHierarchy + Added flow as a supported lint option for all checkers + Determined the suppress warning key reflectively + +Interned Checker + Moved @Interned annotation to checkers.interned.quals package + +NonNull Checker + Moved nonnull annotations to checkers.nonnull.quals package + +Miscellaneous + Included Javadocs in the release + Improved documentation for all checkers + + +Version 0.6.2 (30 May 2008) +--------------------------- + +Checker Framework API + Added support for @Default annotation via TreeAnnotator + Added support for PolymorphicQualifier meta-annotation + Disallow the use of @SupportedAnnotationTypes on checkers + Fixed bugs related to wildcards with super clauses + Improved flow-sensitive analysis for fields + +Javari Checker + Moved Javari qualifiers from checkers.quals to checkers.javari.quals + Fixed bugs causing null pointer exceptions + +NonNull Checker + Fixed bugs related to nonnull flow + Added new tests to test suite + +Basic Checker + Renamed Custom Checker to Basic Checker + + +Version 0.6.1 (26 Apr 2008) +--------------------------- + +Checker Framework API + Added support for @ImplicitFor meta-annotations via the new TypeAnnotator + and TreeAnnotator classes + Improved documentation and specifications + Fixed a bug related to getting supertypes of wildcards + Fixed a crash on class literals of primitive and array types + Framework ignores annotations that are not part of a type system + Fixed several minor bugs in the flow-sensitive inference implementation. + +IGJ Checker + Updated the checker to use AnnotationRelations and TypeRelations + +Javari Checker + Changing RoMaybe annotation to PolyRead + Updated checker to use AnnotationRelations and TypeRelations + Updated the JDK + Fixed bugs related to QReadOnly and type argument subtyping + Fixed bugs related to this-mutable fields in methods with @ReadOnly receiver + Fixed bugs related to primitive type casts + Added new tests to test suit + +NonNull Checker + Updated the annotated JDK + Fixed bugs in which default annotations were not correctly applied + Added @Raw types to handle partial object initialization. + Fixed several minor bugs in the checker implementation. + +Custom Checker + Updated checker to use hierarchy meta-annotations, via -Aquals argument + + +Version 0.6 (11 Apr 2008) +------------------------- + +Checker Framework API + Introduced AnnotationRelations and TypeRelations, more robust classes to + represent type and annotation hierarchies, and deprecated + SimpleSubtypeRelation + Add support for meta-annotations to declare type qualifiers subtype relations + Re-factored AnnotatedTypes and AnnotatedTypeFactory + Added a default implementation of SourceChecker.getSuppressWarningsKey() + that reads the @SuppressWarningsKey class annotation + Improved support for multidimensional arrays and new array expressions + Fixed a bug in which implicit annotations were not being applied to + parenthesized expressions + Framework ignores annotations on a type that do not have @TypeQualifier + Moved error/warning messages into "messages.properties" files in each + checker package + Fixed a bug in which annotations were inferred to liberally by + checkers.flow.Flow + +Interned Checker + Added heuristics that suppress warnings for certain comparisons (namely in + methods that override Comparator.compareTo and Object.equals) + The Interned checker uses flow-sensitive inference by default + +IGJ Checker + Fixed bugs related to resolving immutability variable in method invocation + Fixed a bug related to reassignability of fields + Add more tests + +Javari Checker + Added placeholder annotation for ThisMutable mutability + Re-factored JavariAnnotatedTypeFactory + Fixed self-type resolution for method receivers for readonly classes + Fixed annotations on parameters of readonly methods + Fixed type validation for arrays of primitives + Added more tests + Renamed @RoMaybe annotation to @PolyRead + +NonNull Checker + Removed deprecated checkers.nonnull.flow package + Fixed a bug in which default annotations were not applied correctly + +Miscellaneous + Improved Javadocs + Added FactoryTestChecker, a more modular tester for the annotated type + factory + Simplify error output for some types by stripping package names + + +Version 0.5.1 (21 Mar 2008) +--------------------------- + +Checker Framework API + Added support for conditional expression + Added checks for type validity and assignability + Added support for per-checker customization of asMemberOf + Added support for type parameters in method invocation, + including type inference + Enhanced performance of AnnotatedTypeFactory + Checkers run only when no errors are found by Javac + Fixed bugs related AnnotationUtils.deepCopy() + Fixed support for annotated class type parameters + Fixed some support for annotated type variable bounds + Added enhancements to flow-sensitive qualifier inference + Added checks for type parameter bounds + +Interned Checker + Fixed some failing test cases + Fixed a bug related to autoboxing/unboxing + Added experimental flow-sensitive qualifier inference (use + "-Alint=flow" to enable) + Improved subtype testing, removing some spurious errors + +IGJ Checker + Deleted IGJVisitor! + Fixed some bugs related to immutability type variable resolution + +Javari Checker + Removed redundant methods from JavariVisitor in the new framework + Added support to constructor receivers + Added support to parenthesized expressions + Fixed a bug related to resolving RoMaybe constructors + Fixed a bug related to parsing conditional expressions + Added parsing of parenthesized expressions + Replaced checkers.javari.VisitorState with + checkers.types.VisitorState, present in BaseTypeVisitor + Modified JavariVisitor type parameters (it now extends + BaseTypeVisitor, not BaseTypeVisitor) + Modified JavariAnnotatedTypeFactory TreePreAnnotator to mutate a + AnnotatedTypeMirror parameter instead of returning a + List, in accordance with other parts of the + framework design + Modified test output format + Added tests to test suite + +NonNull Checker + Fixed a bug related to errors produced on package declarations + Exception parameters are now treated as NonNull by default + Added better support for complex conditionals in NonNull-specific + flow-sensitive inference + Fixed some failing test cases + Improved subtype testing, removing some spurious errors + +Custom Checker + Added a new type-checker for type systems with no special semantics, for + which annotations can be provided via the command line + +Miscellaneous + Made corrections and added more links to Javadocs + A platform-independent binary version of the checkers and framework + (checkers.jar) is now included in this release + + +Version 0.5 (7 Mar 2008) +------------------------ + +Checker Framework API + Enhanced the supertype finder to take annotations on extends and + implements clauses of a class type + Fixed a bug related to checking an empty array initializer ("{}") + Fixed a bug related to missing type information when multiple + top-level classes are defined in a single file + Fixed infinite recursion when checking expressions like "Enum>" + Fixed a crash in checkers.flow.Flow related to multiple top-level + classes in a single file + Added better support for annotated wildcard type bounds + Added AnnotatedTypeFactory.annotateImplicit() methods to replace + overriding the getAnnotatedType() methods directly + Fixed a bug in which constructor arguments were not checked + +Interned Checker + Fixed a bug related to auto-unboxing of classes for primitives + Added checks for calling methods with an @Interned receiver + +IGJ Checker + Implemented the immutability inference for self-type (type of + 'this') properly + Enhanced the implicit annotations to make an un-annotated code + type-check + Fixed bugs related to invoking methods based on a method's receiver + annotations + +Javari Checker + Restored in this version, after porting to the new framework + +NonNull Checker + Fixed a bug in which primitive types were considered possibly null + Improvements to support for @Default annotations + +Miscellaneous + Improved error message display for all checkers + + +Version 0.4.1 (22 Feb 2008) +--------------------------- + +Checker Framework API + Introduced AnnotatedTypeFactory.directSupertypes() which finds the + supertypes as annotated types, which can be used by the framework. + Introduced default error messages analogous to javac's error messages. + Fixed bugs related to handling array access and enhanced-for-loop type + testing. + Fixed several bugs that are due AnnotationMirror not overriding .equals() + and .hashCode(). + Improved Javadocs for various classes and methods. + Fixed several bugs that caused crashes in the checkers. + Fixed a bug where varargs annotations were not handled correctly. + +IGJ Checker + Restored in this version, after porting the checker to the new framework. + +NonNull Checker + Fixed a bug where static field accesses were not handled correctly. + Improved error messages for the NonNull checker. + Added the NNEL (NonNull Except Locals) annotation default. + +Interned Checker + Fixed a bug where annotations on type parameter bounds were not handled + correctly. + Improved error messages for the Interned checker. + + +Version 0.4 (11 Feb 2008) +------------------------- + +Checker Framework API + Added checkers.flow, an improved and generalized flow-sensitive type + qualifier inference, and removed redundant parts from + checkers.nonnull.flow. + Fixed a bug that prevented AnnotatedTypeMirror.removeAnnotation from working + correctly. + Fixed incorrect behavior in checkers.util.SimpleSubtypeRelation. + +NonNull Checker + Adopted the new checkers.flow.Flow type qualifier inference. + Clarifications and improvements to Javadocs. + + +Version 0.3.99 (20 Nov 2007) +---------------------------- + +Checker Framework API + Deprecated AnnotatedClassType, AnnotatedMethodType, and AnnotationLocation + in favor of AnnotatedTypeMirror (a new representation of annotated types + based on the javax.lang.model.type hierarchy). + Added checkers.basetype, which provides simple assignment and + pseudo-assignment checking. + Deprecated checkers.subtype in favor of checkers.basetype. + Added options for debugging output from checkers: -Afilenames, -Ashowchecks + +Interned Checker + Adopted the new Checker Framework API. + Fixed a bug in which "new" expressions had an incorrect type. + +NonNull Checker + Adopted the new Checker Framework API. + +Javari Checker +IGJ Checker + Removed in this version, to be restored in a future version pending + completion of updates to these checkers with respect to the new framework + API. + + +Version 0.3 (1 Oct 2007) +------------------------ + +Miscellaneous Changes + Consolidated HTML documentation into a single user manual (see the "manual" + directory in the distribution). + +IGJ Checker + New features: + Added a test suite. + Added annotations (skeleton files) for parts of java.util and java.lang. + +NonNull Checker + New features: + @SuppressWarnings("nonnull") annotation suppresses checker warnings. + @Default annotation can make NonNull (not Nullable) the default. + Added annotations (skeleton classes) for parts of java.util and java.lang. + NonNull checker skips no classes by default (previously skipped JDK). + Improved error messages: checker reports expected and found types. + + Bug fixes: + Fixed a null-pointer exception when checking certain array accesses. + Improved checking for field dereferences. + +Interned Checker + New features: + @SuppressWarnings("interned") annotation suppresses checker warnings. + The checker warns when two @Interned objects are compared with .equals + + Bug fixes: + The checker honors @Interned annotations on method receivers. + java.lang.Class types are treated as @Interned. + +Checker Framework API + New features: + Added support for default annotations and warning suppression in checkers + + +Version 0.2.3 (30 Aug 2007) +--------------------------- + +IGJ Checker + New features: + changed @W(int) annotation to @I(String) to improve readability + improved readability of error messages + added a test for validity of types (testing @Mutable String) + + Bug fixes: + fixed resolving of @I on fields on receiver type + fixed assignment checking assignment validity for enhanced for loop + added check for constructor invocation parameters + +Interned Checker + added the Interned checker, for verifying the absence of equality testing + errors; see "interned-checker.html" for more information + +Javari Checker + New features: + added skeleton classes for parts of java.util and java.lang with Javari + annotations + + Bug fixes: + fixed readonly inner class bug on Javari Checker + +NonNull Checker + New features: + flow-sensitive analysis for assignments from a known @NonNull type (e.g., + when the right-hand of an assignment is @NonNull, the left-hand is + considered @NonNull from the assignment to the next possible + reassignment) + flow-sensitive analysis within conditional checks + + Bug fixes: + fixed several sources of null-pointer errors in the NonNull checker + fixed a bug in the flow-sensitive analysis when a variable was used on + both sides of the "=" operator + +Checker Framework API + New features: + added the TypesUtils.toString() method for pretty-printing annotated types + added AnnotationUtils, a utility class for working with annotations and + their values + added SourceChecker.getDefaultSkipPattern(), so that checkers can + individually specify which classes to skip by default + added preliminary support for suppressing checker warnings via + the @SuppressWarnings annotation + + Bug fixes: + fixed handling of annotations of field values + InternalAnnotation now correctly uses defaults for annotation values + improved support for annotations on class type parameter bounds + fixed an assertion violation when compiling certain uses of arrays + + +Version 0.2.2 (16 Aug 2007) +--------------------------- + + +Code Changes + +* checkers.igj + some bug fixes and improved documentation + +* checkers.javari + fixed standard return value to be @Mutable + fixed generic and array handling of @ReadOnly + fixed @RoMaybe resolution of receivers at method invocation + fixed parsing of parenthesized trees and conditional trees + added initial support for enhanced-for loop + fixed constructor behavior on @ReadOnly classes + added checks for annotations on primitive types inside arrays + +* checkers.nonnull + flow sensitive analysis supports System.exit, new class/array creation + +* checkers.subtype + fixes for method overriding and other generics-related bugs + +* checkers.types + added AnnotatedTypeMirror, a new representation for annotated types that + might be moved to the compiler in later version + added AnnotatedTypeScanner and AnnotatedTypeVisitor, visitors for types + AnnotatedTypeFactory uses GenericsUtils for improved handing of annotated + generic types + +* checkers.util + added AnnotatedTypes, a utility class for AnnotatedTypeMirror + added GenericsUtils, a utility class for working with generic types + +* tests + modified output to print only missing and unexpected diagnostics + added new test cases for the Javari Checker + + +Documentation Changes + +* checkers/igj-checker.html + improvements to page + +* checkers/javari-checker.html + examples now point to test suit files + +Miscellaneous Changes + +* checkers/build.xml + Ant script fails if it doesn't find the correct JSR 308 javac version + + +Version 0.2.1 (1 Aug 2007) +-------------------------- + + +Code Changes + +* checkers.igj & checkers.igj.quals + added an initial implementation for the IGJ language + +* checkers.javari + added a state parameter to the visitor methods + added tests and restructured the test suite + restructured and implemented RoMaybe + modified return type to be mutable by default + fixed mutability type handling for type casts and field access + fixed bug, ensuring no primitives can be ReadOnly + a method receiver type is now based on the correct annotation + fixed parameter type-checking for overridden methods + fixed bug on readonly field initialization + added handling for unary trees + +* checkers.nonnull + added a tests for the flow-senstive analysis and varargs methods + improved flow-sensitive analysis: else statements, asserts, + return/throw statements, instanceof checks, complex conditionals with && + fixed a bug in the flow-sensitive analysis that incorrectly inferred + @NonNull for some elements + removed NonnullAnnotatedClassType, moving its functionality into + NonnullAnnotatedTypeFactory + +* checkers.source + SourceChecker.getSupportedAnnotationTypes() returns ["*"], overriding + AbstractProcessor.getSupportedAnnotationTypes(). This enables all + checkers to run on unannotated code + +* checkers.subtypes + fixed a bug pertaining to method parameter checks for overriding methods + fixed a bug that caused crashes when checking varargs methods + +* checkers.types + AnnotatedTypeFactory.getClass(Element) and getMethod(Element) use the + tree of the passed Element if one exists + AnnotatedClassType.includeAt, .execludeAt, .getAnnotationData were + added and are public + added constructor() and skipParens() methods to InternalUtils + renamed getTypeArgumentLocations() to getAnnotatedTypeArgumentLocations() + in AnnotatedClassType + added AnnotationData to represent annotations instead of Class instances; + primarily allows querying annotation arguments + added switch for whether or not to use includes/excludes in + AnnotatedClassType.hasAnnotationAt() + +* checkers.util + added utility classes + added skeleton class generator utility for annotating external libraries + + +Documentation Changes + +* checkers/nonnull-checker.html + added a note about JML + added a caveat about variable initialization + +* checkers/README-checkers.html + improvements to instructions + + +Version 0.2 (2 Jul 2007) +------------------------ + + +Code Changes + +* checkers.subtype + subtype checker warns for annotated and redundant typecasts + SubtypeVisitor checks for invalid return and parameter types in overriding + methods + added checks for compound assignments (like '+=') + +* checkers.source + SourceChecker honors the "checkers.skipClasses" property as a regex for + suppressing warnings from unannotated code (property is "java.*" by + default) + SourceVisitor extends TreePathScanner instead of + TreeScanner + +* checkers.types + AnnotatedClassType.isAnnotatedWith removed + AnnotatedClassType.getInnerLocations renamed to getTypeArgumentLocations + AnnotatedClassType.include now removes from the exclude list (and + vice-versa) + AnnotatedClassType.setElement and setTree methods are now public + +* checkers.nonnull + added a flow-sensitive analysis for inferring @NonNull in "if (var != + null)"-style checks + added checks for prefix and postfix increment and decrement operations + +* checkers.javari + added initial implementation of a type-checker for the Javari language + + +Version 0.1.1 (7 Jun 2007) +-------------------------- + + +Documentation Changes + +* checkers/nonnull-checker.html + created "Tiny examples" subsection + created "Annotated library" subsection + noted where to read @NonNull-annotated source + moved instructions for unannotated code to README-checkers.html + various minor corrections and clarifications + +* checkers/README-checkers.html + added cross-references to other Checker Framework documents + removed redundant text + moved instructions for unannotated code from nonnull-checker.html + various minor corrections and clarifications + +* checkers/creating-a-checker.html + added note about getSupportedSourceVersion + removed line numbers from @Interned example + added section on SubtypeChecker/SubtypeVisitor + various minor corrections and clarifications + + +Code Changes + +* checkers.subtype + removed deprecated getCheckedAnnotation() mechanism + added missing package Javadocs + package Javadocs reference relevant HTML documentation + various improvements to Javadocs + SubtypeVisitor and SubtypeChecker are now abstract classes + updated with respect to preferred usages of + AnnotatedClassType.hasAnnotationAt and AnnotatedClassType.annotateAt + +* checkers.source + added missing package Javadocs + package Javadocs reference relevant HTML documentation + +* checkers.types + added missing package Javadocs + package Javadocs reference relevant HTML documentation + AnnotatedClassType.annotateAt now correctly handles + AnnotationLocation.RAW argument + AnnotatedClassType.annotate deprecated in favor of + AnnotatedClassType.annotateAt with AnnotationLocation.RAW as an argument + AnnotatedClassType.isAnnotatedWith deprecated in favor of + AnnotatedClassType.hasAnnotationAt with AnnotationLocation.RAW as an + argument + Added fromArray and fromList methods to AnnotationLocation and made + corresponding constructors private. + +* checkers.quals + added Javadocs and meta-annotations on annotation declarations where + missing + package Javadocs reference relevant HTML documentation + +* checkers.nonnull + various improvements to Javadocs + package Javadocs reference relevant HTML documentation + + +Miscellaneous Changes + + improved documentation of ch examples + Checker Framework build file now only attempts to compile .java files + + +Version 0.1.0 (1 May 2007) +-------------------------- + +Initial release. diff --git a/docs/checker-framework-quick-start.html b/docs/checker-framework-quick-start.html index 36ce0a508be1..6ff94aa403d6 100644 --- a/docs/checker-framework-quick-start.html +++ b/docs/checker-framework-quick-start.html @@ -15,12 +15,12 @@

      Checker Framework quick start guide

      A pluggable type-checker, or “checker” for short, prevents certain run-time errors. For example, it can prove that your code never suffers a -NullPointerException. Choose which checker you want to run from the list +NullPointerException. Choose which checker you want to run from the list of checkers.

      -To install the Checker Framework, download and unzip +To install the Checker Framework, download and unzip the Checker Framework distribution: checker-framework-2.1.7.zip.
      Or, the @@ -29,7 +29,7 @@

      Checker Framework quick start guide

      -To run a checker, supply the -processor command-line argument: +To run a checker, supply the -processor command-line argument:

      @@ -38,7 +38,7 @@ 

      Checker Framework quick start guide

      You write annotations in your code. Then, the checker -verifies +verifies two facts: (1) the annotations you wrote are correct (they are consistent with your source code), and (2) your program will not suffer certain exceptions or errors at run time.

      @@ -56,7 +56,7 @@

      Checker Framework quick start guide

      "", but does not include null.
      You write a type annotation right before a use of a type (on the same -line), as in: +line), as in:
         @NonNull String s;
         List<@Positive Integer> l;
      @@ -81,14 +81,14 @@ 

      Checker Framework quick start guide

      For the most part, you only need to write annotations on method signatures and fields. -Most annotations within method bodies are inferred for you automatically. +Most annotations within method bodies are inferred for you automatically. Furthermore, each checker applies certain defaults to further reduce your annotation burden; for example, using the -Nullness +Nullness Checker, you do not need to write @NonNull, only @Nullable which appears less often. See the manual for more -tips +tips about writing annotations.

      @@ -96,14 +96,17 @@

      Checker Framework quick start guide

      To learn more, you can read:

      + + diff --git a/docs/checker-framework-webpage.html b/docs/checker-framework-webpage.html index c30dcd088551..53e4b20bd0e7 100644 --- a/docs/checker-framework-webpage.html +++ b/docs/checker-framework-webpage.html @@ -27,12 +27,11 @@

      The Checker Framework

    6. - Source code repository (at GitHub): https://github.com/typetools/checker-framework/
      + Source code repository (at GitHub): https://github.com/eisop/checker-framework/
      The Checker Framework Manual contains instructions on building from source.
      - Also see the Developer manual. + Also see the Developer manual.
    7. Inference tools automatically add annotations to your code, making it even easier to start using the checkers. The Checker Framework manual contains a list of inference tools. +href="manual/#type-inference-tools">a list of inference tools.
    8. Optional related tools: @@ -94,15 +93,21 @@

      The Checker Framework

      the .class file. The tools support both Java 5 declaration annotations and Java 8 type annotations.
    9. +
    10. The Dataflow + Framework is an industrial-strength dataflow framework for + Java. The Dataflow Framework is used in the Checker Framework, Google’s + Error Prone, Uber’s NullAway, Meta’s Nullsafe, and in other contexts. + It is distributed with the Checker Framework. +
    11. @@ -161,7 +166,7 @@

      Documentation

      Javadoc API documentation
    12. - Changelog + Changelog
    13. @@ -171,7 +176,7 @@

      Bug reports

      If you encounter a problem, please submit a bug report so that we can fix it. To submit a bug report, read these -instructions, and then use the Checker Framework issue tracker. +instructions, and then use the Checker Framework issue tracker.

      @@ -224,7 +229,7 @@

      Mailing lists


      -Last updated: 1 Jul 2020 +Last updated: 1 Mar 2024

      @@ -254,5 +259,5 @@

      Mailing lists

      --> - diff --git a/docs/developer/README-eisop.md b/docs/developer/README-eisop.md new file mode 100644 index 000000000000..b379cc680627 --- /dev/null +++ b/docs/developer/README-eisop.md @@ -0,0 +1,51 @@ +# EISOP Development Notes + +## Updating from a different fork + +To update EISOP with changes in a different Checker Framework fork, follow these steps: + +1. Pull in eisop/master and make sure you don't have any uncommitted files. + +1. Create a new branch, named e.g. `typetools-3.18.0-fixes`, and push to eisop, without any changes. + +1. Create a new branch, named e.g. `typetools-3.18.0-merge`. + +1. If necessary, change to consistent formatting: + - Remove `.aosp()` from `build.gradle`. + - Run `./gradlew spotlessApply` and commit results as e.g. `Change to typetools formatting`. + +1. Look up the commit IDs for the range you want to include, e.g. previous and current releases. + +1. Fetch the new release into a different branch `git fetch typetools toID:typetools-3.18.0-release`. + +1. Do `git cherry-pick fromID..toID`. + +1. If there are conflicts, resolve and do `git cherry-pick --continue`. + +1. If necessary, undo formatting changes and commit `Change back to AOSP formatting`. + +1. Open a pull request (against eisop) merging `typetools-3.18.0-merge` into `typetools-3.18.0-fixes`. + Once this looks OK, squash and merge titled `typetools/checker-framework x.y.z release`, making + sure to keep all authors. + +1. Go through all changes in more detail and clean up any problems. + This two-step process gives us one commit with the external changes and separate commits with + eisop-specific changes and enhancements. + +1. Open a pull request (against eisop) merging `typetools-3.18.0-fixes` into `master` and + merge without squashing. + +## Release process + +TODO: the release process contains many buffalo-specific paths, which still needs to be cleaned up. +Most of the instructions can be followed, ignoring certain steps. + +Without using the release scripts, you can make a Maven Central release using: + +````bash +./gradlew publish -Prelease=true --no-parallel -Psigning.gnupg.keyName=wdietl@gmail.com +```` + +You may need to run `gpg-agent` first and enter the GPG password when prompted. + +Use `--warning-mode all` to see gradle deprecation warnings. diff --git a/docs/developer/developer-manual.html b/docs/developer/developer-manual.html index e16459b06376..eb5f82affdd6 100644 --- a/docs/developer/developer-manual.html +++ b/docs/developer/developer-manual.html @@ -11,8 +11,8 @@

      Checker Framework developer manual If you wish to use the Checker Framework, see its user manual - (HTML, - PDF). + (HTML, + PDF).

      @@ -23,13 +23,20 @@

      Checker Framework developer manualContents:

      -
    14. Testing optimizations
    15. Documenting refactoring ideas
    16. Version numbers for annotated libraries
    17. Making a Checker Framework release
    18. +
    19. Describing a case study
    20. +
    21. Counting annotations
    22. +
    23. Building a historical version of the Checker Framework
    24. @@ -48,7 +57,7 @@

      Checker Framework developer manualDirectory structure

      -The checker-framework +The checker-framework repository contains several related projects:

      @@ -72,17 +81,49 @@

      Directory structure

      docs
      documentation: manual, tutorial, examples, developer docs
      - -
      maven-artifacts
      -
      artifacts to be uploaded to Maven Central
      + + +

      + The checker-framework project + depends on + the eisop/jdk project, + the annotation-tools project, + and the stubparser project. +When making changes to one of these projects, you may also need to make changes to one or more of the others. +

      + +

      +If you make related changes in the checker-framework and jdk +repositories, use the same +branch name for each. The continuous integration framework will find +and use that branch when running tests. +For example, when continuous integration runs for branch B of fork F of checker-framework, it will use branch B of fork F of the other repositories (if they exist). +The same is true for the other projects. +

      + +

      +If a change spans multiple projects, make pull requests for all of them. +Each pull request description's should link to all the others. +

      + +

      +Furthermore, whenever you make a pull request from +a checker-framework branch A into branch B, if B has a +corresponding jdk +branch, then A also needs one. +A needs the branch even if A's branch is identical to B's; in that case, +beware of a bug in Azure Pipelines. +

      + +

      Build tasks

      Full instructions for building the Checker Framework from sources appear in -the Checker +the Checker Framework manual. This section describes the build system (the Gradle build tasks).

      @@ -99,11 +140,13 @@

      Build tasks

      Frequently-used tasks:

        -
      • assemble: builds all jars. +
      • assemble: builds all artifacts, including jars and Javadoc jars. +
      • assembleForJavac: builds only the jars required to use checker/bin/javac; faster than assemble.
      • build: assemble, plus runs all JUnit tests.
      • allTests: runs all tests. -
      • reformat: reformats Java files. -
      • NameOfJUnitTest: runs the JUnit test with that name; for example, NullnessFbcTest. +
      • spotlessApply: reformats Java files. +
      • spotlessCheck: checks formatting of Java files. +
      • NameOfJUnitTest: runs the JUnit test with that name; for example, NullnessTest.
      • task: lists tasks; use --all to see all tasks.
      @@ -121,27 +164,41 @@

      Build tasks

      Testing the Checker Framework

      -For writing new test cases, see file checker/tests/README. +For writing new test cases, see file checker/tests/README. +

      + + +

      Testing optimizations

      + +

      +To test an optimization that should speed up type-checking, see +the test-daikon.sh stage of the daikon_jdk* job +of the Azure Pipelines CI job. Compare the run time of this stage (or of +the entire daikon_jdk* job) between the master branch and a +branch with your improvements. +

      + +

      +You can also compare run times of the Checker Framework test suite.

      -

      Code style

      +

      Code style

      Code in this project follows the Google Java Style - Guide (except 4 spaces are used for indentation), + Guide, Michael -Ernst's coding style guidelines, and Oracle's +Ernst's coding style guidelines, and Oracle's Java code conventions.

      -From the command line, you can format your code by running ./gradlew reformat. +From the command line, you can format your code by running ./gradlew spotlessApply. You can configure -your IDE (Eclipse or IntelliJ) to use the formatting (don't forget the -non-standard -a flag). +your IDE (Eclipse or IntelliJ) to use the formatting.

      @@ -157,18 +214,35 @@

      Code style

      descriptive Javadoc comment.

      +

      +If a nested class has a very common and generic name, do +not import it. In code, write the outer and inner class name +(as in Tree.Kind or Map.Entry) rather than just +the simple name Kind or Entry. +

      + +

      +Use /*package-private*/ to explicitly mark package-private elements. +

      +

      IDE configuration

      First clone and build all projects from their sources, from the command line, using the instructions -at https://checkerframework.org/manual/#build-source. +at https://eisop.github.io/cf/manual/#build-source. After that succeeds, import the projects into your IDE as Gradle projects.

      -If your IDE cannot find com.sun.source.* packages, try changing the project JDK to JDK 11. +If your IDE cannot find com.sun.source.* packages, try changing the project JDK to JDK 11 or JDK 17. +

      + +

      +For VS Code and Eclipse, run ./gradlew eclipseClasspath before running +the IDE for the first time. +This ensures generated files are not put in the standard ../bin/ directories.

      @@ -198,7 +272,7 @@

      Pull requests

      If you make a user-visible change, update the manual (or add a new section) and add a brief description at the top of the changelog -(file changelog.txt). +(file docs/CHANGELOG.md).

      @@ -209,16 +283,10 @@

      Pull requests

      - It is good style to create a branch (in your fork of the Checker - Framework GitHub repository) for each independent change. Do not make - changes to your master branch. -

      - -

      -If you make related changes in the checker-framework -and checker-framework-inference repositories, use the same -branch name for each. The continuous integration framework will find -and use that branch when running tests. +A pull request marked as "draft" means it should not be reviewed. To use a +"draft" pull request for discussions, make it in a fork. If it were in +the eisop GitHub organization, it would use CI resources +and thus slow down CI feedback for other pull requests.

      @@ -228,6 +296,33 @@

      Pull requests

      +

      Branches

      + +

      + It is good style to create a branch (in your fork of the Checker + Framework GitHub repository) for each independent change. + Do not make changes to your master branch. + If you have write access to the eisop repository, don't + work in a branch of it, because such a branch competes for CI resources + with all pull requests. +

      + +

      + You may need to make changes to multiple repositories. + See "Related repositories" above. +

      + +

      +Azure Pipelines has a bug: whenever two CI jobs would run code with the same +commit hash, it re-uses a previous execution result. This is a bug because the +CI job's behavior may depend on the branch name and other factors that are +independent of the commit hash. This means that you may see spurious successes +or failures when your branch of (say) the jdk repository has the +identical commit hash to some other branch that Azure Pipelines previous ran a +job for. +

      + +

      Pull request and commit notes for maintainers

      @@ -265,14 +360,14 @@

      GitHub configuration

      Before you make any commits (even to your own fork), update your GitHub account profile so it contains your complete name. This is necessary to include you in -the list of +the list of contributors.

      You will want your own GitHub fork of any project you plan to modify. We recommend that, for each fork, you configure GitHub -to delete +to delete branches after they are merged.

      @@ -287,7 +382,7 @@

      Continuous Integration

      To enable Azure Pipelines continuous integration for your fork: - (This is a summary of the Azure + (This is a summary of the Azure Pipelines getting started directions.)

        @@ -313,7 +408,7 @@

        Continuous Integration

        To enable Travis CI continuous integration for your fork:

          -
        • Browse to travis-ci.com
        • +
        • Browse to travis-ci.com
        • Click the downarrow for the dropdown, near your face in the upper right corner
        • Click settings
        • Find "checker-framework" in the list, and click the slider to enable it.
        • @@ -334,28 +429,13 @@

          What to do if a Continuous Integration build fails

          -

          Testing optimizations

          - -

          -To test an optimization that should speed up type-checking, see -the test-daikon.sh stage of the daikon_jdk8 job -of the Azure Pipelines CI job. Compare the run time of this stage (or of -the entire daikon_jdk8 job) between the master branch and a -branch with your improvements. -

          - -

          -You can also compare run times of the Checker Framework test suite. -

          - -

          Documenting refactoring ideas

          -If you have an idea for a code improvement (such as a refactoring), please -document it. If it can be described concisely and is low priority, a TODO -comment in the code is more appropriate than an enhancement request in the -issue tracker. The code comment is more likely to be noticed by someone +Don't open issues for code improvement ideas (such as potential refactorings). +If it can be described concisely and is unlikely to be rediscovered by other +people, write a TODO comment in the code. The code comment is more likely to be +noticed by someone working with the code, and it is equally easy to search for. Furthermore, it doesn't clutter the issue tracker. Clutter in the issue tracker reduces morale, makes it harder to search, and makes the project appear @@ -368,7 +448,7 @@

          Version numbers for annotated librari

          We maintain annotated versions of some third-party libraries. The source code appears in a fork in - the GitHub typetools + the GitHub eisop organization. Binaries are hosted at Maven Central in the org.checkerframework.annotatedlib group. @@ -394,15 +474,132 @@

          Version numbers for annotated librari

          Making a Checker Framework release

          -See a separate document about the -Checker Framework release process. +See a separate document about the Checker Framework release process: + +local version (link works from a clone) or +web version (from previous release). +

          + + +

          Describing a case study

          + +

          + After you have performed a case study of applying some checker to an + open-source codebase, you should have: +

          + +
            +
          • + the unannotated code, as of the time/commit you started annotating it. + This might be in your forked repository, or in an upstream repository. +
          • +
          • + the annotated code. Typically, this is in a branch of your forked repository. + +

            + In the annotated code, each @SuppressWarnings annotation should have a brief justification, explaining why the code is correct and the warning is a false positive. + The justification should be a //-style comment, on the same line as the argument to @SuppressWarnings. For example: +

            + +
            +  @SuppressWarnings("nullness:assignment") // dynamic check: checked against null immediately above
            +
            +  @SuppressWarnings({
            +    "nullness:assignment" // dynamic check: checked against null immediately above
            +  })
            +
            + + If there are more than about 10 warning suppressions, prefix each one by a + category followed by a colon (as with "dynamic check:") above, to aid in + computing statistics about the causes of false positive warnings. + +
          • +
          • + the command to run the type-checker(s) on the annotated code. +
          • +
          • + a list of all the bugs you fixed, with a brief description of each. The + description can point to pull requests, or commits, or just be text; provide + whatever is most helpful. +
          • +
          + + +

          Counting annotations

          + +

          +After you have annotated a project, you may wish to count the annotations that you have written. +These programs will help you do that: +

          + + + + +

          Building a historical version of the Checker Framework

          + +

          +The use of four different repositories, as explained in +"Related repositories" above, means that you +cannot just check out an old version of the Checker Framework and build it +with ./gradlew assemble. By default, the Checker Framework build +uses the latest version of the other three repositories, which are rarely +compatible with an old version of the Checker Framework. One symptom of the +problem is a "Can't find stubparser jar" error message. +

          + +

          +To build an old version of the Checker Framework, you need to use old versions +of the other repositories as well. Clone them into siblings of +the checker-framework/ directory that holds your old version of the +Checker Framework. +

          + +

          +Here are general rules: +If the Checker Framework version is a commit on branch B with date D, then use +the commits on other repositories' branch B (or master, if branch B +does not exist) that precede D. (This isn't quite right, because the commits +may have been made earlier but all pushed at the same time, so you might need to +look at commits that are relatively soon after D.) To obtain these, view the +GitHub history and click on the pull request to see the commits that were merged +into it. You cannot obtain these from the Git history, because after a pull +request is merged, the commits in the pull request branch are squashed and the +branch is deleted. +

          + +

          + Here are special cases: +

          +
            +
          • + If you are building an old released version of the Checker Framework, + then annotation-tools should have a corresponding tag for the + release. +
          • +
          • + Recently, any Stubparser change that breaks building the Checker + Framework, should have a new version number. So you should check out + Stubparser to the commit that changed the version number (to what it is + in checker-framework/build.gradle). +
          • +
          + +

          + The script checker/bin-devel/checkout-historical.sh approximates + these rules. It is able to build the Checker Framework back through at least + mid-April 2019. It cannot build the Checker Framework as of mid-January 2019.

          - - diff --git a/docs/developer/gsoc-ideas-old-html b/docs/developer/gsoc-ideas-old-html deleted file mode 100644 index 165ae437face..000000000000 --- a/docs/developer/gsoc-ideas-old-html +++ /dev/null @@ -1,629 +0,0 @@ -

          Avoiding exponential blowup when processing DAGs

          - - - -

          -Google's Bazel open-source project is a -publicly-released version of their build system, Blaze. Blaze builds every -line of source code that is written by any Google programmer — all of -that source code appears in a single repository! Therefore, Bazel/Blaze needs to -be fast. Bazel represents all of the source code and its dependencies as a -large DAG (a -directed -acyclic graph). It needs to manipulate these DAGs efficiently. -

          - -

          -One of the biggest problems that the Bazel developers face is exponential -blowup of DAG sizes and therefore of run time. Periodically, one of the -Bazel developers makes such a mistake, and Bazel becomes unusable until -they can diagnose and fix the problem. -

          - -

          -Here are two different ways to view the problem. -

          - -
            -
          1. - In a DAG, multiple nodes may have the same child. Traversing the DAG - naively would visit the child multiple times — in the worst case, - exponentially many times. It is necessary to avoid doing so. -
          2. -
          3. - Bazel contains a function that takes a DAG as input and generates a DAG - as output (the output is an object graph). The Bazel developers want to - ensure that the size of the output DAG is O(|input DAG|). The input DAG is - processed bottom-up (it ensures that each input node is visited once) and - Bazel stores the results of intermediate computations that construct the - output DAG with nodes of the input DAG. The key thing the Bazel developers - want to avoid is copying intermediate subgraphs that have unbounded size. -
          4. -
          - -

          -More concretely, there is only one Java type for all DAGs, and there is a -method flatten(). It's a mistake to call -flatten() on certain DAGs, because doing so may cause -exponential blowup. -

          - - - -

          -The goal of this project would be to better understand the problem with -Bazel, to formalize it, and to create a program analysis that solves -the problem. You would evaluate your work by running it on the Bazel -codebase to discover latent problems, or by providing it to the Bazel -developers to run each time they propose a code change. The Bazel -team is interested in collaborating by evaluating a tool. -

          - - -

          Array indexing (index-out-of-bounds errors)

          - -

          -An index-out-of-bounds error occurs when a programmer provides an illegal -index to an array or list, as in a[i] or a.get(i) -where i is less than 0 or greater than the length of -a. In languages like C, this is disastrous: buffers -overflows lead to about 1/6 of all security vulnerabilities. In languages -like Java, the result is “merely” that the program crashes. In -both languages, it is desirable to prevent programmers from making this -error and to prevent users from suffering the bad consequences. -

          - -

          -This project will be a substantial case study with -the Index -Checker. The first goal is to identify its merits and limitations. -Does it scale up to big, -interesting programs? Are there common, important code patterns that it -fails to handle? Does it produce too many false positive warnings? Does -it place too heavy a burden on the user, either in terms of annotations or -in terms of complexity of the error messages? Worst of all, are there -unknown unsoundnesses in the tool? -The second goal is to improve its precision enough to make it usable by -real-world programmers. -

          - -

          -A related project is to extend -the Index Checker to handle -mutable collections such as -Lists, where the remove() method makes sound, -precise analysis very tricky. -

          - - -

          Bazel tool

          - - - -

          -This project is related to the -Bazel build system, and was -proposed by its development manager. -

          - -

          -The Bazel codebase contains 1586 occurrences of the @Nullable -annotation. This annotation indicates that a variable may hold a null -value. This is valuable documentation and helps programmers avoid null -pointer exceptions that would crash Bazel. However, these annotations are -not checked by any tool. Instead, programmers have to do their best to -obey the @Nullable specifications in the source code. This is -a lost opportunity, since documentation is most useful when it is -automatically processed and verified. (For several years, Google tried -using FindBugs, but they -eventually abandoned it: its analysis is too weak, suffering too many -false positives and false negatives.) -

          - -

          -Despite the programmers' best efforts, null pointer exceptions do still -creep into the code, impacting users. The Bazel developers would like to -prevent these. They want a guarantee, at compile time, that no null -pointer exceptions will occur at run time. -

          - -

          -Such a tool already exists: the -Nullness -Checker of the Checker -Framework. It runs as a compiler plug-in, and it issues a warning at -every possible null pointer dereference. If it issues no warnings, the -code is guaranteed not to throw a NullPointerException at run time. -

          - -

          -The goal of this project is to do a large-scale case study of the Nullness -Checker on Bazel. The main goal is to understand how the Nullness Checker -can be used on a large-scale industrial codebase. How many lurking bugs -does it find? What -@Nullable -annotations are missing from the codebase because the developers failed to -write them? What are its limitations, such as code patterns that it cannot -recognize as safe? (You might create new analyses and incorporating them -into the Nullness Checker, or you might just reporting bugs to the Nullness -Checker developers for fixing.) What burdens does it place on users? Is -the cost-benefit tradeoff worth the effort — that is, should Google -adopt this tool more broadly? How should it be improved? Are the most -needed improvements in the precision of the analysis, or in the UI of the -tooling? -

          - - -

          BCEL library

          - -

          - Annotate the BCEL library to express its contracts with respect to nullness. - Show that the BCEL library has no null pointer exceptions (or find bugs - in BCEL). There are - already some - annotations in BCEL, but they have not been verified as correct by - running the Nullness Checker on BCEL. (Currently, those annotations are - trusted when type-checking clients of BCEL.) -

          - -

          - To get started: -

          - -
            -
          • Fork https://github.com/typetools/commons-bcel.git
          • -
          • Clone your new fork
          • -
          • git checkout typecheck-nullness
          • -
          • mvn verify
          • -
          - -

          - Some challenging aspects of this case study are: -

          - -
            -
          • - There is some poor design that needs to be resolved in discussions with - the BCEL maintainers. For example, consider the copy() - method. Some implementations of copy() return null, but - are not documented to do so. In addition, some implementations - of copy() catch and ignore exceptions. I think it would - be nicest to change the methods to never return null, but to throw an - exception instead. (This is no more burdensome to users, who currently - have to check for null.) Alternately, the methods could all be - documented to return null. -
          • -
          - - - -

          Invent your own new type system

          - -

          -We also welcome your ideas for new type systems. For example, any run-time -failure can probably be prevented at compile time with the right -analysis. Can you come up with a way to fix your pet peeve? -

          - -

          -It is easiest, but not required, to choose an existing type system from the -literature, since that means you can skip the design stage and go right to -implementation. -

          - -

          -This task can be simple or very -challenging, depending on how ambitious the type system is. Remember to -focus on what helps a software developer most! -

          - - - -

          Bounded-size strings

          - - - -

          -Windows cannot run command lines longer than 8191 characters. Creating a -too-long command line causes failures when the program is run on Windows. -These failures are irritating when discovered during testing, and -embarrassing or worse when discovered during deployment. The same command -line would work on Unix, which has longer command-line limits, and as a -result developers may not realize that their change to a command can cause -such a problem. -

          - -

          -Programmers would like to enforce that they don't accidentally pass a -too-long string to the exec() routine. The goal of this -project is to give a compile-time tool that provides such a guarantee. -

          - -

          -Here are two possible solutions. -

          - -

          -Simple solution: -For each array and list, determine whether its length is known at compile -time. The routines that build a command line are only allowed to take such -constant-length lists, on the assumption that if the length is constant, -its concatenation is probably short enough. -

          - -

          -More complex solution: -For each String, have a compile-time estimate of its maximum length. Only -permit exec() to be called on strings whose estimate is no more than 8191. -String concatenation would return a string whose estimated size is the sum -of the maximums of its arguments, and likewise for concatenating an array -or list of strings. -

          - - -

          Lock ordering

          - -

          -The Lock -Checker prevents race conditions by ensuring that locks are held when -they need to be. It does not prevent deadlocks that can result from locks -being acquired in the wrong order. This project would extend the Lock -Checker to address deadlocks, or create a new checker to do so. -

          - -

          -Suppose that a program contains two different locks. Suppose that one -thread tries to acquire lockA then lockB, and another thread tries to -acquire lockB then lockA, and each thread acquires its first lock. Then -both locks will wait forever for the other lock to become available. The -program will not make any more progress and is said to -be deadlocked. -

          - -

          -If all threads acquire locks in the same order — in our example, say -lockA then lockB — then deadlocks do not happen. You will extend the -Lock Checker to verify this property. -

          - - -

          Upgrade to a newer version of ASM

          - -

          - The - Annotation - File Utilities, or AFU, insert annotations into, and extract - annotations from, .java files, .class files, - and text files. These programs were written before the - ASM bytecode library supported Java 8's - type annotations. Therefore, the AFU has its own custom version of ASM - that supports type annotations. Now that ASM 6 has been released and it - supports type annotations, the AFU needs to be slightly changed to use - the official ASM 6 library instead of its own custom ASM variant. -

          - -

          - This project is a good way to learn about .class files and - Java bytecodes: how they are stored, and how to manipulate them. -

          - -

          - (Kush Gupta is working on this project.) -

          - - -

          Stateful type systems

          - -

          -This project is to improve support for -typestate checking -

          - -

          -Ordinarily, a program variable has -the same type throughout its lifetime from when the variable is declared -until it goes out of scope. "Typestate" -permits the type of an object or variable to change in a controlled way. -Essentially, it is a combination of standard type systems with dataflow -analysis. For instance, a file object changes from unopened, to opened, to -closed; certain operations such as writing to the file are only permitted -when the file is in the opened typestate. Another way of saying this is -that write is permitted after open, but not after close. -Typestate -is applicable to many other types of software properties as well. -

          - -

          -Two typestate checking frameworks -exist for the Checker Framework. Neither is being maintained; a new one -needs to be written. -

          - - -

          Tool for analysis diffs

          - - -

          - Many program analyses are too verbose for a person to read their entire - output. However, after a program change, the analysis results may - change only slightly. An "analysis diff" tool could show the - difference between the analysis run on the old code and the analysis run - on the new code. -

          -
            -
          • - The analysis diffs may help the programmer to better understand the - changes. -
          • -
          • - Bug detection tools. such - as FindBugs or - the Checker Framework, have - extremely verbose output when first run on a program. Programmers - could examine and fix only the warnings about code they have changed - (and that they are currently thinking about). -
          • -
          • - Tools that always have large output, such as inference tools, could - become manageable to users if output is shown in small doses. -
          • -
          • - You can probably think of other uses. -
          • -
          - -

          - The analysis diff tool would take as input two analysis results (the - previous and the current one). It would output only the new parts of its - second input. (It could optionally output a complete diff between two - analysis results.) -

          - -

          - One challenge is dealing with changed line numbers and other analysis - output differences between runs. -

          - -

          -It would be nice to integrate the tool with git pre-commit hooks or GitHub -pull requests, to enable either of the following functionality (for either -commits to master or for pull requests): -

          -
            -
          • - Permit only those commits/pulls that do not add any new analysis warnings. -
          • -
          • - Permit only those commits/pulls that are "clean" — the analysis - issues no warnings for any changed line. -
          • -
          - -

          - A concrete example of an analysis diff tool - is checklink-persistent-errors; - see the documentation at the top of the file. That tool only works for - one particular analysis, the W3C Link Checker. - An analysis diff tool also appears to be built into FindBugs. - The goal of this project is to build a general-purpose tool that is easy - to apply to new analyses. -

          - - -

          Performance improvements

          - -

          - The Checker Framework runs much slower than the standard javac compiler - — often 20 times slower! This is not acceptable as part of a - developer's regular process, so we need to speed up the Checker - Framework. This project involves determining the cause of slowness in - the Checker Framework, and correcting those problems. -

          - -

          -This is a good way to learn about performance tuning for Java applications. -

          - -

          - Some concrete tasks include: -

          - -
            -
          • Profile the Checker Framework. Run a profiler such as - YourKit to determine - which parts of the Checker Framework consume the most CPU time and memory. -
          • - -
          • Perhaps compare a profile of the Checker Framework against a profile - of regular javac. This probably is not necessary because the Checker - Framework is so much slower than regular javac. -
          • - -
          • Consider interning string values. The Checker Framework does a fair - amount of string manipulation, in part because it reads from resources - such as stub files that do not produce Elements. Interning - could save time when doing comparisons. You can verify the correctness - of the optimization by running the - Interning - Checker on the Checker Framework code. Compare the run time of the - Checker Framework before and after this optimization. -
          • - -
          • Based on profiling results, devise other optimizations, implement - them, and evaluate them. -
          • -
          - - -

          Run-time checking

          - -

          -Implement run-time checking to complement compile-time checking. This will -let users combine the power of static checking with that of dynamic -checking. -

          - -

          -Every type system is too strict: it rejects some programs that never go -wrong at run time. A human must insert a type loophole to make such a -program type-check. For example, Java takes this approach with its -cast operation (and in some other places). -

          - -

          -When doing type-checking, it is desirable to automatically insert run-time -checks at each operation that the static checker was unable to verify. -(Again, Java takes exactly this approach.) This guards against mistakes by -the human who inserted the type loopholes. A nice property of this -approach is that it enables you to prevent errors in a program -with no type annotations: whenever the static checker is unable -to verify an operation, it would insert a dynamic check. Run-time checking -would also be useful in verifying whether the suppressed warnings are -correct — whether the programmer made a mistake when writing them. -

          - -

          -The annotation processor (the pluggable type-checker) should automatically -insert the checks, as part of the compilation process. -

          - -

          -There should be various modes for the run-time checks: -

          -
            -
          • fail immediately.
          • -
          • logging, to permit post-mortem debugging without crashing the program.
          • -
          - -

          -The run-time penalty should be small: a run-time check is necessary only -at the location of each cast or suppressed warning. Everywhere that the -compile-time checker reports no possible error, there is no need to insert a -check. But, it will be an interesting project to determine how to minimize -the run-time cost. -

          - -

          -Another interesting, and more challenging, design question is whether you need to add and maintain -a run-time representation of the property being tested. It's easy to test -whether a particular value is null, but how do you test whether it is -tainted, or should be treated as immutable? For a more concrete example, -see the discussion of the (not yet implemented) -[Javari run-time checker](http://pag.csail.mit.edu/pubs/ref-immutability-oopsla2005-abstract.html). -Adding this run-time support would be an interesting and challenging project. -

          - -

          -We developed a prototype for the -EnerJ runtime system. -That code could be used as starting point, or you could start afresh. -

          - -

          -In the short term, this could be prototyped as a source- or -bytecode-rewriting approach; but integrating it into the type checker is a -better long-term implementation strategy. -

          - - - -

          IDE and build system support

          - -

          -The Checker Framework comes with support for -external tools, -including both IDEs (such as an Eclipse plug-in) and build tools -(instructions for Maven, etc.). -

          - -

          - These plug-ins and other integration should be improved. - We have a number of concrete ideas, but you will also probably come up - with some after a few minutes of using the existing IDE plugins! -

          - -

          - This is only a task for someone who is already an expert, such as someone - who has built IDE plugins before or is very familiar with the build system. - One reason is that these tools tend to be complex, which can lead to - subtle problems. - Another reason is that we don't want to be stuck maintaining code written by - someone who is just learning how to write an IDE plugin. -

          - -

          - Rather than modifying the Checker Framework's existing support or - building new support from scratch, it may be better to adapt some other - project's support for build systems and IDEs. For instance, you might - make coala support the - Checker Framework, or you might adapt the tool integration provided - by Error Prone. -

          - - - - - -

          Model checking of a type system

          - -

          -Design and implement an algorithm to check type soundness of a type system -by exhaustively verifying the type checker on all programs up to a certain -size. The challenge lies in efficient enumeration of all programs and -avoiding redundant checks, and in knowing the expected outcome of the -tests. This approach is related to bounded exhaustive -testing and model checking; for a reference, see -[Efficient Software Model Checking of Soundness of Type Systems](http://www.eecs.umich.edu/~bchandra/publications/oopsla08.pdf). -

          - - -

          -A valuable project all by itself would be to compare heavy-weight and -light-weight type inference this whole-program inference vs. Checker -Framework Inference vs. Julia, to understand when each one is worth using. -

          diff --git a/docs/developer/gsoc-ideas.html b/docs/developer/gsoc-ideas.html index e0d1c158b93d..b1c8fb144956 100644 --- a/docs/developer/gsoc-ideas.html +++ b/docs/developer/gsoc-ideas.html @@ -1,1474 +1,17 @@ - Checker Framework organization: GSoC ideas 2020 + Projects for new contributors - - + + - -Checker Framework logo - -

          GSoC ideas 2020

          - -

          Contents:

          - - - - -

          Introduction

          - -

          - The Checker Framework is an - innovative programming tool that prevents bugs at development - time, before they escape to production. -

          - -

          - Java's type system prevents some bugs, such as int count = - "hello";. However, it does not prevent other bugs, such as null - pointer dereferences, concurrency errors, disclosure of private - information, incorrect internationalization, out-of-bounds indices, etc. - Pluggable type-checking replaces a - programming language's built-in type system with a more powerful, - expressive one. -

          - -

          - We have created around 20 - new type - systems, and other people have created - many - more. - The more powerful type system is not just a - bug-finding tool: it is a verification tool that gives a guarantee that - no errors (of certain types) exist in your program. Even though it is - powerful, it is easy to use. It follows the standard typing rules - that programmers already know, and it fits into their workflow. -

          - -

          - The Checker Framework is popular: it is used daily at Google, Amazon, - Uber, on Wall Street, and in other companies from big to small. It is - attractive to programmers who care about their craft and the quality of - their code. The Checker Framework is the motivation for Java's type - annotations feature. It has received multiple awards. - - With this widespread use, there is a need for people to help with the - project: everything from bug fixes, to new features, to case studies, to - integration with other tools. We welcome your contribution! -

          - -

          - Why should you join this project? It's popular, so you will have an - impact. It makes code more robust and secure, which is a socially - important purpose. Past GSOC students have had great success. - (David Lazar became a graduate student at MIT; multiple students - have published papers in scientific conferences, such - as Vlastimil - Dort at ISSTA 2018.) You will - get to scratch your own itch by creating tools that solve problems that - frustrate you. And, we have a lot of fun on this project! -

          - -

          - Prerequisites: You should be very comfortable with the Java - programming language and its type system. You should know how a type - system helps you and where it can hinder you. You should be willing to - dive into a moderately-sized codebase. - You should understand fundamental object-oriented programming concepts, - such as - behavioral - subtyping: subtyping theory - permits argument types to change contravariantly (even though Java forbids it - for reasons related to overloading), whereas return types may change - covariantly - both in theory and in Java. -

          - - -

          -Potential projects: -Most of this document lists potential summer projects. The projects are -grouped roughly from easiest to most challenging. Many of the projects are -applicable beyond Google Summer of Code. - -

          - - - -

          How to get started: do a case study

          - -

          - To get started, first do a case study of using the Checker - Framework: that is, run the Checker Framework on some program. - A case study gives you experience in using the Checker - Framework, and it may reveal bugs in either the Checker Framework or in - the program it is analyzing. -

          - -

          - Do the case study before submitting your proposal. - You might want to start with a small program such as from your - coursework, then repeat the process with an open-source program or library. -

          -
            -
          1. - Install - the Checker Framework. -
          2. - Review - the Checker Framework documentation. -
          3. - Choose an existing library or program to type-check. - Choose a program that is at least 1000 lines long. - The library or program should be under active maintenance; don't choose one - that has not had a commit in several years. - You will find the case study easier if you are already familiar with - the program, or if it is written in good style. -
          4. - Choose one type system, from - among those - distributed with the Checker Framework, that is appropriate for the - program. -
          5. - If the program is hosted on GitHub, fork it and create a branch for - your work. (Leave the master branch unchanged - from upstream.) -
          6. - Annotate the program, based on its documentation. -
            - Please do not make changes unrelated to annotating the - program, such as inserting/removing whitespace or sorting - the import statements. Doing so bloats the size of the - diffs and makes it hard to understand the essential changes. -
          7. - Change the build system so that building the annotated branch runs the type-checker. -
          8. - Run the type-checker. If it issues - warnings, correct them. - This might require adding more annotations, - fixing bugs in the program, or suppressing warnings. - Be sure that the program's test suite continues to pass. - Repeat until - the type-checker passes on the program. -
              -
            • Don't add an if statement that always succeeds, just - 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 - explain - why the checker warning is a false positive and you are certain - the code is safe. -
            -
          9. - Share it with us so that - we can evaluate it and give you feedback. - -

            - Share the case study as soon - as you finish it or as soon as you have a question that is not answered - in the manual; - don't wait until you submit your proposal. - The subject line should be descriptive (not just "Case study", but - "Nullness case study of Apache Commons Exec library"). - You should give us access to -

              -
            • the original (unannotated) version of the program, -
            • the annotated version of the program, and -
            • the exact command that runs the type-checker from the command - line. -
            - The best way to give all this information is a pointer to your GitHub - fork of the library. -
          - -

          -The primary result of your case study is that you will discover bugs in the -subject program, or you will verify that it has no bugs (of some particular -type). If you find bugs in open-source code, report them to the program's -maintainer, and let us know when they are resolved. -

          - -

          -Another outcome of your case study is that you may discover bugs, limitations, -or usability problems in the Checker Framework. Please -report them. -We'll try to fix them, or they might give you inspiration for -improvements you would like to make to the Checker Framework this summer. -

          - -

          -You can also try to fix problems yourself and submit a -pull - request, but that is not a requirement; most successful GSOC - applicants had not submitted a successful pull request before being selected. -(Some GSOC projects have a requirement to fix an issue in the issue tracker. -We do not, because it is unproductive. -Don't try to start fixing issues before you -understand the Checker Framework from the user point of view, which will -not happen until you have completed a case study on an open-source program.) -You may discuss your ideas with us by sending mail -to checker-framework-gsoc@googlegroups.com. -

          - -

          - Why should you start with a case study? - -Before you can contribute to any project, you must -understand the tool from a user point of view, including its strengths, -weaknesses, and how to use it. -A case study is the best way to -learn about the Checker Framework, determine whether you would enjoy -joining the project during the summer, and show your aptitude so that you -will be chosen for the summer. -

          - - -

          How to get help and ask questions

          - -

          -We are very happy to answer your questions, and we are eager to interact -with you! It's OK to have questions, and your questions can lead to -improvements in the documentation and the tool. -

          - -

          -Before you ask a question, read this file and the -"Troubleshooting" - section of the Checker Framework manual - (including "How - to report problems"), -and also search in the -Checker Framework manual -for the answer. -Don't send us a message -that says nothing but “please guide me” -or “tell me how to fix this bug”. Such a message disqualifies -you from participating in GSOC, because it shows that you do not read -instructions, and you haven't thought about the problem nor -tried to solve it. -

          - -

          -Your questions should show that you will be a productive colleague over the -summer: tell us what you have tried, tell us what went wrong or -where you got stuck, and ask a concrete technical question that will -help you get past your problem. If you can do that, then definitely ask -your question, because we don't want you to be stuck or frustrated. -

          - -

          -Whenever you send email (related to GSoC or not), -please use standard email etiquette, such as: avoid all-caps; use a -descriptive subject line; don't put multiple different topics in a single -email message; start a new thread with a new subject line -when you change the topic; don't clutter discussions with irrelevant -remarks; don't use screenshots (unless there is a problem with a GUI), but -instead cut-and-paste the output or code into your message; -if you are making a guess, clearly indicate that it is a guess and -your grounds for it. If you violate these basic rules, you will -demonstrate that you don't read instructions and you don't act -professionally. Bug -reports should be -complete -and should usually be -reported -to the issue tracker. -

          - - -

          Types of projects

          - -

          - Here are some possible focuses for a project: -

          -
            -
          • - Evaluate a recently-written type - system, or a feature used by multiple type systems. -
          • -
          • - Annotate a popular library, so that it - is easier to type-check clients of the library. -
          • -
          • - Create a new type system, to - prevent some Java programming error. -
          • -
          • - Enhance a type system or the Checker Framework - itself. -
          • -
          - -

          -This document gives a few suggestions in each category. -

          - - -

          How to apply

          - -

          - To apply, you will submit a single PDF through the Google Summer - of Code website. This PDF should contain two main parts. We suggest - that you number the parts and subparts to ensure that you don't forget anything, and - to ensure that we don't overlook anything when reading your application. You might find it - easiest to create multiple PDFs for the different parts, then concatenate - them before uploading to the website, but how you create your proposal is - entirely up to you. -

          - -
            -
          1. The proposal itself: what project you want to work on during the - summer. You might propose to do a project listed on this webpage, or - you might propose a different project. - -

            The proposal should have a descriptive title, both in the PDF and in - the GSoC submission system. Don't use a title like "Checker - Proposal" or "Proposal for GSoC". Don't distract from content with - gratuitous graphics. - -

            List the tasks or subparts that are required to complete your - project. This will help you discover a part that you had forgotten. - We do not require a detailed timeline, because you don't yet - know enough to create one. -

            - -

            - If you want to do a - case study, say what program you will do your case study on. - -

            If you want to create a new type system (whether one proposed on - this webpage or one of your own devising), then your proposal should include - the type system's user manual. You don't have to integrate it in the Checker - Framework repository (in other words, use any word processor or text - editor you want to create a PDF file you will submit), but you should describe - your proposed checker's parts - in precise English or simple formalisms, and you should follow the - suggested structure. - -

            - If you want to do exactly what is already listed on this page, then - just say that (but be specific about which one!), and it will not hurt - your chances of being selected. However, show us what progress you - have made so far. You might also give specific ideas - about extensions, about details that are not mentioned on this webpage, - about implementation strategies, and so forth. -

            - -

            Never literally cut-and-paste text that was not written by you, because - that would be plagiarism. If you quote from text written by someone - else, give proper credit. - Don't - submit a proposal that is just a rearrangement of text that already - appears on this page or in the Checker Framework manual, because it does - not help us to assess your likelihood of being successful. -

            - -
          2. - -
          3. Your qualifications. Please convince us that you - are likely to be successful in your proposed summer project. - -
              -
            1. A URL that points to a code sample. - Don't write any new code, but provide code you wrote in the - past, such as for a class assignment - or a project you have worked on outside class. - It does not need to have anything to do with - the Checker Framework project. It should be your own personal work. - The purpose is to assess your programming skills so we can assign you - to an appropriate project. - A common problem is to submit undocumented code; we expect every - programmer to write documentation when working on the Checker - Framework. - Don't put a lot of different files in Google Drive and share that - URL; it's better to upload a single .zip file or - provide a GitHub URL. -
            2. -
            3. - What you have done to prepare yourself - for working with the Checker Framework during the summer. - You may wish to structure this as a list. - Examples of items in the list include: -
                -
              • A URL for code you have annotated as a case study. Please indicate the - original unannotated code, the annotated code, and the exact command to - run the type-checker from the command line. Ensure that the GSoC - mentors can compile your code. - (It is acceptable to use the same code, or different code, for this - item and the code sample above.) -
              • -
              • URLs for bugs or pull requests that you have filed.
              • -
              • Information about other projects you have done, or classes you - have taken, that prepare you for your proposed summer task. This - is optional, because it might already appear in your resume.
              • -
              -
            4. -
            5. A resume. - A resume - contains a brief description of your skills and your job or project - experience. It will often list classes you have taken so far and your - GPA. It should not be longer than one page.
            6. -
            7. An unofficial transcript or grade report (don't spend - money for an official one).
            8. -
            -
          4. -
          - -

          -The best way to impress us is by doing a thoughtful job in the case -study. The case study is even more important than the proposal text, -because it shows us your abilities. -The case study may result in you submitting issues against the issue tracker of the -program you are annotating or of the Checker Framework. -Pull requests against our GitHub project are a plus but are not required: -good submitted bugs are just as valuable as bug fixes! -You can also make a good impression by correctly answering questions from -other students on the GSOC mailing list. -

          - -

          -Get feedback! Feel free to ask questions -to make your application more -competitive. We want you to succeed. Historically, students who start -early and get feedback are most successful. You can submit a draft -proposal via the Google Summer of Code website, and we will review it. We -do not receive any notification when you submit a draft -proposal, so if you want feedback, please tell us that. -Also, we can only see draft proposals; we cannot see final proposals until -after the application deadline has passed. -

          - - -

          Evaluate a type system or a Checker Framework feature

          - -

          - These projects evaluate a recently-written type system or a feature used - by multiple type systems. - Using the type systems on real code is our most important source of new ideas and improvements. - Many people have started out “just” doing a case - study but have ended up making deep, fundamental contributions and even - publishing scientific papers about their discoveries. -

          - -

          -One possible outcome is to identify - weaknesses in the type-checker so that we can improve it. Another - possible outcome is to provide evidence that the type-checker is - effective and convince more users to adopt it. You will probably - also discover defects (bugs) in the codebase being type-checked. -

          - - - -

          Signature strings

          - -

          -Determine whether the ASM library, or -some other library, properly handles signature strings. -

          - -

          - Some challenging aspects of this case study are: -

          -
            -
          • - Some libraries define their own new signature string formats (!), which - you need to define in the Signature String Checker. -
          • -
          • - Sometimes the library's documentation is incorrect, and in other cases the - string format is not defined. -
          • -
          - - -

          Signed and unsigned numbers

          - -

          - The Signedness - Checker ensures that you do not misuse unsigned values, such as - by mixing signed and unsigned values in a computation or by performing a - meaningless operation. -

          - -

          - Perform a case study of the Signedness Checker, in order to detect errors - or guarantee that code is correct. -

          - -

          - A good way to find projects that use unsigned arithmetic is to find a library that supports unsigned - arithmetic, then search on GitHub for projects that use that library. -

          - -

          - Here are some relevant libraries. -

          -
            -
          • In the JDK's Integer - and Long, these include - compareUnsigned, - divideUnsigned, - parseUnsignedInt, - remainderUnsigned, and - toUnsignedLong. -
            - Classes like DataInputStream, ObjectInputStream, - and RandomAccessFile have readUnsignedByte. -
            - Arrays has compareUnsigned. - The JDK is already annotated; search for @Unsigned within - https://github.com/typetools/jdk. -
          • -
          • - In Guava, see - its unsigned - support, such - as UnsignedBytes, - UnsignedLong, - UnsignedLongs, - etc. - Guava is already annotated; search for @Unsigned within - https://github.com/typetools/guava. -
          • -
          • The jOOU library consists of support for unsigned - integers.
          • -
          - -

          - Another possibility is to find Java projects that could use an - unsigned arithmetic library but do not. For - example, bc-java defines - its own unsigned libraries, and some other programs might do direct bit - manipulation. -

          - - - -

          - Your case studies will show the need for enhancements to the Signedness - Checker. For example, the Signedness Checker does not currently handle - boxed integers and BigInteger; these haven't yet come up in case studies - but could be worthwhile enhancements. You may also need to - write more annotations for libraries such as the JDK. -

          - - -

          Java's Optional class

          - -

          -Java 8 introduced the -Optional -class, a container that is either empty or contains a non-null value. -It is intended to solve the problem of null -pointer exceptions. However, Optional has its own problems. -

          - -

          -Because of Optional's problems, many commentators advise programmers to use -Optional only in limited ways. -

          - -

          -The goal of this project is to evaluate -the Optional - Checker, which warns programmers who -have misused Optional. -Another goal is to extend the Optional Checker to make it more precise or -to detect other misuses of Optional. -

          - - -

          Whole-program type inference

          - -

          -A type system is useful because it prevents certain errors. The downside -of a type system is the effort required to write the types. Type inference -is the process of writing the types for a program. -

          - -

          -The Checker Framework includes -a whole-program - inference that inserts type qualifiers in the user's program. -It works well on some programs, but needs more enhancements to work well on -all programs. -

          - - -

          Determinism

          - -

          -Programs are easier to use and debug if their output is deterministic. For -example, it is easier to test a deterministic program, because -nondeterminism can lead to flaky tests that sometimes succeed and sometimes -fail. As another example, it is easier for a user or programmer to compare -two deterministic executions than two nondeterministic executions. -

          - -

          - We have created a prototype Determinism Checker. It is documented in - this draft - manual chapter, and its implementation is in -t-rasmud/nondet-checker. -You will do a case study of this type system. -

          - - -

          Sound checking by default

          - -

          -By default, the Checker Framework is -unsound - in several -circumstances. -“Unsound” means that the Checker Framework -may report no warning even though the program can misbehave at run time. -

          - -

          -The reason that the Checker Framework is unsound is that we believe that -enabling these checks would cause too many false positive warnings: -warnings that the Checker Framework issues because it cannot prove that the -code is safe (even though a human can see that the code is safe). Having -too many false positive warnings would irritate users and lead them not to -use the checker at all, or would force them to simply disable those checks. -

          - -

          -We would like to do studies of these command-line options to see whether -our concern is justified. Is it prohibitive to enable sound checking? Or can we -think of enhancements that would let us turn on those checks that are -currently disabled by default? -

          - -

          -There is no need to annotate new code for this project. Just use existing -annotated codebases, such as those that are type-checked as part of the -Checker -Framework's Azure - Pipeline. In other words, you can start by enabling Azure -Pipelines for your fork and then changing the default behavior in a -branch. The Azure Pipelines job will show you what new warnings appear. -

          - - -

          Comparison to other tools

          - -

          - Many other tools exist for prevention of programming errors, such as - Error Prone, NullAway, FindBugs, JLint, PMD, and IDEs such as Eclipse and - IntelliJ. These tools - are not as powerful as the Checker Framework (some are bug finders rather - than verification tools, and some perform a shallower analysis), but they - may be easier to use. - Programmers who use these tools wonder, "Is it worth my time to switch to - using the Checker Framework?" -

          - -

          - The goal of this project is to perform a head-to-head comparison of as - many different tools as possible. You will quantify: -

          - -
            -
          • the number of annotations that need to be written
          • -
          • the number of bugs detected
          • -
          • the number of bugs missed
          • -
          • the number of false positive warnings
          • -
          - -

          - This project will help programmers to choose among the different tools - — it will show when a programmer should or should not use the - Checker Framework. - This project will also indicate how each tool should be improved. -

          - -

          - One place to start would be with an old version of a program that is - known to contain bugs. Or, start with the latest version of the program - and re-introduce fixed bugs. (Either of these is more realistic than - introducing artificial bugs into the program.) A possibility would be to - use the Lookup program that has been used in previous case studies. -

          - - -

          Android support annotations

          - -

          -Android uses its own annotations that are similar to some in the Checker -Framework. Examples include the -Android - Studio support annotations, - including @NonNull, @IntRange, @IntDef, - and others. -

          - -

          -The goal of this project is to implement support for these annotations. -That is probably as simple as creating aliased annotations -by calling method addAliasedAnnotation() -in AnnotatedTypeFactory. -

          - -

          - Then, do a case study to show the utility (or not) of - pluggable type-checking, by comparison with how Android Studio currently - checks the annotations. -

          - - -

          Annotate a library

          - -

          - These projects annotate a library, so that it is easier to - type-check clients of the library. Another benefit is that this may find - bugs in the library. It can also give evidence for the usefulness of - pluggable type-checking, or point out ways to improve the Checker - Framework. -

          - - -

          -When type-checking a method call, the Checker Framework uses the method -declaration's annotations. -This means that in order to type-check code that uses a library, the -Checker Framework needs an annotated version of the library. -

          +

          Projects for new contributors

          -The Checker Framework comes with a -few annotated -libraries. Increasing this number will make the Checker Framework even -more useful, and easier to use. +Redirecting to https://eisop.github.io/cf/manual/new-contributor-projects.html.

          -

          -After you have chosen a library, -fork the library's source code, adjust -its build -system to run the Checker Framework, and add annotations to it until -the type-checker issues no warnings. -

          - -

          -Before you get started, be sure to read -How -to get started annotating legacy code. More generally, read the -relevant -sections of the Checker Framework manual. -

          - - -

          Choosing a library to annotate

          - -

          -There are several ways to choose a library to annotate: -

          -
            -
          • -The best way to choose a library is to try to annotate a program and notice -that library annotations are needed in order to type-check the program. -
          • -
          • -Alternately, you can - choose a popular - Java library. -
          • -
          - -

          - When annotating a library, it is important to type-check both the library - and at least one client that uses it. Type-checking the client will - ensure that the library annotations are accurate. -

          - -

          - Whatever library you choose, you will need to deeply understand its - source code. You will find it easier to work with a library that is - well-designed and well-documented. -

          - -

          - You should choose a library that is - not already - annotated. There are two exceptions to this. -

          -
            -
          • - A library might be annotated for one type system, but you add - annotations for a different type system. One advantage of this is that - the library's build system is already set up to run the Checker - Framework. You can tell which type systems a library is annotated for - by examining its source code. -
          • -
          • - A library might be annotated, but the annotations have not been - verified by running the type-checker on the library source code. You - would verify that the annotations in the library are correct. -
          • -
          - - -

          Guava library

          - -

          - Guava is already partially annotated with nullness annotations — in - part by Guava's developers, and in part by the Checker Framework team. - However, Guava does not yet type-check without errors. Doing so could - find more errors (the Checker Framework has found nullness and indexing - errors in Guava in the past) and would be a good case study to learn the - limitations of the Nullness Checker. -

          - - -

          Create a new type system

          - -

          -The Checker Framework is shipped with about 20 type-checkers. Users can -create a - new checker of their own. However, some users don't want to go to -that trouble. They would like to have more type-checkers packaged with the -Checker Framework for easy use. -

          - -

          -Each of these projects requires you to design a new type system, -implement it, and perform case studies to demonstrate that it is both -usable and effective in finding/preventing bugs. -

          - - -

          Non-Empty Checker for precise handling of Queue.peek() and poll()

          - -

          -The Nullness Checker issues a false positive warning for this code: -

          - -
          -import java.util.PriorityQueue;
          -import org.checkerframework.checker.nullness.qual.NonNull;
          -
          -public class MyClass {
          -    public static void usePriorityQueue(PriorityQueue<@NonNull Object> active) {
          -        while (!(active.isEmpty())) {
          -            @NonNull Object queueMinPathNode = active.peek();
          -        }
          -    }
          -}
          -
          - -

          -The Checker Framework does not determine that active.peek() returns a non-null value in this context. -

          - -

          -The contract of peek() is that it returns a non-null value if the queue is not empty and the queue contains no null values. -

          - -

          -To handle this code precisely, the Nullness Checker needs to know, for each queue, whether it is empty. -This is analogous to how the Nullness Checker tracks whether a particular -value is a key in a map. -

          - -

          -It should be handled the same way: by adding a new subchecker, called the - Nonempty Checker, to the Nullness Checker. Its types are: -

          -
            -
          • @UnknownNonEmpty — the queue might or might not be empty -
          • @NonEmpty — the queue is definitely non-empty -
          - -

          -There is a start at this type-checker in branch nonempty-checker. It: -

          -
            -
          • defines the annotations -
          • -
          • creates the integration into the Nullness Checker -
          • -
          -

          -However, it is not done. (In fact, it doesn't even compile.) -For information about what needs to be done, see issue #399. -

          - -

          -When you are done, the Nullness Checker should issue only the // :: diagnostics from checker/tests/nullness/IsEmptyPoll.java — no more and no fewer. -You can test that by running the Nullness Checker on the file, and when you are done you should delete the // @skip-test line so that the file is run as part of the Checker Framework test suite. -

          - - -

          Custom tainting checking

          - -

          -The Checker Framework comes with -a Tainting - Checker that is so general that it is not good for much of anything. -In order to be useful in a particular domain, a user must customize it: -

          -
            -
          • - rename the @Tainted and @Untainted qualifiers - to something more specific (such as @Private - or @PaymentDetails or @HtmlQuoted), and -
          • - annotate libraries. -
          - -

          -The first part of this project is to make this customization easier to do -— preferably, a user will not have to change any code in the Checker -Framework (the -Subtyping -Checker already works this way). -As part of making customization easier, a user should be able to specify -multiple levels of taint — many information classification hierarchies -have more than two levels (for example, the US government separates classified -information into three categories: Confidential, Secret, and Top Secret). -

          - -

          -The second part of this project is to provide several examples, and do case -studies showing the utility of compile-time taint checking. -

          - -

          - Possible examples include: -

          - - -

          -For some microbenchmarks, see the Juliette test suite for Java from CWE. -

          - - - - -

          Overflow checking

          - -

          -Overflow is when 32-bit arithmetic differs from ideal arithmetic. For -example, in Java the int computation 2,147,483,647 + 1 yields -a negative number, -2,147,483,648. The goal of this project is to detect -and prevent problems such as these. -

          - -

          -One way to write this is as an extension of the Constant Value Checker, -which already keeps track of integer ranges. It even already -checks - for overflow, but it never issues a warning when it discovers - possible overflow. Your variant would do so. -

          - -

          -This problem is so challenging that there has been almost no previous -research on static approaches to the problem. (Two relevant papers are -IntScope: -Automatically Detecting Integer Overflow Vulnerability in x86 Binary Using -Symbolic Execution and -Integer Overflow -Vulnerabilities Detection in Software Binary Code.) Researchers are -concerned that users will have to write a lot of annotations indicating the -possible ranges of variables, and that even so there will be a lot of false -positive warnings due to approximations in the conservative analysis. -For example, will every loop that contains i++ cause a warning that i might overflow? -That would not be acceptable: users would just disable the check. -

          - -

          -You can convince yourself of the difficulty by manually analyzing programs -to see how clever the analysis has to be, or manually simulating your -proposed analysis on a selection of real-world code to learn its -weaknesses. You might also try it -on good - and bad binary search code. -

          - -

          -One way to make the problem tractable is to limit its scope: instead of -being concerned with all possible arithmetic overflow, focus on a specific -use case. -As one concrete application, -the Index -Checker is currently unsound in the presence of integer overflow. If -an integer i is known to be @Positive, and 1 is -added to it, then the Index Checker believes that its type -remains @Positive. If i was -already Integer.MAX_VALUE, then the result is negative — -that is, the Index Checker's approximation to it is unsound. -

          - -

          -This project involves removing this unsoundness by implementing a type system to track when an -integer value might overflow — but this only matters for values that -are used as an array index. -That is, checking can be restricted to computations that involve an operand -of type @IntRange). -Implementing such an analysis would permit the Index Checker -to extend its guarantees even to programs that might overflow. -

          - -

          -This analysis is important for some indexing bugs in practice. -Using the Index Checker, we found 5 bugs in Google -Guava related to overflow. Google marked these as high priority and -fixed them immediately. In practice, there would be a run-time exception -only for an array of size approximately Integer.MAX_INT. -

          - -

          -You could write an extension of the Constant Value Checker, which already -keeps track of integer ranges and -even determines -when overflow is possible. It doesn't issue a warning, but your -checker could record whether overflow was possible (this could be a -two-element type system) and then issue a warning, if the value is used as -an array index. -Other implementation strategies may be possible. -

          - -

          -Here are some ideas for how to avoid the specific problem -of issuing a warning about potential overflow for every i++ in -a loop (but maybe other approaches are possible): -

          -
            -
          • - The loop checks whether i == Integer.MAX_VALUE before - incrementing. This wide-scale, disruptive code change is not - acceptable. -
          • -
          • - Make the default array size (the length of an unannotated array) be - @ArrayLenRange(0, Integer.MAX_VALUE-1) rather - than @UnknownVal, which is equivalent - to @ArrayLenRange(0, Integer.MAX_VALUE-1). Now, every - array construction requires the client to establish that the length is - not Integer.MAX_VALUE. I don't have a feel for whether - this would be unduly burdensome to users. -
          • -
          - - -

          Index checking for mutable length data structures

          - -

          -The Index -Checker is currently restricted to fixed-size data structures. A -fixed-size data structure is one whose length cannot be changed once it is -created, such as arrays and Strings. This limitation prevents the Index -Checker from verifying indexing operations on mutable-size data structures, -like Lists, that have add or remove -methods. Since these kind of collections are common in practice, this is a -severe limitation for the Index Checker. -

          - -

          -The limitation is caused by the Index Checker's use of types that are dependent on the length of data structures, -like @LTLengthOf("data_structure"). If data_structure's length could change, -then the correctness of this type might change. -

          - -

          -A naive solution would be to invalidate these types any time a method is called on data_structure. -Unfortunately, aliasing makes this still unsound. Even more, a great solution to this problem would keep -the information in the type when a method like add or remove is called on data_structure. -A more complete solution might involve some special annotations on List that permit the information to be persisted. -

          - -

          -This project would involve designing and implementing a solution to this problem. -

          - - -

          Nullness bug detector

          - -

          -Verifying a program to be free of errors can be a daunting task. When -starting out, a user may be more interested in -bug-finding -than verification. The goal of this project is to create a nullness bug -detector that uses the powerful analysis of the Checker Framework and its -Nullness Checker, but omits some of its more confusing or expensive -features. The goal is to create a fast, easy-to-use bug detector. It -would enable users to start small and advance to full verification in the -future, rather than having to start out doing full verification. -

          - -

          -This could be structured as a new NullnessLight Checker, or as a -command-line argument to the current Nullness Checker. Here are some -differences from the real Nullness checker: -

          -
            -
          • No initialization analysis; the checker assumes that every value is - initialized.
          • -
          • No map key analysis; assume that, at every call to - Map.get, the given key appears in the map.
          • -
          • No invalidation of dataflow facts. Assume all method calls are pure, - so method calls do not invalidate dataflow facts. Assume there is no - aliasing, so field updates do not invalidate dataflow facts. -
          • -
          • Assume that boxing of primitives is @Pure: it returns - the same value on every call.
          • -
          • If the Checker Framework cannot infer a type argument, assume that - the type argument is @NonNull.
          • -
          -

          - Each of these behaviors should be controlled by its own command-line - argument, as well as being enabled in the NullnessLight Checker. -

          - -

          - The implementation may be relatively straightforward, since in most cases - the behavior is just to disable some functionality of existing checkers. -

          - -

          Tools such as FindBugs, NullAway, NullnessLight, and the Nullness - Checker form a spectrum from easy-to-use bug detectors to sound - verification. NullnessLight represents a new point in the design space. - It will be interesting to compare these checkers: -

          - -
            -
          • How much easier is it to use? For example, how many fewer - annotations need to be written?
          • -
          • - How many more fewer true positives does it report — in other - words, how many more false negatives does it suffer? -
          • -
          • - How many fewer false positives does it report? -
          • -
          - -

          - Uber's NullAway tool is also - an implementation of this idea (that is, a fast, but incomplete and - unsound, nullness checker). NullAway doesn't let the user specify Java - Generics: it assumes that every type parameter is @NonNull. - Does Uber's tool provide users a good - introduction to the ideas that a user can use to transition to a nullness - type system later? -

          - - - -

          Enhance the toolset

          - - -

          Improving error messages

          - -

          -Compiler writers have come to realize that clarity of error -messages is as important as the speed of the executable -(1, 2, -3, -4). This is especially true when the language or type system has rich features. -

          - -

          -The goal of this project is to improve a compiler's error messages. Here are -some distinct challenges: -

          -
            -
          • - Some type errors can be more concisely or clearly expressed than the - standard "found type A, expected type B" message. -
          • -
          • - Some types are complex. The error message could explain them, or link - to the manual, or give suggested fixes. -
          • -
          • - Compiler messages currently show - the effective - type, which may be different than what the user wrote due to - defaulting, inference, and syntactic sugar. For example, a user-written - @IndexFor("a") annotation is syntactic sugar for - @NonNegative @LTLengthOf("a"), and those types are - the ones that currently appear in error messages. - It might be good to show simpler types or ones that the user wrote. -
          • -
          • - Some checkers combine multiple cooperating type systems; - the Nullness - Checker and - the Index - Checker are examples. If there is a problem with a variable's - lower bound type, then its upper bound type should not be shown in the - error message. This will make the message shorter and more specific, - and avoid distracting the user with irrelevant information. -
          • -
          • - When a checker has multiple type systems, a type error or the lack of one may depend on facts from multiple type systems, and this should be expressed to the user. -
          • -
          - - -

          Flow expression parser

          - -

          -A number of type annotations take, as an -argument, a -Java expression. The representation for these -(the FlowExpressions.Receiver -class) is a hack. The goal of this -project is to remove it. -

          - -

          -The FlowExpressions.Receiver class is about 1000 lines of code -that represent an AST. There is no need for the Checker Framework to -define its own AST when the JavaParser AST already exists and is -maintained. In fact, FlowExpressionParseUtil uses JavaParser, -but needlessly converts a -JavaParser Expression into -a FlowExpressions.Receiver. -

          - -

          - The goals for the project include: -

          - - -

          -Direct replacement of the classes is not possible, or we would have done it -already. For example, Receiver contains some methods that -JavaParser lacks, such as isUnassignableByOtherCode. As a -first step before doing the tasks listed above, you may want to convert -these methods from instance methods of Receiver into static -methods in FlowExpressions, making Receiver more -like a standard AST that can be replaced by JavaParser classes. -You also need to decide how to store the type field -of Receiver, when Receiver is eliminated. -An -alternate design (or a partial step in the refactoring process) would be to -retain the Receiver class, but make it a thin wrapper around -JavaParser classes that do most of the real work. -

          - -

          -Another aspect of this project is -fixing the - issues that are labeled "JavaExpression". -

          - - -

          Dataflow enhancements

          - -

          -The Checker -Framework's dataflow - framework -(manual - here) implements flow-sensitive type refinement (local type -inference) and other features. It is used in the Checker -Framework and also in Error Prone, -NullAway, and elsewhere. -

          - -

          -There are a number -of open -issues — both bugs and feature requests — related to the -dataflow framework. The goal of this project is to address as many of -those issues as possible, which will directly improve all the tools that -use it. -

          - - -

          Purity (side effect) analysis

          - -

          -A program analysis technique makes estimates about the current values of -expressions. When a method call occurs, the analysis has to throw away -most of its estimates, because the method call might change any variable. -If the method is known to have no side effects, then the analysis doesn't -need to throw away its estimates, and the analysis is more precise. -

          - -

          -For example, the Checker Framework unsoundly trusts but does not check -purity annotations. This makes -the system vulnerable to programmer mistakes when writing annotations. The -Checker Framework contains a sound checker for immutability annotations, -but it suffers too many false positive warnings and thus is not usable. A -better checker is necessary. It will also incorporate aspects of an escape -analysis. -

          - -

          -Choosing an algorithm from the literature is the best choice, but there -still might be research work to do: in the past, when implementing -algorithms from research papers, we have sometimes found that they did not -work as well as claimed, and we have had to enhance them. One challenge is -that any technique used by pluggable type-checking to verify immutability -must be modular, but many side effect analyses require examining the whole -program. The system should require few or no method annotations within -method bodies. I'm not sure whether such a system already exists or we -need to design a new one. -

          - -

          -Perhaps one of these existing side effect analyses could be used: -Soot, -Geffken. -

          - - - - -

          Javadoc support

          - -

          -Currently, type annotations are only displayed in Javadoc if they are -explicitly written by the programmer. However, the Checker Framework -provides flexible defaulting mechanisms, reducing the annotation overhead. -This project will integrate the Checker Framework defaulting phase with -Javadoc, showing the signatures after applying defaulting rules. -

          - -

          -There are other type-annotation-related improvements to Javadoc that can be -explored, e.g. using JavaScript to show or hide only the type annotations -currently of interest. -

          - - - - - - - - - - - - - - - - - - - diff --git a/docs/developer/new-contributor-projects.html b/docs/developer/new-contributor-projects.html new file mode 100644 index 000000000000..a208634ade25 --- /dev/null +++ b/docs/developer/new-contributor-projects.html @@ -0,0 +1,1712 @@ + + + + Projects for new contributors + + + + + + + +Checker Framework logo + +

          Projects for new contributors

          + +

          Contents:

          + + + + +

          Introduction

          + +

          + The Checker Framework is an + innovative programming tool that prevents bugs at development + time, before they escape to production. +

          + +

          + Java's type system prevents some bugs, such as int count = + "hello";. However, it does not prevent other bugs, such as null + pointer dereferences, concurrency errors, disclosure of private + information, incorrect internationalization, out-of-bounds indices, etc. + Pluggable type-checking replaces a + programming language's built-in type system with a more powerful, + expressive one. +

          + +

          + We have created over 20 + new type + systems, and other people have created + over 30 + more. + A type system is not just a + bug-finding tool: it is a verification tool that gives a guarantee that + no errors (of certain types) exist in your program. Even though it is + powerful, it is easy to use. It follows the standard typing rules + that programmers already know, and it fits into their workflow. +

          + +

          + The Checker Framework is popular: it is used daily at Amazon, Google, Meta, + Oracle, Uber, on Wall Street, and in other companies from big to small. It is + attractive to programmers who care about their craft and the quality of + their code. The Checker Framework is the motivation for Java's type + annotations feature. It has received multiple awards. + + With this widespread use, there is a need for people to help with the + project: everything from bug fixes, to new features, to case studies, to + integration with other tools. We welcome your contribution! +

          + +

          + Why should you join this project? It's popular, so you will have an + impact. It makes code more robust and secure, which is a socially + important purpose. + You will get to scratch your own itch by creating tools that solve + problems that frustrate you. + It is accessible even to junior software engineers and undergraduates. + (Many undergraduate students have published scientific papers, such as + Jason Waataja, + Vlastimil Dort + Gene Kim, + Siwakorn Srisakaokul, + Stephanie Dietzel, + Nathaniel Mote, + Brian Walker, + Eric Spishak, + Jaime + Quinonez, + Matthew Papi, + Mahmood + Ali, + and + Telmo Correa; + and even more have made significant contributions to the tool.) + Finally, we have a lot of fun on this project! +

          + +

          + Prerequisites: You should be very comfortable with the Java + programming language and its type system. You should know how a type + system helps you and how it can hinder you. You should be willing to + dive into a moderately-sized codebase. + You should understand fundamental object-oriented programming concepts, + such as + behavioral + subtyping: subtyping theory + permits argument types to change contravariantly (even though Java forbids it + for reasons related to overloading), whereas return types may change + covariantly + both in theory and in Java. +

          + + +

          +Potential projects: +Most of this document lists potential projects. The projects are +grouped roughly from easiest to most challenging. +

          + + + +

          How to get started: do a case study

          + +

          + To get started, first do a case study of using the Checker + Framework: that is, run the Checker Framework on some program. + If you have already done so, you can skip this section. + Otherwise, a case study gives you experience in using the Checker + Framework, and it may reveal bugs in either the Checker Framework or in + the program it is analyzing. +

          + +

          +Why should you start with a case study? + +Before you can contribute to any project, you must +understand the tool from a user point of view, including its strengths, +weaknesses, and how to use it. +Using the Checker Framework is the best way to +learn about it and determine whether you would enjoy +contributing to it. +

          + +

          +What is the purpose of a case study? +The primary result of your case study is that you will discover bugs in the +subject program, or you will verify that it has no bugs (of some particular +type). If you find bugs in open-source code, +and let us know when they are resolved.
          +Another outcome of your case study is that you may discover bugs, limitations, +or usability problems in the Checker Framework. Please +report them. +We'll try to fix them, or they might give you inspiration for +improvements you would like to make to the Checker Framework. +

          + +

          + You might want to start with a small program that you wrote, + then repeat the process with a larger open-source program or library. +

          +
            +
          1. + Install + the Checker Framework. +
          2. + Review + the Checker Framework documentation. +
          3. + Choose an existing library or program to type-check. + A program that is about 1000 lines long is a good size for your first + use of the Checker Framework, but you could use a smaller or larger one. + The library or program should be under active maintenance; don't choose one + that has not had a commit in the past year. + You will find the case study easier if you are already familiar with + the program, or if it is written in good style. +
          4. + Choose one type system, from + among those + distributed with the Checker Framework, that is appropriate for the + program. +
          5. + If the program is hosted on GitHub, fork it and create a new branch for + your work. (Leave the master branch of your fork unchanged + from upstream.) +
          6. + Annotate the program, based on its documentation. +
            + Please do not make changes unrelated to annotating the + program, such as inserting/removing whitespace or sorting + the import statements. Doing so bloats the size of the + diffs and makes it hard to understand the essential changes. +
          7. + Change the build system so that building the annotated branch runs the type-checker. +
          8. + Run the type-checker. If it issues + warnings, correct them. + This might require adding more annotations, + fixing bugs in the program, or suppressing warnings. + Be sure that the program's test suite continues to pass. + Repeat until + the type-checker passes on the program. +
              +
            • Don't add an if statement that always succeeds, just + 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 + explain + why the checker warning is a false positive and you are certain + the code is safe. +
            +
          9. + Share it with us; we would be happy to give you feedback. + +

            + The subject line should be descriptive (not just "Case study", but + "Nullness case study of Apache Commons Exec library"). + You should give us access to +

              +
            • the original (unannotated) version of the program, +
            • the annotated version of the program, and +
            • the exact command that runs the type-checker from the command + line. +
            + The best way to give all this information is a pointer to your GitHub + fork of the library. +
          + + +

          +You can also try to fix problems that you find and submit a +pull + request, but that is not a requirement to get started, + because not all problems are good for new contributors. +

          + + + +

          How to get help and ask questions

          + +

          +We are very happy to answer your questions, and we are eager to interact +with you! It's OK to have questions, and your questions can lead to +improvements in the documentation and the tool. +

          + +

          +Before you ask a question, read this file and the +"Troubleshooting" + section of the Checker Framework manual + (including "How + to report problems"), +and also search in the +Checker Framework manual +for the answer. +Don't send us a message +that says nothing but “please guide me” +or “tell me how to fix this issue from the issue tracker”. +

          + +

          +When you ask a question, please +tell us what you have tried, tell us what went wrong or +where you got stuck, and ask a concrete technical question that will +help you get past your problem. If you can do that, then definitely ask +your question, because we don't want you to be stuck or frustrated. +

          + +

          +When you send email, +please use standard email etiquette, such as: avoid all-caps; use a +descriptive subject line; don't put multiple different topics in a single +email message; start a new thread with a new subject line +when you change the topic; don't clutter discussions with irrelevant +remarks; don't use screenshots (unless there is a problem with a GUI), but +instead cut-and-paste the output or code into your message; +if you are making a guess, clearly indicate that it is a guess and +your grounds for it. Bug +reports should be +complete +and should usually be +reported +to the issue tracker. +

          + + +

          Types of projects

          + +

          + Here are some possible focuses for a project: +

          +
            +
          • + Evaluate a recently-written type + system, or a feature used by multiple type systems. +
          • +
          • + Annotate a popular library, so that it + is easier to type-check clients of the library. +
          • +
          • + Create a new type system, to + prevent some Java programming error. +
          • +
          • + Enhance a type system or the Checker Framework + itself. +
          • +
          + +

          +This document gives a few suggestions in each category. +

          + + +

          Evaluate a type system or a Checker Framework feature

          + +

          + These projects evaluate a recently-written type system or a feature used + by multiple type systems. + Using the type systems on real code is our most important source of new ideas and improvements. + Many people have started out “just” doing a case + study but have ended up making deep, fundamental contributions and even + publishing scientific papers about their discoveries. +

          + +

          +One possible outcome is to identify + weaknesses in the type-checker so that we can improve it. Another + possible outcome is to provide evidence that the type-checker is + effective and convince more users to adopt it. You will probably + also discover defects (bugs) in the codebase being type-checked. +

          + + + +

          Signature strings

          + +

          +Determine whether the ASM library, or +some other library, properly handles signature strings. +

          + +

          + Some challenging aspects of this case study are: +

          +
            +
          • + Some libraries define their own new signature string formats (!), which + you need to define in the Signature String Checker. +
          • +
          • + Sometimes the library's documentation is incorrect, and in other cases the + string format is not defined. +
          • +
          + + +

          Preventing mixed signed/unsigned computations

          + + + +

          +An unsigned integer's bits are interpreted differently than a signed +integer's bits. It is meaningless to add a signed and an unsigned integer +— the result will be nonsense bits. The same is true of printing and +of other numeric operators such as multiplication and comparison. +

          + +

          +We have +a prototype + compile-time verification tool that detects and prevents these +errors. The goal of this project is to perform case studies to determine +how often programmers make signedness errors (our initial investigation +suggests that this is common!) and to improve the verification tool. +

          + +

          +The research questions are: +

          +
            +
          • How often do programmers make signedness errors? +
          • +
          • Is it feasible to automatically detect signedness errors? What techniques are useful? +
          • +
          • What is the false positive rate of a signedness verification tool — that is, false alarms from the tool? +
          • +
          • How much effort is required from a programmer? +
          • +
          + +

          +The methodology is: +

          +
            +
          • find open-source projects that use unsigned arithmetic +
          • +
          • run the verification tool on them +
          • +
          • for each tool warning, determine whether it is a defect in the + project or a limitation of the verification tool. For example, the Signedness + Checker does not currently handle boxed integers and BigInteger; these + haven't yet come up in case studies but could be worthwhile enhancements. + You may also need to write more annotations for libraries such as the + JDK. +
          • +
          • submit bug reports against the project, or improve the verification tool +
          • +
          + +

          + A good way to find projects that use unsigned arithmetic is to find a + library that supports unsigned + arithmetic, then search on GitHub for projects that use that library. +

          + +

          + Here are some relevant libraries. +

          +
            +
          • In the JDK's Integer + and Long, these include + compareUnsigned, + divideUnsigned, + parseUnsignedInt, + remainderUnsigned, and + toUnsignedLong. +
            + Classes like DataInputStream, ObjectInputStream, + and RandomAccessFile have readUnsignedByte. +
            + Arrays has compareUnsigned. + The JDK is already annotated; search for @Unsigned within + https://github.com/eisop/jdk. +
          • +
          • + In Guava, see + its unsigned + support, such + as UnsignedBytes, + UnsignedLong, + UnsignedLongs, + etc. + Guava is already annotated; search for @Unsigned within + https://github.com/eisop/guava. +
          • +
          • The jOOU library consists of support for unsigned + integers.
          • +
          + +

          + Another possibility is to find Java projects that could use an + unsigned arithmetic library but do not. For + example, bc-java defines + its own unsigned libraries, and some other programs might do direct bit + manipulation. +

          + + + + +

          Whole-program type inference

          + +

          +A type system is useful because it prevents certain errors. The downside +of a type system is the effort required to write the types. Type inference +is the process of writing the types for a program. +

          + +

          +The Checker Framework includes +a whole-program + inference that inserts type qualifiers in the user's program. +It works well on some programs, but needs more enhancements to work well on +all programs. +

          + + +

          Sound checking by default

          + +

          +By default, the Checker Framework is +unsound + in several +circumstances. +“Unsound” means that the Checker Framework +may report no warning even though the program can misbehave at run time. +

          + +

          +The reason that the Checker Framework is unsound is that we believe that +enabling these checks would cause too many false positive warnings: +warnings that the Checker Framework issues because it cannot prove that the +code is safe (even though a human can see that the code is safe). Having +too many false positive warnings would irritate users and lead them not to +use the checker at all, or would force them to simply disable those checks. +

          + +

          +We would like to do studies of these command-line options to see whether +our concern is justified. Is it prohibitive to enable sound checking? Or can we +think of enhancements that would let us turn on those checks that are +currently disabled by default? +

          + +

          +There is no need to annotate new code for this project. Just use existing +annotated codebases, such as those that are type-checked as part of the +Checker +Framework's Azure + Pipeline. In other words, you can start by enabling Azure +Pipelines for your fork and then changing the default behavior in a +branch. The Azure Pipelines job will show you what new warnings appear. +

          + + +

          Comparison to other tools

          + +

          + Many other tools exist for prevention of programming errors, such as + Error Prone, NullAway, FindBugs, JLint, PMD, and IDEs such as Eclipse and + IntelliJ. These tools + are not as powerful as the Checker Framework (some are bug finders rather + than verification tools, and some perform a shallower analysis), but they + may be easier to use. + Programmers who use these tools wonder, "Is it worth my time to switch to + using the Checker Framework?" +

          + +

          + The goal of this project is to perform a head-to-head comparison of as + many different tools as possible. You will quantify: +

          + +
            +
          • the number of annotations that need to be written
          • +
          • the number of bugs detected
          • +
          • the number of bugs missed
          • +
          • the number of false positive warnings
          • +
          + +

          + This project will help programmers to choose among the different tools + — it will show when a programmer should or should not use the + Checker Framework. + This project will also indicate how each tool should be improved. +

          + +

          + One place to start would be with an old version of a program that is + known to contain bugs. Or, start with the latest version of the program + and re-introduce fixed bugs. (Either of these is more realistic than + introducing artificial bugs into the program.) A possibility would be to + use the Lookup program that has been used in previous case studies. +

          + + +

          Android support annotations

          + +

          +Android uses its own annotations that are similar to some in the Checker +Framework. Examples include the +Android + Studio support annotations, + including @NonNull, @IntRange, @IntDef, + and others. +

          + +

          +The goal of this project is to implement support for these annotations. +That is probably as simple as creating aliased annotations +by calling method addAliasedTypeAnnotation() +in AnnotatedTypeFactory. +

          + +

          + Then, do a case study to show the utility (or not) of + pluggable type-checking, by comparison with how Android Studio currently + checks the annotations. +

          + + +

          Annotate a library

          + +

          + These projects annotate a library, so that it is easier to + type-check clients of the library. Another benefit is that this may find + bugs in the library. It can also give evidence for the usefulness of + pluggable type-checking, or point out ways to improve the Checker + Framework. +

          + + +

          +When type-checking a method call, the Checker Framework uses the method +declaration's annotations. +This means that in order to type-check code that uses a library, the +Checker Framework needs an annotated version of the library. +

          + +

          +The Checker Framework comes with a +few annotated +libraries. Increasing this number will make the Checker Framework even +more useful, and easier to use. +

          + +

          +After you have chosen a library, +fork the library's source code, adjust +its build +system to run the Checker Framework, and add annotations to it until +the type-checker issues no warnings. +

          + +

          +Before you get started, be sure to read +How +to get started annotating legacy code. More generally, read the +relevant +sections of the Checker Framework manual. +

          + + +

          Choosing a library to annotate

          + +

          +There are several ways to choose a library to annotate: +

          +
            +
          • +The best way to choose a library is to try to annotate a program and notice +that library annotations are needed in order to type-check the program. +
          • +
          • +Alternately, you can + choose a popular + Java library. +
          • +
          + +

          + When annotating a library, it is important to type-check both the library + and at least one client that uses it. Type-checking the client will + ensure that the library annotations are accurate. +

          + +

          + Whatever library you choose, you will need to deeply understand its + source code. You will find it easier to work with a library that is + well-designed and well-documented. +

          + +

          + You should choose a library that is + not already + annotated. There are two exceptions to this. +

          +
            +
          • + A library might be annotated for one type system, but you add + annotations for a different type system. One advantage of this is that + the library's build system is already set up to run the Checker + Framework. You can tell which type systems a library is annotated for + by examining its source code. +
          • +
          • + A library might be annotated, but the annotations have not been + verified by running the type-checker on the library source code. You + would verify that the annotations in the library are correct. +
          • +
          + + +

          Guava library

          + +

          + Guava is already partially annotated with nullness annotations — in + part by Guava's developers, and in part by the Checker Framework team. + However, Guava does not yet type-check without errors. Doing so could + find more errors (the Checker Framework has found nullness and indexing + errors in Guava in the past) and would be a good case study to learn the + limitations of the Nullness Checker. +

          + + +

          Create a new type system

          + +

          +The Checker Framework is shipped with about 20 type-checkers. Users can +create a + new checker of their own. However, some users don't want to go to +that trouble. They would like to have more type-checkers packaged with the +Checker Framework for easy use. +

          + +

          +Each of these projects requires you to design a new type system, +implement it, and perform case studies to demonstrate that it is both +usable and effective in finding/preventing bugs. +

          + + +

          Ownership type system

          + +

          +The lightweight +ownership mechanism of the Resource Leak Checker is not implemented as +a type system, but it should be. That would enable writing ownership +annotations on generic type arguments, like List<@Owning +Socket>. It would also enable changing the Resource Leak Checker +so that non-@Owning formal parameters do not have +their @MustCall +annotation erased. +

          + + +

          + We have some notes on possible implementation strategies. +

          + + + +

          Non-Empty Checker for precise handling of Queue.peek() and poll()

          + +

          +The Nullness Checker issues a false positive warning for this code: +

          + +
          +import java.util.PriorityQueue;
          +import org.checkerframework.checker.nullness.qual.NonNull;
          +
          +public class MyClass {
          +    public static void usePriorityQueue(PriorityQueue<@NonNull Object> active) {
          +        while (!(active.isEmpty())) {
          +            @NonNull Object queueMinPathNode = active.peek();
          +        }
          +    }
          +}
          +
          + +

          +The Checker Framework does not determine that active.peek() returns a non-null value in this context. +

          + +

          +The contract of peek() is that it returns a non-null value if the queue is not empty and the queue contains no null values. +

          + +

          +To handle this code precisely, the Nullness Checker needs to know, for each queue, whether it is empty. +This is analogous to how the Nullness Checker tracks whether a particular +value is a key in a map. +

          + +

          +It should be handled the same way: by adding a new subchecker, called the + Nonempty Checker, to the Nullness Checker. Its types are: +

          +
            +
          • @UnknownNonEmpty — the queue might or might not be empty +
          • @NonEmpty — the queue is definitely non-empty +
          + +

          +There is a start at this type-checker in branch nonempty-checker. It: +

          +
            +
          • defines the annotations +
          • +
          • creates the integration into the Nullness Checker +
          • +
          +

          +However, it is not done. (In fact, it doesn't even compile.) +For information about what needs to be done, see issue #399. +

          + +

          +When you are done, the Nullness Checker should issue only the // :: diagnostics from checker/tests/nullness/IsEmptyPoll.java — no more and no fewer. +You can test that by running the Nullness Checker on the file, and when you are done you should delete the // @skip-test line so that the file is run as part of the Checker Framework test suite. +

          + + +

          Iteration Checker to prevent NoSuchElementException

          + + + +

          +A Java program that uses an Iterator can +throw NoSuchElementException if the program +calls next() on the Iterator but +the Iterator has no more elements to iterate over. Such +exceptions even occur in production code (for example, +in Eclipse's +rdf4j). +

          + +

          +We would like a compile-time guarantee that this run-time error will never +happen. Our analysis will statically determine whether +the hasNext() method would return true. The basic type system +has two type qualifiers: @HasNext is a subtype +of @UnknownHasNext. +

          + +

          +A variable's type is @HasNext if the program +calls hasNext() and it returns true. Implementing this is +easy (see +the dataflow +section in +the "How +to create a new checker" chapter). The analysis can also permit some +calls to next() even if the programmer has not +called hasNext(). For example, a call to next() +is permitted on a newly-constructed iterator that is made from a non-empty +collection. (This special case could build upon +the Non-Empty Checker mentioned above.) +There are probably other special cases, which experimentation will reveal. +

          + +

          +Parts of this are already implemented, but it needs to be enhanced. Once +case studies have demonstrated its effectiveness, then it can be released +to the world, and a scientific paper can be written. +

          + + +

          Preventing injection vulnerabilities via specialized taint analysis

          + + + +

          +Many security vulnerabilities result from use of untrusted data without sanitizing it first. +Examples include SQL injection, cross-site scripting, command injection, and many more. +Other vulnerabilities result from leaking private data, such as credit card numbers. +

          + +

          +We have built a generalized taint analysis that can address any of these problems. However, because it is so general, it is not very useful. A user must customize it for each particular problem. +

          + +

          +The goal of this project is to make those customizations, and to evaluate their usefulness. +A specific research question is: "To what extent is a general taint analysis useful in eliminating a wide variety of security vulnerabilities? How much customization, if any, is needed?" +

          + +

          +The generalized taint analysis is the Checker Framework's +a Tainting + Checker. It requires customization to a particular domain: +

          +
            +
          • + rename the @Tainted and @Untainted qualifiers + to something more specific (such as @Private + or @PaymentDetails or @HtmlQuoted), and +
          • + annotate libraries. +
          + +

          +The first part of this project is to make this customization easier to do +— preferably, a user will not have to change any code in the Checker +Framework (the +Subtyping +Checker already works this way). +As part of making customization easier, a user should be able to specify +multiple levels of taint — many information classification hierarchies +have more than two levels. For example, the US government separates +information into four categories: Unclassified, Confidential, Secret, and +Top Secret. +

          + +

          +The second part of this project is to provide several examples, and do case +studies showing the utility of compile-time taint checking. +

          + +

          + Possible examples include: +

          + + +

          +For some microbenchmarks, see the Juliette test suite for Java from CWE. +

          + + +

          Warn about unsupported operations

          + + + +

          +In Java, some objects do not fully implement their interface; they +throw UnsupportedOperationException for some operations. One +example is unmodifiable collections. They throw the exception when a +mutating operation is called, such +as add, addAll, put, remove, +etc. +

          + +

          +The goal of this project is to +design a compile-time verification tool to track which operations might not be supported. +This tool will issue a warning whenever +an UnsupportedOperationException might occur at run time. +This helps programmers to avoid run-time exceptions (crashes) in their Java programs. +

          + +

          +The research questions include: +

          +
            +
          • Is it is possible to build a verification tool to prevent UnsupportedOperationException? What design is effective? +
          • +
          • How difficult is such a tool to use, in terms of programmer effort and number of false alarms? +
          • +
          • Are potential UnsupportedOperationException exceptions + pervasive in Java programs? Is it possible to eliminate them? +
          • +
          + +

          +The methodology is: +

          +
            +
          1. design a static (compile-time) analysis +
          2. +
          3. implement it +
          4. +
          5. evaluate it on open-source projects +
          6. +
          7. report bugs in the projects, and improve the tool +
          8. +
          + +

          +Here is a possible design, as a pluggable type system. +

          +
          +  @Unmodifiable
          +       |
          +  @Modifiable
          +
          +

          +In other words, the @Unmodifiable type qualifier is a +supertype of @Modifiable. This means that a @Modifiable + List can be used where an @Unmodifiable List is +expected, but not vice versa. +

          + +

          + @Modifable is the default, and + methods such +as Arrays.asList +and Collections.emptyList +must be annotated to return the less-capable supertype. +

          + + +

          Overflow checking

          + +

          +Overflow is when 32-bit arithmetic differs from ideal arithmetic. For +example, in Java the int computation 2,147,483,647 + 1 yields +a negative number, -2,147,483,648. The goal of this project is to detect +and prevent problems such as these. +

          + +

          +One way to write this is as an extension of the Constant Value Checker, +which already keeps track of integer ranges. It even already +checks + for overflow, but it never issues a warning when it discovers + possible overflow. Your variant would do so. +

          + +

          +This problem is so challenging that there has been almost no previous +research on static approaches to the problem. (Two relevant papers are +IntScope: +Automatically Detecting Integer Overflow Vulnerability in x86 Binary Using +Symbolic Execution and +Integer Overflow +Vulnerabilities Detection in Software Binary Code.) Researchers are +concerned that users will have to write a lot of annotations indicating the +possible ranges of variables, and that even so there will be a lot of false +positive warnings due to approximations in the conservative analysis. +For example, will every loop that contains i++ cause a warning that i might overflow? +That would not be acceptable: users would just disable the check. +

          + +

          +You can convince yourself of the difficulty by manually analyzing programs +to see how clever the analysis has to be, or manually simulating your +proposed analysis on a selection of real-world code to learn its +weaknesses. You might also try it +on good + and bad binary search code. +

          + +

          +One way to make the problem tractable is to limit its scope: instead of +being concerned with all possible arithmetic overflow, focus on a specific +use case. +As one concrete application, +the Index +Checker is currently unsound in the presence of integer overflow. If +an integer i is known to be @Positive, and 1 is +added to it, then the Index Checker believes that its type +remains @Positive. If i was +already Integer.MAX_VALUE, then the result is negative — +that is, the Index Checker's approximation to it is unsound. +

          + +

          +This project involves removing this unsoundness by implementing a type system to track when an +integer value might overflow — but this only matters for values that +are used as an array index. +That is, checking can be restricted to computations that involve an operand +of type @IntRange). +Implementing such an analysis would permit the Index Checker +to extend its guarantees even to programs that might overflow. +

          + +

          +This analysis is important for some indexing bugs in practice. +Using the Index Checker, we found 5 bugs in Google +Guava related to overflow. Google marked these as high priority and +fixed them immediately. In practice, there would be a run-time exception +only for an array of size approximately Integer.MAX_INT. +

          + +

          +You could write an extension of the Constant Value Checker, which already +keeps track of integer ranges and +even determines +when overflow is possible. It doesn't issue a warning, but your +checker could record whether overflow was possible (this could be a +two-element type system) and then issue a warning, if the value is used as +an array index. +Other implementation strategies may be possible. +

          + +

          +Here are some ideas for how to avoid the specific problem +of issuing a warning about potential overflow for every i++ in +a loop (but maybe other approaches are possible): +

          +
            +
          • + The loop checks whether i == Integer.MAX_VALUE before + incrementing. This wide-scale, disruptive code change is not + acceptable. +
          • +
          • + Make the default array size (the length of an unannotated array) be + @ArrayLenRange(0, Integer.MAX_VALUE-1) rather + than @UnknownVal, which is equivalent + to @ArrayLenRange(0, Integer.MAX_VALUE-1). Now, every + array construction requires the client to establish that the length is + not Integer.MAX_VALUE. I don't have a feel for whether + this would be unduly burdensome to users. +
          • +
          + + +

          Index checking for mutable length data structures

          + +

          +The Index +Checker is currently restricted to fixed-size data structures. A +fixed-size data structure is one whose length cannot be changed once it is +created, such as arrays and Strings. This limitation prevents the Index +Checker from verifying indexing operations on mutable-size data structures, +like Lists, that have add or remove +methods. Since these kind of collections are common in practice, this is a +severe limitation for the Index Checker. +

          + +

          +The limitation is caused by the Index Checker's use of types that are dependent on the length of data structures, +like @LTLengthOf("data_structure"). If data_structure's length could change, +then the correctness of this type might change. +

          + +

          +A naive solution would be to invalidate these types any time a method is called on data_structure. +Unfortunately, aliasing makes this still unsound. Even more, a great solution to this problem would keep +the information in the type when a method like add or remove is called on data_structure. +A more complete solution might involve some special annotations on List that permit the information to be persisted. +

          + +

          +Another approach would be to run a pointer analysis before type-checking, +then use that information for precise information about what lists might be +changed by each call to add or remove. One +possible pointer analysis would be that +of Doop. +

          + +

          +This project would involve designing and implementing a solution to this problem. +

          + + +

          Nullness bug detector

          + +

          +Verifying a program to be free of errors can be a daunting task. When +starting out, a user may be more interested in +bug-finding +than verification. The goal of this project is to create a nullness bug +detector that uses the powerful analysis of the Checker Framework and its +Nullness Checker, but omits some of its more confusing or expensive +features. The goal is to create a fast, easy-to-use bug detector. It +would enable users to start small and advance to full verification in the +future, rather than having to start out doing full verification. +

          + +

          +This could be structured as a new NullnessLight Checker, or as a +command-line argument to the current Nullness Checker. Here are some +differences from the real Nullness checker: +

          +
            +
          • No initialization analysis; the checker assumes that every value is + initialized.
          • +
          • No map key analysis; assume that, at every call to + Map.get, the given key appears in the map.
          • +
          • No invalidation of dataflow facts. Assume all method calls are pure, + so method calls do not invalidate dataflow facts. Assume there is no + aliasing, so field updates do not invalidate dataflow facts. +
          • +
          • Assume that boxing of primitives is @Pure: it returns + the same value on every call.
          • +
          • If the Checker Framework cannot infer a type argument, assume that + the type argument is @NonNull.
          • +
          +

          + Each of these behaviors should be controlled by its own command-line + argument, as well as being enabled in the NullnessLight Checker. +

          + +

          + The implementation may be relatively straightforward, since in most cases + the behavior is just to disable some functionality of existing checkers. +

          + +

          Tools such as FindBugs, NullAway, NullnessLight, and the Nullness + Checker form a spectrum from easy-to-use bug detectors to sound + verification. NullnessLight represents a new point in the design space. + It will be interesting to compare these checkers: +

          + +
            +
          • How much easier is it to use? For example, how many fewer + annotations need to be written?
          • +
          • + How many more fewer true positives does it report — in other + words, how many more false negatives does it suffer? +
          • +
          • + How many fewer false positives does it report? +
          • +
          + +

          + Uber's NullAway tool is also + an implementation of this idea (that is, a fast, but incomplete and + unsound, nullness checker). NullAway doesn't let the user specify Java + Generics: it assumes that every type parameter is @NonNull. + Does Uber's tool provide users a good + introduction to the ideas that a user can use to transition to a nullness + type system later? +

          + + + +

          Enhance the toolset

          + + +

          Indicate library methods that should be used instead

          + +

          + Sometimes, the best way to avoid a checker warning is to use an annotated + library method. Consider this code: +

          + +
          +@FqBinaryName String fqBinaryName = ...;
          +@ClassGetName String componentType = fqBinaryName.substring(0, fqBinaryName.indexOf('['));
          +
          + +

          +The Signature +String Checker issues a warning, because it does not reason about +arbitrary string manipulations. The code is correct, but it is in bad +style. It is confusing to perform string manipulations to convert between +different string representations. It is clearer and less error-prone (the +above code is buggy when fqBinaryName is not an array type!) +to use a library method, and the checker accepts this code because the +library method is appropriately annotated: +

          + +
          +import org.plumelib.reflection.Signatures;
          +...
          +@ClassGetName String componentType = Signatures.getArrayElementType(fqBinaryName);
          +
          + +

          +However, users may not know about the library method. Therefore, the +Checker Framework should issue a warning message, along with the error +message, notifying users of the library method. For example, the Signature +String Checker would heuristically mention +the Signatures.getArrayElementType() +method when it issues an error about string manipulation where some input +is +a FqBinaryName +and the output is annotated +as ClassGetName. +It would behave similarly for other library methods. +

          + + +

          Improving error messages

          + +

          +Compiler writers have come to realize that clarity of error +messages is as important as the speed of the executable +(1, 2, +3, +4). This is especially true when the language or type system has rich features. +

          + +

          +The goal of this project is to improve a compiler's error messages. Here are +some distinct challenges: +

          +
            +
          • + Some type errors can be more concisely or clearly expressed than the + standard "found type A, expected type B" message. +
          • +
          • + Some types are complex. The error message could explain them, or link + to the manual, or give suggested fixes. +
          • +
          • + Compiler messages currently show + the effective + type, which may be different than what the user wrote due to + defaulting, inference, and syntactic sugar. For example, a user-written + @IndexFor("a") annotation is syntactic sugar for + @NonNegative @LTLengthOf("a"), and those types are + the ones that currently appear in error messages. + It might be good to show simpler types or ones that the user wrote. +
          • +
          • + Some checkers combine multiple cooperating type systems; + the Nullness + Checker and + the Index + Checker are examples. If there is a problem with a variable's + lower bound type, then its upper bound type should not be shown in the + error message. This will make the message shorter and more specific, + and avoid distracting the user with irrelevant information. +
          • +
          • + When a checker has multiple type systems, a type error or the lack of one may depend on facts from multiple type systems, and this should be expressed to the user. +
          • +
          + + +

          Java expression parser

          + +

          +A number of type annotations take, as an +argument, a +Java expression. The representation for these +(the JavaExpression +class) is a hack. The goal of this +project is to remove it. +

          + +

          +The JavaExpression class +represents an AST. There is no need for the Checker Framework to +define its own AST when the JavaParser AST already exists and is +maintained. In fact, JavaExpressionParseUtil uses JavaParser, +but needlessly converts a +JavaParser Expression into +a JavaExpression. +

          + +

          + The goals for the project include: +

          + + +

          +Direct replacement of the classes is not possible, or we would have done it +already. For example, JavaExpression contains some methods that +JavaParser lacks, such as isUnassignableByOtherCode. As a +first step before doing the tasks listed above, you may want to convert +these methods from instance methods of JavaExpression into static +methods in JavaExpressions, making JavaExpression more +like a standard AST that can be replaced by JavaParser classes. +You also need to decide how to store the type field +of JavaExpression, when JavaExpression is eliminated. +An +alternate design (or a partial step in the refactoring process) would be to +retain the JavaExpression class, but make it a thin wrapper around +JavaParser classes that do most of the real work. +

          + +

          +Another aspect of this project is +fixing the + issues that are labeled "JavaExpression". +

          + + +

          Dataflow enhancements

          + +

          +The Checker +Framework's dataflow + framework +(manual + here) implements flow-sensitive type refinement (local type +inference) and other features. It is used in the Checker +Framework and also in Error Prone, +NullAway, and elsewhere. +

          + +

          +There are a number +of open +issues — both bugs and feature requests — related to the +dataflow framework. The goal of this project is to address as many of +those issues as possible, which will directly improve all the tools that +use it. +

          + + +

          Side effect analysis, also known as purity analysis

          + + + +

          +A side effect analysis reports what side effects a procedure may perform, +such as what variable values it may modify. A side effect analysis is +essential to other program analyses. A program analysis technique makes +estimates about the current values of expressions. When a method call +occurs, the analysis has to throw away most of its estimates, because the +method call might change any variable. However, if the method is known to +have no side effects, then the analysis doesn't need to throw away its +estimates, and the analysis is more precise. Thus, an improvement to the +foundational side effect analysis can improve many other program analyses. +

          + +

          +The goal of this project is to evaluate existing side effect analysis +algorithms and implementations, in order to determine what is most +effective and to improve them. The research questions include: +

          +
            +
          • What side effect analysis algorithms are most effective? What are their limitations? +
          • +
          • Can the most effective algorithms be combined to become even effective? Or can their limitations be overcome? +
          • +
          • How much does accurate side effect analysis improve other programming tasks? +
          • +
          + +

          +The methodology is to collect existing side effect analysis tools (two examples are +Soot and +Geffken); +run them on open-source projects; examine the result; and then improve them. +

          + + + + +

          Javadoc support

          + +

          +Currently, type annotations are only displayed in Javadoc if they are +explicitly written by the programmer. However, the Checker Framework +provides flexible defaulting mechanisms, reducing the annotation overhead. +This project will integrate the Checker Framework defaulting phase with +Javadoc, showing the signatures after applying defaulting rules. +

          + +

          +There are other type-annotation-related improvements to Javadoc that can be +explored, e.g. using JavaScript to show or hide only the type annotations +currently of interest. +

          + + +

          How to apply to GSoC (relevant to GSoC students only)

          + +

          +This section is relevant only to Google Summer of Code Students. +

          + +

          + To apply, you will submit a single PDF through the Google Summer + of Code website. This PDF should contain two main parts. We suggest + that you number the parts and subparts to ensure that you don't forget anything, and + to ensure that we don't overlook anything when reading your application. You might find it + easiest to create multiple PDFs for the different parts, then concatenate + them before uploading to the website, but how you create your proposal is + entirely up to you. +

          + +
            +
          1. The proposal itself: what project you want to work on during the + summer. You might propose to do a project listed on this webpage, or + you might propose a different project. + +

            The proposal should have a descriptive title, both in the PDF and in + the GSoC submission system. Don't use a title like "Checker + Proposal" or "Proposal for GSoC". Don't distract from content with + gratuitous graphics. + +

            List the tasks or subparts that are required to complete your + project. This will help you discover a part that you had forgotten. + We do not require a detailed timeline, because you don't yet + know enough to create one. +

            + +

            + If you want to do a + case study, say what program you will do your case study on. + +

            If you want to create a new type system (whether one proposed on + this webpage or one of your own devising), then your proposal should include + the type system's user manual. You don't have to integrate it in the Checker + Framework repository (in other words, use any word processor or text + editor you want to create a PDF file you will submit), but you should describe + your proposed checker's parts + in precise English or simple formalisms, and you should follow the + suggested structure. + +

            + If you want to do exactly what is already listed on this page, then + just say that (but be specific about which one!), and it will not hurt + your chances of being selected. However, show us what progress you + have made so far. You might also give specific ideas + about extensions, about details that are not mentioned on this webpage, + about implementation strategies, and so forth. +

            + +

            Never literally cut-and-paste text that was not written by you, because + that would be plagiarism. If you quote from text written by someone + else, give proper credit. + Don't + submit a proposal that is just a rearrangement of text that already + appears on this page or in the Checker Framework manual, because it does + not help us to assess your likelihood of being successful. +

            + +
          2. + +
          3. Your qualifications. Please convince us that you + are likely to be successful in your proposed summer project. + +
              +
            1. A URL that points to a code sample. + Don't write any new code, but provide code you wrote in the + past, such as for a class assignment + or a project you have worked on outside class. + It does not need to have anything to do with + the Checker Framework project. It should be your own personal work. + The purpose is to assess your programming skills so we can assign you + to an appropriate project. + A common problem is to submit undocumented code; we expect every + programmer to write documentation when working on the Checker + Framework. + Don't put a lot of different files in Google Drive and share that + URL; it's better to upload a single .zip file or + provide a GitHub URL. +
            2. +
            3. + What you have done to prepare yourself + for working with the Checker Framework during the summer. + You may wish to structure this as a list. + Examples of items in the list include: +
                +
              • A URL for code you have annotated as a case study. Please indicate the + original unannotated code, the annotated code, and the exact command to + run the type-checker from the command line. Ensure that the GSoC + mentors can compile your code. + (It is acceptable to use the same code, or different code, for this + item and the code sample above.) + +

                + You should have shared the case study as soon as you finished it or + as soon as you had a question that is not answered in + the manual; + don't wait until you submit your proposal, because that does not + give us a chance to help you with feedback. +

                +
              • +
              • URLs for bugs or pull requests that you have filed.
              • +
              • Information about other projects you have done, or classes you + have taken, that prepare you for your proposed summer task. This + is optional. If something already appears in your resume, don't + repeat it here; we will see it in your resume.
              • +
              +
            4. +
            5. A resume. + A resume + contains a brief description of your skills and your job or project + experience. It will often list classes you have taken so far and your + GPA. It should not be longer than one page.
            6. +
            7. An unofficial transcript or grade report (don't spend + money for an official one).
            8. +
            +
          4. +
          + +

          +The best way to impress us is by doing a thoughtful job in the case +study. The case study is even more important than the proposal text, +because it shows us your abilities. +The case study may result in you submitting issues against the issue tracker of the +program you are annotating or of the Checker Framework. +Pull requests against our GitHub project are a plus but are not required: +good submitted bugs are just as valuable as bug fixes! +You can also make a good impression by correctly answering questions from +other students on the GSoC mailing list. +

          + +

          +Some GSoC projects have a requirement to fix an issue in the issue tracker. +We do not, because it is unproductive. +Don't try to start fixing issues before you +understand the Checker Framework from the user point of view, which will +not happen until you have completed a case study on an open-source program. +You may discuss your ideas with us by sending mail +to checker-framework-gsoc@googlegroups.com. +

          + +

          +Get feedback! Feel free to ask questions +to make your application more +competitive. We want you to succeed. Historically, students who start +early and get feedback are most successful. You can submit a draft +proposal via the Google Summer of Code website, and we will review it. We +do not receive any notification when you submit a draft +proposal, so if you want feedback, please tell us that. +Also, we can only see draft proposals; we cannot see final proposals until +after the application deadline has passed. +

          + +

          +Please do not violate the guidelines in +the How to get help and ask questions section +of this document. If you do so, you are disqualified you from participating in +GSoC, because it shows that you do not read instructions, and you haven't +thought about the problem nor tried to solve it. +

          + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/developer/new-contributor-projects.html-old b/docs/developer/new-contributor-projects.html-old new file mode 100644 index 000000000000..7d3b21d922f0 --- /dev/null +++ b/docs/developer/new-contributor-projects.html-old @@ -0,0 +1,711 @@ +

          Avoiding exponential blowup when processing DAGs

          + + + +

          +Google's Bazel open-source project is a +publicly-released version of their build system, Blaze. Blaze builds every +line of source code that is written by any Google programmer — all of +that source code appears in a single repository! Therefore, Bazel/Blaze needs to +be fast. Bazel represents all of the source code and its dependencies as a +large DAG (a +directed +acyclic graph). It needs to manipulate these DAGs efficiently. +

          + +

          +One of the biggest problems that the Bazel developers face is exponential +blowup of DAG sizes and therefore of run time. Periodically, one of the +Bazel developers makes such a mistake, and Bazel becomes unusable until +they can diagnose and fix the problem. +

          + +

          +Here are two different ways to view the problem. +

          + +
            +
          1. + In a DAG, multiple nodes may have the same child. Traversing the DAG + naively would visit the child multiple times — in the worst case, + exponentially many times. It is necessary to avoid doing so. +
          2. +
          3. + Bazel contains a function that takes a DAG as input and generates a DAG + as output (the output is an object graph). The Bazel developers want to + ensure that the size of the output DAG is O(|input DAG|). The input DAG is + processed bottom-up (it ensures that each input node is visited once) and + Bazel stores the results of intermediate computations that construct the + output DAG with nodes of the input DAG. The key thing the Bazel developers + want to avoid is copying intermediate subgraphs that have unbounded size. +
          4. +
          + +

          +More concretely, there is only one Java type for all DAGs, and there is a +method flatten(). It's a mistake to call +flatten() on certain DAGs, because doing so may cause +exponential blowup. +

          + + + +

          +The goal of this project would be to better understand the problem with +Bazel, to formalize it, and to create a program analysis that solves +the problem. You would evaluate your work by running it on the Bazel +codebase to discover latent problems, or by providing it to the Bazel +developers to run each time they propose a code change. The Bazel +team is interested in collaborating by evaluating a tool. +

          + + +

          Array indexing (index-out-of-bounds errors)

          + +

          +An index-out-of-bounds error occurs when a programmer provides an illegal +index to an array or list, as in a[i] or a.get(i) +where i is less than 0 or greater than the length of +a. In languages like C, this is disastrous: buffers +overflows lead to about 1/6 of all security vulnerabilities. In languages +like Java, the result is “merely” that the program crashes. In +both languages, it is desirable to prevent programmers from making this +error and to prevent users from suffering the bad consequences. +

          + +

          +This project will be a substantial case study with +the Index +Checker. The first goal is to identify its merits and limitations. +Does it scale up to big, +interesting programs? Are there common, important code patterns that it +fails to handle? Does it produce too many false positive warnings? Does +it place too heavy a burden on the user, either in terms of annotations or +in terms of complexity of the error messages? Worst of all, are there +unknown unsoundnesses in the tool? +The second goal is to improve its precision enough to make it usable by +real-world programmers. +

          + +

          +A related project is to extend +the Index Checker to handle +mutable collections such as +Lists, where the remove() method makes sound, +precise analysis very tricky. +

          + + +

          Bazel tool

          + + + +

          +This project is related to the +Bazel build system, and was +proposed by its development manager. +

          + +

          +The Bazel codebase contains 1586 occurrences of the @Nullable +annotation. This annotation indicates that a variable may hold a null +value. This is valuable documentation and helps programmers avoid null +pointer exceptions that would crash Bazel. However, these annotations are +not checked by any tool. Instead, programmers have to do their best to +obey the @Nullable specifications in the source code. This is +a lost opportunity, since documentation is most useful when it is +automatically processed and verified. (For several years, Google tried +using FindBugs, but they +eventually abandoned it: its analysis is too weak, suffering too many +false positives and false negatives.) +

          + +

          +Despite the programmers' best efforts, null pointer exceptions do still +creep into the code, impacting users. The Bazel developers would like to +prevent these. They want a guarantee, at compile time, that no null +pointer exceptions will occur at run time. +

          + +

          +Such a tool already exists: the +Nullness +Checker of the Checker +Framework. It runs as a compiler plug-in, and it issues a warning at +every possible null pointer dereference. If it issues no warnings, the +code is guaranteed not to throw a NullPointerException at run time. +

          + +

          +The goal of this project is to do a large-scale case study of the Nullness +Checker on Bazel. The main goal is to understand how the Nullness Checker +can be used on a large-scale industrial codebase. How many lurking bugs +does it find? What +@Nullable +annotations are missing from the codebase because the developers failed to +write them? What are its limitations, such as code patterns that it cannot +recognize as safe? (You might create new analyses and incorporating them +into the Nullness Checker, or you might just reporting bugs to the Nullness +Checker developers for fixing.) What burdens does it place on users? Is +the cost-benefit tradeoff worth the effort — that is, should Google +adopt this tool more broadly? How should it be improved? Are the most +needed improvements in the precision of the analysis, or in the UI of the +tooling? +

          + + +

          BCEL library

          + +

          + Annotate the BCEL library to express its contracts with respect to nullness. + Show that the BCEL library has no null pointer exceptions (or find bugs + in BCEL). There are + already some + annotations in BCEL, but they have not been verified as correct by + running the Nullness Checker on BCEL. (Currently, those annotations are + trusted when type-checking clients of BCEL.) +

          + +

          + To get started: +

          + +
            +
          • Fork https://github.com/typetools/commons-bcel.git
          • +
          • Clone your new fork
          • +
          • git checkout typecheck-nullness
          • +
          • mvn verify
          • +
          + +

          + Some challenging aspects of this case study are: +

          + +
            +
          • + There is some poor design that needs to be resolved in discussions with + the BCEL maintainers. For example, consider the copy() + method. Some implementations of copy() return null, but + are not documented to do so. In addition, some implementations + of copy() catch and ignore exceptions. I think it would + be nicest to change the methods to never return null, but to throw an + exception instead. (This is no more burdensome to users, who currently + have to check for null.) Alternately, the methods could all be + documented to return null. +
          • +
          + + + +

          Invent your own new type system

          + +

          +We also welcome your ideas for new type systems. For example, any run-time +failure can probably be prevented at compile time with the right +analysis. Can you come up with a way to fix your pet peeve? +

          + +

          +It is easiest, but not required, to choose an existing type system from the +literature, since that means you can skip the design stage and go right to +implementation. +

          + +

          +This task can be simple or very +challenging, depending on how ambitious the type system is. Remember to +focus on what helps a software developer most! +

          + + + +

          Bounded-size strings

          + + + +

          +Windows cannot run command lines longer than 8191 characters. Creating a +too-long command line causes failures when the program is run on Windows. +These failures are irritating when discovered during testing, and +embarrassing or worse when discovered during deployment. The same command +line would work on Unix, which has longer command-line limits, and as a +result developers may not realize that their change to a command can cause +such a problem. +

          + +

          +Programmers would like to enforce that they don't accidentally pass a +too-long string to the exec() routine. The goal of this +project is to give a compile-time tool that provides such a guarantee. +

          + +

          +Here are two possible solutions. +

          + +

          +Simple solution: +For each array and list, determine whether its length is known at compile +time. The routines that build a command line are only allowed to take such +constant-length lists, on the assumption that if the length is constant, +its concatenation is probably short enough. +

          + +

          +More complex solution: +For each String, have a compile-time estimate of its maximum length. Only +permit exec() to be called on strings whose estimate is no more than 8191. +String concatenation would return a string whose estimated size is the sum +of the maximums of its arguments, and likewise for concatenating an array +or list of strings. +

          + + +

          Lock ordering

          + +

          +The Lock +Checker prevents race conditions by ensuring that locks are held when +they need to be. It does not prevent deadlocks that can result from locks +being acquired in the wrong order. This project would extend the Lock +Checker to address deadlocks, or create a new checker to do so. +

          + +

          +Suppose that a program contains two different locks. Suppose that one +thread tries to acquire lockA then lockB, and another thread tries to +acquire lockB then lockA, and each thread acquires its first lock. Then +both locks will wait forever for the other lock to become available. The +program will not make any more progress and is said to +be deadlocked. +

          + +

          +If all threads acquire locks in the same order — in our example, say +lockA then lockB — then deadlocks do not happen. You will extend the +Lock Checker to verify this property. +

          + + +

          Upgrade to a newer version of ASM

          + +

          + The + Annotation + File Utilities, or AFU, insert annotations into, and extract + annotations from, .java files, .class files, + and text files. These programs were written before the + ASM bytecode library supported Java 8's + type annotations. Therefore, the AFU has its own custom version of ASM + that supports type annotations. Now that ASM 6 has been released and it + supports type annotations, the AFU needs to be slightly changed to use + the official ASM 6 library instead of its own custom ASM variant. +

          + +

          + This project is a good way to learn about .class files and + Java bytecodes: how they are stored, and how to manipulate them. +

          + +

          + (Kush Gupta is working on this project.) +

          + + +

          Stateful type systems

          + +

          +This project is to improve support for +typestate checking +

          + +

          +Ordinarily, a program variable has +the same type throughout its lifetime from when the variable is declared +until it goes out of scope. "Typestate" +permits the type of an object or variable to change in a controlled way. +Essentially, it is a combination of standard type systems with dataflow +analysis. For instance, a file object changes from unopened, to opened, to +closed; certain operations such as writing to the file are only permitted +when the file is in the opened typestate. Another way of saying this is +that write is permitted after open, but not after close. +Typestate +is applicable to many other types of software properties as well. +

          + +

          +Two typestate checking frameworks +exist for the Checker Framework. Neither is being maintained; a new one +needs to be written. +

          + + +

          Tool for analysis diffs

          + + +

          + Many program analyses are too verbose for a person to read their entire + output. However, after a program change, the analysis results may + change only slightly. An "analysis diff" tool could show the + difference between the analysis run on the old code and the analysis run + on the new code. +

          +
            +
          • + The analysis diffs may help the programmer to better understand the + changes. +
          • +
          • + Bug detection tools. such + as FindBugs or + the Checker Framework, have + extremely verbose output when first run on a program. Programmers + could examine and fix only the warnings about code they have changed + (and that they are currently thinking about). +
          • +
          • + Tools that always have large output, such as inference tools, could + become manageable to users if output is shown in small doses. +
          • +
          • + You can probably think of other uses. +
          • +
          + +

          + The analysis diff tool would take as input two analysis results (the + previous and the current one). It would output only the new parts of its + second input. (It could optionally output a complete diff between two + analysis results.) +

          + +

          + One challenge is dealing with changed line numbers and other analysis + output differences between runs. +

          + +

          +It would be nice to integrate the tool with git pre-commit hooks or GitHub +pull requests, to enable either of the following functionality (for either +commits to master or for pull requests): +

          +
            +
          • + Permit only those commits/pulls that do not add any new analysis warnings. +
          • +
          • + Permit only those commits/pulls that are "clean" — the analysis + issues no warnings for any changed line. +
          • +
          + +

          + A concrete example of an analysis diff tool + is checklink-persistent-errors; + see the documentation at the top of the file. That tool only works for + one particular analysis, the W3C Link Checker. + An analysis diff tool also appears to be built into FindBugs. + The goal of this project is to build a general-purpose tool that is easy + to apply to new analyses. +

          + + +

          Performance improvements

          + +

          + The Checker Framework runs much slower than the standard javac compiler + — often 20 times slower! This is not acceptable as part of a + developer's regular process, so we need to speed up the Checker + Framework. This project involves determining the cause of slowness in + the Checker Framework, and correcting those problems. +

          + +

          +This is a good way to learn about performance tuning for Java applications. +

          + +

          + Some concrete tasks include: +

          + +
            +
          • Profile the Checker Framework. Run a profiler such as + YourKit to determine + which parts of the Checker Framework consume the most CPU time and memory. +
          • + +
          • Perhaps compare a profile of the Checker Framework against a profile + of regular javac. This probably is not necessary because the Checker + Framework is so much slower than regular javac. +
          • + +
          • Consider interning string values. The Checker Framework does a fair + amount of string manipulation, in part because it reads from resources + such as stub files that do not produce Elements. Interning + could save time when doing comparisons. You can verify the correctness + of the optimization by running the + Interning + Checker on the Checker Framework code. Compare the run time of the + Checker Framework before and after this optimization. +
          • + +
          • Based on profiling results, devise other optimizations, implement + them, and evaluate them. +
          • +
          + + +

          Run-time checking

          + +

          +Implement run-time checking to complement compile-time checking. This will +let users combine the power of static checking with that of dynamic +checking. +

          + +

          +Every type system is too strict: it rejects some programs that never go +wrong at run time. A human must insert a type loophole to make such a +program type-check. For example, Java takes this approach with its +cast operation (and in some other places). +

          + +

          +When doing type-checking, it is desirable to automatically insert run-time +checks at each operation that the static checker was unable to verify. +(Again, Java takes exactly this approach.) This guards against mistakes by +the human who inserted the type loopholes. A nice property of this +approach is that it enables you to prevent errors in a program +with no type annotations: whenever the static checker is unable +to verify an operation, it would insert a dynamic check. Run-time checking +would also be useful in verifying whether the suppressed warnings are +correct — whether the programmer made a mistake when writing them. +

          + +

          +The annotation processor (the pluggable type-checker) should automatically +insert the checks, as part of the compilation process. +

          + +

          +There should be various modes for the run-time checks: +

          +
            +
          • fail immediately.
          • +
          • logging, to permit post-mortem debugging without crashing the program.
          • +
          + +

          +The run-time penalty should be small: a run-time check is necessary only +at the location of each cast or suppressed warning. Everywhere that the +compile-time checker reports no possible error, there is no need to insert a +check. But, it will be an interesting project to determine how to minimize +the run-time cost. +

          + +

          +Another interesting, and more challenging, design question is whether you need to add and maintain +a run-time representation of the property being tested. It's easy to test +whether a particular value is null, but how do you test whether it is +tainted, or should be treated as immutable? For a more concrete example, +see the discussion of the (not yet implemented) +[Javari run-time checker](http://pag.csail.mit.edu/pubs/ref-immutability-oopsla2005-abstract.html). +Adding this run-time support would be an interesting and challenging project. +

          + +

          +We developed a prototype for the +EnerJ runtime system. +That code could be used as starting point, or you could start afresh. +

          + +

          +In the short term, this could be prototyped as a source- or +bytecode-rewriting approach; but integrating it into the type checker is a +better long-term implementation strategy. +

          + + + +

          IDE and build system support

          + +

          +The Checker Framework comes with support for +external tools, +including both IDEs (such as an Eclipse plug-in) and build tools +(instructions for Maven, etc.). +

          + +

          + These plug-ins and other integration should be improved. + We have a number of concrete ideas, but you will also probably come up + with some after a few minutes of using the existing IDE plugins! +

          + +

          + This is only a task for someone who is already an expert, such as someone + who has built IDE plugins before or is very familiar with the build system. + One reason is that these tools tend to be complex, which can lead to + subtle problems. + Another reason is that we don't want to be stuck maintaining code written by + someone who is just learning how to write an IDE plugin. +

          + +

          + Rather than modifying the Checker Framework's existing support or + building new support from scratch, it may be better to adapt some other + project's support for build systems and IDEs. For instance, you might + make coala support the + Checker Framework, or you might adapt the tool integration provided + by Error Prone. +

          + + + + + +

          Model checking of a type system

          + +

          +Design and implement an algorithm to check type soundness of a type system +by exhaustively verifying the type checker on all programs up to a certain +size. The challenge lies in efficient enumeration of all programs and +avoiding redundant checks, and in knowing the expected outcome of the +tests. This approach is related to bounded exhaustive +testing and model checking; for a reference, see +[Efficient Software Model Checking of Soundness of Type Systems](http://www.eecs.umich.edu/~bchandra/publications/oopsla08.pdf). +

          + + +

          +A valuable project all by itself would be to compare heavy-weight and +light-weight type inference this whole-program inference vs. Checker +Framework Inference vs. Julia, to understand when each one is worth using. +

          + + +

          Determinism

          + +

          +Programs are easier to use and debug if their output is deterministic. For +example, it is easier to test a deterministic program, because +nondeterminism can lead to flaky tests that sometimes succeed and sometimes +fail. As another example, it is easier for a user or programmer to compare +two deterministic executions than two nondeterministic executions. +

          + +

          + We have created a prototype Determinism Checker. It is documented in + this draft + manual chapter, and its implementation is in +t-rasmud/nondet-checker. +You will do a case study of this type system. +

          + +

          Detecting errors in use of the Optional class

          + + + +

          +Java 8 introduced the +Optional +class, a container that is either empty or contains a non-null value. +It is intended to solve the problem of null +pointer exceptions. However, Optional +has its + own problems, leading +to extensive + advice on when and how to use Optional. It is difficult for +programmers to remember all these rules. +

          + +

          +The goal of this project is to build a tool to check uses of Optional and run it on open-source projects. The research questions are: +

          +
            +
          • Is it possible to build a tool that enforces good style in using Optional? +
          • +
          • How much effort is is for programmers to use such a tool? +
          • +
          • Does real-world code obey the rules about use of Optional, or not? +
          • +
          + +

          +We have +a prototype + verification tool that checks some but not all rules about use of +Optional (https://checkerframework.org/manual/#optional-checker). This +project will do case studies of it and extend it. +

          + +

          +The methodology is to find open-source projects that use Optional(you can +do this by searching GitHub, for example), run the tool on them, and read +the tool's warnings. Each warning will lead to either a bug report against +an open-source project or an improvement to the verification tool. +

          + + + + diff --git a/docs/developer/release/.flake8 b/docs/developer/release/.flake8 new file mode 100644 index 000000000000..5376b8ae3bdb --- /dev/null +++ b/docs/developer/release/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 80 +select = C,E,F,W,B,B950 +ignore = E203, E501, W503 diff --git a/docs/developer/release/.pylintrc b/docs/developer/release/.pylintrc deleted file mode 100644 index 44b6c41deaa6..000000000000 --- a/docs/developer/release/.pylintrc +++ /dev/null @@ -1,380 +0,0 @@ -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Pickle collected data for later comparisons. -persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Use multiple processes to speed up Pylint. -jobs=1 - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Allow optimization of some AST trees. This will activate a peephole AST -# optimizer, which will apply various small optimizations. For instance, it can -# be used to obtain the result of joining multiple strings with the addition -# operator. Joining a lot of strings can lead to a maximum recursion error in -# Pylint and this flag can prevent that. It has one side effect, the resulting -# AST will be different than the one from reality. -optimize-ast=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time. See also the "--disable" option for examples. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating - -disable=fixme,line-too-long,wildcard-import,invalid-name,too-many-arguments,too-many-locals,too-many-branches,too-many-statements,too-many-lines,too-many-function-args,unused-wildcard-import - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# myPackage1.myPackage2.MyReporterClass. -output-format=text - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells whether to display a full report or only the messages -reports=yes - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_$|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). This supports can work -# with qualified names. -ignored-classes= - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=100 - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - - -[BASIC] - -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,input - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct constant names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression matching correct attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - -# Regular expression matching correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - - -[ELIF] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception diff --git a/docs/developer/release/Makefile b/docs/developer/release/Makefile new file mode 100644 index 000000000000..e23ecc1863af --- /dev/null +++ b/docs/developer/release/Makefile @@ -0,0 +1,8 @@ +python-style: + black . + flake8 + +check-python-style: + black --version + black . --check --diff + flake8 diff --git a/docs/developer/release/README-release-process.html b/docs/developer/release/README-release-process.html index 1d7a49cdb371..03254a9c69ac 100644 --- a/docs/developer/release/README-release-process.html +++ b/docs/developer/release/README-release-process.html @@ -1,6 +1,7 @@ + Checker Framework release process @@ -59,6 +60,16 @@

          Release Process: Annotation File Utilities, Checker Framework
          • Step by Step
          • Continuous integration tests
          • +
          • Deploy to local Maven repository
          • +
          • Snapshot release +
          • Pre-release Checklist
          • Release Process Overview
              @@ -105,17 +116,20 @@

              Step by Step

              Update stubparser. If there has been a JavaParser release since the last Checker Framework - release, update + release, update Stubparser from JavaParser. + Do this on your own computer; it doesn't have to be done in the Release Docker image. -
            • Update the Checker Framework source code: +
            • On your own computer, update the Checker Framework source code in the master branch:
              • - Update the list of contributors by running - make contributors.tex - in checker-framework/docs/manual/. Double-check the edits - with git diff; update the plume-scripts/git-authors.sed script if - any customizations are necessary. + Update the AWS Java SDK BOM dependency. In file checker/build.gradle, + edit the com.amazonaws:aws-java-sdk-bom dependency to be the latest version + number at + https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk. + (The next time Dependabot opens a pull request for that dependency, you + might need to respond @dependabot ignore this dependency to + prevent such pull requests for the next month.)
              • Update AFU and Checker Framework change logs by following the instructions @@ -124,10 +138,10 @@

                Step by Step

              • Update the Checker Framework version number in checker-framework/build.gradle, in the allprojects block. - (The AFU version is updated as part of the release scripts.)

                - Update the minor version if there are any incompatibilities with the previous version. + Update the minor version (middle number in the version number) + if there are any incompatibilities with the previous version. This includes incompatibilities for people who are maintaining a checker, which happens if the signature changes for any method in these classes:

                @@ -156,40 +170,43 @@

                Step by Step

                (TODO: Write a command that diffs these classes between the previous and current release.)

                - A rule of thumb is that any framework change that requires changes to more than one type-checker should be at least a minor version bump. + A rule of thumb is that any framework change that requires changes to more than one type-checker should be at least a minor version (middle number in the version number) bump.

              • - Commit and push these changes to typetools/master. + Update the Annotation Tools version number, again in the master branch, + in annotation-file-utilities/build.gradle. + (TODO: Are any other replacements necessary?) +
              • +
              • + Commit and push these changes to eisop/master.
            • - Log into Buffalo
              - ssh $USER@buffalo.cs.washington.edu
              + Log into the Release Docker image and ensure you are using JDK 8 (so that javac.jar is copied to the distribution)
              + TODO
            • In a user-specific temporary directory, clone/update the Checker Framework repository (it contains the release scripts).
              - mkdir -p /scratch/$USER/jsr308-release
              - cd /scratch/$USER/jsr308-release
              - test -d checker-framework && (cd checker-framework && git pull --quiet) || git clone --quiet https://github.com/typetools/checker-framework.git
              + + mkdir -p /tmp/$USER/cf-release
              + chown -R types_www /tmp/$USER/cf-release
              + chmod -R g+rw /tmp/$USER/cf-release
              + cd /tmp/$USER/cf-release
              + test -d checker-framework && (cd checker-framework && git pull --ff-only --quiet) || git clone --quiet https://github.com/eisop/checker-framework.git
              cd checker-framework/docs/developer/release
              (The release scripts will checkout and build all dependencies.)
            • Run release_build to create the release artifacts and place them in the development website
              - python release_build.py all

              - [TODO: Add --auto here, or not?]
              + git pull && python3 release_build.py all

              For verbose debugging output, use the --debug option.

              Note: The "all" argument specifies that all projects should be built. There has been some work done in order to select only specific projects (AFU and Checker Framework) to be built but more work still needs to be done. Just pass "all" for now. - See also Release in Continuous Integration for more information - on the option "--auto", which you can omit in order to - be asked more questions during the build.
            • - Run release_push to move release artifacts from the development website to the live site and to Maven Central
              - python release_push.py release

              - For minimal prompting, use the --auto option.

              + Run release_push to run a few tests and move release artifacts from the development website to the live site and to Maven Central
              + python3 release_push.py release

              Note: The "release" argument states that you intend to do an actual release. If you just want to test the script, leave out "release" @@ -200,16 +217,20 @@

              Step by Step

              in a row. This will sometimes update the repository permissions such that the script can proceed further each time.
            • -
            • Update the Gradle plugin. - Make a pull request - against kelloggm/checkerframework-gradle-plugin - to update the Checker Framework version number by following the instructions in the - documentation. -
            • Update list of qualifiers in Project Lombok. - Following the instructions in HandlerUtil.java. + Follow the instructions in HandlerUtil.java. If the release did not add, remove, or rename any type qualifiers, no changes are required. - Those instructions do not work on macOS, so you may need to use buffalo to make the changes. + Those instructions do not work on macOS, so you may need to use the + Release Docker image to make the changes. +
            • +
            • Update the Annotation Tools' use of the Checker Framework + by making a pull request that changes two occurrences of the old version number in + annotation-file-utilities/build.gradle. +
            • +
            • Make Daikon use the latest Checker Framework version. + + Follow the instructions in + java/lib/README to update the Checker Framework version number in Daikon.

    @@ -217,23 +238,105 @@

    Continuous integration tests

    The following continuous integration tests should all pass before you make changes that you need to test. If not, notify the - relevant team members. + relevant team members. Refresh this page to see the latest status.

    -
      -
    1. The following Continuous Integration builds should be passing (refresh this page to see the latest status):
        -
      • Annotation File Utilities: Travis typetools/annotation-tools status
      • -
      • Checker Framework: Azure Pipelines typetools/checker-framework status
      • -
      • checker-framework.demos: Travis typetools/checker-framework.demos status
      • -
      • checker-framework-inference: Travis typetools/checker-framework-inference status
      • -
      • Daikon: codespecs/daikon Azure Pipelines status
      • -
      • guava-typecheck: Travis typetests/guava-typecheck status
      • +
      • Annotation File Utilities: Azure Pipelines eisop/annotation-tools status
      • +
      • Checker Framework: Azure Pipelines eisop/checker-framework status
      • +
      • checker-framework.demos: Travis eisop/checker-framework.demos status
      • +
      • Daikon: codespecs/daikon Azure Pipelines status
      -
    2. -
    3. Check that a daikon-typecheck Travis job was triggered by the last commit of the Checker Framework before the code freeze. -
    4. -
    + +

    Deploy to local Maven repository

    +

    + To deploy to a local Maven repository, run ./gradlew + PublishToMavenLocal in checker-framework. +

    + +

    + Then, update your project's buildfile. For Maven, + just update + the Checker Framework version number. For Gradle, + also add repositories + { mavenLocal() }. +

    + +

    + Often, the version number in checker-framework/build.gradle + (in allprojects { version ... }) will end + in -SNAPSHOT, but this is not a requirement. Regardless of the + version number, beware that you may get different results than on other + computers that have not deployed the same commit of the Checker Framework to + their local Maven repository. +

    + + +

    Snapshot release

    +

    To release the Maven artifacts to the Maven Central snapshot repository:

    +
      +
    1. + Ensure that ~/.gradle/gradle.properties includes your SONATYPE_NEXUS_USERNAME + and SONATYPE_NEXUS_PASSWORD. +
    2. +
    3. + Ensure that the version number in checker-framework/build.gradle (in allprojects { version ... }) ends in -SNAPSHOT. +
    4. +
    5. + Run ./gradlew publish in checker-framework. +
    6. +
    + +

    Using a snapshot release

    + +

    Gradle Groovy (build.gradle file)

    + + In the build.gradle file of a project that you want + to use the snapshot version of the Checker Framework: +
    +repositories {
    +  ...
    +  maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
    +}
    +
    +ext.checkerFrameworkVersion = '3.28.1-SNAPSHOT'
    +dependencies {
    +  compileOnly "io.github.eisop:checker-qual:${checkerFrameworkVersion}"
    +  testCompileOnly "io.github.eisop:checker-qual:${checkerFrameworkVersion}"
    +  checkerFramework "io.github.eisop:checker:${checkerFrameworkVersion}"
    +}
    +configurations.all {
    +  resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
    +}
    + +

    Gradle Kotlin (build.gradle.kts file)

    + + In the build.gradle.kts file of a project that you want + to use the snapshot version of the Checker Framework: + +
    +val checkerFrameworkVersion = "3.41.1-SNAPSHOT"
    +dependencies {
    +   compileOnly("io.github.eisop:checker-qual:${checkerFrameworkVersion}")
    +   testCompileOnly("io.github.eisop:checker-qual:${checkerFrameworkVersion}")
    +   checkerFramework("io.github.eisop:checker:${checkerFrameworkVersion}")
    +}
    +configurations.all({
    +   resolutionStrategy.cacheChangingModulesFor(0, "seconds")
    +})
    + +

    Maven (pom.xml file)

    + + In the pom.xml file of a project that you want + to use the snapshot version of the Checker Framework: + +
    +    <repository>
    +      <id>snapshots-repo</id>
    +      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
    +      <releases><enabled>false</enabled></releases>
    +      <snapshots><enabled>true</enabled></snapshots>
    +    </repository>

    Pre-release Checklist

    If you have not performed a release before you must follow these steps.

    @@ -241,7 +344,7 @@

    Pre-release Checklist

    1. Ensure you are a member of the types_www and pl_gang groups
    - Run the command "groups" on the file system (perhaps on Buffalo). + Run the command "groups" on the CSE file system (perhaps on tern). If the group types_www or pl_gang do not appear in the list, email the appropriate person to be added (currently Michael Ernst). @@ -249,7 +352,7 @@

    Pre-release Checklist

    2. Import the Checker Framework signing key for PGP
    - SSH into Buffalo and run the following command:
    + SSH into tern and run the following command:
    gpg --allow-secret-key-import --import /projects/swlab1/checker-framework/hosting-info/release-private.key

    Note: The password for this key is located in the file
    /projects/swlab1/checker-framework/hosting-info/release-private.password
    @@ -260,7 +363,7 @@

    Pre-release Checklist

    Sign up for a Sonatype Account
    You will likely want to do this a few days in advance. Directions can be found here. - Remember to be asked to be added to the org.checkerframework repository by creating + Remember to be asked to be added to the io.github.eisop repository by creating a ticket (see the note here). If after signing up for a Sonatype JIRA account you are not able to sign in to @@ -272,11 +375,13 @@

    Pre-release Checklist

    4. - Add your account information to settings.xml in your home directory.
    - Create a ~/.m2/settings.xml file with the contents specified in - /projects/swlab1/checker-framework/hosting-info/release-settings.txt - using the information you just created for your Sonatype Account on Buffalo or other - network host. Since the file contains your password, make it non-readable: chmod og-rw ~/.m2/settings.xml + Add your account information to gradle/build.properties in your home directory.
    + Create a ~/.gradle/gradle.properties file with the following: +
    +SONATYPE_NEXUS_USERNAME=your_user_name
    +SONATYPE_NEXUS_PASSWORD=your_password
    + using the information you just created for your Sonatype Account on tern or other + network host. Since the file contains your password, make it non-readable: chmod og-rw ~/.gradle/gradle.properties @@ -286,22 +391,21 @@

    Pre-release Checklist

    issue tracker for each project that has been released. You will need to be a "developer" for each project so that you have update privileges for the issue trackers. - You should be listed as a member of typetools on Bitbucket and a committer on GitHub. - You should be listed as a member of typetests on Bitbucket and a committer on GitHub. + You should be listed as a member of eisop as a committer on GitHub. 6. Install html5validator
    - If you are going to perform the release on buffalo, you may need to install - html5validator. If html5validator --version issues any errors, try running - pip install --user html5validator. You may need to add .local/bin + If you are going to perform the release outside the Release Docker image, + you may need to install html5validator. If html5validator --version issues any errors, + try running pip3 install --user html5validator. You may need to add .local/bin to your path. 7. - Add GitHub SSH keys to buffalo
    + Add GitHub SSH keys to your Release Docker image
    See GitHub docs. @@ -327,20 +431,20 @@

    Pre-release Checklist

    Release Process Overview

    -

    This section first explains the structure of the projects on disk on Buffalo, then lists scripts used during the release process.

    +

    This section first explains the structure of the projects on disk and then lists scripts used during the release process.

    File Layout

    - + @@ -348,12 +452,12 @@

    File Layout

    @@ -369,27 +473,8 @@

    File Layout

    won't be automatically committed when the release is committed. -
    Release Directory
    /scratch/$USER/jsr308-release/tmp/$USER/cf-release Contains repositories and scripts to perform a release
    build Contains repositories for: - annotation-tools, checker-framework, plume-bib, plume-scripts, stubparser
    + annotation-tools, checker-framework, plume-bib, plume-scripts
    These repositories are used to build the Checker Framework and its dependencies.
    interm Contains repositories for: - annotation-tools, checker-framework, plume-bib, plume-scripts, stubparser
    + annotation-tools, checker-framework, plume-bib, plume-scripts
    The repositories in build are clones of repositories in interm. The repositories - in interm are clones of the GitHub/Bitbucket repositories. This is so that + in interm are clones of the GitHub repositories. This is so that we can test and push the release to the interm repositories then check the release before we have made any irreversible changes. Then, when the release - is validated, all we do is run "git push" or "hg push" on all of the repos in interm. + is validated, all we do is run "git push" on all of the repos in interm.
    - - - - - - - - - - - - - - - - - -
    Jenkins Tools Directory
    /scratch/secs-jenkinsContains files used to run Jenkins including versions of Java.
    javaContains the versions of Java used to build the release.
    toolsContains the Hevea library and some TEXINPUTS for the release.
    @@ -404,8 +489,8 @@

    File Layout

    https://checkerframework.org/m2-repo -
    Live Website Directory
    + @@ -438,7 +523,7 @@

    Release Scripts

    Staging (Development) Website Directory
    - @@ -493,24 +578,24 @@

    Changelog Guidelines

    Content Guidelines

    release_build.pyReverts the build/interm repositories to the state of their master repositories in GitHub/Bitbucket. + Reverts the build/interm repositories to the state of their master repositories in GitHub. It then builds the projects and all their artifacts and then stages a development version of all websites to https://checkerframework.org/dev/ @@ -469,8 +554,8 @@

    Release Scripts

    release_vars.py Global variables used in release_push, release_build, and sanity_checks. These should NOT be used in release_utils as release_utils is supposed to consist of self-contained - reusable methods. These variables are tailored for running the scripts on - buffalo.cs.washington.edu + reusable methods. These variables are tailored for running the scripts in + the Release Docker image.