aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCole Faust <colefaust@google.com>2023-11-20 19:49:19 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2023-11-20 19:49:19 +0000
commit3b51819bb2cb1e6709d975f733e406b3c3b585d7 (patch)
tree3a5ad6fbea5677f86b08c9b99fa0f114694af7b1
parent8c39c391700063ef0600273d37fa9671a6f884fa (diff)
parent683dfed77840832ef99b33df54f4dde40d4ead08 (diff)
downloadnullaway-3b51819bb2cb1e6709d975f733e406b3c3b585d7.tar.gz
Merge commit '24db25eb2160a04377dd8b278b3cf99a89db914a' into update am: 683dfed778
Original change: https://android-review.googlesource.com/c/platform/external/nullaway/+/2831253 Change-Id: Ib783f557c18215d248e7fd285d6d9237f580cffe Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rwxr-xr-x.buildscript/check_git_clean.sh2
-rw-r--r--.github/codecov.yml4
-rw-r--r--.github/workflows/continuous-integration.yml110
-rw-r--r--.github/workflows/gcloud_ssh.sh11
-rw-r--r--.github/workflows/get_repo_details.sh20
-rw-r--r--.github/workflows/jmh-benchmark.yml73
-rw-r--r--.github/workflows/run_gcp_benchmarks.sh20
-rw-r--r--.github/workflows/run_main_benchmarks.sh9
-rw-r--r--.github/workflows/run_pr_benchmarks.sh9
-rwxr-xr-xCHANGELOG.md341
-rwxr-xr-xCONTRIBUTING.md2
-rw-r--r--README.md38
-rwxr-xr-xRELEASING.md27
-rw-r--r--annotations/build.gradle26
-rwxr-xr-xannotations/gradle.properties19
-rw-r--r--annotations/src/main/java/com/uber/nullaway/annotations/Initializer.java49
-rw-r--r--build.gradle94
-rw-r--r--buildSrc/src/main/groovy/nullaway.jacoco-conventions.gradle62
-rw-r--r--buildSrc/src/main/groovy/nullaway.java-test-conventions.gradle120
-rw-r--r--code-coverage-report/build.gradle25
-rwxr-xr-xconfig/hooks/pre-commit2
-rw-r--r--gradle.properties8
-rwxr-xr-xgradle/dependencies.gradle61
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin59536 -> 63721 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties5
-rwxr-xr-xgradlew41
-rw-r--r--gradlew.bat15
-rw-r--r--guava-recent-unit-tests/build.gradle57
-rw-r--r--guava-recent-unit-tests/src/test/java/com/uber/nullaway/guava/NullAwayGuavaParametricNullnessTests.java152
-rw-r--r--jar-infer/android-jarinfer-models-sdk28/build.gradle2
-rw-r--r--jar-infer/android-jarinfer-models-sdk29/build.gradle2
-rw-r--r--jar-infer/android-jarinfer-models-sdk30/build.gradle2
-rw-r--r--jar-infer/android-jarinfer-models-sdk31/build.gradle2
-rw-r--r--jar-infer/jar-infer-cli/build.gradle62
-rw-r--r--jar-infer/jar-infer-lib/build.gradle40
-rw-r--r--jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/BytecodeAnnotator.java6
-rw-r--r--jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/DefinitelyDerefedParamsDriver.java12
-rw-r--r--jar-infer/jar-infer-lib/src/test/java/com/uber/nullaway/jarinfer/JarInferTest.java15
-rw-r--r--jar-infer/nullaway-integration-test/build.gradle38
-rw-r--r--jar-infer/test-android-lib-jarinfer/build.gradle3
-rw-r--r--jar-infer/test-java-lib-jarinfer/build.gradle16
-rw-r--r--jdk-recent-unit-tests/build.gradle57
-rw-r--r--jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayInstanceOfBindingTests.java47
-rw-r--r--jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayModuleInfoTests.java (renamed from jdk17-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayModuleInfoTests.java)0
-rw-r--r--jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayOptionalEmptyTests.java197
-rw-r--r--jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayRecordTests.java (renamed from jdk17-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayRecordTests.java)74
-rw-r--r--jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwaySwitchTests.java (renamed from jdk17-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwaySwitchTests.java)32
-rw-r--r--jdk17-unit-tests/build.gradle60
-rw-r--r--jmh/build.gradle66
-rw-r--r--jmh/src/jmh/java/com/uber/nullaway/jmh/AutodisposeBenchmark.java22
-rw-r--r--jmh/src/jmh/java/com/uber/nullaway/jmh/CaffeineBenchmark.java73
-rw-r--r--jmh/src/jmh/java/com/uber/nullaway/jmh/DFlowMicroBenchmark.java24
-rw-r--r--jmh/src/jmh/java/com/uber/nullaway/jmh/NullawayReleaseBenchmark.java46
-rw-r--r--jmh/src/main/java/com/uber/nullaway/jmh/AbstractBenchmarkCompiler.java82
-rw-r--r--jmh/src/main/java/com/uber/nullaway/jmh/AutodisposeCompiler.java45
-rw-r--r--jmh/src/main/java/com/uber/nullaway/jmh/CaffeineCompiler.java112
-rw-r--r--jmh/src/main/java/com/uber/nullaway/jmh/DataFlowMicroBenchmarkCompiler.java47
-rw-r--r--jmh/src/main/java/com/uber/nullaway/jmh/NullawayJavac.java68
-rw-r--r--jmh/src/main/java/com/uber/nullaway/jmh/NullawayReleaseCompiler.java61
-rw-r--r--jmh/src/main/resources/DFlowBench.java587
-rw-r--r--jmh/src/test/java/com/uber/nullaway/jmh/BenchmarkCompilationTest.java30
-rw-r--r--nullaway/build.gradle161
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/ASTHelpersBackports.java39
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/AbstractConfig.java108
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/CodeAnnotationInfo.java310
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/Config.java77
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/DummyOptionsConfig.java37
-rwxr-xr-xnullaway/src/main/java/com/uber/nullaway/ErrorBuilder.java178
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java14
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java72
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/GenericsChecks.java1060
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/LibraryModels.java45
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/NullAway.java991
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java151
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/Nullness.java42
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/annotations/JacocoIgnoreGenerated.java14
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPath.java332
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathElement.java9
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessAnalysis.java53
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java210
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java64
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/dataflow/DataFlow.java34
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/dataflow/NullnessStore.java30
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/dataflow/cfg/NullAwayCFGBuilder.java202
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/FixSerializationConfig.java207
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/SerializationService.java158
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/Serializer.java228
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/XMLUtil.java184
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/adapters/SerializationAdapter.java74
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/adapters/SerializationV1Adapter.java75
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/adapters/SerializationV3Adapter.java125
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/location/AbstractSymbolLocation.java61
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/location/FieldLocation.java52
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/location/MethodLocation.java52
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/location/MethodParameterLocation.java75
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/location/SymbolLocation.java69
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ClassAndMemberInfo.java137
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ErrorInfo.java141
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/out/FieldInitializationInfo.java68
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/fixserialization/out/SuggestedNullableFixInfo.java101
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/AbstractFieldContractHandler.java22
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/ApacheThriftIsSetHandler.java71
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/AssertionHandler.java27
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/BaseNoOpHandler.java81
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/CompositeHandler.java115
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/FieldInitializationSerializationHandler.java97
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/GrpcHandler.java33
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/GuavaAssertionsHandler.java75
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/Handler.java194
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/Handlers.java14
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java85
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java389
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/LombokHandler.java89
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/MethodNameUtil.java135
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/OptionalEmptinessHandler.java360
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/RestrictiveAnnotationHandler.java160
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/StreamNullabilityPropagator.java41
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/StreamNullabilityPropagatorFactory.java19
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractCheckHandler.java3
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractHandler.java378
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractNullnessStoreInitializer.java4
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractUtils.java25
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/contract/fieldcontract/EnsuresNonNullHandler.java25
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/contract/fieldcontract/RequiresNonNullHandler.java6
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/stream/MaplikeMethodRecord.java1
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/stream/MaplikeToFilterInstanceRecord.java1
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/stream/StreamModelBuilder.java27
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/stream/StreamTypeRecord.java7
-rw-r--r--nullaway/src/main/java/com/uber/nullaway/handlers/temporary/FluentFutureHandler.java91
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/DummyOptionsConfigTest.java2
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayAccessPathsTests.java264
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayAcknowledgeRestrictiveAnnotationsTests.java148
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayAssertionLibsTests.java224
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayAutoSuggestNoCastTest.java39
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayAutoSuggestTest.java251
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayContractsBooleanTests.java570
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayContractsTests.java173
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayCoreTests.java377
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayCustomLibraryModelsTests.java230
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayFrameworkTests.java626
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayFunctionalInterfaceNullabilityTests.java110
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayGuavaAssertionsTests.java355
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayInitializationTests.java54
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyArrayTests.java118
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyGenericsTests.java1506
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyTests.java1133
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayOptionalEmptinessTests.java177
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwaySerializationTest.java2180
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayThriftTests.java49
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayTypeUseAnnotationTests.java38
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayTypeUseAnnotationsTests.java221
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayUnannotatedTests.java63
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/NullAwayVarargsTests.java82
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/handlers/contract/ContractUtilsTest.java37
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/tools/Display.java26
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/tools/DisplayFactory.java37
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/tools/ErrorDisplay.java161
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/tools/FieldInitDisplay.java94
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/tools/FixDisplay.java98
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/tools/SerializationTestHelper.java205
-rw-r--r--nullaway/src/test/java/com/uber/nullaway/tools/version1/ErrorDisplayV1.java164
-rw-r--r--nullaway/src/test/resources/com/uber/nullaway/testdata/CheckFieldInitNegativeCases.java2
-rwxr-xr-xnullaway/src/test/resources/com/uber/nullaway/testdata/CheckFieldInitPositiveCases.java2
-rw-r--r--nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayNativeModels.java4
-rw-r--r--nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayNegativeCases.java7
-rw-r--r--nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayPositiveCases.java3
-rw-r--r--nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayStreamSupportNegativeCases.java56
-rw-r--r--nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayStreamSupportPositiveCases.java44
-rw-r--r--nullaway/src/test/resources/com/uber/nullaway/testdata/ReadBeforeInitNegativeCases.java2
-rw-r--r--nullaway/src/test/resources/com/uber/nullaway/testdata/ReadBeforeInitPositiveCases.java2
-rw-r--r--nullaway/src/test/resources/com/uber/nullaway/testdata/Util.java15
-rw-r--r--nullaway/src/test/resources/com/uber/nullaway/testdata/springboot-annotations/MockBean.java13
-rw-r--r--nullaway/src/test/resources/com/uber/nullaway/testdata/springboot-annotations/SpyBean.java13
-rw-r--r--nullaway/src/test/resources/com/uber/nullaway/testdata/unannotated/CustomStream.java43
-rw-r--r--nullaway/src/test/resources/com/uber/nullaway/testdata/unannotated/CustomStreamWithoutModel.java43
-rw-r--r--nullaway/src/test/resources/com/uber/nullaway/testdata/unannotated/MinimalUnannotatedClass.java42
-rw-r--r--sample-app/build.gradle34
-rw-r--r--sample-app/src/main/java/com/uber/myapplication/MainFragment.java1
-rw-r--r--sample-library-model/build.gradle17
-rw-r--r--sample-library-model/src/main/java/com/uber/modelexample/ExampleLibraryModels.java5
-rw-r--r--sample/build.gradle17
-rw-r--r--settings.gradle23
-rw-r--r--test-java-lib-lombok/build.gradle25
-rw-r--r--test-java-lib-lombok/src/main/java/com/uber/lombok/LombokDTO.java (renamed from test-java-lib-lombok/src/main/java/com/uber/lombok/LombokBuilderInit.java)11
-rw-r--r--test-java-lib-lombok/src/main/java/com/uber/lombok/UsesDTO.java (renamed from test-java-lib-lombok/src/main/java/com/uber/lombok/UsesBuilder.java)15
-rw-r--r--test-java-lib/build.gradle18
-rw-r--r--test-java-lib/src/main/java/com/example/jspecify/annotatedpackage/Utils.java13
-rw-r--r--test-java-lib/src/main/java/com/example/jspecify/annotatedpackage/package-info.java4
-rw-r--r--test-java-lib/src/main/java/com/example/jspecify/unannotatedpackage/Methods.java30
-rw-r--r--test-java-lib/src/main/java/com/example/jspecify/unannotatedpackage/Outer.java14
-rw-r--r--test-java-lib/src/main/java/com/example/jspecify/unannotatedpackage/TopLevel.java10
-rw-r--r--test-java-lib/src/main/java/com/uber/lib/CFNullableStuff.java3
-rw-r--r--test-java-lib/src/main/java/com/uber/lib/unannotated/UnannotatedWithModels.java16
-rw-r--r--test-library-models/build.gradle17
-rw-r--r--test-library-models/src/main/java/com/uber/nullaway/testlibrarymodels/TestLibraryModels.java56
195 files changed, 20246 insertions, 2133 deletions
diff --git a/.buildscript/check_git_clean.sh b/.buildscript/check_git_clean.sh
index 8668c07..1ffa522 100755
--- a/.buildscript/check_git_clean.sh
+++ b/.buildscript/check_git_clean.sh
@@ -1,3 +1,5 @@
+#!/bin/sh -eux
+
if [ -n "$(git status --porcelain)" ]; then
echo 'warning: source tree contains uncommitted changes; .gitignore patterns may need to be fixed'
git status
diff --git a/.github/codecov.yml b/.github/codecov.yml
new file mode 100644
index 0000000..0f6ecbe
--- /dev/null
+++ b/.github/codecov.yml
@@ -0,0 +1,4 @@
+codecov:
+ # Disable report age checking since hits in the build cache can lead to an old timestamp
+ max_report_age: off
+
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index 552088d..62adc2f 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -13,62 +13,71 @@ jobs:
strategy:
matrix:
include:
- - os: macos-latest
- java: 8
- epVersion: 2.4.0
- - os: macos-latest
- java: 11
- epVersion: 2.4.0
- os: ubuntu-latest
- java: 8
- epVersion: 2.4.0
+ java: 11
+ epVersion: 2.10.0
- os: ubuntu-latest
- java: 8
+ java: 17
epVersion: 2.10.0
+ - os: macos-latest
+ java: 11
+ epVersion: 2.23.0
- os: ubuntu-latest
java: 11
- epVersion: 2.4.0
+ epVersion: 2.23.0
- os: windows-latest
- java: 8
- epVersion: 2.4.0
+ java: 11
+ epVersion: 2.23.0
- os: ubuntu-latest
java: 17
- epVersion: 2.10.0
+ epVersion: 2.23.0
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- name: Check out NullAway sources
- uses: actions/checkout@v2
- - name: 'Set up JDK ${{ matrix.java }}'
- uses: actions/setup-java@v2
+ uses: actions/checkout@v3
+ - name: 'Set up JDKs'
+ uses: actions/setup-java@v3
with:
- java-version: ${{ matrix.java }}
+ java-version: |
+ 21
+ 17
+ ${{ matrix.java }}
distribution: 'temurin'
- - name: Build and test using Gradle, Java 8, and Error Prone ${{ matrix.epVersion }}
+ - name: Build and test using Java ${{ matrix.java }} and Error Prone ${{ matrix.epVersion }}
env:
ORG_GRADLE_PROJECT_epApiVersion: ${{ matrix.epVersion }}
- uses: eskatos/gradle-command-action@v1
- with:
- arguments: verGJF build
- if: matrix.java == '8'
- - name: Build and test using Gradle and Java 11
- uses: eskatos/gradle-command-action@v1
+ uses: gradle/gradle-build-action@v2
with:
- arguments: build -x :sample-app:build
- if: matrix.java == '11'
- - name: Build and test using Gradle and Java 17
- uses: eskatos/gradle-command-action@v1
+ arguments: build
+ - name: Run shellcheck
+ uses: gradle/gradle-build-action@v2
with:
- arguments: build -x :sample-app:build -x :jar-infer:jar-infer-lib:build -x :jar-infer:nullaway-integration-test:build -x :jar-infer:test-java-lib-jarinfer:build
- if: matrix.java == '17'
- - name: Report jacoco coverage
- uses: eskatos/gradle-command-action@v1
+ arguments: shellcheck
+ if: runner.os == 'Linux'
+ - name: Aggregate jacoco coverage
+ id: jacoco_report
+ uses: gradle/gradle-build-action@v2
env:
- COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
+ ORG_GRADLE_PROJECT_epApiVersion: ${{ matrix.epVersion }}
with:
- arguments: coveralls
+ arguments: codeCoverageReport
continue-on-error: true
- if: runner.os == 'Linux' && matrix.java == '11' && matrix.epVersion == '2.4.0' && github.repository == 'uber/NullAway'
+ if: runner.os == 'Linux' && matrix.java == '11' && matrix.epVersion == '2.23.0' && github.repository == 'uber/NullAway'
+ - name: Upload coverage reports to Codecov
+ uses: codecov/codecov-action@v3
+ with:
+ files: ./code-coverage-report/build/reports/jacoco/codeCoverageReport/codeCoverageReport.xml
+ if: steps.jacoco_report.outcome == 'success'
+ - name: Test publishToMavenLocal flow
+ env:
+ ORG_GRADLE_PROJECT_epApiVersion: ${{ matrix.epVersion }}
+ ORG_GRADLE_PROJECT_VERSION_NAME: '0.0.0.1-LOCAL'
+ ORG_GRADLE_PROJECT_RELEASE_SIGNING_ENABLED: 'false'
+ uses: gradle/gradle-build-action@v2
+ with:
+ arguments: publishToMavenLocal
+ if: matrix.java == '11'
- name: Check that Git tree is clean after build and test
run: ./.buildscript/check_git_clean.sh
publish_snapshot:
@@ -78,25 +87,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Check out repository'
- uses: actions/checkout@v2
- - name: Cache Gradle caches
- uses: actions/cache@v1
- with:
- path: ~/.gradle/caches
- key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle') }}
- restore-keys: ${{ runner.os }}-gradle-caches-
- - name: Cache Gradle wrapper
- uses: actions/cache@v1
- with:
- path: ~/.gradle/wrapper
- key: ${{ runner.os }}-gradlew-wrapper-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
- restore-keys: ${{ runner.os }}-gradlew-wrapper-
- - name: 'Set up JDK 8'
- uses: actions/setup-java@v1
+ uses: actions/checkout@v3
+ - name: 'Set up JDK 11'
+ uses: actions/setup-java@v3
with:
- java-version: 8
+ java-version: |
+ 21
+ 11
+ distribution: 'temurin'
- name: 'Publish'
+ uses: gradle/gradle-build-action@v2
env:
- ORG_GRADLE_PROJECT_mavenCentralRepositoryUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
- ORG_GRADLE_PROJECT_mavenCentralRepositoryPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
- run: ./gradlew clean publish --no-daemon --no-parallel
+ ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
+ ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
+ with:
+ arguments: clean publish --no-daemon --no-parallel
diff --git a/.github/workflows/gcloud_ssh.sh b/.github/workflows/gcloud_ssh.sh
new file mode 100644
index 0000000..80d9e68
--- /dev/null
+++ b/.github/workflows/gcloud_ssh.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+# This script is used to run commands on a Google Cloud instance via SSH
+
+# Define the variables for Google Cloud project, zone, username, and instance
+PROJECT_ID="ucr-ursa-major-sridharan-lab"
+ZONE="us-central1-a"
+USER="root"
+INSTANCE="nullway-jmh"
+
+gcloud compute ssh --project=$PROJECT_ID --zone=$ZONE $USER@$INSTANCE --command="$1"
diff --git a/.github/workflows/get_repo_details.sh b/.github/workflows/get_repo_details.sh
new file mode 100644
index 0000000..fc448af
--- /dev/null
+++ b/.github/workflows/get_repo_details.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# This script retrieves the repository and branch details of a GitHub pull request
+
+# Assign command line arguments to variables
+# GH_TOKEN is the GitHub authentication token
+# PR_NUMBER is the number of the pull request
+# REPO_NAME is the name of the repository
+GH_TOKEN="$1"
+PR_NUMBER="$2"
+REPO_NAME="$3"
+
+PR_DETAILS=$(curl -s -H "Authorization: token $GH_TOKEN" "https://api.github.com/repos/$REPO_NAME/pulls/$PR_NUMBER")
+
+REPO_FULL_NAME=$(echo "$PR_DETAILS" | jq -r .head.repo.full_name)
+BRANCH_NAME=$(echo "$PR_DETAILS" | jq -r .head.ref)
+
+# Export vars to GITHUB_ENV so they can be used by later scripts
+echo "REPO_FULL_NAME=$REPO_FULL_NAME" >> "$GITHUB_ENV"
+echo "BRANCH_NAME=$BRANCH_NAME" >> "$GITHUB_ENV"
diff --git a/.github/workflows/jmh-benchmark.yml b/.github/workflows/jmh-benchmark.yml
new file mode 100644
index 0000000..aed9887
--- /dev/null
+++ b/.github/workflows/jmh-benchmark.yml
@@ -0,0 +1,73 @@
+# This GitHub Actions workflow runs JMH benchmarks when a new comment is created on a pull request
+name: Run JMH Benchmarks for Pull Request
+
+on:
+ issue_comment: # This workflow triggers when a comment is created
+ types: [created]
+
+# Only allow one instance of JMH benchmarking to be running at any given time
+concurrency: all
+
+jobs:
+ benchmarking:
+ # Only run this job if a comment on a pull request contains '/benchmark' and is a PR on the uber/NullAway repository
+ if: github.event.issue.pull_request && contains(github.event.comment.body, '/benchmark') && github.repository == 'uber/NullAway'
+ runs-on: ubuntu-latest
+ permissions: write-all
+
+ steps:
+ - name: Add reaction
+ uses: peter-evans/create-or-update-comment@v3
+ with:
+ comment-id: ${{ github.event.comment.id }}
+ reactions: '+1'
+
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Set branch name
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ chmod +x ./.github/workflows/get_repo_details.sh
+ ./.github/workflows/get_repo_details.sh "${{ secrets.GITHUB_TOKEN }}" "${{ github.event.issue.number }}" "${{ github.repository }}"
+
+ - id: 'auth'
+ name: Authenticating
+ uses: 'google-github-actions/auth@v1'
+ with:
+ credentials_json: '${{ secrets.GCP_SA_KEY_1 }}'
+
+ - name: Set up Google Cloud SDK
+ uses: google-github-actions/setup-gcloud@v1
+
+ - name: Start VM
+ run: gcloud compute instances start nullway-jmh --zone=us-central1-a
+
+ - name: Run benchmarks
+ run: |
+ chmod +x ./.github/workflows/run_gcp_benchmarks.sh
+ ./.github/workflows/run_gcp_benchmarks.sh
+
+ - name: Cleanup
+ # Delete the branch directory on the Google Cloud instance
+ if: always()
+ run: |
+ ./.github/workflows/gcloud_ssh.sh " export BRANCH_NAME=${BRANCH_NAME} && rm -r -f $BRANCH_NAME"
+
+ - name: Formatting Benchmark # Create a text file containing the benchmark results
+ run: |
+ (echo 'Main Branch:'; echo '```' ; cat main_text.txt; echo '```'; echo 'With This PR:'; echo '```' ; cat pr_text.txt; echo '```') > benchmark.txt
+
+ - name: Comment Benchmark
+ uses: mshick/add-pr-comment@v2
+ if: always() # This step is for adding the comment
+ with:
+ message-path: benchmark.txt # The path to the message file to leave as a comment
+ message-id: benchmark
+ - name: Stop VM
+ if: always()
+ run: gcloud compute instances stop nullway-jmh --zone=us-central1-a
+
+
+
diff --git a/.github/workflows/run_gcp_benchmarks.sh b/.github/workflows/run_gcp_benchmarks.sh
new file mode 100644
index 0000000..9dc8604
--- /dev/null
+++ b/.github/workflows/run_gcp_benchmarks.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# This script is responsible for running benchmarks for a GitHub pull request and the main branch on Google Cloud Compute Engine (GCCE).
+
+
+chmod +x ./.github/workflows/gcloud_ssh.sh
+./.github/workflows/gcloud_ssh.sh "export BRANCH_NAME=${BRANCH_NAME} && mkdir $BRANCH_NAME"
+
+# Using gcloud compute scp to copy the bash scripts that will run the benchmarks onto the GCCE
+gcloud compute scp ./.github/workflows/run_pr_benchmarks.sh root@nullway-jmh:"$BRANCH_NAME/" --zone=us-central1-a
+gcloud compute scp ./.github/workflows/run_main_benchmarks.sh root@nullway-jmh:"$BRANCH_NAME/" --zone=us-central1-a
+
+# Running the benchmark script for the pull request branch and main branch on GCCE
+./.github/workflows/gcloud_ssh.sh " export BRANCH_NAME=${BRANCH_NAME} && export REPO_NAME=${REPO_FULL_NAME} && chmod +x $BRANCH_NAME/run_pr_benchmarks.sh && $BRANCH_NAME/run_pr_benchmarks.sh && cd && chmod +x $BRANCH_NAME/run_main_benchmarks.sh && $BRANCH_NAME/run_main_benchmarks.sh"
+
+# Copying the benchmark results from GCCE back to the Github runner for the PR branch
+gcloud compute scp root@nullway-jmh:"$BRANCH_NAME/pr/NullAway/jmh/build/results/jmh/results.txt" ./pr_text.txt --zone=us-central1-a
+
+# Copying the benchmark results from GCCE back to the Github runner for the main branch
+gcloud compute scp root@nullway-jmh:"$BRANCH_NAME/main/NullAway/jmh/build/results/jmh/results.txt" ./main_text.txt --zone=us-central1-a
diff --git a/.github/workflows/run_main_benchmarks.sh b/.github/workflows/run_main_benchmarks.sh
new file mode 100644
index 0000000..fad59dc
--- /dev/null
+++ b/.github/workflows/run_main_benchmarks.sh
@@ -0,0 +1,9 @@
+#!/bin/bash -eux
+
+cd "$BRANCH_NAME/"
+mkdir main
+cd main/
+git clone git@github.com:Uber/NullAway.git
+cd NullAway/
+
+./gradlew jmh --no-daemon
diff --git a/.github/workflows/run_pr_benchmarks.sh b/.github/workflows/run_pr_benchmarks.sh
new file mode 100644
index 0000000..83c4a3b
--- /dev/null
+++ b/.github/workflows/run_pr_benchmarks.sh
@@ -0,0 +1,9 @@
+#!/bin/bash -eux
+
+cd "$BRANCH_NAME/"
+mkdir pr
+cd pr/
+git clone --branch "$BRANCH_NAME" --single-branch git@github.com:"$REPO_NAME".git NullAway
+cd NullAway/
+
+./gradlew jmh --no-daemon
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fa2cba4..c7a8e0c 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,346 @@
Changelog
=========
+Version 0.10.16
+---------------
+NOTE: Maven Central signing key rotated for this release following a revocation.
+
+* Minor cleanup in AccessPathElement (#851)
+* Support for JSpecify's 0.3.0 annotation [experimental]
+ - JSpecify: handle return types of method references in Java Generics (#847)
+ - JSpecify: handle Nullability for lambda expression parameters for Generic Types (#852)
+ - JSpecify: Modify Array Type Use Annotation Syntax (#850)
+ - JSpecify: handle Nullability for return types of lambda expressions for Generic Types (#854)
+* Build / CI tooling for NullAway itself:
+ - Update to Gradle 8.4 and Error Prone 2.23.0 (#849)
+
+Version 0.10.15
+---------------
+* [IMPORTANT] Update minimum Error Prone version and Guava version (#843)
+ NullAway now requires Error Prone 2.10.0 or later
+* Add Spring mock/testing annotations to excluded field annotation list (#757)
+* Update to Checker Framework 3.39.0 (#839) [Support for JDK 21 constructs]
+* Support for JSpecify's 0.3.0 annotation [experimental]
+ - Properly check generic method overriding in explicitly-typed anonymous classes (#808)
+ - JSpecify: handle incorrect method parameter nullability for method reference (#845)
+ - JSpecify: initial handling of generic enclosing types for inner classes (#837)
+* Build / CI tooling for NullAway itself:
+ - Update Gradle and a couple of plugin versions (#832)
+ - Run recent JDK tests on JDK 21 (#834)
+ - Fix which JDKs are installed on CI (#835)
+ - Update to Error Prone 2.22.0 (#833)
+ - Ignore code coverage for method executed non-deterministically in tests (#838 and #844)
+ - Build NullAway with JSpecify mode enabled (#841)
+
+Version 0.10.14
+---------------
+IMPORTANT: This version introduces EXPERIMENTAL JDK21 support.
+* Bump Checker Framework dependency to 3.38.0 (#819)
+ - Note: Not just an internal implementation change. Needed to support JDK 21!
+* Treat parameter of generated Record.equals() methods as @Nullable (#825)
+* Build / CI tooling for NullAway itself:
+ - Fixes Codecov Report Expired error (#821)
+ - Updated Readme.md with Codecov link (#823)
+ - Remove ASM-related hack in build config (#824)
+ - Run tests on JDK 21 (#826)
+
+Version 0.10.13
+---------------
+* Allow library models to define custom stream classes (#807)
+* Avoid suggesting castToNonNull fixes in certain cases (#799)
+* Ensure castToNonNull insertion/removal suggested fixes do not remove comments (#815)
+* Support for JSpecify's 0.3.0 annotation [experimental]
+ - Generics checks for method overriding (#755)
+ - Make GenericsChecks methods static (#805)
+ - Add visitors for handling different types in generic type invariance check (#806)
+* Build / CI tooling for NullAway itself:
+ - Bump versions for some dependencies (#800)
+ - Update to WALA 1.6.2 (#798)
+ - Update to Error Prone 2.21.1 (#797)
+ - Enable contract checking when building NullAway (#802)
+ - Bump Error Prone Gradle Plugin version (#804)
+ - Modify JMH Benchmark Workflow For Shellcheck (#813)
+ - Bump gradle maven publish plugin from 0.21.0 to 0.25.3 (#810)
+ - Use Spotless to enforce consistent formatting for Gradle build scripts (#809)
+ - Remove unnecessary compile dependence for jar-infer-cli (#816)
+ - Added Codecov to CI Pipeline (#820)
+
+Version 0.10.12
+---------------
+Note: This is the first release built with Java 11. In particular, running
+ JarInfer now requires a JDK 11 JVM. NullAway is still capable of analyzing JDK 8
+ source/target projects, and should be compatible with the Error Prone JDK 9 javac
+ just as the release before, but a JDK 11 javac is recommended.
+* Update to WALA 1.6.1 and remove ability to build on JDK 8 (#777)
+* Fix compatibility issue when building on JDK 17 but running on JDK 8 (#779)
+* Fix JDK compatibility issue in LombokHandler (#795)
+* Improve auto-fixing of unnecessary castToNonNull calls (#796)
+* Support for JSpecify's 0.3.0 annotation [experimental]
+ - JSpecify: avoid crashes when encountering raw types (#792)
+ - Fix off-by-one error in JSpecify checking of parameter passing (#793)
+* Build / CI tooling for NullAway itself:
+ - Fix Publish Snapshot CI job (#774)
+ - Add step to create release on GitHub (#775)
+ - Build the Android sample app on JDK 17 (#776)
+ - Update to Error Prone 2.20.0 (#772)
+ - Add tasks to run JDK 8 tests on JDK 11+ (#778)
+ - Switch to Spotless for formatting Java code (#780)
+ - Added GCP JMH Benchmark Workflow (#770)
+ - Set concurrency for JMH benchmarking workflow (#784)
+ - Disable daemon when running benchmarks (#786)
+ - Update to Gradle 8.2.1 (#781)
+
+Version 0.10.11
+---------------
+* NULL_LITERAL expressions may always be null (#749)
+* Fix error in Lombok generated code for @Nullable @Builder.Default (#765)
+* Support for specific libraries/APIs:
+ - Added support for Apache Validate (#769)
+ - Introduce FluentFutureHandler as a workaround for Guava FluentFuture (#771)
+* Internal code refactorings:
+ - [Refactor] Pass resolved Symbols into Handler methods (#729)
+ - Prepare for Nullable ASTHelpers.getSymbol (#733)
+ - Refactor: streamline mayBeNullExpr flow (#753)
+ - Refactor LibraryModelsHandler.onOverrideMayBeNullExpr (#754)
+ - Refactor simple onOverrideMayBeNullExpr handlers (#747)
+* Support for JSpecify's 0.3.0 annotation [experimental]
+ - JSpecify generics checks for conditional expressions (#739)
+ - Generics checks for parameter passing (#746)
+ - Clearer printing of types in errors related to generics (#758)
+* NullAwayInfer/Annotator data serialization support [experimental]
+ - Update path serialization for class files (#752)
+* Build / CI tooling for NullAway itself:
+ - Update to Gradle 8.0.2 (#743)
+ - Fix CI on Windows (#759)
+ - Upgrade to Error Prone 2.19.1 (#763)
+ - Upgrade maven publish plugin to 0.21.0 (#773)
+
+Version 0.10.10
+---------------
+* Add command line option to skip specific library models. (#741)
+* Support for specific libraries/APIs:
+ - Model Map.getOrDefault (#724)
+ - Model Class.cast (#731)
+ - Model Class.isInstance (#732)
+* Internal code refactorings:
+ - Refactor code to use Map.getOrDefault where possible (#727)
+ - Break loops when result can no longer change (#728)
+* Support for JSpecify's 0.3.0 annotation [experimental]
+ - JSpecify: initial checks for generic type compatibility at assignments (#715)
+ - Add JSpecify checking for return statements (#734)
+* NullAwayInfer/Annotator data serialization support [experimental]
+ - Refactoring in symbol serialization (#736)
+ - Refactoring tabSeparatedToString logic to prepare for serialization version 3 (#738)
+ - Update method serialization to exclude type use annotations and type arguments (#735)
+* Docs fix: -XepExcludedPaths was added in 2.1.3, not 2.13 (#744)
+
+Version 0.10.9
+--------------
+* Add support for external init annotations in constructors (#725)
+* Ignore incompatibly annotated var args from Kotlin code. (#721)
+* Support for specific libraries/APIs:
+ - Add Throwable.getCause and getLocalizedMessage() library models (#717)
+ - Support more test assertions in OptionalEmptinessHandler (#718)
+ - Support isInstanceOf(...) as implying non-null in assertion libraries (#726)
+* [Refactor] Avoid redundant Map lookups (#722)
+* Build / CI tooling for NullAway itself:
+ - Update to Error Prone 2.18.0 (#707)
+
+Version 0.10.8
+--------------
+* Don't do checks for type casts and parameterized trees in unannotated code (#712)
+* Add an initial `nullaway:nullaway-annotations` artifact. (#709)
+ - Contains only an implementation of `@Initializer` for now.
+* NullAwayInfer/Annotator data serialization support [experimental]
+ - Update region selection for initialization errors. (#713)
+ - Update path serialization for reported errors and fixes. (#714)
+* Build / CI tooling for NullAway itself:
+ - Turn up various Error Prone checks (#710)
+
+Version 0.10.7
+--------------
+(Bug fix release)
+* Resolve regression for type annotations directly on inner types. (#706)
+
+Version 0.10.6
+--------------
+* Handle BITWISE_COMPLEMENT operator (#696)
+* Add support for AssertJ (#698)
+* Fix logic for @Nullable annotation on type parameter (#702)
+* Preserve nullness checks in final fields when propagating nullness into inner contexts (#703)
+* NullAwayInfer/Annotator data serialization support [experimental]
+ - Add source offset and path to reported errors in error serialization. (#704)
+* Build / CI tooling for NullAway itself:
+ - [Jspecify] Update test dep to final JSpecify 0.3.0 release (#700)
+ = Intermediate PRs: 0.3.0-alpha-3 (#692), 0.3-alpha2 (#691)
+ - Update to Gradle 7.6 (#690)
+
+
+Version 0.10.5
+--------------
+* Report more unboxing errors in a single compilation (#686)
+* Remove AccessPath.getAccessPathForNodeNoMapGet (#687)
+* NullAwayInfer/Annotator data serialization support [experimental]
+ - Fix Serialization: Split field initialization region into smaller regions (#658)
+ - Add serialization format version to fix serialization output (#688)
+ - Fix serialization field region computation bug fix (#689)
+* EXPERIMENTAL support for JSpecify's 0.3.0 annotations
+ - [Jspecify] Update tests to JSpecify 0.3.0-alpha-1 (#673)
+ - [Jspecify] Add checks for proper JSpecify generic type instantiations (#680)
+ - (Note: Annotation support for generics is not complete/useful just yet)
+
+Version 0.10.4
+--------------
+(Bug fix release)
+* Fix LibraryModels recording of dataflow nullness for Map APs (#685)
+* Proper checking of unboxing in binary trees (#684)
+* Build / CI tooling for NullAway itself:
+ - Bump dependency versions in GitHub Actions config (#683)
+
+Version 0.10.3
+--------------
+* Report an error when casting @Nullable expression to primitive type (#663)
+* Fix an NPE in the optional emptiness handler (#678)
+* Add support for boolean constraints (about nullness) in Contract annotations (#669)
+* Support for specific libraries/APIs:
+ - PreconditionsHandler reflects Guava Preconditions exception types (#668)
+ - Handle Guava Verify functions (#682)
+* Dependency Updates:
+ - checkerframework 3.26.0 (#671)
+* Build / CI tooling for NullAway itself:
+ - Build and test against Error Prone 2.15.0 (#665)
+ - Bump Error Prone and EP plugin to 2.16 (#675)
+
+Version 0.10.2
+--------------
+* Make AbstractConfig collection fields explicity Immutable (#601)
+* NullAwayInfer/Annotator data serialization support [experimental]
+ - Fix crash in fixserialization when ClassSymbol.sourcefile is null (#656)
+
+Version 0.10.1
+--------------
+This is a bug-fixing release for a crash introduced in 0.10.1 on type.class
+(for primitive type = boolean/int/void/etc.).
+* Fix crash when querying null-markedness of primitive.class expressions (#654)
+* Fix for querying for generated code w/ primitive.class expressions. (#655)
+
+Version 0.10.0
+--------------
+* Switch parameter overriding handler to use Nullness[] (#648) [performance opt!]
+* EXPERIMENTAL support for JSpecify's 0.3.0 @NullMarked and @NullUnmarked semantics
+ - [JSpecify] Support @NullMarked on methods. (#644)
+ - [JSpecify] Support @NullUnmarked. (#651)
+ - Allow AcknowledgeRestrictiveAnnotations to work on fields (#652)
+* Dependency Updates:
+ - Update to WALA 1.5.8 (#650)
+* Build / CI tooling for NullAway itself:
+ - Update to Gradle 7.5.1 (#647)
+ - Add Gradle versions plugin and update some "safe" dependencies (#649)
+
+Version 0.9.10
+--------------
+* Improved support for library models on annotated code:
+ - Make library models override annotations by default. (#636)
+ - Generalize handler APIs for argument nullability on (un-)annotated code (#639)
+ - [Follow-up] Optimizations for parameter nullness handler / overriding (#646)
+ - Generalize handler APIs for return nullability on (un-)annotated code (#641)
+* Support for specific libraries/APIs:
+ - Add library model for Guava's Closer.register (#632)
+ - Support for Map.computeIfAbsent(...) (#640)
+* NullAwayInfer/Annotator data serialization support [experimental]
+ - Augment error serializarion info (#643)
+* Dependency Updates:
+ - Update to Checker Framework 3.24.0 (#631)
+* Fix javadoc and CONTRIBUTING.md typos (#642)
+
+Version 0.9.9
+-------------
+* Fix handling of empty contract arguments (#616)
+* Fix inconsistent treament of generated code in RestrictiveAnnotationHandler (#618)
+* Allow Library Models to override annotations. (#624)
+* Allow tracking field accesses outside the this instance and static fields (#625)
+* Add Guava 31+ support by treating @ParametricNullness as @nullable (#629)
+* Refactoring:
+ - Clean up: Remove method parameter protection analysis (#622)
+ - Clean up: Remove nullable annotation configuration in fix serialization. (#621)
+* Build / CI tooling for NullAway itself:
+ - Add a microbenchmark for type inference / dataflow (#617)
+
+Version 0.9.8
+-------------
+* Fix false positive involving type parameter @Nullable annotations (#609)
+* Add config option to register custom @Generated annotations. (#600)
+* Treat Void formal arguments as @Nullable (#613)
+* Generalize support for castToNonNull methods using library models (#614)
+* Support for specific libraries/APIs:
+ - Support for Preconditions.checkArgument (#608)
+ - Model for com.google.api.client.util.Strings.isNullOrEmpty (#605)
+* Refactoring:
+ - Cleanups to AccessPath representation and implementation (#603)
+ - Clean-up: Remove unused fix suggestion code. (#615)
+* Dependency Updates:
+ - Update to Checker Framework 3.22.2 (#610)
+* Build / CI tooling for NullAway itself:
+ - Add NullAway 0.9.7 as a JMH benchmark (#602)
+ - Update to Error Prone 2.14.0 (#606)
+
+Version 0.9.7
+-------------
+* Allow zero-argument static method calls to be the root of an access path (#596)
+* Support for specific libraries/APIs
+ - Add support for Optional.isEmpty() (#590)
+ - Model System.console() as returning @nullable (#591)
+* JDK 17+ support improvements
+ - Add a test of binding patterns (#583)
+* JSpecify support:
+ - Move JSpecify tests to correct package (#587)
+* NullAwayInfer/Annotator data serialization support [experimental]
+ - Fixes line breaks and tabs in serializing errors. (#584)
+ - Using flatNames for LocalType/anon. classes in fix serialization (#592)
+ - Fixes to computing class and method info for error serialization (#599)
+* Dependency updates
+ - [JarInfer] Update Apache Commons IO dependency. (#582)
+ - Update to Checker Framework 3.21.3 (#564)
+* Build / CI tooling for NullAway itself:
+ - NullAway now builds with NullAway (#560)
+ - Switch to using gradle-build-action (#581)
+ - Compile and test against Error Prone 2.12.0 (#585)
+ - Enabled a few more EP checks on our code (#586)
+ (Note: the `Void` related portion of this changes was reverted)
+ - Update to Gradle 7.4.2 (#589)
+ - Update to Error Prone 2.13.1 and latest Lombok (#588)
+
+Version 0.9.6
+-------------
+* Initial support for JSpecify's @NullMarked annotation (#493)
+ - Fix bug in handling of TreatGeneratedAsUnannotated (#580)
+ (Note: this bug is not in any released NullAway version, but was temporarily
+ introduced to the main/master branch by #493)
+* Improved tracking of map nullness
+ - Improve nullness tracking of map calls in the presence of type casts (#537)
+ - Reason about iterating over a map's key set using an enhanced for loop (#554)
+ - Reason about key set iteration for subtypes of Map (#559)
+ - Add support for Map.putIfAbsent. (#568)
+* Add support for data serialization for Nullaway data for UCR's NullAwayAnnotator
+ - Serialization of Type Change Suggestions for Type Violations (#517)
+ - Measurement of Method protection against nullability of arguments (#575)
+ - Enhanced Serialization Test Infrastructure (#579)
+ - Field initialization serialization (#576)
+* Build / CI tooling for NullAway itself:
+ - Enable parallel builds (#549) (#555)
+ - Add dependence from coveralls task to codeCoverageReport (#552)
+ - Switch to temurin on CI (#553)
+ - Separating NullAwayTests into smaller files (#550)
+ - Require braces for all conditionals and loops (#556)
+ - Enable build cache (#562)
+ - Fix JarInfer integration test on Java 11 (#529)
+ - Get Android sample apps building on JDK 11 (#531)
+ - Limit metaspace size (#563)
+ - Update CI jobs (#565)
+ - Set epApiVersion for jacoco coverage reporting (#566)
+ - Compile and test against Error Prone 2.11.0 (#567)
+ - Fix EP version for jacoco coverage step (#571)
+ - Update to latest Google Java Format (#572)
+
Version 0.9.5
-------------
* JDK17 support improvements:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fa118ec..190d079 100755
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,7 +1,7 @@
Contributing to NullAway
=======================
-Uber welcomes contributions of all kinds and sizes. This includes everything from from simple bug reports to large features.
+Uber welcomes contributions of all kinds and sizes. This includes everything from simple bug reports to large features.
Workflow
--------
diff --git a/README.md b/README.md
index 69e22a6..1376997 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-## NullAway: Fast Annotation-Based Null Checking for Java [![Build Status](https://github.com/uber/nullaway/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/uber/nullaway/actions/workflows/continuous-integration.yml) [![Coverage Status](https://coveralls.io/repos/github/uber/NullAway/badge.svg?branch=master)](https://coveralls.io/github/uber/NullAway?branch=master)
+## NullAway: Fast Annotation-Based Null Checking for Java [![Build Status](https://github.com/uber/nullaway/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/uber/nullaway/actions/workflows/continuous-integration.yml) [![Coverage Status](https://codecov.io/github/uber/NullAway/coverage.svg?branch=master)](https://codecov.io/github/uber/NullAway?branch=master)
NullAway is a tool to help eliminate `NullPointerException`s (NPEs) in your Java code. To use NullAway, first add `@Nullable` annotations in your code wherever a field, method parameter, or return value may be `null`. Given these annotations, NullAway performs a series of type-based, local checks to ensure that any pointer that gets dereferenced in your code cannot be `null`. NullAway is similar to the type-based nullability checking in the Kotlin and Swift languages, and the [Checker Framework](https://checkerframework.org/) and [Eradicate](https://fbinfer.com/docs/checker-eradicate/) null checkers for Java.
@@ -8,7 +8,7 @@ NullAway is *fast*. It is built as a plugin to [Error Prone](http://errorprone.
### Overview
-NullAway requires that you build your code with [Error Prone](http://errorprone.info), version 2.4.0 or higher. See the [Error Prone documentation](http://errorprone.info/docs/installation) for instructions on getting started with Error Prone and integration with your build system. The instructions below assume you are using Gradle; see [the docs](https://github.com/uber/NullAway/wiki/Configuration#other-build-systems) for discussion of other build systems.
+NullAway requires that you build your code with [Error Prone](http://errorprone.info), version 2.10.0 or higher. See the [Error Prone documentation](http://errorprone.info/docs/installation) for instructions on getting started with Error Prone and integration with your build system. The instructions below assume you are using Gradle; see [the docs](https://github.com/uber/NullAway/wiki/Configuration#other-build-systems) for discussion of other build systems.
### Gradle
@@ -19,19 +19,18 @@ To integrate NullAway into your non-Android Java project, add the following to y
```gradle
plugins {
// we assume you are already using the Java plugin
- id "net.ltgt.errorprone" version "0.6"
+ id "net.ltgt.errorprone" version "<plugin version>"
}
dependencies {
- annotationProcessor "com.uber.nullaway:nullaway:0.9.5"
+ errorprone "com.uber.nullaway:nullaway:<NullAway version>"
// Optional, some source of nullability annotations.
// Not required on Android if you use the support
// library nullability annotations.
compileOnly "com.google.code.findbugs:jsr305:3.0.2"
- errorprone "com.google.errorprone:error_prone_core:2.4.0"
- errorproneJavac "com.google.errorprone:javac:9+181-r4173-1"
+ errorprone "com.google.errorprone:error_prone_core:<Error Prone version>"
}
import net.ltgt.gradle.errorprone.CheckSeverity
@@ -47,23 +46,11 @@ tasks.withType(JavaCompile) {
}
```
-Let's walk through this script step by step. The `plugins` section pulls in the [Gradle Error Prone plugin](https://github.com/tbroyer/gradle-errorprone-plugin) for Error Prone integration. If you are using the older `apply plugin` syntax instead of a `plugins` block, the following is equivalent:
-```gradle
-buildscript {
- repositories {
- gradlePluginPortal()
- }
- dependencies {
- classpath "net.ltgt.gradle:gradle-errorprone-plugin:0.6"
- }
-}
-
-apply plugin: 'net.ltgt.errorprone'
-```
+Let's walk through this script step by step. The `plugins` section pulls in the [Gradle Error Prone plugin](https://github.com/tbroyer/gradle-errorprone-plugin) for Error Prone integration.
-In `dependencies`, the `annotationProcessor` line loads NullAway, and the `compileOnly` line loads a [JSR 305](https://jcp.org/en/jsr/detail?id=305) library which provides a suitable `@Nullable` annotation (`javax.annotation.Nullable`). NullAway allows for any `@Nullable` annotation to be used, so, e.g., `@Nullable` from the Android Support Library or JetBrains annotations is also fine. The `errorprone` line ensures that a compatible version of Error Prone is used, and the `errorproneJavac` line is needed for JDK 8 compatibility.
+In `dependencies`, the first `errorprone` line loads NullAway, and the `compileOnly` line loads a [JSR 305](https://jcp.org/en/jsr/detail?id=305) library which provides a suitable `@Nullable` annotation (`javax.annotation.Nullable`). NullAway allows for any `@Nullable` annotation to be used, so, e.g., `@Nullable` from the Android Support Library or JetBrains annotations is also fine. The second `errorprone` line sets the version of Error Prone is used.
-Finally, in the `tasks.withType(JavaCompile)` section, we pass some configuration options to NullAway. First `check("NullAway", CheckSeverity.ERROR)` sets NullAway issues to the error level (it's equivalent to the `-Xep:NullAway:ERROR` standard Error Prone argument); by default NullAway emits warnings. Then, `option("NullAway:AnnotatedPackages", "com.uber")` (equivalent to the `-XepOpt:NullAway:AnnotatedPackages=com.uber` standard Error Prone argument), tells NullAway that source code in packages under the `com.uber` namespace should be checked for null dereferences and proper usage of `@Nullable` annotations, and that class files in these packages should be assumed to have correct usage of `@Nullable` (see [the docs](https://github.com/uber/NullAway/wiki/Configuration) for more detail). NullAway requires at least the `AnnotatedPackages` configuration argument to run, in order to distinguish between annotated and unannotated code. See [the configuration docs](https://github.com/uber/NullAway/wiki/Configuration) for other useful configuration options.
+Finally, in the `tasks.withType(JavaCompile)` section, we pass some configuration options to NullAway. First `check("NullAway", CheckSeverity.ERROR)` sets NullAway issues to the error level (it's equivalent to the `-Xep:NullAway:ERROR` standard Error Prone argument); by default NullAway emits warnings. Then, `option("NullAway:AnnotatedPackages", "com.uber")` (equivalent to the `-XepOpt:NullAway:AnnotatedPackages=com.uber` standard Error Prone argument) tells NullAway that source code in packages under the `com.uber` namespace should be checked for null dereferences and proper usage of `@Nullable` annotations, and that class files in these packages should be assumed to have correct usage of `@Nullable` (see [the docs](https://github.com/uber/NullAway/wiki/Configuration) for more detail). NullAway requires at least the `AnnotatedPackages` configuration argument to run, in order to distinguish between annotated and unannotated code. See [the configuration docs](https://github.com/uber/NullAway/wiki/Configuration) for other useful configuration options. For even simpler configuration of NullAway options, use the [Gradle NullAway plugin](https://github.com/tbroyer/gradle-nullaway-plugin).
We recommend addressing all the issues that Error Prone reports, particularly those reported as errors (rather than warnings). But, if you'd like to try out NullAway without running other Error Prone checks, you can use `options.errorprone.disableAllChecks` (equivalent to passing `"-XepDisableAllChecks"` to the compiler, before the NullAway-specific arguments).
@@ -75,16 +62,15 @@ The configuration for an Android project is very similar to the Java case, with
```gradle
dependencies {
- annotationProcessor "com.uber.nullaway:nullaway:0.9.5"
- errorprone "com.google.errorprone:error_prone_core:2.4.0"
- errorproneJavac "com.google.errorprone:javac:9+181-r4173-1"
+ errorprone "com.uber.nullaway:nullaway:<NullAway version>"
+ errorprone "com.google.errorprone:error_prone_core:<Error Prone version>"
}
```
-A complete Android `build.gradle` example is [here](https://gist.github.com/msridhar/6cacd429567f1d1ad9a278e06809601c). Also see our [sample app](https://github.com/uber/NullAway/blob/master/sample-app/). (The sample app's [`build.gradle`](https://github.com/uber/NullAway/blob/master/sample-app/) is not suitable for direct copy-pasting, as some configuration is inherited from the top-level `build.gradle`.)
+For a more complete example see our [sample app](https://github.com/uber/NullAway/blob/master/sample-app/). (The sample app's [`build.gradle`](https://github.com/uber/NullAway/blob/master/sample-app/) is not suitable for direct copy-pasting, as some configuration is inherited from the top-level `build.gradle`.)
#### Annotation Processors / Generated Code
-Some annotation processors like [Dagger](https://google.github.io/dagger/) and [AutoValue](https://github.com/google/auto/tree/master/value) generate code into the same package namespace as your own code. This can cause problems when setting NullAway to the `ERROR` level as suggested above, since errors in this generated code will block the build. Currently the best solution to this problem is to completely disable Error Prone on generated code, using the `-XepExcludedPaths` option added in Error Prone 2.13 (documented [here](http://errorprone.info/docs/flags), use `options.errorprone.excludedPaths=` in Gradle). To use, figure out which directory contains the generated code, and add that directory to the excluded path regex.
+Some annotation processors like [Dagger](https://google.github.io/dagger/) and [AutoValue](https://github.com/google/auto/tree/master/value) generate code into the same package namespace as your own code. This can cause problems when setting NullAway to the `ERROR` level as suggested above, since errors in this generated code will block the build. Currently the best solution to this problem is to completely disable Error Prone on generated code, using the `-XepExcludedPaths` option added in Error Prone 2.1.3 (documented [here](http://errorprone.info/docs/flags), use `options.errorprone.excludedPaths=` in Gradle). To use, figure out which directory contains the generated code, and add that directory to the excluded path regex.
**Note for Dagger users**: Dagger versions older than 2.12 can have bad interactions with NullAway; see [here](https://github.com/uber/NullAway/issues/48#issuecomment-340018409). Please update to Dagger 2.12 to fix the problem.
diff --git a/RELEASING.md b/RELEASING.md
index a848eaf..cecfedf 100755
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -1,3 +1,14 @@
+(For testing only) Publishing an unsigned LOCAL build
+=====================================================
+By default, we set `RELEASE_SIGNING_ENABLED=true` in `gradle.properties`, which means
+published builds must be signed unless they are for a `SNAPSHOT` version. To publish
+a non-`SNAPSHOT` build locally without signing (e.g., a `LOCAL` version), use the
+following command:
+
+```bash
+ORG_GRADLE_PROJECT_RELEASE_SIGNING_ENABLED=false ./gradlew publishToMavenLocal
+```
+
(Recommended, but optional) Update JarInfer Android SDK Models
==============================================================
@@ -18,11 +29,11 @@ Releasing
1. Change the version in `gradle.properties` to a non-SNAPSHOT version.
2. Update the `CHANGELOG.md` for the impending release.
- 3. Update the `README.md` with the new version.
- 4. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version)
- 5. `git tag -a vX.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version)
- 6. `./gradlew clean publish --no-daemon --no-parallel`
- 7. Update the `gradle.properties` to the next SNAPSHOT version.
- 8. `git commit -am "Prepare next development version."`
- 9. `git push && git push --tags`
- 10. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifact.
+ 3. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version)
+ 4. `git tag -a vX.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version)
+ 5. `./gradlew clean publish`
+ 6. Update the `gradle.properties` to the next SNAPSHOT version.
+ 7. `git commit -am "Prepare next development version."`
+ 8. `git push && git push --tags`
+ 9. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifact.
+ 10. Go to [this page](https://github.com/uber/NullAway/releases/new) to create a new release on GitHub, using the release notes from `CHANGELOG.md`.
diff --git a/annotations/build.gradle b/annotations/build.gradle
new file mode 100644
index 0000000..ebb8ff5
--- /dev/null
+++ b/annotations/build.gradle
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017. Uber Technologies
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import net.ltgt.gradle.errorprone.CheckSeverity
+
+plugins {
+ id 'java-library'
+ id 'nullaway.java-test-conventions'
+}
+
+dependencies {
+}
+
+apply plugin: 'com.vanniktech.maven.publish'
diff --git a/annotations/gradle.properties b/annotations/gradle.properties
new file mode 100755
index 0000000..d2ca561
--- /dev/null
+++ b/annotations/gradle.properties
@@ -0,0 +1,19 @@
+#
+# Copyright (C) 2017. Uber Technologies
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+POM_NAME=NullAwayAnnotations
+POM_ARTIFACT_ID=nullaway-annotations
+POM_PACKAGING=jar
diff --git a/annotations/src/main/java/com/uber/nullaway/annotations/Initializer.java b/annotations/src/main/java/com/uber/nullaway/annotations/Initializer.java
new file mode 100644
index 0000000..7da10c7
--- /dev/null
+++ b/annotations/src/main/java/com/uber/nullaway/annotations/Initializer.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2023 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation used to mark a method as an initializer.
+ *
+ * <p>During initialization checking (see <a
+ * href=https://github.com/uber/NullAway/wiki/Error-Messages#initializer-method-does-not-guarantee-nonnull-field-is-initialized--nonnull-field--not-initialized>NullAway
+ * Wiki</a>), NullAway considers a method marked with any annotation with simple name
+ * {@code @Initializer} to denote an initializer method. Initializer methods are assumed by NullAway
+ * to always be called before any other method of the class that is not a constructor or called from
+ * a constructor. This means a non-null field is considered to be properly initialized if it's set
+ * by such an initializer method. By design, NullAway doesn't check for such initialization, since
+ * an important use case of initializer methods is documenting methods used by annotation processors
+ * or external frameworks as part of object set up (e.g. {@code android.app.Activity.onCreate} or
+ * {@code javax.annotation.processing.Processor.init}). Note that there are other ways of defining
+ * initializer methods from external libraries (i.e. library models), and that a method overriding
+ * an initializer method is always considered an initializer method (again, for the sake of
+ * framework events such as {@code onCreate}).
+ */
+@Retention(RetentionPolicy.CLASS)
+@Target({ElementType.METHOD})
+public @interface Initializer {}
diff --git a/build.gradle b/build.gradle
index dbc8f51..729ffc2 100644
--- a/build.gradle
+++ b/build.gradle
@@ -22,22 +22,22 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.1.1'
- classpath 'com.vanniktech:gradle-maven-publish-plugin:0.14.2'
+ classpath 'com.android.tools.build:gradle:7.3.0'
+ classpath 'com.vanniktech:gradle-maven-publish-plugin:0.25.3'
}
}
plugins {
- id "com.github.sherter.google-java-format" version "0.9"
- id "net.ltgt.errorprone" version "2.0.1" apply false
- id "com.github.johnrengelman.shadow" version "6.1.0" apply false
- id "com.github.kt3k.coveralls" version "2.12.0" apply false
- id "com.android.application" version "3.5.0" apply false
- id "me.champeau.jmh" version "0.6.6" apply false
+ id "com.diffplug.spotless" version "6.21.0"
+ id "net.ltgt.errorprone" version "3.1.0" apply false
+ id "com.github.johnrengelman.shadow" version "8.1.1" apply false
+ id "me.champeau.jmh" version "0.7.1" apply false
+ id "com.github.ben-manes.versions" version "0.48.0"
+ id "com.felipefzdz.gradle.shellcheck" version "1.4.6"
}
repositories {
- // to get the google-java-format jar and dependencies
- mavenCentral()
+ // to get the google-java-format jar and dependencies
+ mavenCentral()
}
apply from: "gradle/dependencies.gradle"
@@ -46,7 +46,6 @@ subprojects { project ->
project.apply plugin: "net.ltgt.errorprone"
project.dependencies {
errorprone deps.build.errorProneCore
- errorproneJavac deps.build.errorProneJavac
}
project.tasks.withType(JavaCompile) {
dependsOn(installGitHooks)
@@ -59,19 +58,32 @@ subprojects { project ->
options.errorprone {
// disable warnings in generated code; AutoValue code fails UnnecessaryParentheses check
disableWarningsInGeneratedCode = true
- // Triggers for generated Android code (R.java)
- check("MutablePublicArray", CheckSeverity.OFF)
// this check is too noisy
check("StringSplitter", CheckSeverity.OFF)
+ // https://github.com/google/error-prone/issues/3366
+ check("CanIgnoreReturnValueSuggester", CheckSeverity.OFF)
+ // turn up various checks
check("WildcardImport", CheckSeverity.ERROR)
check("MissingBraces", CheckSeverity.ERROR)
+ check("TypeToString", CheckSeverity.ERROR)
+ check("SymbolToString", CheckSeverity.ERROR)
+ check("MultipleTopLevelClasses", CheckSeverity.ERROR)
+ check("ClassName", CheckSeverity.ERROR)
+ check("PackageLocation", CheckSeverity.ERROR)
+ check("UnnecessaryAnonymousClass", CheckSeverity.ERROR)
+ check("UnusedException", CheckSeverity.ERROR)
+ // To enable auto-patching, uncomment the line below, replace [CheckerName] with
+ // the checker(s) you want to apply patches for (comma-separated), and above, disable
+ // "-Werror"
+ // errorproneArgs.addAll("-XepPatchChecks:[CheckerName]", "-XepPatchLocation:IN_PLACE")
}
}
- if (JavaVersion.current().java9Compatible) {
- tasks.withType(JavaCompile) {
- options.release = 8
- }
+ // Target JDK 8. We need to use the older sourceCompatibility / targetCompatibility settings to get
+ // the build to work on JDK 11+. Once we stop supporting JDK 8, switch to using the javac "release" option
+ tasks.withType(JavaCompile) {
+ java.sourceCompatibility = "1.8"
+ java.targetCompatibility = "1.8"
}
tasks.withType(Test).configureEach {
@@ -83,12 +95,42 @@ subprojects { project ->
google()
}
+ // For some reason, spotless complains when applied to the jar-infer folder itself, even
+ // though there is no top-level :jar-infer project
+ if (project.name != "jar-infer") {
+ project.apply plugin: "com.diffplug.spotless"
+ spotless {
+ java {
+ googleJavaFormat()
+ }
+ }
+ }
}
-googleJavaFormat {
- toolVersion = "1.6"
- // don't enforce formatting for generated Java code under buildSrc
- exclude 'buildSrc/build/**/*.java'
+spotless {
+ predeclareDeps()
+ groovyGradle {
+ target '**/*.gradle'
+ greclipse()
+ indentWithSpaces(4)
+ trimTrailingWhitespace()
+ endWithNewline()
+ }
+}
+spotlessPredeclare {
+ java { googleJavaFormat('1.17.0') }
+ groovyGradle {
+ greclipse()
+ }
+}
+
+shellcheck {
+ useDocker = false
+ shellcheckBinary = "shellcheck"
+ sourceFiles =
+ fileTree(".") {
+ include("**/*.sh")
+ }
}
////////////////////////////////////////////////////////////////////////
@@ -97,9 +139,9 @@ googleJavaFormat {
//
tasks.register('installGitHooks', Copy) {
- from(file('config/hooks/pre-commit-stub')) {
- rename 'pre-commit-stub', 'pre-commit'
- }
- into file('.git/hooks')
- fileMode 0777
+ from(file('config/hooks/pre-commit-stub')) {
+ rename 'pre-commit-stub', 'pre-commit'
+ }
+ into file('.git/hooks')
+ fileMode 0777
}
diff --git a/buildSrc/src/main/groovy/nullaway.jacoco-conventions.gradle b/buildSrc/src/main/groovy/nullaway.jacoco-conventions.gradle
deleted file mode 100644
index 1c7c97c..0000000
--- a/buildSrc/src/main/groovy/nullaway.jacoco-conventions.gradle
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Copyright (C) 2021. Uber Technologies
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// Mostly taken from official Gradle sample: https://docs.gradle.org/current/samples/sample_jvm_multi_project_with_code_coverage.html
-plugins {
- id 'jacoco'
-}
-
-jacoco {
- toolVersion = "0.8.7"
-}
-
-// Do not generate reports for individual projects
-tasks.named("jacocoTestReport") {
- enabled = false
-}
-
-// Share sources folder with other projects for aggregated JaCoCo reports
-configurations.create('transitiveSourcesElements') {
- visible = false
- canBeResolved = false
- canBeConsumed = true
- extendsFrom(configurations.implementation)
- attributes {
- attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
- attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.DOCUMENTATION))
- attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType, 'source-folders'))
- }
- sourceSets.main.java.srcDirs.forEach {
- outgoing.artifact(it)
- }
-}
-
-// Share the coverage data to be aggregated for the whole product
-configurations.create('coverageDataElements') {
- visible = false
- canBeResolved = false
- canBeConsumed = true
- extendsFrom(configurations.implementation)
- attributes {
- attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
- attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.DOCUMENTATION))
- attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType, 'jacoco-coverage-data'))
- }
- // This will cause the test task to run if the coverage data is requested by the aggregation task
- outgoing.artifact(tasks.named("test").map { task ->
- task.extensions.getByType(JacocoTaskExtension).destinationFile
- })
-}
diff --git a/buildSrc/src/main/groovy/nullaway.java-test-conventions.gradle b/buildSrc/src/main/groovy/nullaway.java-test-conventions.gradle
new file mode 100644
index 0000000..07f8f26
--- /dev/null
+++ b/buildSrc/src/main/groovy/nullaway.java-test-conventions.gradle
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023. Uber Technologies
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Mostly taken from official Gradle sample: https://docs.gradle.org/current/samples/sample_jvm_multi_project_with_code_coverage.html
+plugins {
+ id 'jacoco'
+}
+
+jacoco {
+ toolVersion = "0.8.10"
+}
+
+// Do not generate reports for individual projects
+tasks.named("jacocoTestReport") {
+ enabled = false
+}
+
+// Share sources folder with other projects for aggregated JaCoCo reports
+configurations.create('transitiveSourcesElements') {
+ visible = false
+ canBeResolved = false
+ canBeConsumed = true
+ extendsFrom(configurations.implementation)
+ attributes {
+ attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
+ attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.DOCUMENTATION))
+ attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType, 'source-folders'))
+ }
+ sourceSets.main.java.srcDirs.forEach {
+ outgoing.artifact(it)
+ }
+}
+
+// Share the coverage data to be aggregated for the whole product
+configurations.create('coverageDataElements') {
+ visible = false
+ canBeResolved = false
+ canBeConsumed = true
+ extendsFrom(configurations.implementation)
+ attributes {
+ attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
+ attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category, Category.DOCUMENTATION))
+ attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType, 'jacoco-coverage-data'))
+ }
+ // This will cause the test task to run if the coverage data is requested by the aggregation task
+ outgoing.artifact(tasks.named("test").map { task ->
+ task.extensions.getByType(JacocoTaskExtension).destinationFile
+ })
+}
+
+test {
+ maxHeapSize = "1024m"
+ // to expose necessary JDK types on JDK 16+; see https://errorprone.info/docs/installation#java-9-and-newer
+ 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",
+ // Accessed by Lombok tests
+ "--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED",
+ ]
+}
+
+// Create a task to test on JDK 21
+def testJdk21 = tasks.register("testJdk21", Test) {
+ onlyIf {
+ // Only test on JDK 21 when using the latest Error Prone version
+ deps.versions.errorProneApi == deps.versions.errorProneLatest
+ }
+ javaLauncher = javaToolchains.launcherFor {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
+
+ description = "Runs the test suite on JDK 21"
+ group = LifecycleBasePlugin.VERIFICATION_GROUP
+
+ // Copy inputs from normal Test task.
+ def testTask = tasks.getByName("test")
+ classpath = testTask.classpath
+ testClassesDirs = testTask.testClassesDirs
+ maxHeapSize = "1024m"
+ // to expose necessary JDK types on JDK 16+; see https://errorprone.info/docs/installation#java-9-and-newer
+ 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",
+ // Accessed by Lombok tests
+ "--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED",
+ ]
+}
+
+tasks.named('check').configure {
+ dependsOn testJdk21
+}
diff --git a/code-coverage-report/build.gradle b/code-coverage-report/build.gradle
index 0862c69..3e5420f 100644
--- a/code-coverage-report/build.gradle
+++ b/code-coverage-report/build.gradle
@@ -18,9 +18,15 @@
plugins {
id 'java'
id 'jacoco'
- id 'com.github.kt3k.coveralls'
}
+// Use JDK 21 for this module, via a toolchain. We need JDK 21 since this module
+// depends on jdk-recent-unit-tests.
+// We must null out sourceCompatibility and targetCompatibility to use toolchains.
+java.sourceCompatibility = null
+java.targetCompatibility = null
+java.toolchain.languageVersion.set JavaLanguageVersion.of(21)
+
// A resolvable configuration to collect source code
def sourcesPath = configurations.create("sourcesPath") {
visible = false
@@ -64,25 +70,14 @@ def codeCoverageReport = tasks.register('codeCoverageReport', JacocoReport) {
}
}
-coveralls {
- jacocoReportPath = "build/reports/jacoco/codeCoverageReport/codeCoverageReport.xml"
- afterEvaluate {
- sourceDirs = sourcesPath.incoming.artifactView { lenient(true) }.files as List<String>
- }
-}
-
-def coverallsTask = tasks.named('coveralls')
-
-coverallsTask.configure {
- dependsOn 'codeCoverageReport'
-}
-
// These dependencies indicate which projects have tests or tested code we want to include
// when computing overall coverage. We aim to measure coverage for all code that actually ships
// in a Maven artifact (so, e.g., we do not measure coverage for the jmh module)
dependencies {
+ implementation project(':annotations')
implementation project(':nullaway')
implementation project(':jar-infer:jar-infer-lib')
implementation project(':jar-infer:nullaway-integration-test')
- implementation project(':jdk17-unit-tests')
+ implementation project(':guava-recent-unit-tests')
+ implementation project(':jdk-recent-unit-tests')
}
diff --git a/config/hooks/pre-commit b/config/hooks/pre-commit
index f31a3cf..ffdb168 100755
--- a/config/hooks/pre-commit
+++ b/config/hooks/pre-commit
@@ -7,6 +7,6 @@ REPO_ROOT_DIR="$(git rev-parse --show-toplevel)"
files=$((git diff --cached --name-only --diff-filter=ACMR | grep -Ei "\.java$") || true)
if [ ! -z "${files}" ]; then
comma_files=$(echo "$files" | paste -s -d "," -)
- "${REPO_ROOT_DIR}/gradlew" goJF -DgoogleJavaFormat.include="$comma_files" &>/dev/null
+ "${REPO_ROOT_DIR}/gradlew" spotlessApply -Pspotless.ratchet.from=HEAD >/dev/null 2>&1
git add $(echo "$files" | paste -s -d " " -)
fi
diff --git a/gradle.properties b/gradle.properties
index 484b078..3b5fd0c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -9,10 +9,10 @@
org.gradle.parallel=true
org.gradle.caching=true
-org.gradle.jvmargs=-Xmx2g
+org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
GROUP=com.uber.nullaway
-VERSION_NAME=0.9.6-SNAPSHOT
+VERSION_NAME=0.10.16
POM_DESCRIPTION=A fast annotation-based null checker for Java
@@ -28,3 +28,7 @@ POM_LICENCE_DIST=repo
POM_DEVELOPER_ID=uber
POM_DEVELOPER_NAME=Uber Technologies
POM_DEVELOPER_URL=https://uber.com
+
+# Publishing configuration for vanniktech/gradle-maven-publish-plugin
+SONATYPE_HOST=DEFAULT
+RELEASE_SIGNING_ENABLED=true
diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle
index c882a84..1af2c45 100755
--- a/gradle/dependencies.gradle
+++ b/gradle/dependencies.gradle
@@ -17,31 +17,41 @@ import org.gradle.util.VersionNumber
*/
// The oldest version of Error Prone that we support running on
-def oldestErrorProneApi = "2.4.0"
+def oldestErrorProneVersion = "2.10.0"
+// Latest released Error Prone version that we've tested with
+def latestErrorProneVersion = "2.23.0"
+// Default to using latest tested Error Prone version
+def defaultErrorProneVersion = latestErrorProneVersion
+def errorProneVersionToCompileAgainst = defaultErrorProneVersion
+// If the epApiVersion project property is set, compile and test against that version of Error Prone
if (project.hasProperty("epApiVersion")) {
def epApiVNum = VersionNumber.parse(epApiVersion)
if (epApiVNum.equals(VersionNumber.UNKNOWN)) {
throw new IllegalArgumentException("Invalid Error Prone API version " + epApiVersion)
}
- if (epApiVNum.compareTo(VersionNumber.parse(oldestErrorProneApi)) < 0) {
+ if (epApiVNum.compareTo(VersionNumber.parse(oldestErrorProneVersion)) < 0) {
throw new IllegalArgumentException(
- "Error Prone API version " + epApiVersion + " is too old; "
- + oldestErrorProneApi + " is the oldest supported version")
+ "Error Prone API version " + epApiVersion + " is too old; "
+ + oldestErrorProneVersion + " is the oldest supported version")
}
+ errorProneVersionToCompileAgainst = epApiVersion
}
+
def versions = [
- asm : "7.1",
- checkerFramework : "3.21.1",
- // The version of Error Prone used to check NullAway's code
- errorProne : "2.10.0",
+ asm : "9.3",
+ checkerFramework : "3.39.0",
+ // for comparisons in other parts of the build
+ errorProneLatest : latestErrorProneVersion,
+ // The version of Error Prone used to check NullAway's code.
+ errorProne : defaultErrorProneVersion,
// The version of Error Prone that NullAway is compiled and tested against
- errorProneApi : project.hasProperty("epApiVersion") ? epApiVersion : oldestErrorProneApi,
+ errorProneApi : errorProneVersionToCompileAgainst,
support : "27.1.1",
- wala : "1.5.4",
+ wala : "1.6.2",
commonscli : "1.4",
- autoValue : "1.9",
- autoService : "1.0.1",
+ autoValue : "1.10.2",
+ autoService : "1.1.1",
]
def apt = [
@@ -62,17 +72,19 @@ def build = [
errorProneJavac : "com.google.errorprone:javac:9+181-r4173-1",
errorProneTestHelpers : "com.google.errorprone:error_prone_test_helpers:${versions.errorProneApi}",
checkerDataflow : "org.checkerframework:dataflow-nullaway:${versions.checkerFramework}",
- guava : "com.google.guava:guava:24.1.1-jre",
+ guava : "com.google.guava:guava:30.1-jre",
javaxValidation : "javax.validation:validation-api:2.0.1.Final",
+ jspecify : "org.jspecify:jspecify:0.3.0",
jsr305Annotations : "com.google.code.findbugs:jsr305:3.0.2",
- commonsIO : "commons-io:commons-io:2.4",
- wala : ["com.ibm.wala:com.ibm.wala.util:${versions.wala}",
- "com.ibm.wala:com.ibm.wala.shrike:${versions.wala}",
- "com.ibm.wala:com.ibm.wala.core:${versions.wala}"],
+ commonsIO : "commons-io:commons-io:2.11.0",
+ wala : [
+ "com.ibm.wala:com.ibm.wala.util:${versions.wala}",
+ "com.ibm.wala:com.ibm.wala.shrike:${versions.wala}",
+ "com.ibm.wala:com.ibm.wala.core:${versions.wala}"
+ ],
commonscli : "commons-cli:commons-cli:${versions.commonscli}",
// android stuff
- buildToolsVersion: "30.0.3",
compileSdkVersion: 30,
ci: "true" == System.getenv("CI"),
minSdkVersion: 16,
@@ -85,20 +97,25 @@ def support = [
]
def test = [
- junit4 : "junit:junit:4.12",
- junit5Jupiter : ["org.junit.jupiter:junit-jupiter-api:5.0.2","org.apiguardian:apiguardian-api:1.0.0"],
+ junit4 : "junit:junit:4.13.2",
+ junit5Jupiter : [
+ "org.junit.jupiter:junit-jupiter-api:5.0.2",
+ "org.apiguardian:apiguardian-api:1.0.0"
+ ],
jetbrainsAnnotations : "org.jetbrains:annotations:13.0",
- inferAnnotations : "com.facebook.infer.annotation:infer-annotation:0.11.0",
cfQual : "org.checkerframework:checker-qual:${versions.checkerFramework}",
// 2.5.5 is the last release to contain this artifact
cfCompatQual : "org.checkerframework:checker-compat-qual:2.5.5",
rxjava2 : "io.reactivex.rxjava2:rxjava:2.1.2",
commonsLang3 : "org.apache.commons:commons-lang3:3.8.1",
commonsLang : "commons-lang:commons-lang:2.6",
- lombok : "org.projectlombok:lombok:1.18.12",
+ lombok : "org.projectlombok:lombok:1.18.24",
springBeans : "org.springframework:spring-beans:5.3.7",
springContext : "org.springframework:spring-context:5.3.7",
grpcCore : "io.grpc:grpc-core:1.15.1", // Should upgrade, but this matches our guava version
+ mockito : "org.mockito:mockito-core:5.5.0",
+ javaxAnnotationApi : "javax.annotation:javax.annotation-api:1.3.2",
+ assertJ : "org.assertj:assertj-core:3.23.1",
]
ext.deps = [
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 7454180..7f93135 100644
--- a/gradle/wrapper/gradle-wrapper.jar
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 2e6e589..46671ac 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,8 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
+distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 1b6c787..1aa94a4 100755
--- a/gradlew
+++ b/gradlew
@@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +80,11 @@ do
esac
done
-APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
-
-APP_NAME="Gradle"
+# This is normally unused
+# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -133,22 +131,29 @@ location of your Java installation."
fi
else
JAVACMD=java
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -193,11 +198,15 @@ if "$cygwin" || "$msys" ; then
done
fi
-# Collect all arguments for the java command;
-# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
-# shell script including quotes and variable substitutions, so put them in
-# double quotes to make sure that they get re-expanded; and
-# * put everything else in single quotes, so that it's not re-expanded.
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
@@ -205,6 +214,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
diff --git a/gradlew.bat b/gradlew.bat
index ac1b06f..6689b85 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/guava-recent-unit-tests/build.gradle b/guava-recent-unit-tests/build.gradle
new file mode 100644
index 0000000..7740123
--- /dev/null
+++ b/guava-recent-unit-tests/build.gradle
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022. Uber Technologies
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+plugins {
+ id 'java-library'
+ id 'nullaway.java-test-conventions'
+}
+
+// We need this separate build target to test newer versions of Guava
+// (e.g. 31+) than that which NullAway currently depends on.
+
+dependencies {
+ testImplementation project(":nullaway")
+ testImplementation deps.test.junit4
+ testImplementation(deps.build.errorProneTestHelpers) {
+ exclude group: "junit", module: "junit"
+ }
+ testImplementation deps.build.jsr305Annotations
+ testImplementation "com.google.guava:guava:31.1-jre"
+}
+
+// Create a task to test on JDK 8
+def jdk8Test = tasks.register("testJdk8", Test) {
+ onlyIf {
+ // Only if we are using a version of Error Prone compatible with JDK 8
+ deps.versions.errorProneApi == "2.10.0"
+ }
+
+ javaLauncher = javaToolchains.launcherFor {
+ languageVersion = JavaLanguageVersion.of(8)
+ }
+
+ description = "Runs the test suite on JDK 8"
+ group = LifecycleBasePlugin.VERIFICATION_GROUP
+
+ // Copy inputs from normal Test task.
+ def testTask = tasks.getByName("test")
+ classpath = testTask.classpath
+ testClassesDirs = testTask.testClassesDirs
+ jvmArgs "-Xbootclasspath/p:${configurations.errorproneJavac.asPath}"
+}
+
+tasks.named('check').configure {
+ dependsOn(jdk8Test)
+}
diff --git a/guava-recent-unit-tests/src/test/java/com/uber/nullaway/guava/NullAwayGuavaParametricNullnessTests.java b/guava-recent-unit-tests/src/test/java/com/uber/nullaway/guava/NullAwayGuavaParametricNullnessTests.java
new file mode 100644
index 0000000..156ea04
--- /dev/null
+++ b/guava-recent-unit-tests/src/test/java/com/uber/nullaway/guava/NullAwayGuavaParametricNullnessTests.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+import com.google.errorprone.CompilationTestHelper;
+import com.uber.nullaway.NullAway;
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class NullAwayGuavaParametricNullnessTests {
+ @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private CompilationTestHelper defaultCompilationHelper;
+
+ @Before
+ public void setup() {
+ defaultCompilationHelper =
+ CompilationTestHelper.newInstance(NullAway.class, getClass())
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber,com.google.common",
+ "-XepOpt:NullAway:UnannotatedSubPackages=com.uber.nullaway.[a-zA-Z0-9.]+.unannotated"));
+ }
+
+ @Test
+ public void testFutureCallbackParametricNullness() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.google.common.util.concurrent.FutureCallback;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " public static <T> FutureCallback<T> wrapFutureCallback(FutureCallback<T> futureCallback) {",
+ " return new FutureCallback<T>() {",
+ " @Override",
+ " public void onSuccess(@Nullable T result) {",
+ " futureCallback.onSuccess(result);",
+ " }",
+ " @Override",
+ " public void onFailure(Throwable throwable) {",
+ " futureCallback.onFailure(throwable);",
+ " }",
+ " };",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testIterableParametricNullness() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.google.common.collect.ImmutableList;",
+ "import com.google.common.collect.Iterables;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " public static String test1() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return Iterables.getFirst(ImmutableList.<String>of(), null);",
+ " }",
+ " public static @Nullable String test2() {",
+ " return Iterables.getFirst(ImmutableList.<String>of(), null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testCloserParametricNullness() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.google.common.io.Closer;",
+ "import java.io.Closeable;",
+ "import java.io.FileInputStream;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " public static FileInputStream test1(Closer closer, FileInputStream fis1) {",
+ " // safe: non-null arg to non-null return",
+ " return closer.register(fis1);",
+ " }",
+ " @Nullable",
+ " public static FileInputStream test2(Closer closer, @Nullable FileInputStream fis2) {",
+ " // safe: nullable arg to nullable return",
+ " return closer.register(fis2);",
+ " }",
+ " public static FileInputStream test3(Closer closer, @Nullable FileInputStream fis3) {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return closer.register(fis3);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testFunctionMethodOverride() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.google.common.base.Function;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " public static void testFunctionOverride() {",
+ " Function<Object, Object> f = new Function<Object, Object>() {",
+ " @Override",
+ " public Object apply(Object input) {",
+ " return input;",
+ " }",
+ " };",
+ " }",
+ " public static void testFunctionOverrideNullableReturn() {",
+ " Function<Object, Object> f = new Function<Object, Object>() {",
+ " @Override",
+ " @Nullable",
+ " // BUG: Diagnostic contains: method returns @Nullable, but superclass method",
+ " public Object apply(Object input) {",
+ " return null;",
+ " }",
+ " };",
+ " }",
+ "}")
+ .doTest();
+ }
+}
diff --git a/jar-infer/android-jarinfer-models-sdk28/build.gradle b/jar-infer/android-jarinfer-models-sdk28/build.gradle
index 9ff005f..fe50fe8 100644
--- a/jar-infer/android-jarinfer-models-sdk28/build.gradle
+++ b/jar-infer/android-jarinfer-models-sdk28/build.gradle
@@ -2,8 +2,6 @@ plugins {
id "java-library"
}
-sourceCompatibility = 1.8
-
repositories {
mavenCentral()
}
diff --git a/jar-infer/android-jarinfer-models-sdk29/build.gradle b/jar-infer/android-jarinfer-models-sdk29/build.gradle
index 9ff005f..fe50fe8 100644
--- a/jar-infer/android-jarinfer-models-sdk29/build.gradle
+++ b/jar-infer/android-jarinfer-models-sdk29/build.gradle
@@ -2,8 +2,6 @@ plugins {
id "java-library"
}
-sourceCompatibility = 1.8
-
repositories {
mavenCentral()
}
diff --git a/jar-infer/android-jarinfer-models-sdk30/build.gradle b/jar-infer/android-jarinfer-models-sdk30/build.gradle
index 9ff005f..fe50fe8 100644
--- a/jar-infer/android-jarinfer-models-sdk30/build.gradle
+++ b/jar-infer/android-jarinfer-models-sdk30/build.gradle
@@ -2,8 +2,6 @@ plugins {
id "java-library"
}
-sourceCompatibility = 1.8
-
repositories {
mavenCentral()
}
diff --git a/jar-infer/android-jarinfer-models-sdk31/build.gradle b/jar-infer/android-jarinfer-models-sdk31/build.gradle
index 9ff005f..fe50fe8 100644
--- a/jar-infer/android-jarinfer-models-sdk31/build.gradle
+++ b/jar-infer/android-jarinfer-models-sdk31/build.gradle
@@ -2,8 +2,6 @@ plugins {
id "java-library"
}
-sourceCompatibility = 1.8
-
repositories {
mavenCentral()
}
diff --git a/jar-infer/jar-infer-cli/build.gradle b/jar-infer/jar-infer-cli/build.gradle
index fb0252a..3245d66 100644
--- a/jar-infer/jar-infer-cli/build.gradle
+++ b/jar-infer/jar-infer-cli/build.gradle
@@ -4,8 +4,11 @@ plugins {
id "com.github.johnrengelman.shadow"
}
-sourceCompatibility = "1.8"
-targetCompatibility = "1.8"
+// JarInfer requires JDK 11+, due to its dependence on WALA
+tasks.withType(JavaCompile) {
+ java.sourceCompatibility = JavaVersion.VERSION_11
+ java.targetCompatibility = JavaVersion.VERSION_11
+}
repositories {
mavenCentral()
@@ -14,8 +17,6 @@ repositories {
dependencies {
implementation deps.build.commonscli
implementation deps.build.guava
- compileOnly deps.build.errorProneCheckApi
-
implementation project(":jar-infer:jar-infer-lib")
testImplementation deps.test.junit4
@@ -24,11 +25,14 @@ dependencies {
}
}
+java {
+ withJavadocJar()
+ withSourcesJar()
+}
+
jar {
manifest {
- attributes(
- 'Main-Class': 'com.uber.nullaway.jarinfer.JarInfer'
- )
+ attributes('Main-Class': 'com.uber.nullaway.jarinfer.JarInfer')
}
// add this classifier so that the output file for the jar task differs from
// the output file for the shadowJar task (otherwise they overwrite each other's
@@ -38,12 +42,15 @@ jar {
shadowJar {
mergeServiceFiles()
- configurations = [project.configurations.runtimeClasspath]
- classifier = null
+ configurations = [
+ project.configurations.runtimeClasspath
+ ]
+ archiveClassifier = ""
}
shadowJar.dependsOn jar
assemble.dependsOn shadowJar
+
// We disable the default maven publications to make sure only
// our custom shadow publication is used. Since we use the empty
// classifier for our fat jar, it would otherwise clash with the
@@ -70,12 +77,8 @@ publishing {
// Since we are skipping the default maven publication, we append the `:sources` and
// `:javadoc` artifacts here. They are also required for Maven Central validation.
afterEvaluate {
- artifact project.sourcesJar {
- classifier "sources"
- }
- artifact project.javadocsJar {
- classifier "javadoc"
- }
+ artifact project.sourcesJar
+ artifact project.javadocJar
}
// The shadow publication does not auto-configure the pom.xml file for us, so we need to
// set it up manually. We use the opportunity to change the name and description from
@@ -106,4 +109,33 @@ publishing {
}
}
}
+
+ afterEvaluate {
+ // Below is a series of hacks needed to get publication to work with
+ // gradle-maven-publish-plugin >= 0.15.0 (itself needed after the upgrade to Gradle 8.0.2).
+ // Not sure why e.g. publishShadowPublicationToMavenCentralRepository must depend on signMavenPublication
+ // (rather than just signShadowPublication)
+ project.tasks.named('generateMetadataFileForMavenPublication').configure {
+ dependsOn 'sourcesJar'
+ dependsOn 'simpleJavadocJar'
+ }
+ if (project.tasks.findByName('signShadowPublication')) {
+ project.tasks.named('signShadowPublication').configure {
+ dependsOn 'sourcesJar'
+ dependsOn 'simpleJavadocJar'
+ }
+ }
+ project.tasks.named('publishShadowPublicationToMavenCentralRepository').configure {
+ if (project.tasks.findByName('signMavenPublication')) {
+ dependsOn 'signMavenPublication'
+ }
+ }
+ project.tasks.named('publishShadowPublicationToMavenLocal').configure {
+ dependsOn 'sourcesJar'
+ dependsOn 'simpleJavadocJar'
+ if (project.tasks.findByName('signMavenPublication')) {
+ dependsOn 'signMavenPublication'
+ }
+ }
+ }
}
diff --git a/jar-infer/jar-infer-lib/build.gradle b/jar-infer/jar-infer-lib/build.gradle
index e150daa..2ae8bea 100644
--- a/jar-infer/jar-infer-lib/build.gradle
+++ b/jar-infer/jar-infer-lib/build.gradle
@@ -15,10 +15,14 @@
*/
plugins {
id "java-library"
- id 'nullaway.jacoco-conventions'
+ id 'nullaway.java-test-conventions'
}
-sourceCompatibility = 1.8
+// JarInfer requires JDK 11+, due to its dependence on WALA
+tasks.withType(JavaCompile) {
+ java.sourceCompatibility = JavaVersion.VERSION_11
+ java.targetCompatibility = JavaVersion.VERSION_11
+}
repositories {
mavenCentral()
@@ -39,32 +43,22 @@ dependencies {
exclude group: "junit", module: "junit"
}
testImplementation project(":jar-infer:test-java-lib-jarinfer")
- testImplementation project(path: ":jar-infer:test-android-lib-jarinfer", configuration: "default")
testImplementation files("${System.properties['java.home']}/../lib/tools.jar") // is there a better way?
testRuntimeOnly deps.build.errorProneCheckApi
}
test {
- maxHeapSize = "1024m"
- if (!JavaVersion.current().java9Compatible) {
- jvmArgs "-Xbootclasspath/p:${configurations.errorproneJavac.asPath}"
- } else {
- // to expose necessary JDK types on JDK 16+; see https://errorprone.info/docs/installation#java-9-and-newer
- 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",
- // Accessed by Lombok tests
- "--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED",
- ]
- }
+ dependsOn ':jar-infer:test-android-lib-jarinfer:bundleReleaseAar'
+}
+
+tasks.named('testJdk21', Test).configure {
+ // Tests fail since WALA does not yet support JDK 21; see https://github.com/uber/NullAway/issues/829
+ // So, disable them
+ onlyIf { false }
+}
+
+tasks.withType(JavaCompile).configureEach {
+ options.compilerArgs += "--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED"
}
apply plugin: 'com.vanniktech.maven.publish'
diff --git a/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/BytecodeAnnotator.java b/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/BytecodeAnnotator.java
index 6b226fa..94e128c 100644
--- a/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/BytecodeAnnotator.java
+++ b/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/BytecodeAnnotator.java
@@ -15,6 +15,8 @@
*/
package com.uber.nullaway.jarinfer;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.io.BufferedReader;
@@ -227,7 +229,7 @@ public final class BytecodeAnnotator {
} else if (entryName.equals("META-INF/MANIFEST.MF")) {
// Read full file
StringBuilder stringBuilder = new StringBuilder();
- BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+ BufferedReader br = new BufferedReader(new InputStreamReader(is, UTF_8));
String currentLine;
while ((currentLine = br.readLine()) != null) {
stringBuilder.append(currentLine + "\n");
@@ -240,7 +242,7 @@ public final class BytecodeAnnotator {
throw new SignedJarException(SIGNED_JAR_ERROR_MESSAGE);
}
jarOS.putNextEntry(new ZipEntry(jarEntry.getName()));
- jarOS.write(manifestMinusDigests.getBytes("UTF-8"));
+ jarOS.write(manifestMinusDigests.getBytes(UTF_8));
} else if (entryName.startsWith("META-INF/")
&& (entryName.endsWith(".DSA")
|| entryName.endsWith(".RSA")
diff --git a/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/DefinitelyDerefedParamsDriver.java b/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/DefinitelyDerefedParamsDriver.java
index 469d3d0..fc85241 100644
--- a/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/DefinitelyDerefedParamsDriver.java
+++ b/jar-infer/jar-infer-lib/src/main/java/com/uber/nullaway/jarinfer/DefinitelyDerefedParamsDriver.java
@@ -25,6 +25,8 @@ import com.ibm.wala.classLoader.IClassLoader;
import com.ibm.wala.classLoader.IMethod;
import com.ibm.wala.classLoader.PhantomClass;
import com.ibm.wala.classLoader.ShrikeCTMethod;
+import com.ibm.wala.core.util.config.AnalysisScopeReader;
+import com.ibm.wala.core.util.warnings.Warnings;
import com.ibm.wala.ipa.callgraph.AnalysisCache;
import com.ibm.wala.ipa.callgraph.AnalysisCacheImpl;
import com.ibm.wala.ipa.callgraph.AnalysisOptions;
@@ -33,16 +35,14 @@ import com.ibm.wala.ipa.callgraph.impl.Everywhere;
import com.ibm.wala.ipa.cha.ClassHierarchyException;
import com.ibm.wala.ipa.cha.ClassHierarchyFactory;
import com.ibm.wala.ipa.cha.IClassHierarchy;
-import com.ibm.wala.shrikeCT.InvalidClassFileException;
+import com.ibm.wala.shrike.shrikeCT.InvalidClassFileException;
import com.ibm.wala.ssa.IR;
import com.ibm.wala.ssa.ISSABasicBlock;
import com.ibm.wala.ssa.SSAInstruction;
import com.ibm.wala.types.ClassLoaderReference;
import com.ibm.wala.types.TypeReference;
import com.ibm.wala.util.collections.Iterator2Iterable;
-import com.ibm.wala.util.config.AnalysisScopeReader;
import com.ibm.wala.util.config.FileOfClasses;
-import com.ibm.wala.util.warnings.Warnings;
import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.io.File;
@@ -228,14 +228,15 @@ public class DefinitelyDerefedParamsDriver {
} else if (!new File(inPath).exists()) {
return;
}
- AnalysisScope scope = AnalysisScopeReader.makePrimordialScope(null);
+ AnalysisScope scope = AnalysisScopeReader.instance.makeBasePrimordialScope(null);
scope.setExclusions(
new FileOfClasses(
new ByteArrayInputStream(DEFAULT_EXCLUSIONS.getBytes(StandardCharsets.UTF_8))));
if (jarIS != null) {
scope.addInputStreamForJarToScope(ClassLoaderReference.Application, jarIS);
} else {
- AnalysisScopeReader.addClassPathToScope(inPath, scope, ClassLoaderReference.Application);
+ AnalysisScopeReader.instance.addClassPathToScope(
+ inPath, scope, ClassLoaderReference.Application);
}
AnalysisOptions options = new AnalysisOptions(scope, null);
AnalysisCache cache = new AnalysisCacheImpl();
@@ -509,6 +510,7 @@ public class DefinitelyDerefedParamsDriver {
+ strArgTypes
+ ")";
}
+
/**
* Get simple unqualified type name.
*
diff --git a/jar-infer/jar-infer-lib/src/test/java/com/uber/nullaway/jarinfer/JarInferTest.java b/jar-infer/jar-infer-lib/src/test/java/com/uber/nullaway/jarinfer/JarInferTest.java
index 67d5356..897e96e 100644
--- a/jar-infer/jar-infer-lib/src/test/java/com/uber/nullaway/jarinfer/JarInferTest.java
+++ b/jar-infer/jar-infer-lib/src/test/java/com/uber/nullaway/jarinfer/JarInferTest.java
@@ -57,14 +57,15 @@ public class JarInferTest {
private CompilationTestHelper compilationTestHelper;
- @BugPattern(
- name = "DummyChecker",
- summary = "Dummy checker to use CompilationTestHelper",
- severity = WARNING)
/**
* A dummy checker to allow us to use {@link CompilationTestHelper} to compile Java code for
* testing, as it requires a {@link BugChecker} to run.
*/
+ @BugPattern(
+ name = "DummyChecker",
+ summary = "Dummy checker to use CompilationTestHelper",
+ severity = WARNING)
+ @SuppressWarnings("BugPatternNaming") // remove once we require EP 2.11+
public static class DummyChecker extends BugChecker {
public DummyChecker() {}
}
@@ -169,7 +170,7 @@ public class JarInferTest {
* @param result Map of 'method signatures' to their 'inferred list of NonNull parameters'.
* @param expected Map of 'method signatures' to their 'expected list of NonNull parameters'.
*/
- private boolean verify(Map<String, Set<Integer>> result, HashMap<String, Set<Integer>> expected) {
+ private boolean verify(Map<String, Set<Integer>> result, Map<String, Set<Integer>> expected) {
for (Map.Entry<String, Set<Integer>> entry : result.entrySet()) {
String mtd_sign = entry.getKey();
Set<Integer> ddParams = entry.getValue();
@@ -435,6 +436,10 @@ public class JarInferTest {
@Test
public void toyAARAnnotatingClasses() throws Exception {
+ if (System.getProperty("java.version").startsWith("1.8")) {
+ // We only build the sample Android apps on JDK 11+
+ return;
+ }
testAnnotationInAarTemplate(
"toyAARAnnotatingClasses",
"com.uber.nullaway.jarinfer.toys.unannotated",
diff --git a/jar-infer/nullaway-integration-test/build.gradle b/jar-infer/nullaway-integration-test/build.gradle
index f47fe58..94a8808 100644
--- a/jar-infer/nullaway-integration-test/build.gradle
+++ b/jar-infer/nullaway-integration-test/build.gradle
@@ -14,41 +14,15 @@
* limitations under the License.
*/
plugins {
- id "java-library"
- id "nullaway.jacoco-conventions"
+ id "java-library"
+ id "nullaway.java-test-conventions"
}
-sourceCompatibility = "1.8"
-targetCompatibility = "1.8"
-
dependencies {
- testImplementation deps.test.junit4
- testImplementation(deps.build.errorProneTestHelpers) {
+ testImplementation deps.test.junit4
+ testImplementation(deps.build.errorProneTestHelpers) {
exclude group: "junit", module: "junit"
}
- testImplementation project(":nullaway")
- testImplementation project(":jar-infer:test-java-lib-jarinfer")
-
-}
-
-
-test {
- maxHeapSize = "1024m"
- if (!JavaVersion.current().java9Compatible) {
- jvmArgs "-Xbootclasspath/p:${configurations.errorproneJavac.asPath}"
- } else {
- // to expose necessary JDK types on JDK 16+; see https://errorprone.info/docs/installation#java-9-and-newer
- 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",
- ]
- }
+ testImplementation project(":nullaway")
+ testImplementation project(":jar-infer:test-java-lib-jarinfer")
}
diff --git a/jar-infer/test-android-lib-jarinfer/build.gradle b/jar-infer/test-android-lib-jarinfer/build.gradle
index 6f1cade..b79ed03 100644
--- a/jar-infer/test-android-lib-jarinfer/build.gradle
+++ b/jar-infer/test-android-lib-jarinfer/build.gradle
@@ -16,11 +16,8 @@
apply plugin: 'com.android.library'
-sourceCompatibility = 1.8
-
android {
compileSdkVersion deps.build.compileSdkVersion
- buildToolsVersion deps.build.buildToolsVersion
defaultConfig {
minSdkVersion deps.build.minSdkVersion
diff --git a/jar-infer/test-java-lib-jarinfer/build.gradle b/jar-infer/test-java-lib-jarinfer/build.gradle
index c38c688..df1a7a0 100644
--- a/jar-infer/test-java-lib-jarinfer/build.gradle
+++ b/jar-infer/test-java-lib-jarinfer/build.gradle
@@ -15,14 +15,11 @@
*/
plugins {
- id "java-library"
+ id "java-library"
}
evaluationDependsOn(":jar-infer:jar-infer-cli")
-sourceCompatibility = "1.8"
-targetCompatibility = "1.8"
-
def astubxPath = "com/uber/nullaway/jarinfer/provider/jarinfer.astubx"
jar {
@@ -31,15 +28,20 @@ jar {
'Created-By' : "Gradle ${gradle.gradleVersion}",
'Build-Jdk' : "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})",
'Build-OS' : "${System.properties['os.name']} ${System.properties['os.arch']} ${System.properties['os.version']}"
- )
+ )
}
}
jar.doLast {
javaexec {
- classpath = files("${rootProject.projectDir}/jar-infer/jar-infer-cli/build/libs/jar-infer-cli-${VERSION_NAME}.jar")
+ classpath = files("${rootProject.projectDir}/jar-infer/jar-infer-cli/build/libs/jar-infer-cli.jar")
main = "com.uber.nullaway.jarinfer.JarInfer"
- args = ["-i", jar.archivePath, "-o", "${jar.destinationDir}/${astubxPath}"]
+ args = [
+ "-i",
+ jar.archiveFile.get(),
+ "-o",
+ "${jar.destinationDirectory.get()}/${astubxPath}"
+ ]
}
exec {
workingDir "./build/libs"
diff --git a/jdk-recent-unit-tests/build.gradle b/jdk-recent-unit-tests/build.gradle
new file mode 100644
index 0000000..b5573a0
--- /dev/null
+++ b/jdk-recent-unit-tests/build.gradle
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2021. Uber Technologies
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+plugins {
+ id 'java-library'
+ id 'nullaway.java-test-conventions'
+}
+
+// Use JDK 21 for this module, via a toolchain
+// We must null out sourceCompatibility and targetCompatibility to use toolchains.
+java.sourceCompatibility = null
+java.targetCompatibility = null
+java.toolchain.languageVersion.set JavaLanguageVersion.of(21)
+
+configurations {
+ // We use this configuration to expose a module path that can be
+ // used to test analysis of module-info.java files.
+ // See com.uber.nullaway.jdk17.NullAwayModuleInfoTests
+ testModulePath
+}
+
+dependencies {
+ testImplementation project(":nullaway")
+ testImplementation deps.test.junit4
+ testImplementation deps.test.junit5Jupiter
+ testImplementation deps.test.assertJ
+ testImplementation(deps.build.errorProneTestHelpers) {
+ exclude group: "junit", module: "junit"
+ }
+ testImplementation deps.build.jsr305Annotations
+ testModulePath deps.test.cfQual
+}
+
+test {
+ jvmArgs += [
+ // Expose a module path for tests as a JVM property.
+ // Used by com.uber.nullaway.jdk17.NullAwayModuleInfoTests
+ "-Dtest.module.path=${configurations.testModulePath.asPath}"
+ ]
+}
+
+tasks.getByName('testJdk21').configure {
+ // We don't need this task since we already run the tests on JDK 21
+ onlyIf { false }
+}
diff --git a/jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayInstanceOfBindingTests.java b/jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayInstanceOfBindingTests.java
new file mode 100644
index 0000000..38079e5
--- /dev/null
+++ b/jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayInstanceOfBindingTests.java
@@ -0,0 +1,47 @@
+package com.uber.nullaway.jdk17;
+
+import com.google.errorprone.CompilationTestHelper;
+import com.uber.nullaway.NullAway;
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class NullAwayInstanceOfBindingTests {
+
+ @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private CompilationTestHelper defaultCompilationHelper;
+
+ @Before
+ public void setup() {
+ defaultCompilationHelper =
+ CompilationTestHelper.newInstance(NullAway.class, getClass())
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"));
+ }
+
+ @Test
+ public void testInstanceOfBinding() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "InstanceOfBinding.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class InstanceOfBinding {",
+ " public void testInstanceOfBinding(@Nullable Object o) {",
+ " if (o instanceof String s) {",
+ " s.toString();",
+ " o.toString();",
+ " }",
+ " // BUG: Diagnostic contains: dereferenced expression o",
+ " o.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+}
diff --git a/jdk17-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayModuleInfoTests.java b/jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayModuleInfoTests.java
index 64f0966..64f0966 100644
--- a/jdk17-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayModuleInfoTests.java
+++ b/jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayModuleInfoTests.java
diff --git a/jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayOptionalEmptyTests.java b/jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayOptionalEmptyTests.java
new file mode 100644
index 0000000..2e2ceee
--- /dev/null
+++ b/jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayOptionalEmptyTests.java
@@ -0,0 +1,197 @@
+package com.uber.nullaway.jdk17;
+
+import com.google.errorprone.CompilationTestHelper;
+import com.uber.nullaway.NullAway;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/** Tests for support of the {@code Optional.isEmpty()} API. This API was introduced in JDK 11. */
+public class NullAwayOptionalEmptyTests {
+
+ @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private CompilationTestHelper compilationTestHelper;
+
+ @Before
+ public void setup() {
+ compilationTestHelper =
+ CompilationTestHelper.newInstance(NullAway.class, getClass())
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:CheckOptionalEmptiness=true"));
+ }
+
+ @Test
+ public void optionalIsEmptyNegative() {
+ compilationTestHelper
+ .addSourceLines(
+ "TestNegative.java",
+ "package com.uber;",
+ "import java.util.Optional;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Function;",
+ "public class TestNegative {",
+ " void foo() {",
+ " Optional<Object> a = Optional.empty();",
+ " // no error since a.isEmpty() is called",
+ " if(!a.isEmpty()){",
+ " a.get().toString();",
+ " }",
+ " }",
+ " void foo2() {",
+ " Optional<Object> a = Optional.empty();",
+ " // no error since a.isEmpty() is called",
+ " if(a.isEmpty()){",
+ " } else {",
+ " a.get().toString();",
+ " }",
+ " }",
+ " public void lambdaConsumer(Function a){",
+ " return;",
+ " }",
+ " void bar() {",
+ " Optional<Object> b = Optional.empty();",
+ " if(!b.isEmpty()){",
+ " lambdaConsumer(v -> b.get().toString());",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void optionalIsEmptyPositive() {
+ compilationTestHelper
+ .addSourceLines(
+ "TestPositive.java",
+ "package com.uber;",
+ "import java.util.Optional;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Function;",
+ "public class TestPositive {",
+ " void foo() {",
+ " Optional<Object> a = Optional.empty();",
+ " if (a.isEmpty()) {",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional a",
+ " a.get().toString();",
+ " }",
+ " }",
+ " public void lambdaConsumer(Function a){",
+ " return;",
+ " }",
+ " void bar() {",
+ " Optional<Object> b = Optional.empty();",
+ " if (b.isEmpty()) {",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional b",
+ " lambdaConsumer(v -> b.get().toString());",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void optionalIsEmptyHandleAssertionLibraryTruthAssertThat() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:CheckOptionalEmptiness=true",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Optional;",
+ "import com.google.common.truth.Truth;",
+ "",
+ "public class Test {",
+ " void truthAssertThatIsEmptyIsFalse() {",
+ " Optional<Object> b = Optional.empty();",
+ " Truth.assertThat(b.isEmpty()).isTrue(); // no impact",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional b",
+ " b.get().toString();",
+ " Truth.assertThat(b.isEmpty()).isFalse();",
+ " b.get().toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void optionalIsEmptyHandleAssertionLibraryAssertJAssertThat() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:CheckOptionalEmptiness=true",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Optional;",
+ "import org.assertj.core.api.Assertions;",
+ "",
+ "public class Test {",
+ " void assertJAssertThatIsEmptyIsFalse() {",
+ " Optional<Object> b = Optional.empty();",
+ " Assertions.assertThat(b.isEmpty()).isTrue(); // no impact",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional b",
+ " b.get().toString();",
+ " Assertions.assertThat(b.isEmpty()).isFalse();",
+ " b.get().toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void optionalIsEmptyHandleAssertionLibraryJUnitAssertions() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:CheckOptionalEmptiness=true",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Optional;",
+ "import org.junit.Assert;",
+ "import org.junit.jupiter.api.Assertions;",
+ "",
+ "public class Test {",
+ " void junit4AssertFalseIsEmpty() {",
+ " Optional<Object> b = Optional.empty();",
+ " Assert.assertTrue(b.isEmpty()); // no impact",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional b",
+ " b.get().toString();",
+ " Assert.assertFalse(\"errormsg\", b.isEmpty());",
+ " b.get().toString();",
+ " }",
+ "",
+ " void junit5AssertFalseIsEmpty() {",
+ " Optional<Object> d = Optional.empty();",
+ " Assertions.assertTrue(d.isEmpty()); // no impact",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional d",
+ " d.get().toString();",
+ " Assertions.assertFalse(d.isEmpty(), \"errormsg\");",
+ " d.get().toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ protected CompilationTestHelper makeTestHelperWithArgs(List<String> args) {
+ return CompilationTestHelper.newInstance(NullAway.class, getClass()).setArgs(args);
+ }
+}
diff --git a/jdk17-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayRecordTests.java b/jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayRecordTests.java
index 8135deb..69047ba 100644
--- a/jdk17-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayRecordTests.java
+++ b/jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwayRecordTests.java
@@ -178,4 +178,78 @@ public class NullAwayRecordTests {
"}")
.doTest();
}
+
+ @Test
+ public void recordEqualsNull() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Records.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Records {",
+ " public void recordEqualsNull() {",
+ " record Rec() {",
+ " void print(Object o) { System.out.println(o.toString()); }",
+ " void equals(Integer i1, Integer i2) { }",
+ " boolean equals(String i1, String i2) { return false; }",
+ " boolean equals(Long l1) { return false; }",
+ " }",
+ " Object o = null;",
+ " // null can be passed to the generated equals() method taking an Object parameter",
+ " new Rec().equals(o);",
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'null'",
+ " new Rec().print(null);",
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'null'",
+ " new Rec().equals(null, Integer.valueOf(100));",
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'null'",
+ " new Rec().equals(\"hello\", null);",
+ " Long l = null;",
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'l'",
+ " new Rec().equals(l);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void recordDeconstructionPatternInstanceOf() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Records.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Records {",
+ " record Rec(Object first, @Nullable Object second) { }",
+ " void recordDeconstructionInstanceOf(Object obj) {",
+ " if (obj instanceof Rec(Object f, @Nullable Object s)) {",
+ " f.toString();",
+ " // TODO: NullAway should report a warning here!",
+ " // See https://github.com/uber/NullAway/issues/840",
+ " s.toString();",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void recordDeconstructionPatternSwitchCase() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Records.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Records {",
+ " record Rec(Object first, @Nullable Object second) { }",
+ " int recordDeconstructionSwitchCase(Object obj) {",
+ " return switch (obj) {",
+ " // TODO: NullAway should report a warning here!",
+ " // See https://github.com/uber/NullAway/issues/840",
+ " case Rec(Object f, @Nullable Object s) -> s.toString().length();",
+ " default -> 0;",
+ " };",
+ " }",
+ "}")
+ .doTest();
+ }
}
diff --git a/jdk17-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwaySwitchTests.java b/jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwaySwitchTests.java
index faeb145..ca349f5 100644
--- a/jdk17-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwaySwitchTests.java
+++ b/jdk-recent-unit-tests/src/test/java/com/uber/nullaway/jdk17/NullAwaySwitchTests.java
@@ -174,4 +174,36 @@ public class NullAwaySwitchTests {
"}")
.doTest();
}
+
+ @Test
+ public void testSwitchExprNullCase() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "SwitchExpr.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class SwitchExpr {",
+ " public enum NullableEnum {",
+ " A,",
+ " B,",
+ " }",
+ " static Object handleNullableEnum(@Nullable NullableEnum nullableEnum) {",
+ " return switch (nullableEnum) {",
+ " case A -> new Object();",
+ " case B -> new Object();",
+ " case null -> throw new IllegalArgumentException(\"NullableEnum parameter is required\");",
+ " };",
+ " }",
+ " static Object handleNullableEnumNoCaseNull(@Nullable NullableEnum nullableEnum) {",
+ " // NOTE: in this case NullAway should report a bug, as there will be an NPE if nullableEnum",
+ " // is null (since there is no `case null` in the switch). This requires Error Prone support",
+ " // for matching on switch expressions (https://github.com/google/error-prone/issues/4119)",
+ " return switch (nullableEnum) {",
+ " case A -> new Object();",
+ " case B -> new Object();",
+ " };",
+ " }",
+ "}")
+ .doTest();
+ }
}
diff --git a/jdk17-unit-tests/build.gradle b/jdk17-unit-tests/build.gradle
deleted file mode 100644
index 6752d8a..0000000
--- a/jdk17-unit-tests/build.gradle
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2021. Uber Technologies
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-plugins {
- id 'java-library'
- id 'nullaway.jacoco-conventions'
-}
-
-// Use JDK 17 for this module, via a toolchain
-java.toolchain.languageVersion.set JavaLanguageVersion.of(17)
-
-configurations {
- // We use this configuration to expose a module path that can be
- // used to test analysis of module-info.java files.
- // See com.uber.nullaway.jdk17.NullAwayModuleInfoTests
- testModulePath
-}
-
-dependencies {
- testImplementation project(":nullaway")
- testImplementation deps.test.junit4
- testImplementation(deps.build.errorProneTestHelpers) {
- exclude group: "junit", module: "junit"
- }
- testImplementation deps.build.jsr305Annotations
- testModulePath deps.test.cfQual
-}
-
-test {
- maxHeapSize = "1024m"
- // to expose necessary JDK types on JDK 16+; see https://errorprone.info/docs/installation#java-9-and-newer
- 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",
- // Expose a module path for tests as a JVM property.
- // Used by com.uber.nullaway.jdk17.NullAwayModuleInfoTests
- "-Dtest.module.path=${configurations.testModulePath.asPath}"
- ]
-}
-
diff --git a/jmh/build.gradle b/jmh/build.gradle
index 94b92c1..68e3c58 100644
--- a/jmh/build.gradle
+++ b/jmh/build.gradle
@@ -15,7 +15,7 @@
*/
plugins {
id 'java-library'
- id 'nullaway.jacoco-conventions'
+ id 'nullaway.java-test-conventions'
id 'me.champeau.jmh'
}
@@ -26,6 +26,10 @@ configurations {
autodisposeSources
autodisposeDeps
+
+ nullawayReleaseSources
+ nullawayReleaseDeps
+ nullawayReleaseProcessors
}
dependencies {
@@ -46,15 +50,28 @@ dependencies {
autodisposeSources('com.uber.autodispose2:autodispose:2.1.0:sources') {
transitive = false
}
+ nullawayReleaseSources('com.uber.nullaway:nullaway:0.9.7:sources') {
+ transitive = false
+ }
caffeineDeps 'com.github.ben-manes.caffeine:caffeine:3.0.2'
autodisposeDeps 'com.uber.autodispose2:autodispose:2.1.0'
+ nullawayReleaseDeps 'com.uber.nullaway:nullaway:0.9.7'
+ // Add in the compile-only dependencies of NullAway
+ // Use fixed versions here since we are compiling a particular version of NullAway
+ nullawayReleaseDeps "com.google.errorprone:error_prone_core:2.13.1"
+ nullawayReleaseDeps "com.facebook.infer.annotation:infer-annotation:0.11.0"
+ nullawayReleaseDeps "org.jetbrains:annotations:13.0"
+
+ // To run AutoValue during NullAway compilation
+ nullawayReleaseProcessors "com.google.auto.value:auto-value:1.9"
testImplementation deps.test.junit4
}
def caffeineSourceDir = project.layout.buildDirectory.dir('caffeineSources')
def autodisposeSourceDir = project.layout.buildDirectory.dir('autodisposeSources')
+def nullawayReleaseSourceDir = project.layout.buildDirectory.dir('nullawayReleaseSources')
task extractCaffeineSources(type: Copy) {
from zipTree(configurations.caffeineSources.singleFile)
@@ -66,33 +83,56 @@ task extractAutodisposeSources(type: Copy) {
into autodisposeSourceDir
}
+task extractNullawayReleaseSources(type: Copy) {
+ from zipTree(configurations.nullawayReleaseSources.singleFile)
+ into nullawayReleaseSourceDir
+}
+
compileJava.dependsOn(extractCaffeineSources)
compileJava.dependsOn(extractAutodisposeSources)
+compileJava.dependsOn(extractNullawayReleaseSources)
// always run jmh
tasks.getByName('jmh').outputs.upToDateWhen { false }
+// a trick: to get the classpath for a benchmark, create a configuration that depends on the benchmark, and
+// then filter out the benchmark itself
+def caffeineClasspath = configurations.caffeineDeps.filter({f -> !f.toString().contains("caffeine-3.0.2")}).asPath
+def autodisposeClasspath = configurations.autodisposeDeps.filter({f -> !f.toString().contains("autodispose-2.1.0")}).asPath
+def nullawayReleaseClasspath = configurations.nullawayReleaseDeps.filter({f -> !f.toString().contains("nullaway-0.9.7")}).asPath
+
+def nullawayReleaseProcessorpath = configurations.nullawayReleaseProcessors.asPath
+
+// Extra JVM arguments to expose relevant paths for compiling benchmarks
+def extraJVMArgs = [
+ "-Dnullaway.caffeine.sources=${caffeineSourceDir.get()}",
+ "-Dnullaway.caffeine.classpath=$caffeineClasspath",
+ "-Dnullaway.autodispose.sources=${autodisposeSourceDir.get()}",
+ "-Dnullaway.autodispose.classpath=$autodisposeClasspath",
+ "-Dnullaway.nullawayRelease.sources=${nullawayReleaseSourceDir.get()}",
+ "-Dnullaway.nullawayRelease.classpath=$nullawayReleaseClasspath",
+ "-Dnullaway.nullawayRelease.processorpath=$nullawayReleaseProcessorpath",
+]
+
jmh {
// seems we need more iterations to fully warm up the JIT
warmupIterations = 10
- // a trick: to get the classpath for a benchmark, create a configuration that depends on the benchmark, and
- // then filter out the benchmark itself
- def caffeineClasspath = configurations.caffeineDeps.filter({f -> !f.toString().contains("caffeine-3.0.2")}).asPath
- def autodisposeClasspath = configurations.autodisposeDeps.filter({f -> !f.toString().contains("autodispose-2.1.0")}).asPath
- jvmArgsAppend = [
- "-Dnullaway.caffeine.sources=${caffeineSourceDir.get()}",
- "-Dnullaway.caffeine.classpath=$caffeineClasspath",
- "-Dnullaway.autodispose.sources=${autodisposeSourceDir.get()}",
- "-Dnullaway.autodispose.classpath=$autodisposeClasspath",
- ]
+ jvmArgsAppend = extraJVMArgs
// commented-out examples of how to tweak other jmh parameters; they show the default values
// for more examples see https://github.com/melix/jmh-gradle-plugin/blob/master/README.adoc#configuration-options
// iterations = 5
// fork = 5
+ // includes = ['DFlowMicro']
}
-// don't run test task on pre-JDK-11 VMs
-test.onlyIf { JavaVersion.current() >= JavaVersion.VERSION_11 }
+tasks.named('test') {
+ // pass the extra JVM args so we can compile benchmarks in unit tests
+ jvmArgs += extraJVMArgs
+}
+
+tasks.getByName('testJdk21').configure {
+ jvmArgs += extraJVMArgs
+}
diff --git a/jmh/src/jmh/java/com/uber/nullaway/jmh/AutodisposeBenchmark.java b/jmh/src/jmh/java/com/uber/nullaway/jmh/AutodisposeBenchmark.java
index ca042b1..1f0acb8 100644
--- a/jmh/src/jmh/java/com/uber/nullaway/jmh/AutodisposeBenchmark.java
+++ b/jmh/src/jmh/java/com/uber/nullaway/jmh/AutodisposeBenchmark.java
@@ -23,12 +23,6 @@
package com.uber.nullaway.jmh;
import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
@@ -38,23 +32,15 @@ import org.openjdk.jmh.infra.Blackhole;
@State(Scope.Benchmark)
public class AutodisposeBenchmark {
- private NullawayJavac nullawayJavac;
+ private AutodisposeCompiler compiler;
@Setup
public void setup() throws IOException {
- String sourceDir = System.getProperty("nullaway.autodispose.sources");
- String classpath = System.getProperty("nullaway.autodispose.classpath");
- try (Stream<Path> stream =
- Files.find(
- Paths.get(sourceDir), 100, (p, bfa) -> p.getFileName().toString().endsWith(".java"))) {
- List<String> sourceFileNames =
- stream.map(p -> p.toFile().getAbsolutePath()).collect(Collectors.toList());
- nullawayJavac = NullawayJavac.create(sourceFileNames, "autodispose2", classpath);
- }
+ compiler = new AutodisposeCompiler();
}
@Benchmark
- public void compile(Blackhole bh) throws Exception {
- bh.consume(nullawayJavac.compile());
+ public void compile(Blackhole bh) {
+ bh.consume(compiler.compile());
}
}
diff --git a/jmh/src/jmh/java/com/uber/nullaway/jmh/CaffeineBenchmark.java b/jmh/src/jmh/java/com/uber/nullaway/jmh/CaffeineBenchmark.java
index ffcc6ab..d5d6691 100644
--- a/jmh/src/jmh/java/com/uber/nullaway/jmh/CaffeineBenchmark.java
+++ b/jmh/src/jmh/java/com/uber/nullaway/jmh/CaffeineBenchmark.java
@@ -22,11 +22,7 @@
package com.uber.nullaway.jmh;
-import com.google.common.collect.ImmutableList;
-import java.io.File;
import java.io.IOException;
-import java.util.List;
-import java.util.stream.Collectors;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
@@ -36,76 +32,15 @@ import org.openjdk.jmh.infra.Blackhole;
@State(Scope.Benchmark)
public class CaffeineBenchmark {
- /**
- * we use a subset of the source files since many are auto-generated and annotated with
- * {@code @SuppressWarnings("NullAway")}
- */
- private static final ImmutableList<String> SOURCE_FILE_NAMES =
- ImmutableList.of(
- "com/github/benmanes/caffeine/cache/AbstractLinkedDeque.java",
- "com/github/benmanes/caffeine/cache/AccessOrderDeque.java",
- "com/github/benmanes/caffeine/cache/Async.java",
- "com/github/benmanes/caffeine/cache/AsyncCache.java",
- "com/github/benmanes/caffeine/cache/AsyncCacheLoader.java",
- "com/github/benmanes/caffeine/cache/AsyncLoadingCache.java",
- "com/github/benmanes/caffeine/cache/BoundedBuffer.java",
- "com/github/benmanes/caffeine/cache/BoundedLocalCache.java",
- "com/github/benmanes/caffeine/cache/Buffer.java",
- "com/github/benmanes/caffeine/cache/Cache.java",
- "com/github/benmanes/caffeine/cache/CacheLoader.java",
- "com/github/benmanes/caffeine/cache/Caffeine.java",
- "com/github/benmanes/caffeine/cache/CaffeineSpec.java",
- "com/github/benmanes/caffeine/cache/Expiry.java",
- "com/github/benmanes/caffeine/cache/FrequencySketch.java",
- "com/github/benmanes/caffeine/cache/LinkedDeque.java",
- "com/github/benmanes/caffeine/cache/LoadingCache.java",
- "com/github/benmanes/caffeine/cache/LocalAsyncCache.java",
- "com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java",
- "com/github/benmanes/caffeine/cache/LocalCache.java",
- "com/github/benmanes/caffeine/cache/LocalCacheFactory.java",
- "com/github/benmanes/caffeine/cache/LocalLoadingCache.java",
- "com/github/benmanes/caffeine/cache/LocalManualCache.java",
- "com/github/benmanes/caffeine/cache/MpscGrowableArrayQueue.java",
- "com/github/benmanes/caffeine/cache/Node.java",
- "com/github/benmanes/caffeine/cache/NodeFactory.java",
- "com/github/benmanes/caffeine/cache/Pacer.java",
- "com/github/benmanes/caffeine/cache/Policy.java",
- "com/github/benmanes/caffeine/cache/References.java",
- "com/github/benmanes/caffeine/cache/RemovalCause.java",
- "com/github/benmanes/caffeine/cache/RemovalListener.java",
- "com/github/benmanes/caffeine/cache/Scheduler.java",
- "com/github/benmanes/caffeine/cache/SerializationProxy.java",
- "com/github/benmanes/caffeine/cache/StripedBuffer.java",
- "com/github/benmanes/caffeine/cache/Ticker.java",
- "com/github/benmanes/caffeine/cache/TimerWheel.java",
- "com/github/benmanes/caffeine/cache/UnboundedLocalCache.java",
- "com/github/benmanes/caffeine/cache/Weigher.java",
- "com/github/benmanes/caffeine/cache/WriteOrderDeque.java",
- "com/github/benmanes/caffeine/cache/WriteThroughEntry.java",
- "com/github/benmanes/caffeine/cache/stats/CacheStats.java",
- "com/github/benmanes/caffeine/cache/stats/ConcurrentStatsCounter.java",
- "com/github/benmanes/caffeine/cache/stats/DisabledStatsCounter.java",
- "com/github/benmanes/caffeine/cache/stats/GuardedStatsCounter.java",
- "com/github/benmanes/caffeine/cache/stats/StatsCounter.java");
-
- private NullawayJavac nullawayJavac;
+ private CaffeineCompiler compiler;
@Setup
public void setup() throws IOException {
- String caffeineSourceDir = System.getProperty("nullaway.caffeine.sources");
- String caffeineClasspath = System.getProperty("nullaway.caffeine.classpath");
- List<String> realSourceFileNames =
- SOURCE_FILE_NAMES
- .stream()
- .map(s -> caffeineSourceDir + File.separator + s.replaceAll("/", File.separator))
- .collect(Collectors.toList());
- nullawayJavac =
- NullawayJavac.create(
- realSourceFileNames, "com.github.benmanes.caffeine", caffeineClasspath);
+ compiler = new CaffeineCompiler();
}
@Benchmark
- public void compile(Blackhole bh) throws Exception {
- bh.consume(nullawayJavac.compile());
+ public void compile(Blackhole bh) {
+ bh.consume(compiler.compile());
}
}
diff --git a/jmh/src/jmh/java/com/uber/nullaway/jmh/DFlowMicroBenchmark.java b/jmh/src/jmh/java/com/uber/nullaway/jmh/DFlowMicroBenchmark.java
new file mode 100644
index 0000000..fa45073
--- /dev/null
+++ b/jmh/src/jmh/java/com/uber/nullaway/jmh/DFlowMicroBenchmark.java
@@ -0,0 +1,24 @@
+package com.uber.nullaway.jmh;
+
+import java.io.IOException;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.infra.Blackhole;
+
+@State(Scope.Benchmark)
+public class DFlowMicroBenchmark {
+
+ private DataFlowMicroBenchmarkCompiler compiler;
+
+ @Setup
+ public void setup() throws IOException {
+ compiler = new DataFlowMicroBenchmarkCompiler();
+ }
+
+ @Benchmark
+ public void compile(Blackhole bh) {
+ bh.consume(compiler.compile());
+ }
+}
diff --git a/jmh/src/jmh/java/com/uber/nullaway/jmh/NullawayReleaseBenchmark.java b/jmh/src/jmh/java/com/uber/nullaway/jmh/NullawayReleaseBenchmark.java
new file mode 100644
index 0000000..eee8669
--- /dev/null
+++ b/jmh/src/jmh/java/com/uber/nullaway/jmh/NullawayReleaseBenchmark.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2021 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.jmh;
+
+import java.io.IOException;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.infra.Blackhole;
+
+@State(Scope.Benchmark)
+public class NullawayReleaseBenchmark {
+
+ private NullawayReleaseCompiler compiler;
+
+ @Setup
+ public void setup() throws IOException {
+ compiler = new NullawayReleaseCompiler();
+ }
+
+ @Benchmark
+ public void compile(Blackhole bh) {
+ bh.consume(compiler.compile());
+ }
+}
diff --git a/jmh/src/main/java/com/uber/nullaway/jmh/AbstractBenchmarkCompiler.java b/jmh/src/main/java/com/uber/nullaway/jmh/AbstractBenchmarkCompiler.java
new file mode 100644
index 0000000..d96e040
--- /dev/null
+++ b/jmh/src/main/java/com/uber/nullaway/jmh/AbstractBenchmarkCompiler.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2021 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+package com.uber.nullaway.jmh;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/** common logic for compiling a benchmark in JMH performance testing */
+public abstract class AbstractBenchmarkCompiler {
+
+ private final NullawayJavac nullawayJavac;
+
+ public AbstractBenchmarkCompiler() throws IOException {
+ nullawayJavac =
+ NullawayJavac.create(
+ getSourceFileNames(),
+ getAnnotatedPackages(),
+ getClasspath(),
+ getExtraErrorProneArgs(),
+ getExtraProcessorPath());
+ }
+
+ public final boolean compile() {
+ return nullawayJavac.compile();
+ }
+
+ /** Get the names of source files to be compiled */
+ protected List<String> getSourceFileNames() throws IOException {
+ String sourceDir = getSourceDirectory();
+ try (Stream<Path> stream =
+ Files.find(
+ Paths.get(sourceDir), 100, (p, bfa) -> p.getFileName().toString().endsWith(".java"))) {
+ List<String> sourceFileNames =
+ stream.map(p -> p.toFile().getAbsolutePath()).collect(Collectors.toList());
+ return sourceFileNames;
+ }
+ }
+
+ /** Get the root directory containing the benchmark source files */
+ protected abstract String getSourceDirectory();
+
+ /** Get the value to pass for {@code -XepOpt:NullAway:AnnotatedPackages} */
+ protected abstract String getAnnotatedPackages();
+
+ /** Get the classpath required to compile the benchmark */
+ protected abstract String getClasspath();
+
+ /** Get any extra arguments that should be passed to Error Prone */
+ protected List<String> getExtraErrorProneArgs() {
+ return Collections.emptyList();
+ }
+
+ /** Get a path of additional jars to be included in the processor path when compiling */
+ protected String getExtraProcessorPath() {
+ return "";
+ }
+}
diff --git a/jmh/src/main/java/com/uber/nullaway/jmh/AutodisposeCompiler.java b/jmh/src/main/java/com/uber/nullaway/jmh/AutodisposeCompiler.java
new file mode 100644
index 0000000..9215551
--- /dev/null
+++ b/jmh/src/main/java/com/uber/nullaway/jmh/AutodisposeCompiler.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2021 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+package com.uber.nullaway.jmh;
+
+import java.io.IOException;
+
+public class AutodisposeCompiler extends AbstractBenchmarkCompiler {
+ public AutodisposeCompiler() throws IOException {
+ super();
+ }
+
+ @Override
+ protected String getSourceDirectory() {
+ return System.getProperty("nullaway.autodispose.sources");
+ }
+
+ @Override
+ protected String getAnnotatedPackages() {
+ return "autodispose2";
+ }
+
+ @Override
+ protected String getClasspath() {
+ return System.getProperty("nullaway.autodispose.classpath");
+ }
+}
diff --git a/jmh/src/main/java/com/uber/nullaway/jmh/CaffeineCompiler.java b/jmh/src/main/java/com/uber/nullaway/jmh/CaffeineCompiler.java
new file mode 100644
index 0000000..59525f2
--- /dev/null
+++ b/jmh/src/main/java/com/uber/nullaway/jmh/CaffeineCompiler.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2021 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+package com.uber.nullaway.jmh;
+
+import com.google.common.collect.ImmutableList;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class CaffeineCompiler extends AbstractBenchmarkCompiler {
+
+ /**
+ * we use a subset of the source files since many are auto-generated and annotated with
+ * {@code @SuppressWarnings("NullAway")}
+ */
+ private static final ImmutableList<String> SOURCE_FILE_NAMES =
+ ImmutableList.of(
+ "com/github/benmanes/caffeine/cache/AbstractLinkedDeque.java",
+ "com/github/benmanes/caffeine/cache/AccessOrderDeque.java",
+ "com/github/benmanes/caffeine/cache/Async.java",
+ "com/github/benmanes/caffeine/cache/AsyncCache.java",
+ "com/github/benmanes/caffeine/cache/AsyncCacheLoader.java",
+ "com/github/benmanes/caffeine/cache/AsyncLoadingCache.java",
+ "com/github/benmanes/caffeine/cache/BoundedBuffer.java",
+ "com/github/benmanes/caffeine/cache/BoundedLocalCache.java",
+ "com/github/benmanes/caffeine/cache/Buffer.java",
+ "com/github/benmanes/caffeine/cache/Cache.java",
+ "com/github/benmanes/caffeine/cache/CacheLoader.java",
+ "com/github/benmanes/caffeine/cache/Caffeine.java",
+ "com/github/benmanes/caffeine/cache/CaffeineSpec.java",
+ "com/github/benmanes/caffeine/cache/Expiry.java",
+ "com/github/benmanes/caffeine/cache/FrequencySketch.java",
+ "com/github/benmanes/caffeine/cache/LinkedDeque.java",
+ "com/github/benmanes/caffeine/cache/LoadingCache.java",
+ "com/github/benmanes/caffeine/cache/LocalAsyncCache.java",
+ "com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.java",
+ "com/github/benmanes/caffeine/cache/LocalCache.java",
+ "com/github/benmanes/caffeine/cache/LocalCacheFactory.java",
+ "com/github/benmanes/caffeine/cache/LocalLoadingCache.java",
+ "com/github/benmanes/caffeine/cache/LocalManualCache.java",
+ "com/github/benmanes/caffeine/cache/MpscGrowableArrayQueue.java",
+ "com/github/benmanes/caffeine/cache/Node.java",
+ "com/github/benmanes/caffeine/cache/NodeFactory.java",
+ "com/github/benmanes/caffeine/cache/Pacer.java",
+ "com/github/benmanes/caffeine/cache/Policy.java",
+ "com/github/benmanes/caffeine/cache/References.java",
+ "com/github/benmanes/caffeine/cache/RemovalCause.java",
+ "com/github/benmanes/caffeine/cache/RemovalListener.java",
+ "com/github/benmanes/caffeine/cache/Scheduler.java",
+ "com/github/benmanes/caffeine/cache/SerializationProxy.java",
+ "com/github/benmanes/caffeine/cache/StripedBuffer.java",
+ "com/github/benmanes/caffeine/cache/Ticker.java",
+ "com/github/benmanes/caffeine/cache/TimerWheel.java",
+ "com/github/benmanes/caffeine/cache/UnboundedLocalCache.java",
+ "com/github/benmanes/caffeine/cache/Weigher.java",
+ "com/github/benmanes/caffeine/cache/WriteOrderDeque.java",
+ "com/github/benmanes/caffeine/cache/WriteThroughEntry.java",
+ "com/github/benmanes/caffeine/cache/stats/CacheStats.java",
+ "com/github/benmanes/caffeine/cache/stats/ConcurrentStatsCounter.java",
+ "com/github/benmanes/caffeine/cache/stats/DisabledStatsCounter.java",
+ "com/github/benmanes/caffeine/cache/stats/GuardedStatsCounter.java",
+ "com/github/benmanes/caffeine/cache/stats/StatsCounter.java");
+
+ public CaffeineCompiler() throws IOException {
+ super();
+ }
+
+ @Override
+ protected String getSourceDirectory() {
+ return System.getProperty("nullaway.caffeine.sources");
+ }
+
+ @Override
+ protected List<String> getSourceFileNames() throws IOException {
+ String caffeineSourceDir = getSourceDirectory();
+ List<String> realSourceFileNames =
+ SOURCE_FILE_NAMES.stream()
+ .map(s -> caffeineSourceDir + File.separator + s.replace("/", File.separator))
+ .collect(Collectors.toList());
+ return realSourceFileNames;
+ }
+
+ @Override
+ protected String getAnnotatedPackages() {
+ return "com.github.benmanes.caffeine";
+ }
+
+ @Override
+ protected String getClasspath() {
+ return System.getProperty("nullaway.caffeine.classpath");
+ }
+}
diff --git a/jmh/src/main/java/com/uber/nullaway/jmh/DataFlowMicroBenchmarkCompiler.java b/jmh/src/main/java/com/uber/nullaway/jmh/DataFlowMicroBenchmarkCompiler.java
new file mode 100644
index 0000000..ef0f1c3
--- /dev/null
+++ b/jmh/src/main/java/com/uber/nullaway/jmh/DataFlowMicroBenchmarkCompiler.java
@@ -0,0 +1,47 @@
+package com.uber.nullaway.jmh;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+public class DataFlowMicroBenchmarkCompiler {
+
+ private final NullawayJavac nullawayJavac;
+
+ public DataFlowMicroBenchmarkCompiler() throws IOException {
+ nullawayJavac = NullawayJavac.createFromSourceString("DFlowBench", SOURCE, "com.uber");
+ }
+
+ public boolean compile() {
+ return nullawayJavac.compile();
+ }
+
+ private static final String SOURCE;
+
+ static {
+ // For larger benchmarks, we pass file paths to NullawayJavac, based on JVM properties computed
+ // in build.gradle. Here, to avoid creating and passing yet another JVM property, we just store
+ // the benchmark source code in a resource file and then load it eagerly into a String via the
+ // classloader
+ ClassLoader classLoader = DataFlowMicroBenchmarkCompiler.class.getClassLoader();
+ try (InputStream inputStream = classLoader.getResourceAsStream("DFlowBench.java")) {
+ SOURCE = readFromInputStream(inputStream);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static String readFromInputStream(InputStream inputStream) throws IOException {
+ StringBuilder result = new StringBuilder();
+ try (BufferedReader br =
+ new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ result.append(line).append("\n");
+ }
+ }
+ return result.toString();
+ }
+}
diff --git a/jmh/src/main/java/com/uber/nullaway/jmh/NullawayJavac.java b/jmh/src/main/java/com/uber/nullaway/jmh/NullawayJavac.java
index e7da10a..1d094d8 100644
--- a/jmh/src/main/java/com/uber/nullaway/jmh/NullawayJavac.java
+++ b/jmh/src/main/java/com/uber/nullaway/jmh/NullawayJavac.java
@@ -79,7 +79,11 @@ public class NullawayJavac {
+ " }\n"
+ "}\n";
return new NullawayJavac(
- Collections.singletonList(new JavaSourceFromString("Test", testClass)), "com.uber", null);
+ Collections.singletonList(new JavaSourceFromString("Test", testClass)),
+ "com.uber",
+ null,
+ Collections.emptyList(),
+ "");
}
/**
@@ -88,10 +92,17 @@ public class NullawayJavac {
* @param sourceFileNames absolute paths to the source files to be compiled
* @param annotatedPackages argument to pass for "-XepOpt:NullAway:AnnotatedPackages" option
* @param classpath classpath for the benchmark
+ * @param extraErrorProneArgs extra arguments to pass to Error Prone
+ * @param extraProcessorPath additional elements to concatenate to the processor path
* @throws IOException if a temporary output directory cannot be created
*/
public static NullawayJavac create(
- List<String> sourceFileNames, String annotatedPackages, String classpath) throws IOException {
+ List<String> sourceFileNames,
+ String annotatedPackages,
+ String classpath,
+ List<String> extraErrorProneArgs,
+ String extraProcessorPath)
+ throws IOException {
List<JavaFileObject> compilationUnits = new ArrayList<>();
for (String sourceFileName : sourceFileNames) {
// we read every source file into memory in the prepare phase, to avoid some I/O during
@@ -103,7 +114,28 @@ public class NullawayJavac {
compilationUnits.add(new JavaSourceFromString(classname, content));
}
- return new NullawayJavac(compilationUnits, annotatedPackages, classpath);
+ return new NullawayJavac(
+ compilationUnits, annotatedPackages, classpath, extraErrorProneArgs, extraProcessorPath);
+ }
+
+ /**
+ * Create a NullawayJavac object to compile a single source file given as a String. This only
+ * supports cases where no additional classpath, Error Prone arguments, or processor path
+ * arguments need to be specified.
+ *
+ * @param className name of the class to compile
+ * @param source source code of the class to compile
+ * @param annotatedPackages argument to pass for "-XepOpt:NullAway:AnnotatedPackages" option
+ * @throws IOException if a temporary output directory cannot be created
+ */
+ public static NullawayJavac createFromSourceString(
+ String className, String source, String annotatedPackages) throws IOException {
+ return new NullawayJavac(
+ Collections.singletonList(new JavaSourceFromString(className, source)),
+ annotatedPackages,
+ null,
+ Collections.emptyList(),
+ "");
}
/**
@@ -119,10 +151,16 @@ public class NullawayJavac {
* @param compilationUnits input sources to be compiled
* @param annotatedPackages argument to pass for "-XepOpt:NullAway:AnnotatedPackages" option
* @param classpath classpath for the program to be compiled
+ * @param extraErrorProneArgs additional arguments to pass to Error Prone
+ * @param extraProcessorPath additional elements to concatenate to the processor path
* @throws IOException if a temporary output directory cannot be created
*/
private NullawayJavac(
- List<JavaFileObject> compilationUnits, String annotatedPackages, @Nullable String classpath)
+ List<JavaFileObject> compilationUnits,
+ String annotatedPackages,
+ @Nullable String classpath,
+ List<String> extraErrorProneArgs,
+ String extraProcessorPath)
throws IOException {
this.compilationUnits = compilationUnits;
this.compiler = ToolProvider.getSystemJavaCompiler();
@@ -139,15 +177,33 @@ public class NullawayJavac {
if (classpath != null) {
options.addAll(Arrays.asList("-classpath", classpath));
}
+ String processorPath =
+ System.getProperty("java.class.path") + File.pathSeparator + extraProcessorPath;
options.addAll(
Arrays.asList(
"-processorpath",
- System.getProperty("java.class.path"),
+ processorPath,
"-d",
outputDir.toAbsolutePath().toString(),
"-XDcompilePolicy=simple",
"-Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR -XepOpt:NullAway:AnnotatedPackages="
- + annotatedPackages));
+ + annotatedPackages
+ + String.join(" ", extraErrorProneArgs)));
+ // add these options since we have at least one benchmark that only compiles with access to
+ // javac-internal APIs
+ options.addAll(
+ Arrays.asList(
+ "--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.code=ALL-UNNAMED",
+ "--add-exports=jdk.compiler/com.sun.tools.javac.comp=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-exports=jdk.compiler/com.sun.source.tree=ALL-UNNAMED"));
}
/**
diff --git a/jmh/src/main/java/com/uber/nullaway/jmh/NullawayReleaseCompiler.java b/jmh/src/main/java/com/uber/nullaway/jmh/NullawayReleaseCompiler.java
new file mode 100644
index 0000000..d539657
--- /dev/null
+++ b/jmh/src/main/java/com/uber/nullaway/jmh/NullawayReleaseCompiler.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2021 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+package com.uber.nullaway.jmh;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+public class NullawayReleaseCompiler extends AbstractBenchmarkCompiler {
+
+ public NullawayReleaseCompiler() throws IOException {
+ super();
+ }
+
+ @Override
+ protected String getSourceDirectory() {
+ return System.getProperty("nullaway.nullawayRelease.sources");
+ }
+
+ @Override
+ protected List<String> getExtraErrorProneArgs() {
+ return Arrays.asList(
+ "-XepOpt:NullAway:CheckOptionalEmptiness=true",
+ "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true",
+ "-XepOpt:NullAway:CastToNonNullMethod=com.uber.nullaway.NullabilityUtil.castToNonNull");
+ }
+
+ @Override
+ protected String getAnnotatedPackages() {
+ return "com.uber,org.checkerframework.nullaway";
+ }
+
+ @Override
+ protected String getClasspath() {
+ return System.getProperty("nullaway.nullawayRelease.classpath");
+ }
+
+ @Override
+ protected String getExtraProcessorPath() {
+ return System.getProperty("nullaway.nullawayRelease.processorpath");
+ }
+}
diff --git a/jmh/src/main/resources/DFlowBench.java b/jmh/src/main/resources/DFlowBench.java
new file mode 100644
index 0000000..22c073a
--- /dev/null
+++ b/jmh/src/main/resources/DFlowBench.java
@@ -0,0 +1,587 @@
+package com.uber.nullaway.testdata;
+
+import javax.annotation.Nullable;
+
+public final class DFlowBench {
+ @Nullable Object f;
+
+ public static void test(boolean b) {
+ DFlowBench x0 = new DFlowBench();
+ DFlowBench x1 = new DFlowBench();
+ DFlowBench x2 = new DFlowBench();
+ DFlowBench x3 = new DFlowBench();
+ DFlowBench x4 = new DFlowBench();
+ DFlowBench x5 = new DFlowBench();
+ DFlowBench x6 = new DFlowBench();
+ DFlowBench x7 = new DFlowBench();
+ DFlowBench x8 = new DFlowBench();
+ DFlowBench x9 = new DFlowBench();
+ DFlowBench x10 = new DFlowBench();
+ DFlowBench x11 = new DFlowBench();
+ DFlowBench x12 = new DFlowBench();
+ DFlowBench x13 = new DFlowBench();
+ DFlowBench x14 = new DFlowBench();
+ DFlowBench x15 = new DFlowBench();
+ DFlowBench x16 = new DFlowBench();
+ DFlowBench x17 = new DFlowBench();
+ DFlowBench x18 = new DFlowBench();
+ DFlowBench x19 = new DFlowBench();
+ DFlowBench x20 = new DFlowBench();
+ DFlowBench x21 = new DFlowBench();
+ DFlowBench x22 = new DFlowBench();
+ DFlowBench x23 = new DFlowBench();
+ DFlowBench x24 = new DFlowBench();
+ DFlowBench x25 = new DFlowBench();
+ DFlowBench x26 = new DFlowBench();
+ DFlowBench x27 = new DFlowBench();
+ DFlowBench x28 = new DFlowBench();
+ DFlowBench x29 = new DFlowBench();
+ DFlowBench x30 = new DFlowBench();
+ DFlowBench x31 = new DFlowBench();
+ DFlowBench x32 = new DFlowBench();
+ DFlowBench x33 = new DFlowBench();
+ DFlowBench x34 = new DFlowBench();
+ DFlowBench x35 = new DFlowBench();
+ DFlowBench x36 = new DFlowBench();
+ DFlowBench x37 = new DFlowBench();
+ DFlowBench x38 = new DFlowBench();
+ DFlowBench x39 = new DFlowBench();
+ DFlowBench x40 = new DFlowBench();
+ DFlowBench x41 = new DFlowBench();
+ DFlowBench x42 = new DFlowBench();
+ DFlowBench x43 = new DFlowBench();
+ DFlowBench x44 = new DFlowBench();
+ DFlowBench x45 = new DFlowBench();
+ DFlowBench x46 = new DFlowBench();
+ DFlowBench x47 = new DFlowBench();
+ DFlowBench x48 = new DFlowBench();
+ DFlowBench x49 = new DFlowBench();
+ DFlowBench x50 = new DFlowBench();
+ DFlowBench x51 = new DFlowBench();
+ DFlowBench x52 = new DFlowBench();
+ DFlowBench x53 = new DFlowBench();
+ DFlowBench x54 = new DFlowBench();
+ DFlowBench x55 = new DFlowBench();
+ DFlowBench x56 = new DFlowBench();
+ DFlowBench x57 = new DFlowBench();
+ DFlowBench x58 = new DFlowBench();
+ DFlowBench x59 = new DFlowBench();
+ DFlowBench x60 = new DFlowBench();
+ DFlowBench x61 = new DFlowBench();
+ DFlowBench x62 = new DFlowBench();
+ DFlowBench x63 = new DFlowBench();
+ DFlowBench x64 = new DFlowBench();
+ DFlowBench x65 = new DFlowBench();
+ DFlowBench x66 = new DFlowBench();
+ DFlowBench x67 = new DFlowBench();
+ DFlowBench x68 = new DFlowBench();
+ DFlowBench x69 = new DFlowBench();
+ DFlowBench x70 = new DFlowBench();
+ DFlowBench x71 = new DFlowBench();
+ DFlowBench x72 = new DFlowBench();
+ DFlowBench x73 = new DFlowBench();
+ DFlowBench x74 = new DFlowBench();
+ DFlowBench x75 = new DFlowBench();
+ DFlowBench x76 = new DFlowBench();
+ DFlowBench x77 = new DFlowBench();
+ DFlowBench x78 = new DFlowBench();
+ DFlowBench x79 = new DFlowBench();
+ DFlowBench x80 = new DFlowBench();
+ DFlowBench x81 = new DFlowBench();
+ DFlowBench x82 = new DFlowBench();
+ DFlowBench x83 = new DFlowBench();
+ DFlowBench x84 = new DFlowBench();
+ DFlowBench x85 = new DFlowBench();
+ DFlowBench x86 = new DFlowBench();
+ DFlowBench x87 = new DFlowBench();
+ DFlowBench x88 = new DFlowBench();
+ DFlowBench x89 = new DFlowBench();
+ DFlowBench x90 = new DFlowBench();
+ DFlowBench x91 = new DFlowBench();
+ DFlowBench x92 = new DFlowBench();
+ DFlowBench x93 = new DFlowBench();
+ DFlowBench x94 = new DFlowBench();
+ DFlowBench x95 = new DFlowBench();
+ DFlowBench x96 = new DFlowBench();
+ DFlowBench x97 = new DFlowBench();
+ DFlowBench x98 = new DFlowBench();
+ DFlowBench x99 = new DFlowBench();
+ if (x0.f != null) {
+ x0.f.toString();
+ if (x1.f != null) {
+ x1.f.toString();
+ if (x2.f != null) {
+ x2.f.toString();
+ if (x3.f != null) {
+ x3.f.toString();
+ if (x4.f != null) {
+ x4.f.toString();
+ if (x5.f != null) {
+ x5.f.toString();
+ if (x6.f != null) {
+ x6.f.toString();
+ if (x7.f != null) {
+ x7.f.toString();
+ if (x8.f != null) {
+ x8.f.toString();
+ if (x9.f != null) {
+ x9.f.toString();
+ if (x10.f != null) {
+ x10.f.toString();
+ if (x11.f != null) {
+ x11.f.toString();
+ if (x12.f != null) {
+ x12.f.toString();
+ if (x13.f != null) {
+ x13.f.toString();
+ if (x14.f != null) {
+ x14.f.toString();
+ if (x15.f != null) {
+ x15.f.toString();
+ if (x16.f != null) {
+ x16.f.toString();
+ if (x17.f != null) {
+ x17.f.toString();
+ if (x18.f != null) {
+ x18.f.toString();
+ if (x19.f != null) {
+ x19.f.toString();
+ if (x20.f != null) {
+ x20.f.toString();
+ if (x21.f != null) {
+ x21.f.toString();
+ if (x22.f != null) {
+ x22.f.toString();
+ if (x23.f != null) {
+ x23.f.toString();
+ if (x24.f != null) {
+ x24.f.toString();
+ if (x25.f != null) {
+ x25.f.toString();
+ if (x26.f != null) {
+ x26.f.toString();
+ if (x27.f != null) {
+ x27.f.toString();
+ if (x28.f != null) {
+ x28.f.toString();
+ if (x29.f != null) {
+ x29.f.toString();
+ if (x30.f != null) {
+ x30.f.toString();
+ if (x31.f != null) {
+ x31.f.toString();
+ if (x32.f != null) {
+ x32.f.toString();
+ if (x33.f != null) {
+ x33.f.toString();
+ if (x34.f != null) {
+ x34.f.toString();
+ if (x35.f != null) {
+ x35.f.toString();
+ if (x36.f != null) {
+ x36.f.toString();
+ if (x37.f != null) {
+ x37.f.toString();
+ if (x38.f != null) {
+ x38.f.toString();
+ if (x39.f
+ != null) {
+ x39.f
+ .toString();
+ if (x40.f
+ != null) {
+ x40.f
+ .toString();
+ if (x41.f
+ != null) {
+ x41.f
+ .toString();
+ if (x42.f
+ != null) {
+ x42.f
+ .toString();
+ if (x43.f
+ != null) {
+ x43.f
+ .toString();
+ if (x44.f
+ != null) {
+ x44.f
+ .toString();
+ if (x45.f
+ != null) {
+ x45
+ .f
+ .toString();
+ if (x46.f
+ != null) {
+ x46
+ .f
+ .toString();
+ if (x47.f
+ != null) {
+ x47
+ .f
+ .toString();
+ if (x48.f
+ != null) {
+ x48
+ .f
+ .toString();
+ if (x49.f
+ != null) {
+ x49
+ .f
+ .toString();
+ if (x50.f
+ != null) {
+ x50
+ .f
+ .toString();
+ if (x51.f
+ != null) {
+ x51
+ .f
+ .toString();
+ if (x52.f
+ != null) {
+ x52
+ .f
+ .toString();
+ if (x53.f
+ != null) {
+ x53
+ .f
+ .toString();
+ if (x54.f
+ != null) {
+ x54
+ .f
+ .toString();
+ if (x55.f
+ != null) {
+ x55
+ .f
+ .toString();
+ if (x56.f
+ != null) {
+ x56
+ .f
+ .toString();
+ if (x57.f
+ != null) {
+ x57
+ .f
+ .toString();
+ if (x58.f
+ != null) {
+ x58
+ .f
+ .toString();
+ if (x59.f
+ != null) {
+ x59
+ .f
+ .toString();
+ if (x60.f
+ != null) {
+ x60
+ .f
+ .toString();
+ if (x61.f
+ != null) {
+ x61
+ .f
+ .toString();
+ if (x62.f
+ != null) {
+ x62
+ .f
+ .toString();
+ if (x63.f
+ != null) {
+ x63
+ .f
+ .toString();
+ if (x64.f
+ != null) {
+ x64
+ .f
+ .toString();
+ if (x65.f
+ != null) {
+ x65
+ .f
+ .toString();
+ if (x66.f
+ != null) {
+ x66
+ .f
+ .toString();
+ if (x67.f
+ != null) {
+ x67
+ .f
+ .toString();
+ if (x68.f
+ != null) {
+ x68
+ .f
+ .toString();
+ if (x69.f
+ != null) {
+ x69
+ .f
+ .toString();
+ if (x70.f
+ != null) {
+ x70
+ .f
+ .toString();
+ if (x71.f
+ != null) {
+ x71
+ .f
+ .toString();
+ if (x72.f
+ != null) {
+ x72
+ .f
+ .toString();
+ if (x73.f
+ != null) {
+ x73
+ .f
+ .toString();
+ if (x74.f
+ != null) {
+ x74
+ .f
+ .toString();
+ if (x75.f
+ != null) {
+ x75
+ .f
+ .toString();
+ if (x76.f
+ != null) {
+ x76
+ .f
+ .toString();
+ if (x77.f
+ != null) {
+ x77
+ .f
+ .toString();
+ if (x78.f
+ != null) {
+ x78
+ .f
+ .toString();
+ if (x79.f
+ != null) {
+ x79
+ .f
+ .toString();
+ if (x80.f
+ != null) {
+ x80
+ .f
+ .toString();
+ if (x81.f
+ != null) {
+ x81
+ .f
+ .toString();
+ if (x82.f
+ != null) {
+ x82
+ .f
+ .toString();
+ if (x83.f
+ != null) {
+ x83
+ .f
+ .toString();
+ if (x84.f
+ != null) {
+ x84
+ .f
+ .toString();
+ if (x85.f
+ != null) {
+ x85
+ .f
+ .toString();
+ if (x86.f
+ != null) {
+ x86
+ .f
+ .toString();
+ if (x87.f
+ != null) {
+ x87
+ .f
+ .toString();
+ if (x88.f
+ != null) {
+ x88
+ .f
+ .toString();
+ if (x89.f
+ != null) {
+ x89
+ .f
+ .toString();
+ if (x90.f
+ != null) {
+ x90
+ .f
+ .toString();
+ if (x91.f
+ != null) {
+ x91
+ .f
+ .toString();
+ if (x92.f
+ != null) {
+ x92
+ .f
+ .toString();
+ if (x93.f
+ != null) {
+ x93
+ .f
+ .toString();
+ if (x94.f
+ != null) {
+ x94
+ .f
+ .toString();
+ if (x95.f
+ != null) {
+ x95
+ .f
+ .toString();
+ if (x96.f
+ != null) {
+ x96
+ .f
+ .toString();
+ if (x97.f
+ != null) {
+ x97
+ .f
+ .toString();
+ if (x98.f
+ != null) {
+ x98
+ .f
+ .toString();
+ if (x99.f
+ != null) {
+ x99
+ .f
+ .toString();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/jmh/src/test/java/com/uber/nullaway/jmh/BenchmarkCompilationTest.java b/jmh/src/test/java/com/uber/nullaway/jmh/BenchmarkCompilationTest.java
new file mode 100644
index 0000000..d9adff9
--- /dev/null
+++ b/jmh/src/test/java/com/uber/nullaway/jmh/BenchmarkCompilationTest.java
@@ -0,0 +1,30 @@
+package com.uber.nullaway.jmh;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import org.junit.Test;
+
+/** Tests that all our JMH benchmarks compile successfully */
+public class BenchmarkCompilationTest {
+
+ @Test
+ public void testAutodispose() throws IOException {
+ assertTrue(new AutodisposeCompiler().compile());
+ }
+
+ @Test
+ public void testCaffeine() throws IOException {
+ assertTrue(new CaffeineCompiler().compile());
+ }
+
+ @Test
+ public void testNullawayRelease() throws IOException {
+ assertTrue(new NullawayReleaseCompiler().compile());
+ }
+
+ @Test
+ public void testDFlowMicro() throws IOException {
+ assertTrue(new DataFlowMicroBenchmarkCompiler().compile());
+ }
+}
diff --git a/nullaway/build.gradle b/nullaway/build.gradle
index c57c7e2..8000dbc 100644
--- a/nullaway/build.gradle
+++ b/nullaway/build.gradle
@@ -16,27 +16,30 @@
import net.ltgt.gradle.errorprone.CheckSeverity
plugins {
- id 'java-library'
- id 'nullaway.jacoco-conventions'
+ id 'java-library'
+ id 'nullaway.java-test-conventions'
}
-sourceCompatibility = "1.8"
-targetCompatibility = "1.8"
-
configurations {
nullawayJar
}
dependencies {
+ compileOnly project(":annotations")
compileOnly deps.apt.autoValueAnnot
annotationProcessor deps.apt.autoValue
compileOnly deps.apt.autoServiceAnnot
annotationProcessor deps.apt.autoService
+ compileOnly deps.build.jsr305Annotations
+ compileOnly deps.test.jetbrainsAnnotations
+ compileOnly deps.apt.javaxInject
+
compileOnly deps.build.errorProneCheckApi
implementation deps.build.checkerDataflow
implementation deps.build.guava
+ testImplementation project(":annotations")
testImplementation deps.test.junit4
testImplementation(deps.build.errorProneTestHelpers) {
exclude group: "junit", module: "junit"
@@ -45,8 +48,8 @@ dependencies {
testImplementation deps.test.junit5Jupiter
testImplementation deps.test.cfQual
testImplementation deps.test.cfCompatQual
+ testImplementation deps.build.jspecify
testImplementation project(":test-java-lib")
- testImplementation deps.test.inferAnnotations
testImplementation deps.apt.jakartaInject
testImplementation deps.apt.javaxInject
testImplementation deps.test.rxjava2
@@ -57,66 +60,114 @@ dependencies {
testImplementation deps.test.springBeans
testImplementation deps.test.springContext
testImplementation deps.test.grpcCore
-
- // This ends up being resolved to the NullAway jar under nullaway/build/libs
- nullawayJar "com.uber.nullaway:nullaway:$VERSION_NAME"
+ testImplementation project(":test-java-lib-lombok")
+ testImplementation deps.test.mockito
+ testImplementation deps.test.javaxAnnotationApi
+ testImplementation deps.test.assertJ
}
javadoc {
failOnError = false
}
+apply plugin: 'com.vanniktech.maven.publish'
+
+// These --add-exports arguments are required when targeting JDK 11+ since Error Prone and NullAway access a bunch of
+// JDK-internal APIs that are not exposed otherwise. Since we currently target JDK 8, we do not need to pass the
+// arguments, as encapsulation of JDK internals is not enforced on JDK 8. In fact, the arguments cause a compiler error
+// when targeting JDK 8. Leaving commented so we can easily add them back once we target JDK 11.
+// tasks.withType(JavaCompile).configureEach {
+// options.compilerArgs += [
+// "--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.code=ALL-UNNAMED",
+// "--add-exports=jdk.compiler/com.sun.tools.javac.comp=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-exports=jdk.compiler/com.sun.source.tree=ALL-UNNAMED",
+// ]
+// }
+
+// Create a task to test on JDK 8
+def jdk8Test = tasks.register("testJdk8", Test) {
+ onlyIf {
+ // Only if we are using a version of Error Prone compatible with JDK 8
+ deps.versions.errorProneApi == "2.10.0"
+ }
+
+ javaLauncher = javaToolchains.launcherFor {
+ languageVersion = JavaLanguageVersion.of(8)
+ }
+
+ description = "Runs the test suite on JDK 8"
+ group = LifecycleBasePlugin.VERIFICATION_GROUP
-test {
- maxHeapSize = "1024m"
- if (!JavaVersion.current().java9Compatible) {
+ // Copy inputs from normal Test task.
+ def testTask = tasks.getByName("test")
+ classpath = testTask.classpath
+ testClassesDirs = testTask.testClassesDirs
jvmArgs "-Xbootclasspath/p:${configurations.errorproneJavac.asPath}"
- } else {
- // to expose necessary JDK types on JDK 16+; see https://errorprone.info/docs/installation#java-9-and-newer
- 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",
- // Accessed by Lombok tests
- "--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED",
- ]
- }
+ filter {
+ // JDK 8 does not support diamonds on anonymous classes
+ excludeTestsMatching "com.uber.nullaway.NullAwayJSpecifyGenericsTests.overrideDiamondAnonymousClass"
+ // tests cannot run on JDK 8 since Mockito version no longer supports it
+ excludeTestsMatching "com.uber.nullaway.NullAwaySerializationTest.initializationError"
+ excludeTestsMatching "com.uber.nullaway.handlers.contract.ContractUtilsTest.getEmptyAntecedent"
+ }
}
-apply plugin: 'com.vanniktech.maven.publish'
+tasks.named('check').configure {
+ dependsOn(jdk8Test)
+}
+
+tasks.named('testJdk21', Test).configure {
+ filter {
+ // JSpecify Generics tests do not yet pass on JDK 21
+ // See https://github.com/uber/NullAway/issues/827
+ excludeTestsMatching "com.uber.nullaway.NullAwayJSpecifyGenericsTests"
+ }
+}
// Create a task to build NullAway with NullAway checking enabled
-// For some reason, this doesn't work on Java 8
-if (JavaVersion.current() >= JavaVersion.VERSION_11) {
- tasks.register('buildWithNullAway', JavaCompile) {
- // Configure compilation to run with Error Prone and NullAway
- source = sourceSets.main.java
- classpath = sourceSets.main.compileClasspath
- destinationDirectory = file("$buildDir/ignoredClasses")
- def nullawayDeps = configurations.nullawayJar.asCollection()
- options.annotationProcessorPath = files(
- configurations.errorprone.asCollection(),
- sourceSets.main.annotationProcessorPath,
- nullawayDeps)
- options.errorprone.enabled = true
- options.errorprone {
- option("NullAway:AnnotatedPackages", "com.uber")
- }
- // Make sure the jar has already been built
- dependsOn 'jar'
- // Check that the NullAway jar actually exists (without this,
- // Gradle will run the compilation even if the jar doesn't exist)
- doFirst {
- nullawayDeps.forEach { f ->
- assert f.exists()
- }
- }
+tasks.register('buildWithNullAway', JavaCompile) {
+ onlyIf {
+ // We only do NullAway checks when compiling against the latest
+ // version of Error Prone (as nullability annotations on the APIs
+ // can change between versions)
+ deps.versions.errorProneApi == deps.versions.errorProneLatest
+ }
+ // Configure compilation to run with Error Prone and NullAway
+ source = sourceSets.main.java
+ classpath = sourceSets.main.compileClasspath
+ destinationDirectory = file("$buildDir/ignoredClasses")
+ options.annotationProcessorPath = files(
+ configurations.errorprone.asCollection(),
+ sourceSets.main.annotationProcessorPath,
+ // This refers to the NullAway jar built from the current source
+ jar.archiveFile.get(),
+ sourceSets.main.compileClasspath)
+ options.errorprone.enabled = true
+ options.errorprone {
+ option("NullAway:AnnotatedPackages", "com.uber,org.checkerframework.nullaway")
+ option("NullAway:CastToNonNullMethod", "com.uber.nullaway.NullabilityUtil.castToNonNull")
+ option("NullAway:CheckOptionalEmptiness")
+ option("NullAway:AcknowledgeRestrictiveAnnotations")
+ option("NullAway:CheckContracts")
+ option("NullAway:JSpecifyMode")
+ }
+ // Make sure the jar has already been built
+ dependsOn 'jar'
+ // Check that the NullAway jar actually exists (without this,
+ // Gradle will run the compilation even if the jar doesn't exist)
+ doFirst {
+ assert jar.archiveFile.get().getAsFile().exists()
}
}
+
+project.tasks.named('check').configure {
+ dependsOn 'buildWithNullAway'
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/ASTHelpersBackports.java b/nullaway/src/main/java/com/uber/nullaway/ASTHelpersBackports.java
new file mode 100644
index 0000000..06a91f9
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/ASTHelpersBackports.java
@@ -0,0 +1,39 @@
+package com.uber.nullaway;
+
+import com.sun.tools.javac.code.Symbol;
+import java.util.List;
+
+/**
+ * Methods backported from {@link com.google.errorprone.util.ASTHelpers} since we do not yet require
+ * a recent-enough Error Prone version. The methods should be removed once we bump our minimum Error
+ * Prone version accordingly.
+ */
+public class ASTHelpersBackports {
+
+ private ASTHelpersBackports() {}
+
+ /**
+ * Returns true if the symbol is static. Returns {@code false} for module symbols. Remove once we
+ * require Error Prone 2.16.0 or higher.
+ */
+ @SuppressWarnings("ASTHelpersSuggestions")
+ public static boolean isStatic(Symbol symbol) {
+ if (symbol.getKind().name().equals("MODULE")) {
+ return false;
+ }
+ return symbol.isStatic();
+ }
+
+ /**
+ * A wrapper for {@link Symbol#getEnclosedElements} to avoid binary compatibility issues for
+ * covariant overrides in subtypes of {@link Symbol}.
+ *
+ * <p>Same as this ASTHelpers method in Error Prone:
+ * https://github.com/google/error-prone/blame/a1318e4b0da4347dff7508108835d77c470a7198/check_api/src/main/java/com/google/errorprone/util/ASTHelpers.java#L1148
+ * TODO: delete this method and switch to ASTHelpers once we can require Error Prone 2.20.0
+ */
+ @SuppressWarnings("ASTHelpersSuggestions")
+ public static List<Symbol> getEnclosedElements(Symbol symbol) {
+ return symbol.getEnclosedElements();
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/AbstractConfig.java b/nullaway/src/main/java/com/uber/nullaway/AbstractConfig.java
index afc6cb1..9303b93 100644
--- a/nullaway/src/main/java/com/uber/nullaway/AbstractConfig.java
+++ b/nullaway/src/main/java/com/uber/nullaway/AbstractConfig.java
@@ -24,16 +24,17 @@ package com.uber.nullaway;
import com.google.auto.value.AutoValue;
import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.errorprone.util.ASTHelpers;
import com.sun.tools.javac.code.Symbol;
-import java.util.LinkedHashSet;
-import java.util.Set;
+import com.uber.nullaway.fixserialization.FixSerializationConfig;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/** abstract base class for null checker {@link Config} implementations */
+@SuppressWarnings("NullAway") // TODO: get rid of this class to avoid suppression
public abstract class AbstractConfig implements Config {
/**
@@ -74,32 +75,34 @@ public abstract class AbstractConfig implements Config {
protected boolean handleTestAssertionLibraries;
- protected Set<String> optionalClassPaths;
+ protected ImmutableSet<String> optionalClassPaths;
protected boolean assertsEnabled;
- /**
- * if true, {@link #fromAnnotatedPackage(Symbol.ClassSymbol)} will return false for any class
- * annotated with {@link javax.annotation.Generated}
- */
protected boolean treatGeneratedAsUnannotated;
protected boolean acknowledgeAndroidRecent;
- protected Set<MethodClassAndName> knownInitializers;
+ protected boolean jspecifyMode;
+
+ protected ImmutableSet<MethodClassAndName> knownInitializers;
- protected Set<String> excludedClassAnnotations;
+ protected ImmutableSet<String> excludedClassAnnotations;
- protected Set<String> initializerAnnotations;
+ protected ImmutableSet<String> generatedCodeAnnotations;
- protected Set<String> externalInitAnnotations;
+ protected ImmutableSet<String> initializerAnnotations;
- protected Set<String> contractAnnotations;
+ protected ImmutableSet<String> externalInitAnnotations;
+
+ protected ImmutableSet<String> contractAnnotations;
@Nullable protected String castToNonNullMethod;
protected String autofixSuppressionComment;
+ protected ImmutableSet<String> skippedLibraryModels;
+
/** --- JarInfer configs --- */
protected boolean jarInferEnabled;
@@ -111,11 +114,32 @@ public abstract class AbstractConfig implements Config {
protected String errorURL;
/** --- Fully qualified names of custom nonnull/nullable annotation --- */
- protected Set<String> customNonnullAnnotations;
+ protected ImmutableSet<String> customNonnullAnnotations;
- protected Set<String> customNullableAnnotations;
+ protected ImmutableSet<String> customNullableAnnotations;
- protected static Pattern getPackagePattern(Set<String> packagePrefixes) {
+ /**
+ * If active, NullAway will write all reporting errors in output directory. The output directory
+ * along with the activation status of other serialization features are stored in {@link
+ * FixSerializationConfig}.
+ */
+ protected boolean serializationActivationFlag;
+
+ protected FixSerializationConfig fixSerializationConfig;
+
+ @Override
+ public boolean serializationIsActive() {
+ return serializationActivationFlag;
+ }
+
+ @Override
+ public FixSerializationConfig getSerializationConfig() {
+ Preconditions.checkArgument(
+ serializationActivationFlag, "Fix Serialization is not active, cannot access it's config.");
+ return fixSerializationConfig;
+ }
+
+ protected static Pattern getPackagePattern(ImmutableSet<String> packagePrefixes) {
// noinspection ConstantConditions
String choiceRegexp =
Joiner.on("|")
@@ -124,12 +148,18 @@ public abstract class AbstractConfig implements Config {
}
@Override
- public boolean fromAnnotatedPackage(Symbol.ClassSymbol symbol) {
- String className = symbol.getQualifiedName().toString();
- return annotatedPackages.matcher(className).matches()
- && !unannotatedSubPackages.matcher(className).matches()
- && (!treatGeneratedAsUnannotated
- || !ASTHelpers.hasDirectAnnotationWithSimpleName(symbol, "Generated"));
+ public boolean fromExplicitlyAnnotatedPackage(String className) {
+ return annotatedPackages.matcher(className).matches();
+ }
+
+ @Override
+ public boolean fromExplicitlyUnannotatedPackage(String className) {
+ return unannotatedSubPackages.matcher(className).matches();
+ }
+
+ @Override
+ public boolean treatGeneratedAsUnannotated() {
+ return treatGeneratedAsUnannotated;
}
@Override
@@ -161,7 +191,12 @@ public abstract class AbstractConfig implements Config {
@Override
public ImmutableSet<String> getExcludedClassAnnotations() {
- return ImmutableSet.copyOf(excludedClassAnnotations);
+ return excludedClassAnnotations;
+ }
+
+ @Override
+ public ImmutableSet<String> getGeneratedCodeAnnotations() {
+ return generatedCodeAnnotations;
}
@Override
@@ -225,7 +260,7 @@ public abstract class AbstractConfig implements Config {
}
@Override
- public Set<String> getOptionalClassPaths() {
+ public ImmutableSet<String> getOptionalClassPaths() {
return optionalClassPaths;
}
@@ -259,15 +294,9 @@ public abstract class AbstractConfig implements Config {
return contractAnnotations.contains(annotationName);
}
- protected Set<MethodClassAndName> getKnownInitializers(Set<String> qualifiedNames) {
- Set<MethodClassAndName> result = new LinkedHashSet<>();
- for (String name : qualifiedNames) {
- int lastDot = name.lastIndexOf('.');
- String methodName = name.substring(lastDot + 1);
- String className = name.substring(0, lastDot);
- result.add(MethodClassAndName.create(className, methodName));
- }
- return result;
+ @Override
+ public boolean isSkippedLibraryModel(String classDotMethod) {
+ return skippedLibraryModels.contains(classDotMethod);
}
@AutoValue
@@ -277,6 +306,13 @@ public abstract class AbstractConfig implements Config {
return new AutoValue_AbstractConfig_MethodClassAndName(enclosingClass, methodName);
}
+ static MethodClassAndName fromClassDotMethod(String classDotMethod) {
+ int lastDot = classDotMethod.lastIndexOf('.');
+ String methodName = classDotMethod.substring(lastDot + 1);
+ String className = classDotMethod.substring(0, lastDot);
+ return MethodClassAndName.create(className, methodName);
+ }
+
abstract String enclosingClass();
abstract String methodName();
@@ -309,12 +345,12 @@ public abstract class AbstractConfig implements Config {
}
@Override
- public boolean treatGeneratedAsUnannotated() {
- return treatGeneratedAsUnannotated;
+ public boolean acknowledgeAndroidRecent() {
+ return acknowledgeAndroidRecent;
}
@Override
- public boolean acknowledgeAndroidRecent() {
- return acknowledgeAndroidRecent;
+ public boolean isJSpecifyMode() {
+ return jspecifyMode;
}
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/CodeAnnotationInfo.java b/nullaway/src/main/java/com/uber/nullaway/CodeAnnotationInfo.java
new file mode 100644
index 0000000..013e8f4
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/CodeAnnotationInfo.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (c) 2017-2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway;
+
+import static com.uber.nullaway.NullabilityUtil.castToNonNull;
+
+import com.google.common.base.Preconditions;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.util.Context;
+import java.util.HashMap;
+import java.util.Map;
+import javax.lang.model.element.ElementKind;
+
+/**
+ * Provides APIs for querying whether code is annotated for nullness checking, and for related
+ * queries on what annotations are present on a class/method and/or on relevant enclosing scopes
+ * (i.e. enclosing classes or methods). Makes use of caching internally for performance.
+ */
+public final class CodeAnnotationInfo {
+
+ private static final Context.Key<CodeAnnotationInfo> ANNOTATION_INFO_KEY = new Context.Key<>();
+
+ private static final int MAX_CLASS_CACHE_SIZE = 200;
+
+ private final Cache<Symbol.ClassSymbol, ClassCacheRecord> classCache =
+ CacheBuilder.newBuilder().maximumSize(MAX_CLASS_CACHE_SIZE).build();
+
+ private CodeAnnotationInfo() {}
+
+ /**
+ * Get the CodeAnnotationInfo for the given javac context. We ensure there is one instance per
+ * context (as opposed to using static fields) to avoid memory leaks.
+ */
+ public static CodeAnnotationInfo instance(Context context) {
+ CodeAnnotationInfo annotationInfo = context.get(ANNOTATION_INFO_KEY);
+ if (annotationInfo == null) {
+ annotationInfo = new CodeAnnotationInfo();
+ context.put(ANNOTATION_INFO_KEY, annotationInfo);
+ }
+ return annotationInfo;
+ }
+
+ /**
+ * Checks if a symbol comes from an annotated package, as determined by either configuration flags
+ * (e.g. {@code -XepOpt:NullAway::AnnotatedPackages}) or package level annotations (e.g. {@code
+ * org.jspecify.annotations.NullMarked}).
+ *
+ * @param outermostClassSymbol symbol for class (must be an outermost class)
+ * @param config NullAway config
+ * @return true if the class is from a package that should be treated as properly annotated
+ * according to our convention (every possibly null parameter / return / field
+ * annotated @Nullable), false otherwise
+ */
+ private static boolean fromAnnotatedPackage(
+ Symbol.ClassSymbol outermostClassSymbol, Config config) {
+ final String className = outermostClassSymbol.getQualifiedName().toString();
+ Symbol.PackageSymbol enclosingPackage = ASTHelpers.enclosingPackage(outermostClassSymbol);
+ if (!config.fromExplicitlyAnnotatedPackage(className)
+ && !(enclosingPackage != null
+ && ASTHelpers.hasDirectAnnotationWithSimpleName(
+ enclosingPackage, NullabilityUtil.NULLMARKED_SIMPLE_NAME))) {
+ // By default, unknown code is unannotated unless @NullMarked or configured as annotated by
+ // package name
+ return false;
+ }
+ if (config.fromExplicitlyUnannotatedPackage(className)
+ || (enclosingPackage != null
+ && ASTHelpers.hasDirectAnnotationWithSimpleName(
+ enclosingPackage, NullabilityUtil.NULLUNMARKED_SIMPLE_NAME))) {
+ // Any code explicitly marked as unannotated in our configuration is unannotated, no matter
+ // what. Similarly, any package annotated as @NullUnmarked is unannotated, even if
+ // explicitly passed to -XepOpt:NullAway::AnnotatedPackages
+ return false;
+ }
+ // Finally, if we are here, the code was marked as annotated (either by configuration or
+ // @NullMarked) and nothing overrides it.
+ return true;
+ }
+
+ /**
+ * Check if a symbol comes from generated code.
+ *
+ * @param symbol symbol for entity
+ * @return true if symbol represents an entity contained in a class annotated with
+ * {@code @Generated}; false otherwise
+ */
+ public boolean isGenerated(Symbol symbol, Config config) {
+ Symbol.ClassSymbol classSymbol = ASTHelpers.enclosingClass(symbol);
+ if (classSymbol == null) {
+ Preconditions.checkArgument(
+ isClassFieldOfPrimitiveType(
+ symbol), // One known case where this can happen: int.class, void.class, etc.
+ String.format(
+ "Unexpected symbol passed to CodeAnnotationInfo.isGenerated(...) with null enclosing class: %s",
+ symbol));
+ return false;
+ }
+ Symbol.ClassSymbol outermostClassSymbol = get(classSymbol, config).outermostClassSymbol;
+ return ASTHelpers.hasDirectAnnotationWithSimpleName(outermostClassSymbol, "Generated");
+ }
+
+ /**
+ * Check if the symbol represents the .class field of a primitive type.
+ *
+ * <p>e.g. int.class, boolean.class, void.class, etc.
+ *
+ * @param symbol symbol for entity
+ * @return true iff this symbol represents t.class for a primitive type t.
+ */
+ private static boolean isClassFieldOfPrimitiveType(Symbol symbol) {
+ return symbol.name.contentEquals("class")
+ && symbol.owner != null
+ && symbol.owner.getKind().equals(ElementKind.CLASS)
+ && symbol.owner.getQualifiedName().equals(symbol.owner.getSimpleName())
+ && symbol.owner.enclClass() == null;
+ }
+
+ /**
+ * Check if a symbol comes from unannotated code.
+ *
+ * @param symbol symbol for entity
+ * @param config NullAway config
+ * @return true if symbol represents an entity contained in a class that is unannotated; false
+ * otherwise
+ */
+ public boolean isSymbolUnannotated(Symbol symbol, Config config) {
+ Symbol.ClassSymbol classSymbol;
+ if (symbol instanceof Symbol.ClassSymbol) {
+ classSymbol = (Symbol.ClassSymbol) symbol;
+ } else if (isClassFieldOfPrimitiveType(symbol)) {
+ // As a special case, int.class, boolean.class, etc, cause ASTHelpers.enclosingClass(...) to
+ // return null, even though int/boolean/etc. are technically ClassSymbols. We consider this
+ // class "field" of primitive types to be always unannotated. (In the future, we could check
+ // here for whether java.lang is in the annotated packages, but if it is, I suspect we will
+ // have weirder problems than this)
+ return true;
+ } else {
+ classSymbol = castToNonNull(ASTHelpers.enclosingClass(symbol));
+ }
+ final ClassCacheRecord classCacheRecord = get(classSymbol, config);
+ boolean inAnnotatedClass = classCacheRecord.isNullnessAnnotated;
+ if (symbol.getKind().equals(ElementKind.METHOD)
+ || symbol.getKind().equals(ElementKind.CONSTRUCTOR)) {
+ return !classCacheRecord.isMethodNullnessAnnotated((Symbol.MethodSymbol) symbol);
+ } else {
+ return !inAnnotatedClass;
+ }
+ }
+
+ /**
+ * Check whether a class should be treated as nullness-annotated.
+ *
+ * @param classSymbol The symbol for the class to be checked
+ * @return Whether this class should be treated as null-annotated, taking into account annotations
+ * on enclosing classes, the containing package, and other NullAway configuration like
+ * annotated packages
+ */
+ public boolean isClassNullAnnotated(Symbol.ClassSymbol classSymbol, Config config) {
+ return get(classSymbol, config).isNullnessAnnotated;
+ }
+
+ /**
+ * Retrieve the (outermostClass, isNullMarked) record for a given class symbol.
+ *
+ * <p>This method is recursive, using the cache on the way up and populating it on the way down.
+ *
+ * @param classSymbol The class to query, possibly an inner class
+ * @return A record including the outermost class in which the given class is nested, as well as
+ * boolean flag noting whether it should be treated as nullness-annotated, taking into account
+ * annotations on enclosing classes, the containing package, and other NullAway configuration
+ * like annotated packages
+ */
+ private ClassCacheRecord get(Symbol.ClassSymbol classSymbol, Config config) {
+ ClassCacheRecord record = classCache.getIfPresent(classSymbol);
+ if (record != null) {
+ return record;
+ }
+ if (classSymbol.getNestingKind().isNested()) {
+ Symbol owner = classSymbol.owner;
+ Preconditions.checkNotNull(owner, "Symbol.owner should only be null for modules!");
+ Symbol.MethodSymbol enclosingMethod = null;
+ if (owner.getKind().equals(ElementKind.METHOD)
+ || owner.getKind().equals(ElementKind.CONSTRUCTOR)) {
+ enclosingMethod = (Symbol.MethodSymbol) owner;
+ }
+ Symbol.ClassSymbol enclosingClass = ASTHelpers.enclosingClass(classSymbol);
+ // enclosingClass can be null in weird cases like for array methods
+ if (enclosingClass != null) {
+ ClassCacheRecord recordForEnclosing = get(enclosingClass, config);
+ // Check if this class is annotated, recall that enclosed scopes override enclosing scopes
+ boolean isAnnotated = recordForEnclosing.isNullnessAnnotated;
+ if (enclosingMethod != null) {
+ isAnnotated = recordForEnclosing.isMethodNullnessAnnotated(enclosingMethod);
+ }
+ if (ASTHelpers.hasDirectAnnotationWithSimpleName(
+ classSymbol, NullabilityUtil.NULLUNMARKED_SIMPLE_NAME)) {
+ isAnnotated = false;
+ } else if (ASTHelpers.hasDirectAnnotationWithSimpleName(
+ classSymbol, NullabilityUtil.NULLMARKED_SIMPLE_NAME)) {
+ isAnnotated = true;
+ }
+ if (shouldTreatAsUnannotated(classSymbol, config)) {
+ isAnnotated = false;
+ }
+ record = new ClassCacheRecord(recordForEnclosing.outermostClassSymbol, isAnnotated);
+ }
+ }
+ if (record == null) {
+ // We are already at the outermost class (we can find), so let's create a record for it
+ record = new ClassCacheRecord(classSymbol, isAnnotatedTopLevelClass(classSymbol, config));
+ }
+ classCache.put(classSymbol, record);
+ return record;
+ }
+
+ private boolean shouldTreatAsUnannotated(Symbol.ClassSymbol classSymbol, Config config) {
+ if (config.isUnannotatedClass(classSymbol)) {
+ return true;
+ } else if (config.treatGeneratedAsUnannotated()) {
+ // Generated code is or isn't excluded, depending on configuration
+ // Note: In the future, we might want finer grain controls to distinguish code that is
+ // generated with nullability info and without.
+ if (ASTHelpers.hasDirectAnnotationWithSimpleName(classSymbol, "Generated")) {
+ return true;
+ }
+ ImmutableSet<String> generatedCodeAnnotations = config.getGeneratedCodeAnnotations();
+ if (classSymbol.getAnnotationMirrors().stream()
+ .map(anno -> anno.getAnnotationType().toString())
+ .anyMatch(generatedCodeAnnotations::contains)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isAnnotatedTopLevelClass(Symbol.ClassSymbol classSymbol, Config config) {
+ // First, check for an explicitly @NullUnmarked top level class
+ if (ASTHelpers.hasDirectAnnotationWithSimpleName(
+ classSymbol, NullabilityUtil.NULLUNMARKED_SIMPLE_NAME)) {
+ return false;
+ }
+ // Then, check if the class has a @NullMarked annotation or comes from an annotated package
+ if ((ASTHelpers.hasDirectAnnotationWithSimpleName(
+ classSymbol, NullabilityUtil.NULLMARKED_SIMPLE_NAME)
+ || fromAnnotatedPackage(classSymbol, config))) {
+ // make sure it's not explicitly configured as unannotated
+ return !shouldTreatAsUnannotated(classSymbol, config);
+ }
+ return false;
+ }
+
+ /**
+ * Immutable record holding the outermost class symbol and the nullness-annotated state for a
+ * given (possibly inner) class.
+ *
+ * <p>The class being referenced by the record is not represented by this object, but rather the
+ * key used to retrieve it.
+ */
+ private static final class ClassCacheRecord {
+ public final Symbol.ClassSymbol outermostClassSymbol;
+ public final boolean isNullnessAnnotated;
+ public final Map<Symbol.MethodSymbol, Boolean> methodNullnessCache;
+
+ public ClassCacheRecord(Symbol.ClassSymbol outermostClassSymbol, boolean isAnnotated) {
+ this.outermostClassSymbol = outermostClassSymbol;
+ this.isNullnessAnnotated = isAnnotated;
+ this.methodNullnessCache = new HashMap<>();
+ }
+
+ public boolean isMethodNullnessAnnotated(Symbol.MethodSymbol methodSymbol) {
+ return methodNullnessCache.computeIfAbsent(
+ methodSymbol,
+ m -> {
+ if (ASTHelpers.hasDirectAnnotationWithSimpleName(
+ m, NullabilityUtil.NULLUNMARKED_SIMPLE_NAME)) {
+ return false;
+ } else if (this.isNullnessAnnotated) {
+ return true;
+ } else {
+ return ASTHelpers.hasDirectAnnotationWithSimpleName(
+ m, NullabilityUtil.NULLMARKED_SIMPLE_NAME);
+ }
+ });
+ }
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/Config.java b/nullaway/src/main/java/com/uber/nullaway/Config.java
index 2d92da2..8b0b084 100644
--- a/nullaway/src/main/java/com/uber/nullaway/Config.java
+++ b/nullaway/src/main/java/com/uber/nullaway/Config.java
@@ -24,6 +24,7 @@ package com.uber.nullaway;
import com.google.common.collect.ImmutableSet;
import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.fixserialization.FixSerializationConfig;
import java.util.Set;
import javax.annotation.Nullable;
@@ -31,14 +32,49 @@ import javax.annotation.Nullable;
public interface Config {
/**
- * Checks if a symbol comes from an annotated package.
+ * Checks if Serialization feature is active.
*
- * @param symbol symbol for class
- * @return true if the class is from a package that should be treated as properly annotated
- * according to our convention (every possibly null parameter / return / field
- * annotated @Nullable), false otherwise
+ * @return true, if Fix Serialization feature is active.
+ */
+ boolean serializationIsActive();
+
+ /**
+ * Getter method for {@link FixSerializationConfig}.
+ *
+ * <p>Fix Serialization feature must be activated, otherwise calling this method will fail the
+ * execution.
+ *
+ * @return {@link FixSerializationConfig} instance in Config.
+ */
+ FixSerializationConfig getSerializationConfig();
+
+ /**
+ * Checks if a class comes from an explicitly annotated package.
+ *
+ * @param className fully qualified name for class
+ * @return true if the class is from a package that is explicitly configured to be treated as
+ * properly annotated according to our convention (every possibly null parameter / return /
+ * field annotated @Nullable), false otherwise
+ */
+ boolean fromExplicitlyAnnotatedPackage(String className);
+
+ /**
+ * Checks if a class comes from an explicitly unannotated (sub-)package.
+ *
+ * @param className fully qualified name for class
+ * @return true if the class is from a package that is explicitly configured to be treated as
+ * unannotated (even if it is a subpackage of a package configured to be explicitly annotated
+ * or if it's marked @NullMarked), false otherwise
+ */
+ boolean fromExplicitlyUnannotatedPackage(String className);
+
+ /**
+ * Checks if (tool) generated code should be considered always unannoatated.
+ *
+ * @return true if code marked as generated code should be treated as unannotated, even if it
+ * comes from a package otherwise configured as annotated.
*/
- boolean fromAnnotatedPackage(Symbol.ClassSymbol symbol);
+ boolean treatGeneratedAsUnannotated();
/**
* Checks if a class should be excluded.
@@ -65,6 +101,8 @@ public interface Config {
*/
ImmutableSet<String> getExcludedClassAnnotations();
+ ImmutableSet<String> getGeneratedCodeAnnotations();
+
/**
* Checks if the annotation is an @Initializer annotation.
*
@@ -119,6 +157,9 @@ public interface Config {
* Checks if annotation marks an "external-init class," i.e., a class where some external
* framework initializes object fields after invoking the zero-argument constructor.
*
+ * <p>Note that this annotation can be on the class itself, or on the zero-arguments constructor,
+ * but will be ignored anywhere else.
+ *
* @param annotationName fully-qualified annotation name
* @return true if classes with the annotation are external-init
*/
@@ -208,6 +249,20 @@ public interface Config {
*/
String getAutofixSuppressionComment();
+ /**
+ * Checks if the given library model should be skipped/ignored.
+ *
+ * <p>For ease of configuration in the command line, this works at the level of the (class, method
+ * name) pair, meaning it applies for all methods with the same name in the same class, even if
+ * they have different signatures, and to all library models applicable to that method (i.e. on
+ * the method's return, arguments, etc).
+ *
+ * @param classDotMethod The method from the model, in [fully_qualified_class_name].[method_name]
+ * format (no args)
+ * @return True if the library model should be skipped.
+ */
+ boolean isSkippedLibraryModel(String classDotMethod);
+
// --- JarInfer configs ---
/**
@@ -247,13 +302,6 @@ public interface Config {
String getErrorURL();
/**
- * Checks whether (tool-)generated code should be treated as unannotated.
- *
- * @return true if generated code should be treated as unannotated
- */
- boolean treatGeneratedAsUnannotated();
-
- /**
* Checks if acknowledging {@code @RecentlyNullable} and {@code @RecentlyNonNull} annotations is
* enabled.
*
@@ -261,4 +309,7 @@ public interface Config {
* similarly for {@code @RecentlyNonNull}
*/
boolean acknowledgeAndroidRecent();
+
+ /** Should new checks based on JSpecify (like checks for generic types) be enabled? */
+ boolean isJSpecifyMode();
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/DummyOptionsConfig.java b/nullaway/src/main/java/com/uber/nullaway/DummyOptionsConfig.java
index b424eb9..de423e3 100644
--- a/nullaway/src/main/java/com/uber/nullaway/DummyOptionsConfig.java
+++ b/nullaway/src/main/java/com/uber/nullaway/DummyOptionsConfig.java
@@ -27,6 +27,7 @@ import static com.uber.nullaway.ErrorProneCLIFlagsConfig.FL_ANNOTATED_PACKAGES;
import com.google.common.collect.ImmutableSet;
import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.fixserialization.FixSerializationConfig;
import java.util.Set;
import javax.annotation.Nullable;
@@ -54,7 +55,27 @@ public class DummyOptionsConfig implements Config {
public DummyOptionsConfig() {}
@Override
- public boolean fromAnnotatedPackage(Symbol.ClassSymbol symbol) {
+ public boolean fromExplicitlyAnnotatedPackage(String className) {
+ throw new IllegalStateException(ERROR_MESSAGE);
+ }
+
+ @Override
+ public boolean fromExplicitlyUnannotatedPackage(String className) {
+ throw new IllegalStateException(ERROR_MESSAGE);
+ }
+
+ @Override
+ public boolean treatGeneratedAsUnannotated() {
+ throw new IllegalStateException(ERROR_MESSAGE);
+ }
+
+ @Override
+ public boolean serializationIsActive() {
+ throw new IllegalStateException(ERROR_MESSAGE);
+ }
+
+ @Override
+ public FixSerializationConfig getSerializationConfig() {
throw new IllegalStateException(ERROR_MESSAGE);
}
@@ -74,6 +95,11 @@ public class DummyOptionsConfig implements Config {
}
@Override
+ public ImmutableSet<String> getGeneratedCodeAnnotations() {
+ throw new IllegalStateException(ERROR_MESSAGE);
+ }
+
+ @Override
public boolean exhaustiveOverride() {
throw new IllegalStateException(ERROR_MESSAGE);
}
@@ -160,6 +186,11 @@ public class DummyOptionsConfig implements Config {
}
@Override
+ public boolean isSkippedLibraryModel(String classDotMethod) {
+ throw new IllegalStateException(ERROR_MESSAGE);
+ }
+
+ @Override
public boolean isJarInferEnabled() {
throw new IllegalStateException(ERROR_MESSAGE);
}
@@ -186,12 +217,12 @@ public class DummyOptionsConfig implements Config {
}
@Override
- public boolean treatGeneratedAsUnannotated() {
+ public boolean acknowledgeAndroidRecent() {
throw new IllegalStateException(ERROR_MESSAGE);
}
@Override
- public boolean acknowledgeAndroidRecent() {
+ public boolean isJSpecifyMode() {
throw new IllegalStateException(ERROR_MESSAGE);
}
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/ErrorBuilder.java b/nullaway/src/main/java/com/uber/nullaway/ErrorBuilder.java
index 30d66ed..fb7aee6 100755
--- a/nullaway/src/main/java/com/uber/nullaway/ErrorBuilder.java
+++ b/nullaway/src/main/java/com/uber/nullaway/ErrorBuilder.java
@@ -22,6 +22,7 @@
package com.uber.nullaway;
+import static com.uber.nullaway.ASTHelpersBackports.isStatic;
import static com.uber.nullaway.ErrorMessage.MessageTypes.FIELD_NO_INIT;
import static com.uber.nullaway.ErrorMessage.MessageTypes.GET_ON_EMPTY_OPTIONAL;
import static com.uber.nullaway.ErrorMessage.MessageTypes.METHOD_NO_INIT;
@@ -30,8 +31,11 @@ import static com.uber.nullaway.NullAway.CORE_CHECK_NAME;
import static com.uber.nullaway.NullAway.INITIALIZATION_CHECK_NAME;
import static com.uber.nullaway.NullAway.OPTIONAL_CHECK_NAME;
import static com.uber.nullaway.NullAway.getTreesInstance;
+import static com.uber.nullaway.Nullness.hasNullableAnnotation;
import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -51,12 +55,14 @@ import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.util.DiagnosticSource;
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
+import com.uber.nullaway.fixserialization.SerializationService;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
import javax.tools.JavaFileObject;
/** A class to construct error message to be displayed after the analysis finds error. */
@@ -82,12 +88,18 @@ public class ErrorBuilder {
* @param errorMessage the error message object.
* @param descriptionBuilder the description builder for the error.
* @param state the visitor state (used for e.g. suppression finding).
+ * @param nonNullTarget if non-null, this error involved a pseudo-assignment of a @Nullable
+ * expression into a @NonNull target, and this parameter is the Symbol for that target.
* @return the error description
*/
Description createErrorDescription(
- ErrorMessage errorMessage, Description.Builder descriptionBuilder, VisitorState state) {
+ ErrorMessage errorMessage,
+ Description.Builder descriptionBuilder,
+ VisitorState state,
+ @Nullable Symbol nonNullTarget) {
Tree enclosingSuppressTree = suppressibleNode(state.getPath());
- return createErrorDescription(errorMessage, enclosingSuppressTree, descriptionBuilder, state);
+ return createErrorDescription(
+ errorMessage, enclosingSuppressTree, descriptionBuilder, state, nonNullTarget);
}
/**
@@ -97,13 +109,16 @@ public class ErrorBuilder {
* @param suggestTree the location at which a fix suggestion should be made
* @param descriptionBuilder the description builder for the error.
* @param state the visitor state (used for e.g. suppression finding).
+ * @param nonNullTarget if non-null, this error involved a pseudo-assignment of a @Nullable
+ * expression into a @NonNull target, and this parameter is the Symbol for that target.
* @return the error description
*/
public Description createErrorDescription(
ErrorMessage errorMessage,
@Nullable Tree suggestTree,
Description.Builder descriptionBuilder,
- VisitorState state) {
+ VisitorState state,
+ @Nullable Symbol nonNullTarget) {
Description.Builder builder = descriptionBuilder.setMessage(errorMessage.message);
String checkName = CORE_CHECK_NAME;
if (errorMessage.messageType.equals(GET_ON_EMPTY_OPTIONAL)) {
@@ -121,8 +136,27 @@ public class ErrorBuilder {
}
if (config.suggestSuppressions() && suggestTree != null) {
- builder = addSuggestedSuppression(errorMessage, suggestTree, builder);
+ builder = addSuggestedSuppression(errorMessage, suggestTree, builder, state);
+ }
+
+ if (config.serializationIsActive()) {
+ if (nonNullTarget != null) {
+ SerializationService.serializeFixSuggestion(config, state, nonNullTarget, errorMessage);
+ }
+ // For the case of initializer errors, the leaf of state.getPath() may not be the field /
+ // method on which the error is being reported (since we do a class-wide analysis to find such
+ // errors). In such cases, the suggestTree is the appropriate field / method tree, so use
+ // that as the errorTree for serialization.
+ Tree errorTree =
+ (suggestTree != null
+ && (errorMessage.messageType.equals(FIELD_NO_INIT)
+ || errorMessage.messageType.equals(METHOD_NO_INIT)))
+ ? suggestTree
+ : state.getPath().getLeaf();
+ SerializationService.serializeReportingError(
+ config, state, errorTree, nonNullTarget, errorMessage);
}
+
// #letbuildersbuild
return builder.build();
}
@@ -156,21 +190,30 @@ public class ErrorBuilder {
}
private Description.Builder addSuggestedSuppression(
- ErrorMessage errorMessage, Tree suggestTree, Description.Builder builder) {
+ ErrorMessage errorMessage,
+ Tree suggestTree,
+ Description.Builder builder,
+ VisitorState state) {
switch (errorMessage.messageType) {
case DEREFERENCE_NULLABLE:
case RETURN_NULLABLE:
case PASS_NULLABLE:
case ASSIGN_FIELD_NULLABLE:
case SWITCH_EXPRESSION_NULLABLE:
- if (config.getCastToNonNullMethod() != null) {
- builder = addCastToNonNullFix(suggestTree, builder);
+ if (config.getCastToNonNullMethod() != null && canBeCastToNonNull(suggestTree)) {
+ builder = addCastToNonNullFix(suggestTree, builder, state);
} else {
- builder = addSuppressWarningsFix(suggestTree, builder, suppressionName);
+ // When there is a castToNonNull method, suggestTree is set to the expression to be
+ // casted, which is not suppressible. For simplicity, we just always recompute the
+ // suppressible node here.
+ Tree suppressibleNode = suppressibleNode(state.getPath());
+ if (suppressibleNode != null) {
+ builder = addSuppressWarningsFix(suppressibleNode, builder, suppressionName);
+ }
}
break;
case CAST_TO_NONNULL_ARG_NONNULL:
- builder = removeCastToNonNullFix(suggestTree, builder);
+ builder = removeCastToNonNullFix(suggestTree, builder, state);
break;
case WRONG_OVERRIDE_RETURN:
builder = addSuppressWarningsFix(suggestTree, builder, suppressionName);
@@ -201,19 +244,26 @@ public class ErrorBuilder {
* @param descriptionBuilder the description builder for the error.
* @param state the visitor state for the location which triggered the error (i.e. for suppression
* finding)
+ * @param nonNullTarget if non-null, this error involved a pseudo-assignment of a @Nullable
+ * expression into a @NonNull target, and this parameter is the Symbol for that target.
* @return the error description.
*/
Description createErrorDescriptionForNullAssignment(
ErrorMessage errorMessage,
@Nullable Tree suggestTreeIfCastToNonNull,
Description.Builder descriptionBuilder,
- VisitorState state) {
+ VisitorState state,
+ @Nullable Symbol nonNullTarget) {
if (config.getCastToNonNullMethod() != null) {
return createErrorDescription(
- errorMessage, suggestTreeIfCastToNonNull, descriptionBuilder, state);
+ errorMessage, suggestTreeIfCastToNonNull, descriptionBuilder, state, nonNullTarget);
} else {
return createErrorDescription(
- errorMessage, suppressibleNode(state.getPath()), descriptionBuilder, state);
+ errorMessage,
+ suppressibleNode(state.getPath()),
+ descriptionBuilder,
+ state,
+ nonNullTarget);
}
}
@@ -278,13 +328,44 @@ public class ErrorBuilder {
.orElse(null);
}
- private Description.Builder addCastToNonNullFix(Tree suggestTree, Description.Builder builder) {
+ /**
+ * Checks if it would be appropriate to wrap {@code tree} in a {@code castToNonNull} call. There
+ * are two cases where this method returns {@code false}:
+ *
+ * <ol>
+ * <li>{@code tree} represents the {@code null} literal
+ * <li>{@code tree} represents a {@code @Nullable} formal parameter of the enclosing method
+ * </ol>
+ */
+ private boolean canBeCastToNonNull(Tree tree) {
+ switch (tree.getKind()) {
+ case NULL_LITERAL:
+ // never do castToNonNull(null)
+ return false;
+ case IDENTIFIER:
+ // Don't wrap a @Nullable parameter in castToNonNull, as this misleads callers into thinking
+ // they can pass in null without causing an NPE. A more appropriate fix would likely be to
+ // make the parameter @NonNull and add casts at call sites, but that is beyond the scope of
+ // our suggested fixes
+ Symbol symbol = ASTHelpers.getSymbol(tree);
+ return !(symbol != null
+ && symbol.getKind().equals(ElementKind.PARAMETER)
+ && hasNullableAnnotation(symbol, config));
+ default:
+ return true;
+ }
+ }
+
+ private Description.Builder addCastToNonNullFix(
+ Tree suggestTree, Description.Builder builder, VisitorState state) {
final String fullMethodName = config.getCastToNonNullMethod();
- assert fullMethodName != null;
+ if (fullMethodName == null) {
+ throw new IllegalStateException("cast-to-non-null method not set");
+ }
// Add a call to castToNonNull around suggestTree:
final String[] parts = fullMethodName.split("\\.");
final String shortMethodName = parts[parts.length - 1];
- final String replacement = shortMethodName + "(" + suggestTree.toString() + ")";
+ final String replacement = shortMethodName + "(" + state.getSourceForNode(suggestTree) + ")";
final SuggestedFix fix =
SuggestedFix.builder()
.replace(suggestTree, replacement)
@@ -294,28 +375,43 @@ public class ErrorBuilder {
}
private Description.Builder removeCastToNonNullFix(
- Tree suggestTree, Description.Builder builder) {
- assert suggestTree.getKind() == Tree.Kind.METHOD_INVOCATION;
- final MethodInvocationTree invTree = (MethodInvocationTree) suggestTree;
- final Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(invTree);
- final String qualifiedName =
- ASTHelpers.enclosingClass(methodSymbol) + "." + methodSymbol.getSimpleName().toString();
- if (!qualifiedName.equals(config.getCastToNonNullMethod())) {
- throw new RuntimeException("suggestTree should point to the castToNonNull invocation.");
- }
+ Tree suggestTree, Description.Builder builder, VisitorState state) {
+ // Note: Here suggestTree refers to the argument being cast. We need to find the
+ // castToNonNull(...) invocation to be replaced by it. Fortunately, state.getPath()
+ // should be currently pointing at said call.
+ Tree currTree = state.getPath().getLeaf();
+ Preconditions.checkArgument(
+ currTree.getKind() == Tree.Kind.METHOD_INVOCATION,
+ String.format("Expected castToNonNull invocation expression, found:\n%s", currTree));
+ final MethodInvocationTree invTree = (MethodInvocationTree) currTree;
+ Preconditions.checkArgument(
+ invTree.getArguments().contains(suggestTree),
+ String.format(
+ "Method invocation tree %s does not contain the expression %s as an argument being cast",
+ invTree, suggestTree));
// Remove the call to castToNonNull:
final SuggestedFix fix =
- SuggestedFix.builder()
- .replace(suggestTree, invTree.getArguments().get(0).toString())
- .build();
+ SuggestedFix.builder().replace(invTree, state.getSourceForNode(suggestTree)).build();
return builder.addFix(fix);
}
+ /**
+ * Reports initialization errors where a constructor fails to guarantee non-null fields are
+ * initialized along all paths at exit points.
+ *
+ * @param methodSymbol Constructor symbol.
+ * @param message Error message.
+ * @param state The VisitorState object.
+ * @param descriptionBuilder the description builder for the error.
+ * @param nonNullFields list of @Nonnull fields that are not guaranteed to be initialized along
+ * all paths at exit points of the constructor.
+ */
void reportInitializerError(
Symbol.MethodSymbol methodSymbol,
String message,
VisitorState state,
- Description.Builder descriptionBuilder) {
+ Description.Builder descriptionBuilder,
+ ImmutableList<Symbol> nonNullFields) {
// Check needed here, despite check in hasPathSuppression because initialization
// checking happens at the class-level (meaning state.getPath() might not include the
// method itself).
@@ -323,9 +419,16 @@ public class ErrorBuilder {
return;
}
Tree methodTree = getTreesInstance(state).getTree(methodSymbol);
+ ErrorMessage errorMessage = new ErrorMessage(METHOD_NO_INIT, message);
state.reportMatch(
- createErrorDescription(
- new ErrorMessage(METHOD_NO_INIT, message), methodTree, descriptionBuilder, state));
+ createErrorDescription(errorMessage, methodTree, descriptionBuilder, state, null));
+ if (config.serializationIsActive()) {
+ // For now, we serialize each fix suggestion separately and measure their effectiveness
+ // separately
+ nonNullFields.forEach(
+ symbol ->
+ SerializationService.serializeFixSuggestion(config, state, symbol, errorMessage));
+ }
}
boolean symbolHasSuppressWarningsAnnotation(Symbol symbol, String suppression) {
@@ -346,10 +449,9 @@ public class ErrorBuilder {
if (symbol instanceof Symbol.ClassSymbol) {
ImmutableSet<String> excludedClassAnnotations = config.getExcludedClassAnnotations();
return ((Symbol.ClassSymbol) symbol)
- .getAnnotationMirrors()
- .stream()
- .map(anno -> anno.getAnnotationType().toString())
- .anyMatch(excludedClassAnnotations::contains);
+ .getAnnotationMirrors().stream()
+ .map(anno -> anno.getAnnotationType().toString())
+ .anyMatch(excludedClassAnnotations::contains);
}
return false;
}
@@ -422,21 +524,23 @@ public class ErrorBuilder {
fieldName = flatName.substring(index) + "." + fieldName;
}
- if (symbol.isStatic()) {
+ if (isStatic(symbol)) {
state.reportMatch(
createErrorDescription(
new ErrorMessage(
FIELD_NO_INIT, "@NonNull static field " + fieldName + " not initialized"),
tree,
builder,
- state));
+ state,
+ symbol));
} else {
state.reportMatch(
createErrorDescription(
new ErrorMessage(FIELD_NO_INIT, "@NonNull field " + fieldName + " not initialized"),
tree,
builder,
- state));
+ state,
+ symbol));
}
}
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java b/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java
index bbcde7d..186b386 100644
--- a/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java
+++ b/nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java
@@ -52,5 +52,19 @@ public class ErrorMessage {
PRECONDITION_NOT_SATISFIED,
WRONG_OVERRIDE_POSTCONDITION,
WRONG_OVERRIDE_PRECONDITION,
+ TYPE_PARAMETER_CANNOT_BE_NULLABLE,
+ ASSIGN_GENERIC_NULLABLE,
+ RETURN_NULLABLE_GENERIC,
+ PASS_NULLABLE_GENERIC,
+ WRONG_OVERRIDE_RETURN_GENERIC,
+ WRONG_OVERRIDE_PARAM_GENERIC,
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public MessageTypes getMessageType() {
+ return messageType;
}
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java b/nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java
index 075743e..9a189fe 100644
--- a/nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java
+++ b/nullaway/src/main/java/com/uber/nullaway/ErrorProneCLIFlagsConfig.java
@@ -24,6 +24,8 @@ package com.uber.nullaway;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.ErrorProneFlags;
+import com.uber.nullaway.fixserialization.FixSerializationConfig;
+import com.uber.nullaway.fixserialization.adapters.SerializationAdapter;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Optional;
@@ -47,8 +49,12 @@ final class ErrorProneCLIFlagsConfig extends AbstractConfig {
static final String FL_CLASS_ANNOTATIONS_TO_EXCLUDE =
EP_FL_NAMESPACE + ":ExcludedClassAnnotations";
static final String FL_SUGGEST_SUPPRESSIONS = EP_FL_NAMESPACE + ":SuggestSuppressions";
+
+ static final String FL_CLASS_ANNOTATIONS_GENERATED =
+ EP_FL_NAMESPACE + ":CustomGeneratedCodeAnnotations";
static final String FL_GENERATED_UNANNOTATED = EP_FL_NAMESPACE + ":TreatGeneratedAsUnannotated";
static final String FL_ACKNOWLEDGE_ANDROID_RECENT = EP_FL_NAMESPACE + ":AcknowledgeAndroidRecent";
+ static final String FL_JSPECIFY_MODE = EP_FL_NAMESPACE + ":JSpecifyMode";
static final String FL_EXCLUDED_FIELD_ANNOT = EP_FL_NAMESPACE + ":ExcludedFieldAnnotations";
static final String FL_INITIALIZER_ANNOT = EP_FL_NAMESPACE + ":CustomInitializerAnnotations";
static final String FL_NULLABLE_ANNOT = EP_FL_NAMESPACE + ":CustomNullableAnnotations";
@@ -66,6 +72,9 @@ final class ErrorProneCLIFlagsConfig extends AbstractConfig {
static final String FL_OPTIONAL_CLASS_PATHS =
EP_FL_NAMESPACE + ":CheckOptionalEmptinessCustomClasses";
static final String FL_SUPPRESS_COMMENT = EP_FL_NAMESPACE + ":AutoFixSuppressionComment";
+
+ static final String FL_SKIP_LIBRARY_MODELS = EP_FL_NAMESPACE + ":IgnoreLibraryModelsFor";
+
/** --- JarInfer configs --- */
static final String FL_JI_ENABLED = EP_FL_NAMESPACE + ":JarInferEnabled";
@@ -75,11 +84,27 @@ final class ErrorProneCLIFlagsConfig extends AbstractConfig {
static final String FL_JI_REGEX_CODE_PATH = EP_FL_NAMESPACE + ":JarInferRegexStripCodeJar";
static final String FL_ERROR_URL = EP_FL_NAMESPACE + ":ErrorURL";
+ /** --- Serialization configs --- */
+ static final String FL_FIX_SERIALIZATION = EP_FL_NAMESPACE + ":SerializeFixMetadata";
+
+ static final String FL_FIX_SERIALIZATION_VERSION =
+ EP_FL_NAMESPACE + ":SerializeFixMetadataVersion";
+
+ static final String FL_FIX_SERIALIZATION_CONFIG_PATH =
+ EP_FL_NAMESPACE + ":FixSerializationConfigPath";
+
private static final String DELIMITER = ",";
static final ImmutableSet<String> DEFAULT_CLASS_ANNOTATIONS_TO_EXCLUDE =
ImmutableSet.of("lombok.Generated");
+ // Annotations with simple name ".Generated" need not be manually listed, and are always matched
+ // by default
+ // TODO: org.apache.avro.specific.AvroGenerated should go here, but we are skipping it for the
+ // next release to better test the effect of this feature (users can always manually configure
+ // it).
+ static final ImmutableSet<String> DEFAULT_CLASS_ANNOTATIONS_GENERATED = ImmutableSet.of();
+
static final ImmutableSet<String> DEFAULT_KNOWN_INITIALIZERS =
ImmutableSet.of(
"android.view.View.onFinishInflate",
@@ -127,7 +152,9 @@ final class ErrorProneCLIFlagsConfig extends AbstractConfig {
"javax.inject.Inject", // no explicit initialization when there is dependency injection
"com.google.errorprone.annotations.concurrent.LazyInit",
"org.checkerframework.checker.nullness.qual.MonotonicNonNull",
- "org.springframework.beans.factory.annotation.Autowired");
+ "org.springframework.beans.factory.annotation.Autowired",
+ "org.springframework.boot.test.mock.mockito.MockBean",
+ "org.springframework.boot.test.mock.mockito.SpyBean");
private static final String DEFAULT_URL = "http://t.uber.com/nullaway";
@@ -147,11 +174,15 @@ final class ErrorProneCLIFlagsConfig extends AbstractConfig {
sourceClassesToExclude = getFlagStringSet(flags, FL_CLASSES_TO_EXCLUDE);
unannotatedClasses = getFlagStringSet(flags, FL_UNANNOTATED_CLASSES);
knownInitializers =
- getKnownInitializers(
- getFlagStringSet(flags, FL_KNOWN_INITIALIZERS, DEFAULT_KNOWN_INITIALIZERS));
+ getFlagStringSet(flags, FL_KNOWN_INITIALIZERS, DEFAULT_KNOWN_INITIALIZERS).stream()
+ .map(MethodClassAndName::fromClassDotMethod)
+ .collect(ImmutableSet.toImmutableSet());
excludedClassAnnotations =
getFlagStringSet(
flags, FL_CLASS_ANNOTATIONS_TO_EXCLUDE, DEFAULT_CLASS_ANNOTATIONS_TO_EXCLUDE);
+ generatedCodeAnnotations =
+ getFlagStringSet(
+ flags, FL_CLASS_ANNOTATIONS_GENERATED, DEFAULT_CLASS_ANNOTATIONS_GENERATED);
initializerAnnotations =
getFlagStringSet(flags, FL_INITIALIZER_ANNOT, DEFAULT_INITIALIZER_ANNOT);
customNullableAnnotations = getFlagStringSet(flags, FL_NULLABLE_ANNOT, ImmutableSet.of());
@@ -168,6 +199,7 @@ final class ErrorProneCLIFlagsConfig extends AbstractConfig {
flags.getBoolean(FL_HANDLE_TEST_ASSERTION_LIBRARIES).orElse(false);
treatGeneratedAsUnannotated = flags.getBoolean(FL_GENERATED_UNANNOTATED).orElse(false);
acknowledgeAndroidRecent = flags.getBoolean(FL_ACKNOWLEDGE_ANDROID_RECENT).orElse(false);
+ jspecifyMode = flags.getBoolean(FL_JSPECIFY_MODE).orElse(false);
assertsEnabled = flags.getBoolean(FL_ASSERTS_ENABLED).orElse(false);
fieldAnnotPattern =
getPackagePattern(
@@ -183,7 +215,8 @@ final class ErrorProneCLIFlagsConfig extends AbstractConfig {
throw new IllegalStateException(
"Invalid -XepOpt:" + FL_SUPPRESS_COMMENT + " value. Comment must be single line.");
}
- /** --- JarInfer configs --- */
+ skippedLibraryModels = getFlagStringSet(flags, FL_SKIP_LIBRARY_MODELS);
+ /* --- JarInfer configs --- */
jarInferEnabled = flags.getBoolean(FL_JI_ENABLED).orElse(false);
jarInferUseReturnAnnotations = flags.getBoolean(FL_JI_USE_RETURN).orElse(false);
// The defaults of these two options translate to: remove .aar/.jar from the file name, and also
@@ -200,6 +233,37 @@ final class ErrorProneCLIFlagsConfig extends AbstractConfig {
+ FL_ACKNOWLEDGE_RESTRICTIVE
+ " is also set");
}
+ serializationActivationFlag = flags.getBoolean(FL_FIX_SERIALIZATION).orElse(false);
+ Optional<String> fixSerializationConfigPath = flags.get(FL_FIX_SERIALIZATION_CONFIG_PATH);
+ if (serializationActivationFlag && !fixSerializationConfigPath.isPresent()) {
+ throw new IllegalStateException(
+ "DO NOT report an issue to Error Prone for this crash! NullAway Fix Serialization configuration is "
+ + "incorrect. "
+ + "Must specify AutoFixer Output Directory, using the "
+ + "-XepOpt:"
+ + FL_FIX_SERIALIZATION_CONFIG_PATH
+ + " flag. If you feel you have gotten this message in error report an issue"
+ + " at https://github.com/uber/NullAway/issues.");
+ }
+ int serializationVersion =
+ flags.getInteger(FL_FIX_SERIALIZATION_VERSION).orElse(SerializationAdapter.LATEST_VERSION);
+ /*
+ * if fixSerializationActivationFlag is false, the default constructor is invoked for
+ * creating FixSerializationConfig which all features are deactivated. This lets the
+ * field be @Nonnull, allowing us to avoid null checks in various places.
+ */
+ fixSerializationConfig =
+ serializationActivationFlag && fixSerializationConfigPath.isPresent()
+ ? new FixSerializationConfig(fixSerializationConfigPath.get(), serializationVersion)
+ : new FixSerializationConfig();
+ if (serializationActivationFlag && isSuggestSuppressions) {
+ throw new IllegalStateException(
+ "In order to activate Fix Serialization mode ("
+ + FL_FIX_SERIALIZATION
+ + "), Suggest Suppressions mode must be deactivated ("
+ + FL_SUGGEST_SUPPRESSIONS
+ + ")");
+ }
}
private static ImmutableSet<String> getFlagStringSet(ErrorProneFlags flags, String flagName) {
diff --git a/nullaway/src/main/java/com/uber/nullaway/GenericsChecks.java b/nullaway/src/main/java/com/uber/nullaway/GenericsChecks.java
new file mode 100644
index 0000000..af79f42
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/GenericsChecks.java
@@ -0,0 +1,1060 @@
+package com.uber.nullaway;
+
+import static com.google.common.base.Verify.verify;
+import static com.uber.nullaway.NullabilityUtil.castToNonNull;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.base.Preconditions;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.suppliers.Supplier;
+import com.google.errorprone.suppliers.Suppliers;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.AnnotatedTypeTree;
+import com.sun.source.tree.AnnotationTree;
+import com.sun.source.tree.ArrayTypeTree;
+import com.sun.source.tree.AssignmentTree;
+import com.sun.source.tree.ConditionalExpressionTree;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.MemberSelectTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.NewClassTree;
+import com.sun.source.tree.ParameterizedTypeTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.VariableTree;
+import com.sun.source.util.SimpleTreeVisitor;
+import com.sun.source.util.TreePath;
+import com.sun.tools.javac.code.Attribute;
+import com.sun.tools.javac.code.BoundKind;
+import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.code.Type;
+import com.sun.tools.javac.code.TypeMetadata;
+import com.sun.tools.javac.code.Types;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
+import javax.lang.model.type.ExecutableType;
+
+/** Methods for performing checks related to generic types and nullability. */
+public final class GenericsChecks {
+
+ private static final String NULLABLE_NAME = "org.jspecify.annotations.Nullable";
+
+ private static final Supplier<Type> NULLABLE_TYPE_SUPPLIER =
+ Suppliers.typeFromString(NULLABLE_NAME);
+
+ /** Do not instantiate; all methods should be static */
+ private GenericsChecks() {}
+
+ /**
+ * Checks that for an instantiated generic type, {@code @Nullable} types are only used for type
+ * variables that have a {@code @Nullable} upper bound.
+ *
+ * @param tree the tree representing the instantiated type
+ * @param state visitor state
+ * @param analysis the analysis object
+ * @param config the analysis config
+ */
+ public static void checkInstantiationForParameterizedTypedTree(
+ ParameterizedTypeTree tree, VisitorState state, NullAway analysis, Config config) {
+ if (!config.isJSpecifyMode()) {
+ return;
+ }
+ List<? extends Tree> typeArguments = tree.getTypeArguments();
+ if (typeArguments.size() == 0) {
+ return;
+ }
+ Map<Integer, Tree> nullableTypeArguments = new HashMap<>();
+ for (int i = 0; i < typeArguments.size(); i++) {
+ Tree curTypeArg = typeArguments.get(i);
+ if (curTypeArg instanceof AnnotatedTypeTree) {
+ AnnotatedTypeTree annotatedType = (AnnotatedTypeTree) curTypeArg;
+ for (AnnotationTree annotation : annotatedType.getAnnotations()) {
+ Type annotationType = ASTHelpers.getType(annotation);
+ if (annotationType != null
+ && Nullness.isNullableAnnotation(annotationType.toString(), config)) {
+ nullableTypeArguments.put(i, curTypeArg);
+ break;
+ }
+ }
+ }
+ }
+ // base type that is being instantiated
+ Type baseType = ASTHelpers.getType(tree);
+ if (baseType == null) {
+ return;
+ }
+ com.sun.tools.javac.util.List<Type> baseTypeArgs = baseType.tsym.type.getTypeArguments();
+ for (int i = 0; i < baseTypeArgs.size(); i++) {
+ if (nullableTypeArguments.containsKey(i)) {
+
+ Type typeVariable = baseTypeArgs.get(i);
+ Type upperBound = typeVariable.getUpperBound();
+ com.sun.tools.javac.util.List<Attribute.TypeCompound> annotationMirrors =
+ upperBound.getAnnotationMirrors();
+ boolean hasNullableAnnotation =
+ Nullness.hasNullableAnnotation(annotationMirrors.stream(), config);
+ // if base type argument does not have @Nullable annotation then the instantiation is
+ // invalid
+ if (!hasNullableAnnotation) {
+ reportInvalidInstantiationError(
+ nullableTypeArguments.get(i), baseType, typeVariable, state, analysis);
+ }
+ }
+ }
+ }
+
+ private static void reportInvalidInstantiationError(
+ Tree tree, Type baseType, Type baseTypeVariable, VisitorState state, NullAway analysis) {
+ ErrorBuilder errorBuilder = analysis.getErrorBuilder();
+ ErrorMessage errorMessage =
+ new ErrorMessage(
+ ErrorMessage.MessageTypes.TYPE_PARAMETER_CANNOT_BE_NULLABLE,
+ String.format(
+ "Generic type parameter cannot be @Nullable, as type variable %s of type %s does not have a @Nullable upper bound",
+ baseTypeVariable.tsym.toString(), baseType.tsym.toString()));
+ state.reportMatch(
+ errorBuilder.createErrorDescription(
+ errorMessage, analysis.buildDescription(tree), state, null));
+ }
+
+ private static void reportInvalidAssignmentInstantiationError(
+ Tree tree, Type lhsType, Type rhsType, VisitorState state, NullAway analysis) {
+ ErrorBuilder errorBuilder = analysis.getErrorBuilder();
+ ErrorMessage errorMessage =
+ new ErrorMessage(
+ ErrorMessage.MessageTypes.ASSIGN_GENERIC_NULLABLE,
+ String.format(
+ "Cannot assign from type "
+ + prettyTypeForError(rhsType, state)
+ + " to type "
+ + prettyTypeForError(lhsType, state)
+ + " due to mismatched nullability of type parameters"));
+ state.reportMatch(
+ errorBuilder.createErrorDescription(
+ errorMessage, analysis.buildDescription(tree), state, null));
+ }
+
+ private static void reportInvalidReturnTypeError(
+ Tree tree, Type methodType, Type returnType, VisitorState state, NullAway analysis) {
+ ErrorBuilder errorBuilder = analysis.getErrorBuilder();
+ ErrorMessage errorMessage =
+ new ErrorMessage(
+ ErrorMessage.MessageTypes.RETURN_NULLABLE_GENERIC,
+ String.format(
+ "Cannot return expression of type "
+ + prettyTypeForError(returnType, state)
+ + " from method with return type "
+ + prettyTypeForError(methodType, state)
+ + " due to mismatched nullability of type parameters"));
+ state.reportMatch(
+ errorBuilder.createErrorDescription(
+ errorMessage, analysis.buildDescription(tree), state, null));
+ }
+
+ private static void reportMismatchedTypeForTernaryOperator(
+ Tree tree, Type expressionType, Type subPartType, VisitorState state, NullAway analysis) {
+ ErrorBuilder errorBuilder = analysis.getErrorBuilder();
+ ErrorMessage errorMessage =
+ new ErrorMessage(
+ ErrorMessage.MessageTypes.ASSIGN_GENERIC_NULLABLE,
+ String.format(
+ "Conditional expression must have type "
+ + prettyTypeForError(expressionType, state)
+ + " but the sub-expression has type "
+ + prettyTypeForError(subPartType, state)
+ + ", which has mismatched nullability of type parameters"));
+ state.reportMatch(
+ errorBuilder.createErrorDescription(
+ errorMessage, analysis.buildDescription(tree), state, null));
+ }
+
+ private static void reportInvalidParametersNullabilityError(
+ Type formalParameterType,
+ Type actualParameterType,
+ ExpressionTree paramExpression,
+ VisitorState state,
+ NullAway analysis) {
+ ErrorBuilder errorBuilder = analysis.getErrorBuilder();
+ ErrorMessage errorMessage =
+ new ErrorMessage(
+ ErrorMessage.MessageTypes.PASS_NULLABLE_GENERIC,
+ "Cannot pass parameter of type "
+ + prettyTypeForError(actualParameterType, state)
+ + ", as formal parameter has type "
+ + prettyTypeForError(formalParameterType, state)
+ + ", which has mismatched type parameter nullability");
+ state.reportMatch(
+ errorBuilder.createErrorDescription(
+ errorMessage, analysis.buildDescription(paramExpression), state, null));
+ }
+
+ private static void reportInvalidOverridingMethodReturnTypeError(
+ Tree methodTree,
+ Type overriddenMethodReturnType,
+ Type overridingMethodReturnType,
+ NullAway analysis,
+ VisitorState state) {
+ ErrorBuilder errorBuilder = analysis.getErrorBuilder();
+ ErrorMessage errorMessage =
+ new ErrorMessage(
+ ErrorMessage.MessageTypes.WRONG_OVERRIDE_RETURN_GENERIC,
+ "Method returns "
+ + prettyTypeForError(overridingMethodReturnType, state)
+ + ", but overridden method returns "
+ + prettyTypeForError(overriddenMethodReturnType, state)
+ + ", which has mismatched type parameter nullability");
+ state.reportMatch(
+ errorBuilder.createErrorDescription(
+ errorMessage, analysis.buildDescription(methodTree), state, null));
+ }
+
+ private static void reportInvalidOverridingMethodParamTypeError(
+ Tree formalParameterTree,
+ Type typeParameterType,
+ Type methodParamType,
+ NullAway analysis,
+ VisitorState state) {
+ ErrorBuilder errorBuilder = analysis.getErrorBuilder();
+ ErrorMessage errorMessage =
+ new ErrorMessage(
+ ErrorMessage.MessageTypes.WRONG_OVERRIDE_PARAM_GENERIC,
+ "Parameter has type "
+ + prettyTypeForError(methodParamType, state)
+ + ", but overridden method has parameter type "
+ + prettyTypeForError(typeParameterType, state)
+ + ", which has mismatched type parameter nullability");
+ state.reportMatch(
+ errorBuilder.createErrorDescription(
+ errorMessage, analysis.buildDescription(formalParameterTree), state, null));
+ }
+
+ /**
+ * This method returns the type of the given tree, including any type use annotations.
+ *
+ * <p>This method is required because in some cases, the type returned by {@link
+ * com.google.errorprone.util.ASTHelpers#getType(Tree)} fails to preserve type use annotations,
+ * particularly when dealing with {@link com.sun.source.tree.NewClassTree} (e.g., {@code new
+ * Foo<@Nullable A>}).
+ *
+ * @param tree A tree for which we need the type with preserved annotations.
+ * @param state the visitor state
+ * @return Type of the tree with preserved annotations.
+ */
+ @Nullable
+ private static Type getTreeType(Tree tree, VisitorState state) {
+ if (tree instanceof NewClassTree
+ && ((NewClassTree) tree).getIdentifier() instanceof ParameterizedTypeTree) {
+ ParameterizedTypeTree paramTypedTree =
+ (ParameterizedTypeTree) ((NewClassTree) tree).getIdentifier();
+ if (paramTypedTree.getTypeArguments().isEmpty()) {
+ // diamond operator, which we do not yet support; for now, return null
+ // TODO: support diamond operators
+ return null;
+ }
+ return typeWithPreservedAnnotations(paramTypedTree, state);
+ } else {
+ Type result = ASTHelpers.getType(tree);
+ if (result != null && result.isRaw()) {
+ // bail out of any checking involving raw types for now
+ return null;
+ }
+ return result;
+ }
+ }
+
+ /**
+ * For a tree representing an assignment, ensures that from the perspective of type parameter
+ * nullability, the type of the right-hand side is assignable to (a subtype of) the type of the
+ * left-hand side. This check ensures that for every parameterized type nested in each of the
+ * types, the type parameters have identical nullability.
+ *
+ * @param tree the tree to check, which must be either an {@link AssignmentTree} or a {@link
+ * VariableTree}
+ * @param analysis the analysis object
+ * @param state the visitor state
+ */
+ public static void checkTypeParameterNullnessForAssignability(
+ Tree tree, NullAway analysis, VisitorState state) {
+ if (!analysis.getConfig().isJSpecifyMode()) {
+ return;
+ }
+ Tree lhsTree;
+ Tree rhsTree;
+ if (tree instanceof VariableTree) {
+ VariableTree varTree = (VariableTree) tree;
+ lhsTree = varTree.getType();
+ rhsTree = varTree.getInitializer();
+ } else {
+ AssignmentTree assignmentTree = (AssignmentTree) tree;
+ lhsTree = assignmentTree.getVariable();
+ rhsTree = assignmentTree.getExpression();
+ }
+ // rhsTree can be null for a VariableTree. Also, we don't need to do a check
+ // if rhsTree is the null literal
+ if (rhsTree == null || rhsTree.getKind().equals(Tree.Kind.NULL_LITERAL)) {
+ return;
+ }
+ Type lhsType = getTreeType(lhsTree, state);
+ Type rhsType = getTreeType(rhsTree, state);
+
+ if (lhsType instanceof Type.ClassType && rhsType instanceof Type.ClassType) {
+ boolean isAssignmentValid =
+ compareNullabilityAnnotations((Type.ClassType) lhsType, (Type.ClassType) rhsType, state);
+ if (!isAssignmentValid) {
+ reportInvalidAssignmentInstantiationError(tree, lhsType, rhsType, state, analysis);
+ }
+ }
+ }
+
+ /**
+ * Checks that the nullability of type parameters for a returned expression matches that of the
+ * type parameters of the enclosing method's return type.
+ *
+ * @param retExpr the returned expression
+ * @param methodSymbol symbol for enclosing method
+ * @param analysis the analysis object
+ * @param state the visitor state
+ */
+ public static void checkTypeParameterNullnessForFunctionReturnType(
+ ExpressionTree retExpr,
+ Symbol.MethodSymbol methodSymbol,
+ NullAway analysis,
+ VisitorState state) {
+ if (!analysis.getConfig().isJSpecifyMode()) {
+ return;
+ }
+
+ Type formalReturnType = methodSymbol.getReturnType();
+ // check nullability of parameters only for generics
+ if (formalReturnType.getTypeArguments().isEmpty()) {
+ return;
+ }
+ Type returnExpressionType = getTreeType(retExpr, state);
+ if (formalReturnType instanceof Type.ClassType
+ && returnExpressionType instanceof Type.ClassType) {
+ boolean isReturnTypeValid =
+ compareNullabilityAnnotations(
+ (Type.ClassType) formalReturnType, (Type.ClassType) returnExpressionType, state);
+ if (!isReturnTypeValid) {
+ reportInvalidReturnTypeError(
+ retExpr, formalReturnType, returnExpressionType, state, analysis);
+ }
+ }
+ }
+
+ /**
+ * Compare two types from an assignment for identical type parameter nullability, recursively
+ * checking nested generic types. See <a
+ * href="https://jspecify.dev/docs/spec/#nullness-delegating-subtyping">the JSpecify
+ * specification</a> and <a
+ * href="https://docs.oracle.com/javase/specs/jls/se14/html/jls-4.html#jls-4.10.2">the JLS
+ * subtyping rules for class and interface types</a>.
+ *
+ * @param lhsType type for the lhs of the assignment
+ * @param rhsType type for the rhs of the assignment
+ * @param state the visitor state
+ */
+ private static boolean compareNullabilityAnnotations(
+ Type lhsType, Type rhsType, VisitorState state) {
+ // it is fair to assume rhyType should be the same as lhsType as the Java compiler has passed
+ // before NullAway.
+ return lhsType.accept(new CompareNullabilityVisitor(state), rhsType);
+ }
+
+ /**
+ * For the Parameterized typed trees, ASTHelpers.getType(tree) does not return a Type with
+ * preserved annotations. This method takes a Parameterized typed tree as an input and returns the
+ * Type of the tree with the annotations.
+ *
+ * @param tree A parameterized typed tree for which we need class type with preserved annotations.
+ * @param state the visitor state
+ * @return A Type with preserved annotations.
+ */
+ private static Type.ClassType typeWithPreservedAnnotations(
+ ParameterizedTypeTree tree, VisitorState state) {
+ return (Type.ClassType) tree.accept(new PreservedAnnotationTreeVisitor(state), null);
+ }
+
+ /**
+ * For a conditional expression <em>c</em>, check whether the type parameter nullability for each
+ * sub-expression of <em>c</em> matches the type parameter nullability of <em>c</em> itself.
+ *
+ * <p>Note that the type parameter nullability for <em>c</em> is computed by javac and reflects
+ * what is required of the surrounding context (an assignment, parameter pass, etc.). It is
+ * possible that both sub-expressions of <em>c</em> will have identical type parameter
+ * nullability, but will still not match the type parameter nullability of <em>c</em> itself, due
+ * to requirements from the surrounding context. In such a case, our error messages may be
+ * somewhat confusing; we may want to improve this in the future.
+ *
+ * @param tree A conditional expression tree to check
+ * @param analysis the analysis object
+ * @param state the visitor state
+ */
+ public static void checkTypeParameterNullnessForConditionalExpression(
+ ConditionalExpressionTree tree, NullAway analysis, VisitorState state) {
+ if (!analysis.getConfig().isJSpecifyMode()) {
+ return;
+ }
+
+ Tree truePartTree = tree.getTrueExpression();
+ Tree falsePartTree = tree.getFalseExpression();
+
+ Type condExprType = getTreeType(tree, state);
+ Type truePartType = getTreeType(truePartTree, state);
+ Type falsePartType = getTreeType(falsePartTree, state);
+ // The condExpr type should be the least-upper bound of the true and false part types. To check
+ // the nullability annotations, we check that the true and false parts are assignable to the
+ // type of the whole expression
+ if (condExprType instanceof Type.ClassType) {
+ if (truePartType instanceof Type.ClassType) {
+ if (!compareNullabilityAnnotations(
+ (Type.ClassType) condExprType, (Type.ClassType) truePartType, state)) {
+ reportMismatchedTypeForTernaryOperator(
+ truePartTree, condExprType, truePartType, state, analysis);
+ }
+ }
+ if (falsePartType instanceof Type.ClassType) {
+ if (!compareNullabilityAnnotations(
+ (Type.ClassType) condExprType, (Type.ClassType) falsePartType, state)) {
+ reportMismatchedTypeForTernaryOperator(
+ falsePartTree, condExprType, falsePartType, state, analysis);
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks that for each parameter p at a call, the type parameter nullability for p's type matches
+ * that of the corresponding formal parameter. If a mismatch is found, report an error.
+ *
+ * @param formalParams the formal parameters
+ * @param actualParams the actual parameters
+ * @param isVarArgs true if the call is to a varargs method
+ * @param analysis the analysis object
+ * @param state the visitor state
+ */
+ public static void compareGenericTypeParameterNullabilityForCall(
+ List<Symbol.VarSymbol> formalParams,
+ List<? extends ExpressionTree> actualParams,
+ boolean isVarArgs,
+ NullAway analysis,
+ VisitorState state) {
+ if (!analysis.getConfig().isJSpecifyMode()) {
+ return;
+ }
+ int n = formalParams.size();
+ if (isVarArgs) {
+ // If the last argument is var args, don't check it now, it will be checked against
+ // all remaining actual arguments in the next loop.
+ n = n - 1;
+ }
+ for (int i = 0; i < n; i++) {
+ Type formalParameter = formalParams.get(i).type;
+ if (!formalParameter.getTypeArguments().isEmpty()) {
+ Type actualParameter = getTreeType(actualParams.get(i), state);
+ if (formalParameter instanceof Type.ClassType
+ && actualParameter instanceof Type.ClassType) {
+ if (!compareNullabilityAnnotations(
+ (Type.ClassType) formalParameter, (Type.ClassType) actualParameter, state)) {
+ reportInvalidParametersNullabilityError(
+ formalParameter, actualParameter, actualParams.get(i), state, analysis);
+ }
+ }
+ }
+ }
+ if (isVarArgs && !formalParams.isEmpty()) {
+ Type.ArrayType varargsArrayType =
+ (Type.ArrayType) formalParams.get(formalParams.size() - 1).type;
+ Type varargsElementType = varargsArrayType.elemtype;
+ if (varargsElementType.getTypeArguments().size() > 0) {
+ for (int i = formalParams.size() - 1; i < actualParams.size(); i++) {
+ Type actualParameter = getTreeType(actualParams.get(i), state);
+ if (varargsElementType instanceof Type.ClassType
+ && actualParameter instanceof Type.ClassType) {
+ if (!compareNullabilityAnnotations(
+ (Type.ClassType) varargsElementType, (Type.ClassType) actualParameter, state)) {
+ reportInvalidParametersNullabilityError(
+ varargsElementType, actualParameter, actualParams.get(i), state, analysis);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Visitor that is called from compareNullabilityAnnotations which recursively compares the
+ * Nullability annotations for the nested generic type arguments. Compares the Type it is called
+ * upon, i.e. the LHS type and the Type passed as an argument, i.e. The RHS type.
+ */
+ public static class CompareNullabilityVisitor extends Types.DefaultTypeVisitor<Boolean, Type> {
+ private final VisitorState state;
+
+ CompareNullabilityVisitor(VisitorState state) {
+ this.state = state;
+ }
+
+ @Override
+ public Boolean visitClassType(Type.ClassType lhsType, Type rhsType) {
+ Types types = state.getTypes();
+ // The base type of rhsType may be a subtype of lhsType's base type. In such cases, we must
+ // compare lhsType against the supertype of rhsType with a matching base type.
+ rhsType = (Type.ClassType) types.asSuper(rhsType, lhsType.tsym);
+ // This is impossible, considering the fact that standard Java subtyping succeeds before
+ // running NullAway
+ if (rhsType == null) {
+ throw new RuntimeException("Did not find supertype of " + rhsType + " matching " + lhsType);
+ }
+ List<Type> lhsTypeArguments = lhsType.getTypeArguments();
+ List<Type> rhsTypeArguments = rhsType.getTypeArguments();
+ // This is impossible, considering the fact that standard Java subtyping succeeds before
+ // running NullAway
+ if (lhsTypeArguments.size() != rhsTypeArguments.size()) {
+ throw new RuntimeException(
+ "Number of types arguments in " + rhsType + " does not match " + lhsType);
+ }
+ for (int i = 0; i < lhsTypeArguments.size(); i++) {
+ Type lhsTypeArgument = lhsTypeArguments.get(i);
+ Type rhsTypeArgument = rhsTypeArguments.get(i);
+ boolean isLHSNullableAnnotated = false;
+ List<Attribute.TypeCompound> lhsAnnotations = lhsTypeArgument.getAnnotationMirrors();
+ // To ensure that we are checking only jspecify nullable annotations
+ for (Attribute.TypeCompound annotation : lhsAnnotations) {
+ if (annotation.getAnnotationType().toString().equals(NULLABLE_NAME)) {
+ isLHSNullableAnnotated = true;
+ break;
+ }
+ }
+ boolean isRHSNullableAnnotated = false;
+ List<Attribute.TypeCompound> rhsAnnotations = rhsTypeArgument.getAnnotationMirrors();
+ // To ensure that we are checking only jspecify nullable annotations
+ for (Attribute.TypeCompound annotation : rhsAnnotations) {
+ if (annotation.getAnnotationType().toString().equals(NULLABLE_NAME)) {
+ isRHSNullableAnnotated = true;
+ break;
+ }
+ }
+ if (isLHSNullableAnnotated != isRHSNullableAnnotated) {
+ return false;
+ }
+ // nested generics
+ if (!lhsTypeArgument.accept(this, rhsTypeArgument)) {
+ return false;
+ }
+ }
+ // If there is an enclosing type (for non-static inner classes), its type argument nullability
+ // should also match. When there is no enclosing type, getEnclosingType() returns a NoType
+ // object, which gets handled by the fallback visitType() method
+ return lhsType.getEnclosingType().accept(this, rhsType.getEnclosingType());
+ }
+
+ @Override
+ public Boolean visitArrayType(Type.ArrayType lhsType, Type rhsType) {
+ Type.ArrayType arrRhsType = (Type.ArrayType) rhsType;
+ return lhsType.getComponentType().accept(this, arrRhsType.getComponentType());
+ }
+
+ @Override
+ public Boolean visitType(Type t, Type type) {
+ return true;
+ }
+ }
+
+ /**
+ * Visitor For getting the preserved Annotation Type for the nested generic type arguments within
+ * the ParameterizedTypeTree originally passed to TypeWithPreservedAnnotations method, since these
+ * nested arguments may not always be ParameterizedTypeTrees and may be of different types for
+ * e.g. ArrayTypeTree.
+ */
+ public static class PreservedAnnotationTreeVisitor extends SimpleTreeVisitor<Type, Void> {
+
+ private final VisitorState state;
+
+ PreservedAnnotationTreeVisitor(VisitorState state) {
+ this.state = state;
+ }
+
+ @Override
+ public Type visitArrayType(ArrayTypeTree tree, Void p) {
+ Type elemType = tree.getType().accept(this, null);
+ return new Type.ArrayType(elemType, castToNonNull(ASTHelpers.getType(tree)).tsym);
+ }
+
+ @Override
+ public Type visitParameterizedType(ParameterizedTypeTree tree, Void p) {
+ Type.ClassType type = (Type.ClassType) ASTHelpers.getType(tree);
+ Preconditions.checkNotNull(type);
+ Type nullableType = NULLABLE_TYPE_SUPPLIER.get(state);
+ List<? extends Tree> typeArguments = tree.getTypeArguments();
+ List<Type> newTypeArgs = new ArrayList<>();
+ for (int i = 0; i < typeArguments.size(); i++) {
+ AnnotatedTypeTree annotatedType = null;
+ Tree curTypeArg = typeArguments.get(i);
+ // If the type argument has an annotation, it will either be an AnnotatedTypeTree, or a
+ // ParameterizedTypeTree in the case of a nested generic type
+ if (curTypeArg instanceof AnnotatedTypeTree) {
+ annotatedType = (AnnotatedTypeTree) curTypeArg;
+ } else if (curTypeArg instanceof ParameterizedTypeTree
+ && ((ParameterizedTypeTree) curTypeArg).getType() instanceof AnnotatedTypeTree) {
+ annotatedType = (AnnotatedTypeTree) ((ParameterizedTypeTree) curTypeArg).getType();
+ }
+ List<? extends AnnotationTree> annotations =
+ annotatedType != null ? annotatedType.getAnnotations() : Collections.emptyList();
+ boolean hasNullableAnnotation = false;
+ for (AnnotationTree annotation : annotations) {
+ if (ASTHelpers.isSameType(
+ nullableType, ASTHelpers.getType(annotation.getAnnotationType()), state)) {
+ hasNullableAnnotation = true;
+ break;
+ }
+ }
+ // construct a TypeMetadata object containing a nullability annotation if needed
+ com.sun.tools.javac.util.List<Attribute.TypeCompound> nullableAnnotationCompound =
+ hasNullableAnnotation
+ ? com.sun.tools.javac.util.List.from(
+ Collections.singletonList(
+ new Attribute.TypeCompound(
+ nullableType, com.sun.tools.javac.util.List.nil(), null)))
+ : com.sun.tools.javac.util.List.nil();
+ TypeMetadata typeMetadata =
+ new TypeMetadata(new TypeMetadata.Annotations(nullableAnnotationCompound));
+ Type currentTypeArgType = curTypeArg.accept(this, null);
+ Type newTypeArgType = currentTypeArgType.cloneWithMetadata(typeMetadata);
+ newTypeArgs.add(newTypeArgType);
+ }
+ Type.ClassType finalType =
+ new Type.ClassType(
+ type.getEnclosingType(), com.sun.tools.javac.util.List.from(newTypeArgs), type.tsym);
+ return finalType;
+ }
+
+ /** By default, just use the type computed by javac */
+ @Override
+ protected Type defaultAction(Tree node, Void unused) {
+ return castToNonNull(ASTHelpers.getType(node));
+ }
+ }
+
+ /**
+ * Checks that type parameter nullability is consistent between an overriding method and the
+ * corresponding overridden method.
+ *
+ * @param tree A method tree to check
+ * @param overridingMethod A symbol of the overriding method
+ * @param overriddenMethod A symbol of the overridden method
+ * @param analysis the analysis object
+ * @param state the visitor state
+ */
+ public static void checkTypeParameterNullnessForMethodOverriding(
+ MethodTree tree,
+ Symbol.MethodSymbol overridingMethod,
+ Symbol.MethodSymbol overriddenMethod,
+ NullAway analysis,
+ VisitorState state) {
+ if (!analysis.getConfig().isJSpecifyMode()) {
+ return;
+ }
+ // Obtain type parameters for the overridden method within the context of the overriding
+ // method's class
+ Type methodWithTypeParams =
+ state.getTypes().memberType(overridingMethod.owner.type, overriddenMethod);
+
+ checkTypeParameterNullnessForOverridingMethodReturnType(
+ tree, methodWithTypeParams, analysis, state);
+ checkTypeParameterNullnessForOverridingMethodParameterType(
+ tree, methodWithTypeParams, analysis, state);
+ }
+
+ /**
+ * Computes the nullability of the return type of some generic method when seen as a member of
+ * some class {@code C}, based on type parameter nullability within {@code C}.
+ *
+ * <p>Consider the following example:
+ *
+ * <pre>
+ * interface Fn<P extends @Nullable Object, R extends @Nullable Object> {
+ * R apply(P p);
+ * }
+ * class C implements Fn<String, @Nullable String> {
+ * public @Nullable String apply(String p) {
+ * return null;
+ * }
+ * }
+ * </pre>
+ *
+ * Within the context of class {@code C}, the method {@code Fn.apply} has a return type of
+ * {@code @Nullable String}, since {@code @Nullable String} is passed as the type parameter for
+ * {@code R}. Hence, it is valid for overriding method {@code C.apply} to return {@code @Nullable
+ * String}.
+ *
+ * @param method the generic method
+ * @param enclosingSymbol the enclosing class in which we want to know {@code method}'s return
+ * type nullability
+ * @param state Visitor state
+ * @param config The analysis config
+ * @return nullability of the return type of {@code method} in the context of {@code
+ * enclosingType}
+ */
+ public static Nullness getGenericMethodReturnTypeNullness(
+ Symbol.MethodSymbol method, Symbol enclosingSymbol, VisitorState state, Config config) {
+ Type enclosingType = getTypeForSymbol(enclosingSymbol, state);
+ return getGenericMethodReturnTypeNullness(method, enclosingType, state, config);
+ }
+
+ /**
+ * Get the type for the symbol, accounting for anonymous classes
+ *
+ * @param symbol the symbol
+ * @param state the visitor state
+ * @return the type for {@code symbol}
+ */
+ @Nullable
+ private static Type getTypeForSymbol(Symbol symbol, VisitorState state) {
+ if (symbol.isAnonymous()) {
+ // For anonymous classes, symbol.type does not contain annotations on generic type parameters.
+ // So, we get a correct type from the enclosing NewClassTree.
+ TreePath path = state.getPath();
+ NewClassTree newClassTree = ASTHelpers.findEnclosingNode(path, NewClassTree.class);
+ if (newClassTree == null) {
+ throw new RuntimeException(
+ "method should be inside a NewClassTree " + state.getSourceForNode(path.getLeaf()));
+ }
+ Type typeFromTree = getTreeType(newClassTree, state);
+ if (typeFromTree != null) {
+ verify(state.getTypes().isAssignable(symbol.type, typeFromTree));
+ }
+ return typeFromTree;
+ } else {
+ return symbol.type;
+ }
+ }
+
+ static Nullness getGenericMethodReturnTypeNullness(
+ Symbol.MethodSymbol method, @Nullable Type enclosingType, VisitorState state, Config config) {
+ if (enclosingType == null) {
+ // we have no additional information from generics, so return NONNULL (presence of a @Nullable
+ // annotation should have been handled by the caller)
+ return Nullness.NONNULL;
+ }
+ Type overriddenMethodType = state.getTypes().memberType(enclosingType, method);
+ verify(
+ overriddenMethodType instanceof ExecutableType,
+ "expected ExecutableType but instead got %s",
+ overriddenMethodType.getClass());
+ return getTypeNullness(overriddenMethodType.getReturnType(), config);
+ }
+
+ /**
+ * Computes the nullness of the return of a generic method at an invocation, in the context of the
+ * declared type of its receiver argument. If the return type is a type variable, its nullness
+ * depends on the nullability of the corresponding type parameter in the receiver's type.
+ *
+ * <p>Consider the following example:
+ *
+ * <pre>
+ * interface Fn<P extends @Nullable Object, R extends @Nullable Object> {
+ * R apply(P p);
+ * }
+ * class C implements Fn<String, @Nullable String> {
+ * public @Nullable String apply(String p) {
+ * return null;
+ * }
+ * }
+ * static void m() {
+ * Fn<String, @Nullable String> f = new C();
+ * f.apply("hello").hashCode(); // NPE
+ * }
+ * </pre>
+ *
+ * The declared type of {@code f} passes {@code Nullable String} as the type parameter for type
+ * variable {@code R}. So, the call {@code f.apply("hello")} returns {@code @Nullable} and an
+ * error should be reported.
+ *
+ * @param invokedMethodSymbol symbol for the invoked method
+ * @param tree the tree for the invocation
+ * @return Nullness of invocation's return type, or {@code NONNULL} if the call does not invoke an
+ * instance method
+ */
+ public static Nullness getGenericReturnNullnessAtInvocation(
+ Symbol.MethodSymbol invokedMethodSymbol,
+ MethodInvocationTree tree,
+ VisitorState state,
+ Config config) {
+ if (!(tree.getMethodSelect() instanceof MemberSelectTree)) {
+ return Nullness.NONNULL;
+ }
+ Type methodReceiverType =
+ castToNonNull(
+ getTreeType(((MemberSelectTree) tree.getMethodSelect()).getExpression(), state));
+ return getGenericMethodReturnTypeNullness(
+ invokedMethodSymbol, methodReceiverType, state, config);
+ }
+
+ /**
+ * Computes the nullness of a formal parameter of a generic method at an invocation, in the
+ * context of the declared type of its receiver argument. If the formal parameter's type is a type
+ * variable, its nullness depends on the nullability of the corresponding type parameter in the
+ * receiver's type.
+ *
+ * <p>Consider the following example:
+ *
+ * <pre>
+ * interface Fn<P extends @Nullable Object, R extends @Nullable Object> {
+ * R apply(P p);
+ * }
+ * class C implements Fn<@Nullable String, String> {
+ * public String apply(@Nullable String p) {
+ * return "";
+ * }
+ * }
+ * static void m() {
+ * Fn<@Nullable String, String> f = new C();
+ * f.apply(null);
+ * }
+ * </pre>
+ *
+ * The declared type of {@code f} passes {@code Nullable String} as the type parameter for type
+ * variable {@code P}. So, it is legal to pass {@code null} as a parameter to {@code f.apply}.
+ *
+ * @param paramIndex parameter index
+ * @param invokedMethodSymbol symbol for the invoked method
+ * @param tree the tree for the invocation
+ * @param state the visitor state
+ * @param config the analysis config
+ * @return Nullness of parameter at {@code paramIndex}, or {@code NONNULL} if the call does not
+ * invoke an instance method
+ */
+ public static Nullness getGenericParameterNullnessAtInvocation(
+ int paramIndex,
+ Symbol.MethodSymbol invokedMethodSymbol,
+ MethodInvocationTree tree,
+ VisitorState state,
+ Config config) {
+ if (!(tree.getMethodSelect() instanceof MemberSelectTree)) {
+ return Nullness.NONNULL;
+ }
+ Type enclosingType =
+ castToNonNull(
+ getTreeType(((MemberSelectTree) tree.getMethodSelect()).getExpression(), state));
+ return getGenericMethodParameterNullness(
+ paramIndex, invokedMethodSymbol, enclosingType, state, config);
+ }
+
+ /**
+ * Computes the nullability of a parameter type of some generic method when seen as a member of
+ * some class {@code C}, based on type parameter nullability within {@code C}.
+ *
+ * <p>Consider the following example:
+ *
+ * <pre>
+ * interface Fn<P extends @Nullable Object, R extends @Nullable Object> {
+ * R apply(P p);
+ * }
+ * class C implements Fn<@Nullable String, String> {
+ * public String apply(@Nullable String p) {
+ * return "";
+ * }
+ * }
+ * </pre>
+ *
+ * Within the context of class {@code C}, the method {@code Fn.apply} has a parameter type of
+ * {@code @Nullable String}, since {@code @Nullable String} is passed as the type parameter for
+ * {@code P}. Hence, overriding method {@code C.apply} must take a {@code @Nullable String} as a
+ * parameter.
+ *
+ * @param parameterIndex index of the parameter
+ * @param method the generic method
+ * @param enclosingSymbol the enclosing symbol in which we want to know {@code method}'s parameter
+ * type nullability
+ * @param state the visitor state
+ * @param config the config
+ * @return nullability of the relevant parameter type of {@code method} in the context of {@code
+ * enclosingSymbol}
+ */
+ public static Nullness getGenericMethodParameterNullness(
+ int parameterIndex,
+ Symbol.MethodSymbol method,
+ Symbol enclosingSymbol,
+ VisitorState state,
+ Config config) {
+ Type enclosingType = getTypeForSymbol(enclosingSymbol, state);
+ return getGenericMethodParameterNullness(parameterIndex, method, enclosingType, state, config);
+ }
+
+ /**
+ * Just like {@link #getGenericMethodParameterNullness(int, Symbol.MethodSymbol, Symbol,
+ * VisitorState, Config)}, but takes the enclosing {@code Type} rather than the enclosing {@code
+ * Symbol}.
+ *
+ * @param parameterIndex index of the parameter
+ * @param method the generic method
+ * @param enclosingType the enclosing type in which we want to know {@code method}'s parameter
+ * type nullability
+ * @param state the visitor state
+ * @param config the analysis config
+ * @return nullability of the relevant parameter type of {@code method} in the context of {@code
+ * enclosingType}
+ */
+ public static Nullness getGenericMethodParameterNullness(
+ int parameterIndex,
+ Symbol.MethodSymbol method,
+ @Nullable Type enclosingType,
+ VisitorState state,
+ Config config) {
+ if (enclosingType == null) {
+ // we have no additional information from generics, so return NONNULL (presence of a top-level
+ // @Nullable annotation is handled elsewhere)
+ return Nullness.NONNULL;
+ }
+ Type methodType = state.getTypes().memberType(enclosingType, method);
+ Type paramType = methodType.getParameterTypes().get(parameterIndex);
+ return getTypeNullness(paramType, config);
+ }
+
+ /**
+ * This method compares the type parameter annotations for overriding method parameters with
+ * corresponding type parameters for the overridden method and reports an error if they don't
+ * match
+ *
+ * @param tree tree for overriding method
+ * @param overriddenMethodType type of the overridden method
+ * @param analysis the analysis object
+ * @param state the visitor state
+ */
+ private static void checkTypeParameterNullnessForOverridingMethodParameterType(
+ MethodTree tree, Type overriddenMethodType, NullAway analysis, VisitorState state) {
+ List<? extends VariableTree> methodParameters = tree.getParameters();
+ List<Type> overriddenMethodParameterTypes = overriddenMethodType.getParameterTypes();
+ // TODO handle varargs; they are not handled for now
+ for (int i = 0; i < methodParameters.size(); i++) {
+ Type overridingMethodParameterType = ASTHelpers.getType(methodParameters.get(i));
+ Type overriddenMethodParameterType = overriddenMethodParameterTypes.get(i);
+ if (overriddenMethodParameterType instanceof Type.ClassType
+ && overridingMethodParameterType instanceof Type.ClassType) {
+ if (!compareNullabilityAnnotations(
+ (Type.ClassType) overriddenMethodParameterType,
+ (Type.ClassType) overridingMethodParameterType,
+ state)) {
+ reportInvalidOverridingMethodParamTypeError(
+ methodParameters.get(i),
+ overriddenMethodParameterType,
+ overridingMethodParameterType,
+ analysis,
+ state);
+ }
+ }
+ }
+ }
+
+ /**
+ * This method compares the type parameter annotations for an overriding method's return type with
+ * corresponding type parameters for the overridden method and reports an error if they don't
+ * match
+ *
+ * @param tree tree for overriding method
+ * @param overriddenMethodType type of the overridden method
+ * @param analysis the analysis object
+ * @param state the visitor state
+ */
+ private static void checkTypeParameterNullnessForOverridingMethodReturnType(
+ MethodTree tree, Type overriddenMethodType, NullAway analysis, VisitorState state) {
+ Type overriddenMethodReturnType = overriddenMethodType.getReturnType();
+ Type overridingMethodReturnType = ASTHelpers.getType(tree.getReturnType());
+ if (!(overriddenMethodReturnType instanceof Type.ClassType)) {
+ return;
+ }
+ Preconditions.checkArgument(overridingMethodReturnType instanceof Type.ClassType);
+ if (!compareNullabilityAnnotations(
+ (Type.ClassType) overriddenMethodReturnType,
+ (Type.ClassType) overridingMethodReturnType,
+ state)) {
+ reportInvalidOverridingMethodReturnTypeError(
+ tree, overriddenMethodReturnType, overridingMethodReturnType, analysis, state);
+ }
+ }
+
+ /**
+ * @param type A type for which we need the Nullness.
+ * @param config The analysis config
+ * @return Returns the Nullness of the type based on the Nullability annotation.
+ */
+ private static Nullness getTypeNullness(Type type, Config config) {
+ boolean hasNullableAnnotation =
+ Nullness.hasNullableAnnotation(type.getAnnotationMirrors().stream(), config);
+ if (hasNullableAnnotation) {
+ return Nullness.NULLABLE;
+ }
+ return Nullness.NONNULL;
+ }
+
+ /**
+ * Returns a pretty-printed representation of type suitable for error messages. The representation
+ * uses simple names rather than fully-qualified names, and retains all type-use annotations.
+ */
+ public static String prettyTypeForError(Type type, VisitorState state) {
+ return type.accept(new PrettyTypeVisitor(state), null);
+ }
+
+ /** This code is a modified version of code in {@link com.google.errorprone.util.Signatures} */
+ private static final class PrettyTypeVisitor extends Types.DefaultTypeVisitor<String, Void> {
+
+ private final VisitorState state;
+
+ PrettyTypeVisitor(VisitorState state) {
+ this.state = state;
+ }
+
+ @Override
+ public String visitWildcardType(Type.WildcardType t, Void unused) {
+ // NOTE: we have not tested this code yet as we do not yet support wildcard types
+ StringBuilder sb = new StringBuilder();
+ sb.append(t.kind);
+ if (t.kind != BoundKind.UNBOUND) {
+ sb.append(t.type.accept(this, null));
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public String visitClassType(Type.ClassType t, Void s) {
+ StringBuilder sb = new StringBuilder();
+ Type enclosingType = t.getEnclosingType();
+ if (!ASTHelpers.isSameType(enclosingType, Type.noType, state)) {
+ sb.append(enclosingType.accept(this, null)).append('.');
+ }
+ for (Attribute.TypeCompound compound : t.getAnnotationMirrors()) {
+ sb.append('@');
+ sb.append(compound.type.accept(this, null));
+ sb.append(' ');
+ }
+ sb.append(t.tsym.getSimpleName());
+ if (t.getTypeArguments().nonEmpty()) {
+ sb.append('<');
+ sb.append(
+ t.getTypeArguments().stream().map(a -> a.accept(this, null)).collect(joining(", ")));
+ sb.append(">");
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public String visitCapturedType(Type.CapturedType t, Void s) {
+ return t.wildcard.accept(this, null);
+ }
+
+ @Override
+ public String visitArrayType(Type.ArrayType t, Void unused) {
+ // TODO properly print cases like int @Nullable[]
+ return t.elemtype.accept(this, null) + "[]";
+ }
+
+ @Override
+ public String visitType(Type t, Void s) {
+ return t.toString();
+ }
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/LibraryModels.java b/nullaway/src/main/java/com/uber/nullaway/LibraryModels.java
index c7ac987..a0816b3 100644
--- a/nullaway/src/main/java/com/uber/nullaway/LibraryModels.java
+++ b/nullaway/src/main/java/com/uber/nullaway/LibraryModels.java
@@ -22,9 +22,11 @@
package com.uber.nullaway;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.handlers.stream.StreamTypeRecord;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -84,7 +86,9 @@ public interface LibraryModels {
* Get (method, parameter) pairs that cause the method to return <code>null</code> when passed
* <code>null</code> on that parameter.
*
- * <p>This is equivalent to annotating a method with a contract like:
+ * <p>This is equivalent to annotating a method with both a {@code @Nullable} return type
+ * <em>and</em> a {@code @Contract} annotation specifying that if the parameter is
+ * {@code @NonNull} then the return is {@code @NonNull}, e.g.:
*
* <pre><code>@Contract("!null -&gt; !null") @Nullable</code></pre>
*
@@ -108,7 +112,43 @@ public interface LibraryModels {
ImmutableSet<MethodRef> nonNullReturns();
/**
- * representation of a method as a qualified class name + a signature for the method
+ * Get (method, parameter) pairs that act as castToNonNull(...) methods.
+ *
+ * <p>Here, the parameter index determines the argument position of the reference being cast to
+ * non-null.
+ *
+ * <p>We still provide the CLI configuration `-XepOpt:NullAway:CastToNonNullMethod` as the default
+ * way to define the common case of a single-argument {@code @NonNull Object
+ * castToNonNull(@Nullable Object o)}} cast method.
+ *
+ * <p>However, in some cases, the user might wish to have a cast method that takes multiple
+ * arguments, in addition to the <code>@Nullable</code> value being cast. For these cases,
+ * providing a library model allows for more precise error reporting whenever a known non-null
+ * value is passed to such method, rendering the cast unnecessary.
+ *
+ * <p>Note that we can't auto-add castToNonNull(...) methods taking more than one argument, simply
+ * because there might be no general, automated way of synthesizing the required arguments.
+ */
+ ImmutableSetMultimap<MethodRef, Integer> castToNonNullMethods();
+
+ /**
+ * Get a list of custom stream library specifications.
+ *
+ * <p>This allows users to define filter/map/other methods for APIs which behave similarly to Java
+ * 8 streams or ReactiveX streams, so that NullAway is able to understand nullability invariants
+ * across stream API calls. See {@link com.uber.nullaway.handlers.stream.StreamModelBuilder} for
+ * details on how to construct these {@link com.uber.nullaway.handlers.stream.StreamTypeRecord}
+ * specs. A full example is available at {@link
+ * com.uber.nullaway.testlibrarymodels.TestLibraryModels}.
+ *
+ * @return A list of StreamTypeRecord specs (usually generated using StreamModelBuilder).
+ */
+ default ImmutableList<StreamTypeRecord> customStreamNullabilitySpecs() {
+ return ImmutableList.of();
+ }
+
+ /**
+ * Representation of a method as a qualified class name + a signature for the method
*
* <p>The formatting of a method signature should match the result of calling {@link
* Symbol.MethodSymbol#toString()} on the corresponding symbol. See {@link
@@ -129,6 +169,7 @@ public interface LibraryModels {
final class MethodRef {
public final String enclosingClass;
+
/**
* we store the method name separately to enable fast comparison with MethodSymbols. See {@link
* com.uber.nullaway.handlers.LibraryModelsHandler.OptimizedLibraryModels}
diff --git a/nullaway/src/main/java/com/uber/nullaway/NullAway.java b/nullaway/src/main/java/com/uber/nullaway/NullAway.java
index 2070ff3..ff4ee80 100644
--- a/nullaway/src/main/java/com/uber/nullaway/NullAway.java
+++ b/nullaway/src/main/java/com/uber/nullaway/NullAway.java
@@ -28,7 +28,9 @@ import static com.sun.source.tree.Tree.Kind.IDENTIFIER;
import static com.sun.source.tree.Tree.Kind.OTHER;
import static com.sun.source.tree.Tree.Kind.PARENTHESIZED;
import static com.sun.source.tree.Tree.Kind.TYPE_CAST;
+import static com.uber.nullaway.ASTHelpersBackports.isStatic;
import static com.uber.nullaway.ErrorBuilder.errMsgForInitializer;
+import static com.uber.nullaway.NullabilityUtil.castToNonNull;
import com.google.auto.service.AutoService;
import com.google.auto.value.AutoValue;
@@ -44,13 +46,11 @@ import com.google.errorprone.BugPattern;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
-import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
import com.google.errorprone.suppliers.Suppliers;
import com.google.errorprone.util.ASTHelpers;
-import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ArrayAccessTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.BinaryTree;
@@ -70,6 +70,7 @@ import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewClassTree;
+import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.StatementTree;
@@ -83,6 +84,7 @@ import com.sun.source.tree.WhileLoopTree;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
@@ -97,12 +99,14 @@ import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
+import javax.inject.Inject;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
@@ -148,6 +152,7 @@ import org.checkerframework.nullaway.javacutil.TreeUtils;
summary = "Nullability type error.",
tags = BugPattern.StandardTags.LIKELY_ERROR,
severity = WARNING)
+@SuppressWarnings("BugPatternNaming") // remove once we require EP 2.11+
public class NullAway extends BugChecker
implements BugChecker.MethodInvocationTreeMatcher,
BugChecker.AssignmentTreeMatcher,
@@ -169,7 +174,9 @@ public class NullAway extends BugChecker
BugChecker.IdentifierTreeMatcher,
BugChecker.MemberReferenceTreeMatcher,
BugChecker.CompoundAssignmentTreeMatcher,
- BugChecker.SwitchTreeMatcher {
+ BugChecker.SwitchTreeMatcher,
+ BugChecker.TypeCastTreeMatcher,
+ BugChecker.ParameterizedTypeTreeMatcher {
static final String INITIALIZATION_CHECK_NAME = "NullAway.Init";
static final String OPTIONAL_CHECK_NAME = "NullAway.Optional";
@@ -180,11 +187,45 @@ public class NullAway extends BugChecker
private final Predicate<MethodInvocationNode> nonAnnotatedMethod;
- /** should we match within the current top level class? */
- private boolean matchWithinTopLevelClass = true;
+ /**
+ * Possible levels of null-marking / annotatedness for a class. This may be set to FULLY_MARKED or
+ * FULLY_UNMARKED optimistically but then adjusted to PARTIALLY_MARKED later based on annotations
+ * within the class; see {@link #matchClass(ClassTree, VisitorState)}
+ */
+ private enum NullMarking {
+ /** full class is annotated for nullness checking */
+ FULLY_MARKED,
+ /** full class is unannotated */
+ FULLY_UNMARKED,
+ /**
+ * class has a mix of annotatedness, depending on presence of {@link
+ * org.jspecify.annotations.NullMarked} annotations
+ */
+ PARTIALLY_MARKED
+ }
+
+ /**
+ * Null-marking level for the current top-level class. The initial value of this field doesn't
+ * matter, as it will be set appropriately in {@link #matchClass(ClassTree, VisitorState)}
+ */
+ private NullMarking nullMarkingForTopLevelClass = NullMarking.FULLY_MARKED;
+
+ /**
+ * We store the CodeAnnotationInfo object in a field for convenience; it is initialized in {@link
+ * #matchClass(ClassTree, VisitorState)}
+ */
+ // suppress initialization warning rather than casting everywhere; we know matchClass() will
+ // always be called before the field gets dereferenced
+ @SuppressWarnings("NullAway.Init")
+ private CodeAnnotationInfo codeAnnotationInfo;
private final Config config;
+ /** Returns the configuration being used for this analysis. */
+ public Config getConfig() {
+ return config;
+ }
+
private final ErrorBuilder errorBuilder;
/**
@@ -244,6 +285,7 @@ public class NullAway extends BugChecker
moduleElementClass = null;
}
+ @Inject // For future Error Prone versions in which checkers are loaded using Guice
public NullAway(ErrorProneFlags flags) {
config = new ErrorProneCLIFlagsConfig(flags);
handler = Handlers.buildDefault(config);
@@ -261,7 +303,47 @@ public class NullAway extends BugChecker
private boolean isMethodUnannotated(MethodInvocationNode invocationNode) {
return invocationNode == null
- || NullabilityUtil.isUnannotated(ASTHelpers.getSymbol(invocationNode.getTree()), config);
+ || codeAnnotationInfo.isSymbolUnannotated(
+ ASTHelpers.getSymbol(invocationNode.getTree()), config);
+ }
+
+ private boolean withinAnnotatedCode(VisitorState state) {
+ switch (nullMarkingForTopLevelClass) {
+ case FULLY_MARKED:
+ return true;
+ case FULLY_UNMARKED:
+ return false;
+ case PARTIALLY_MARKED:
+ return checkMarkingForPath(state);
+ }
+ // unreachable but needed to make code compile
+ throw new IllegalStateException("unexpected marking state " + nullMarkingForTopLevelClass);
+ }
+
+ private boolean checkMarkingForPath(VisitorState state) {
+ TreePath path = state.getPath();
+ Tree currentTree = path.getLeaf();
+ // Find the closest class or method symbol, since those are the only ones we have code
+ // annotation info for.
+ // For the purposes of determining whether we are inside annotated code or not, when matching
+ // a class its enclosing class is itself (otherwise we might not process initialization for
+ // top-level classes in general, or @NullMarked inner classes), same for the enclosing method of
+ // a method.
+ // We use instanceof, since there are multiple Kind's which represent ClassTree's: ENUM,
+ // INTERFACE, etc, and we are actually interested in all of them.
+ while (!(currentTree instanceof ClassTree || currentTree instanceof MethodTree)) {
+ path = path.getParentPath();
+ if (path == null) {
+ // Not within a class or method (e.g. the package identifier or an import statement)
+ return false;
+ }
+ currentTree = path.getLeaf();
+ }
+ Symbol enclosingMarkableSymbol = ASTHelpers.getSymbol(currentTree);
+ if (enclosingMarkableSymbol == null) {
+ return false;
+ }
+ return !codeAnnotationInfo.isSymbolUnannotated(enclosingMarkableSymbol, config);
}
@Override
@@ -276,7 +358,7 @@ public class NullAway extends BugChecker
*/
@Override
public Description matchReturn(ReturnTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
handler.onMatchReturn(this, tree, state);
@@ -298,21 +380,21 @@ public class NullAway extends BugChecker
}
Tree leaf = enclosingMethodOrLambda.getLeaf();
Symbol.MethodSymbol methodSymbol;
+ LambdaExpressionTree lambdaTree = null;
if (leaf instanceof MethodTree) {
MethodTree enclosingMethod = (MethodTree) leaf;
methodSymbol = ASTHelpers.getSymbol(enclosingMethod);
} else {
// we have a lambda
- methodSymbol =
- NullabilityUtil.getFunctionalInterfaceMethod(
- (LambdaExpressionTree) leaf, state.getTypes());
+ lambdaTree = (LambdaExpressionTree) leaf;
+ methodSymbol = NullabilityUtil.getFunctionalInterfaceMethod(lambdaTree, state.getTypes());
}
- return checkReturnExpression(tree, retExpr, methodSymbol, state);
+ return checkReturnExpression(retExpr, methodSymbol, lambdaTree, tree, state);
}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
final Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(tree);
@@ -327,7 +409,7 @@ public class NullAway extends BugChecker
@Override
public Description matchNewClass(NewClassTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(tree);
@@ -350,10 +432,10 @@ public class NullAway extends BugChecker
* Updates the {@link EnclosingEnvironmentNullness} with an entry for lambda or anonymous class,
* capturing nullability info for locals just before the declaration of the entity
*
- * @param tree either a lambda or a local / anonymous class
+ * @param treePath either a lambda or a local / anonymous class, identified by its tree path
* @param state visitor state
*/
- private void updateEnvironmentMapping(Tree tree, VisitorState state) {
+ private void updateEnvironmentMapping(TreePath treePath, VisitorState state) {
AccessPathNullnessAnalysis analysis = getNullnessAnalysis(state);
// two notes:
// 1. we are free to take local variable information from the program point before
@@ -362,7 +444,7 @@ public class NullAway extends BugChecker
// 2. we keep info on all locals rather than just effectively final ones for simplicity
EnclosingEnvironmentNullness.instance(state.context)
.addEnvironmentMapping(
- tree, analysis.getNullnessInfoBeforeNewContext(state.getPath(), state, handler));
+ treePath.getLeaf(), analysis.getNullnessInfoBeforeNewContext(treePath, state, handler));
}
private Symbol.MethodSymbol getSymbolOfSuperConstructor(
@@ -385,13 +467,18 @@ public class NullAway extends BugChecker
@Override
public Description matchAssignment(AssignmentTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
Type lhsType = ASTHelpers.getType(tree.getVariable());
if (lhsType != null && lhsType.isPrimitive()) {
- return doUnboxingCheck(state, tree.getExpression());
+ doUnboxingCheck(state, tree.getExpression());
}
+ // generics check
+ if (lhsType != null && lhsType.getTypeArguments().length() > 0) {
+ GenericsChecks.checkTypeParameterNullnessForAssignability(tree, this, state);
+ }
+
Symbol assigned = ASTHelpers.getSymbol(tree.getVariable());
if (assigned == null || assigned.getKind() != ElementKind.FIELD) {
// not a field of nullable type
@@ -409,41 +496,41 @@ public class NullAway extends BugChecker
new ErrorMessage(MessageTypes.ASSIGN_FIELD_NULLABLE, message),
expression,
buildDescription(tree),
- state);
+ state,
+ ASTHelpers.getSymbol(tree.getVariable()));
}
+ handler.onNonNullFieldAssignment(assigned, getNullnessAnalysis(state), state);
return Description.NO_MATCH;
}
@Override
public Description matchCompoundAssignment(CompoundAssignmentTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
Type lhsType = ASTHelpers.getType(tree.getVariable());
Type stringType = Suppliers.STRING_TYPE.get(state);
if (lhsType != null && !state.getTypes().isSameType(lhsType, stringType)) {
// both LHS and RHS could get unboxed
- return doUnboxingCheck(state, tree.getVariable(), tree.getExpression());
+ doUnboxingCheck(state, tree.getVariable(), tree.getExpression());
}
return Description.NO_MATCH;
}
@Override
public Description matchArrayAccess(ArrayAccessTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
Description description = matchDereference(tree.getExpression(), tree, state);
- if (!description.equals(Description.NO_MATCH)) {
- return description;
- }
// also check for unboxing of array index expression
- return doUnboxingCheck(state, tree.getIndex());
+ doUnboxingCheck(state, tree.getIndex());
+ return description;
}
@Override
public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
Symbol symbol = ASTHelpers.getSymbol(tree);
@@ -479,9 +566,59 @@ public class NullAway extends BugChecker
return moduleElementClass != null && moduleElementClass.isAssignableFrom(symbol.getClass());
}
+ /**
+ * Look for @NullMarked or @NullUnmarked annotations at the method level and adjust our scan for
+ * annotated code accordingly (fast scan for a fully annotated/unannotated top-level class or
+ * slower scan for mixed nullmarkedness code).
+ */
+ private void checkForMethodNullMarkedness(MethodTree tree, VisitorState state) {
+ boolean markedMethodInUnmarkedContext = false;
+ Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(tree);
+ switch (nullMarkingForTopLevelClass) {
+ case FULLY_MARKED:
+ if (ASTHelpers.hasDirectAnnotationWithSimpleName(
+ methodSymbol, NullabilityUtil.NULLUNMARKED_SIMPLE_NAME)) {
+ nullMarkingForTopLevelClass = NullMarking.PARTIALLY_MARKED;
+ }
+ break;
+ case FULLY_UNMARKED:
+ if (ASTHelpers.hasDirectAnnotationWithSimpleName(
+ methodSymbol, NullabilityUtil.NULLMARKED_SIMPLE_NAME)) {
+ nullMarkingForTopLevelClass = NullMarking.PARTIALLY_MARKED;
+ markedMethodInUnmarkedContext = true;
+ }
+ break;
+ case PARTIALLY_MARKED:
+ if (ASTHelpers.hasDirectAnnotationWithSimpleName(
+ methodSymbol, NullabilityUtil.NULLMARKED_SIMPLE_NAME)) {
+ // We still care here if this is a transition between @NullUnmarked and @NullMarked code,
+ // within partially marked code, see checks below for markedMethodInUnmarkedContext.
+ if (!codeAnnotationInfo.isClassNullAnnotated(methodSymbol.enclClass(), config)) {
+ markedMethodInUnmarkedContext = true;
+ }
+ }
+ break;
+ }
+ if (markedMethodInUnmarkedContext) {
+ // If this is a @NullMarked method of a @NullUnmarked local or anonymous class, we need to set
+ // its environment mapping, since we skipped it during matchClass.
+ TreePath pathToEnclosingClass =
+ ASTHelpers.findPathFromEnclosingNodeToTopLevel(state.getPath(), ClassTree.class);
+ ClassTree enclosingClass = (ClassTree) pathToEnclosingClass.getLeaf();
+ if (enclosingClass == null) {
+ return;
+ }
+ NestingKind nestingKind = ASTHelpers.getSymbol(enclosingClass).getNestingKind();
+ if (nestingKind.equals(NestingKind.LOCAL) || nestingKind.equals(NestingKind.ANONYMOUS)) {
+ updateEnvironmentMapping(pathToEnclosingClass, state);
+ }
+ }
+ }
+
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ checkForMethodNullMarkedness(tree, state);
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
// if the method is overriding some other method,
@@ -490,12 +627,18 @@ public class NullAway extends BugChecker
// package)
Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(tree);
handler.onMatchMethod(this, tree, state, methodSymbol);
- boolean isOverriding = ASTHelpers.hasAnnotation(methodSymbol, Override.class, state);
+ boolean isOverriding = ASTHelpers.hasAnnotation(methodSymbol, "java.lang.Override", state);
boolean exhaustiveOverride = config.exhaustiveOverride();
if (isOverriding || !exhaustiveOverride) {
Symbol.MethodSymbol closestOverriddenMethod =
NullabilityUtil.getClosestOverriddenMethod(methodSymbol, state.getTypes());
if (closestOverriddenMethod != null) {
+ if (config.isJSpecifyMode()) {
+ // Check that any generic type parameters in the return type and parameter types are
+ // identical (invariant) across the overriding and overridden methods
+ GenericsChecks.checkTypeParameterNullnessForMethodOverriding(
+ tree, methodSymbol, closestOverriddenMethod, this, state);
+ }
return checkOverriding(closestOverriddenMethod, methodSymbol, null, state);
}
}
@@ -504,7 +647,7 @@ public class NullAway extends BugChecker
@Override
public Description matchSwitch(SwitchTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
@@ -525,12 +668,35 @@ public class NullAway extends BugChecker
errorMessage,
switchSelectorExpression,
buildDescription(switchSelectorExpression),
- state);
+ state,
+ null);
}
return Description.NO_MATCH;
}
+ @Override
+ public Description matchTypeCast(TypeCastTree tree, VisitorState state) {
+ if (!withinAnnotatedCode(state)) {
+ return Description.NO_MATCH;
+ }
+ Type castExprType = ASTHelpers.getType(tree);
+ if (castExprType != null && castExprType.isPrimitive()) {
+ // casting to a primitive type performs unboxing
+ doUnboxingCheck(state, tree.getExpression());
+ }
+ return Description.NO_MATCH;
+ }
+
+ @Override
+ public Description matchParameterizedType(ParameterizedTypeTree tree, VisitorState state) {
+ if (!withinAnnotatedCode(state)) {
+ return Description.NO_MATCH;
+ }
+ GenericsChecks.checkInstantiationForParameterizedTypedTree(tree, state, this, config);
+ return Description.NO_MATCH;
+ }
+
/**
* checks that an overriding method does not override a {@code @Nullable} parameter with a
* {@code @NonNull} parameter
@@ -550,61 +716,83 @@ public class NullAway extends BugChecker
@Nullable MemberReferenceTree memberReferenceTree,
VisitorState state) {
com.sun.tools.javac.util.List<VarSymbol> superParamSymbols = overriddenMethod.getParameters();
- boolean unboundMemberRef =
+ final boolean unboundMemberRef =
(memberReferenceTree != null)
&& ((JCTree.JCMemberReference) memberReferenceTree).kind.isUnbound();
- // if we have an unbound method reference, the first parameter of the overridden method must be
- // @NonNull, as this parameter will be used as a method receiver inside the generated lambda
- if (unboundMemberRef) {
- boolean isFirstParamNull = false;
- // Two cases: for annotated code, look first at the annotation
- if (!NullabilityUtil.isUnannotated(overriddenMethod, config)) {
- isFirstParamNull = Nullness.hasNullableAnnotation(superParamSymbols.get(0), config);
- }
- // For both annotated and unannotated code, look then at handler overrides (e.g. Library
- // Models)
- isFirstParamNull =
- handler
- .onUnannotatedInvocationGetExplicitlyNullablePositions(
- state.context,
- overriddenMethod,
- isFirstParamNull ? ImmutableSet.of(0) : ImmutableSet.of())
- .contains(0);
- if (isFirstParamNull) {
- String message =
- "unbound instance method reference cannot be used, as first parameter of "
- + "functional interface method "
- + ASTHelpers.enclosingClass(overriddenMethod)
- + "."
- + overriddenMethod.toString()
- + " is @Nullable";
- return errorBuilder.createErrorDescription(
- new ErrorMessage(MessageTypes.WRONG_OVERRIDE_PARAM, message),
- buildDescription(memberReferenceTree),
- state);
+ final boolean isOverriddenMethodAnnotated =
+ !codeAnnotationInfo.isSymbolUnannotated(overriddenMethod, config);
+
+ // Get argument nullability for the overridden method. If overriddenMethodArgNullnessMap[i] is
+ // null, parameter i is treated as unannotated.
+ Nullness[] overriddenMethodArgNullnessMap = new Nullness[superParamSymbols.size()];
+
+ // Collect @Nullable params of overridden method iff the overridden method is in annotated code
+ // (otherwise, whether we acknowledge @Nullable in unannotated code or not depends on the
+ // -XepOpt:NullAway:AcknowledgeRestrictiveAnnotations flag and its handler).
+ if (isOverriddenMethodAnnotated) {
+ for (int i = 0; i < superParamSymbols.size(); i++) {
+ Nullness paramNullness;
+ if (Nullness.paramHasNullableAnnotation(overriddenMethod, i, config)) {
+ paramNullness = Nullness.NULLABLE;
+ } else if (config.isJSpecifyMode()) {
+ // Check if the parameter type is a type variable and the corresponding generic type
+ // argument is @Nullable
+ if (memberReferenceTree != null) {
+ // For a method reference, we get generic type arguments from the javac's inferred type
+ // for the tree, which seems to properly preserve type-use annotations
+ paramNullness =
+ GenericsChecks.getGenericMethodParameterNullness(
+ i, overriddenMethod, ASTHelpers.getType(memberReferenceTree), state, config);
+ } else {
+ // Use the enclosing class of the overriding method to find generic type arguments
+ paramNullness =
+ GenericsChecks.getGenericMethodParameterNullness(
+ i, overriddenMethod, overridingParamSymbols.get(i).owner.owner, state, config);
+ }
+ } else {
+ paramNullness = Nullness.NONNULL;
+ }
+ overriddenMethodArgNullnessMap[i] = paramNullness;
}
}
+
+ // Check handlers for any further/overriding nullness information
+ overriddenMethodArgNullnessMap =
+ handler.onOverrideMethodInvocationParametersNullability(
+ state.context,
+ overriddenMethod,
+ isOverriddenMethodAnnotated,
+ overriddenMethodArgNullnessMap);
+
+ // If we have an unbound method reference, the first parameter of the overridden method must be
+ // @NonNull, as this parameter will be used as a method receiver inside the generated lambda.
+ // e.g. String::length is implemented as (@NonNull s -> s.length()) when used as a
+ // SomeFunc<String> and thus incompatible with, for example, SomeFunc.apply(@Nullable T).
+ if (unboundMemberRef && Objects.equals(overriddenMethodArgNullnessMap[0], Nullness.NULLABLE)) {
+ String message =
+ "unbound instance method reference cannot be used, as first parameter of "
+ + "functional interface method "
+ + ASTHelpers.enclosingClass(overriddenMethod)
+ + "."
+ + overriddenMethod.toString()
+ + " is @Nullable";
+ return errorBuilder.createErrorDescription(
+ new ErrorMessage(MessageTypes.WRONG_OVERRIDE_PARAM, message),
+ buildDescription(memberReferenceTree),
+ state,
+ null);
+ }
+
// for unbound member references, we need to adjust parameter indices by 1 when matching with
// overridden method
- int startParam = unboundMemberRef ? 1 : 0;
- // Collect @Nullable params of overriden method
- ImmutableSet<Integer> nullableParamsOfOverriden;
- if (NullabilityUtil.isUnannotated(overriddenMethod, config)) {
- nullableParamsOfOverriden =
- handler.onUnannotatedInvocationGetExplicitlyNullablePositions(
- state.context, overriddenMethod, ImmutableSet.of());
- } else {
- ImmutableSet.Builder<Integer> builder = ImmutableSet.builder();
- for (int i = startParam; i < superParamSymbols.size(); i++) {
- // we need to call paramHasNullableAnnotation here since overriddenMethod may be defined
- // in a class file
- if (Nullness.paramHasNullableAnnotation(overriddenMethod, i, config)) {
- builder.add(i);
- }
+ final int startParam = unboundMemberRef ? 1 : 0;
+
+ for (int i = 0; i < superParamSymbols.size(); i++) {
+ if (!Objects.equals(overriddenMethodArgNullnessMap[i], Nullness.NULLABLE)) {
+ // No need to check, unless the argument of the overridden method is effectively @Nullable,
+ // in which case it can't be overridding a @NonNull arg.
+ continue;
}
- nullableParamsOfOverriden = builder.build();
- }
- for (int i : nullableParamsOfOverriden) {
int methodParamInd = i - startParam;
VarSymbol paramSymbol = overridingParamSymbols.get(methodParamInd);
// in the case where we have a parameter of a lambda expression, we do
@@ -639,7 +827,8 @@ public class NullAway extends BugChecker
return errorBuilder.createErrorDescription(
new ErrorMessage(MessageTypes.WRONG_OVERRIDE_PARAM, message),
buildDescription(errorTree),
- state);
+ state,
+ paramSymbol);
}
}
return Description.NO_MATCH;
@@ -649,44 +838,98 @@ public class NullAway extends BugChecker
return Trees.instance(JavacProcessingEnvironment.instance(state.context));
}
+ private Nullness getMethodReturnNullness(
+ Symbol.MethodSymbol methodSymbol, VisitorState state, Nullness defaultForUnannotated) {
+ final boolean isMethodAnnotated = !codeAnnotationInfo.isSymbolUnannotated(methodSymbol, config);
+ Nullness methodReturnNullness =
+ defaultForUnannotated; // Permissive default for unannotated code.
+ if (isMethodAnnotated) {
+ methodReturnNullness =
+ Nullness.hasNullableAnnotation(methodSymbol, config)
+ ? Nullness.NULLABLE
+ : Nullness.NONNULL;
+ }
+ return handler.onOverrideMethodReturnNullability(
+ methodSymbol, state, isMethodAnnotated, methodReturnNullness);
+ }
+
+ /**
+ * Checks that if a returned expression is {@code @Nullable}, the enclosing method does not have a
+ * {@code @NonNull} return type. Also performs an unboxing check on the returned expression.
+ * Finally, in JSpecify mode, also checks that the nullability of generic type arguments of the
+ * returned expression's type match the method return type.
+ *
+ * @param retExpr the expression being returned
+ * @param methodSymbol symbol for the enclosing method
+ * @param lambdaTree if return is inside a lambda, the tree for the lambda, otherwise {@code null}
+ * @param errorTree tree on which to report an error if needed
+ * @param state the visitor state
+ * @return {@link Description} of the returning {@code @Nullable} from {@code @NonNull} method
+ * error if one is to be reported, otherwise {@link Description#NO_MATCH}
+ */
private Description checkReturnExpression(
- Tree tree, ExpressionTree retExpr, Symbol.MethodSymbol methodSymbol, VisitorState state) {
+ ExpressionTree retExpr,
+ Symbol.MethodSymbol methodSymbol,
+ @Nullable LambdaExpressionTree lambdaTree,
+ Tree errorTree,
+ VisitorState state) {
Type returnType = methodSymbol.getReturnType();
if (returnType.isPrimitive()) {
// check for unboxing
- return doUnboxingCheck(state, retExpr);
+ doUnboxingCheck(state, retExpr);
+ return Description.NO_MATCH;
}
- if (returnType.toString().equals("java.lang.Void")) {
+ if (ASTHelpers.isSameType(returnType, Suppliers.JAVA_LANG_VOID_TYPE.get(state), state)) {
+ // Temporarily treat a Void return type as if it were @Nullable Void. Change this once
+ // we are confident that all use cases can be type checked reasonably (may require generics
+ // support)
return Description.NO_MATCH;
}
- if (NullabilityUtil.isUnannotated(methodSymbol, config)
- || Nullness.hasNullableAnnotation(methodSymbol, config)) {
+
+ // Check generic type arguments for returned expression here, since we need to check the type
+ // arguments regardless of the top-level nullability of the return type
+ GenericsChecks.checkTypeParameterNullnessForFunctionReturnType(
+ retExpr, methodSymbol, this, state);
+
+ // Now, perform the check for returning @Nullable from @NonNull. First, we check if the return
+ // type is @Nullable, and if so, bail out.
+ if (getMethodReturnNullness(methodSymbol, state, Nullness.NULLABLE).equals(Nullness.NULLABLE)) {
+ return Description.NO_MATCH;
+ } else if (config.isJSpecifyMode()
+ && lambdaTree != null
+ && GenericsChecks.getGenericMethodReturnTypeNullness(
+ methodSymbol, ASTHelpers.getType(lambdaTree), state, config)
+ .equals(Nullness.NULLABLE)) {
+ // In JSpecify mode, the return type of a lambda may be @Nullable via a type argument
return Description.NO_MATCH;
}
+
+ // Return type is @NonNull. Check if the expression is @Nullable
if (mayBeNullExpr(state, retExpr)) {
- final ErrorMessage errorMessage =
+ return errorBuilder.createErrorDescriptionForNullAssignment(
new ErrorMessage(
MessageTypes.RETURN_NULLABLE,
- "returning @Nullable expression from method with @NonNull return type");
-
- return errorBuilder.createErrorDescriptionForNullAssignment(
- errorMessage, retExpr, buildDescription(tree), state);
+ "returning @Nullable expression from method with @NonNull return type"),
+ retExpr,
+ buildDescription(errorTree),
+ state,
+ methodSymbol);
}
return Description.NO_MATCH;
}
@Override
public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
Symbol.MethodSymbol funcInterfaceMethod =
NullabilityUtil.getFunctionalInterfaceMethod(tree, state.getTypes());
// we need to update environment mapping before running the handler, as some handlers
// (like Rx nullability) run dataflow analysis
- updateEnvironmentMapping(tree, state);
+ updateEnvironmentMapping(state.getPath(), state);
handler.onMatchLambdaExpression(this, tree, state, funcInterfaceMethod);
- if (NullabilityUtil.isUnannotated(funcInterfaceMethod, config)) {
+ if (codeAnnotationInfo.isSymbolUnannotated(funcInterfaceMethod, config)) {
return Description.NO_MATCH;
}
Description description =
@@ -704,7 +947,7 @@ public class NullAway extends BugChecker
if (tree.getBodyKind() == LambdaExpressionTree.BodyKind.EXPRESSION
&& funcInterfaceMethod.getReturnType().getKind() != TypeKind.VOID) {
ExpressionTree resExpr = (ExpressionTree) tree.getBody();
- return checkReturnExpression(tree, resExpr, funcInterfaceMethod, state);
+ return checkReturnExpression(resExpr, funcInterfaceMethod, tree, tree, state);
}
return Description.NO_MATCH;
}
@@ -715,7 +958,7 @@ public class NullAway extends BugChecker
*/
@Override
public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
Symbol.MethodSymbol referencedMethod = ASTHelpers.getSymbol(tree);
@@ -742,19 +985,15 @@ public class NullAway extends BugChecker
Symbol.MethodSymbol overridingMethod,
@Nullable MemberReferenceTree memberReferenceTree,
VisitorState state) {
- final boolean isOverridenMethodUnannotated =
- NullabilityUtil.isUnannotated(overriddenMethod, config);
- final boolean overriddenMethodReturnsNonNull =
- ((isOverridenMethodUnannotated
- && handler.onUnannotatedInvocationGetExplicitlyNonNullReturn(
- overriddenMethod, false))
- || (!isOverridenMethodUnannotated
- && !Nullness.hasNullableAnnotation(overriddenMethod, config)));
- // if the super method returns nonnull,
- // overriding method better not return nullable
- if (overriddenMethodReturnsNonNull
- && Nullness.hasNullableAnnotation(overridingMethod, config)
- && getComputedNullness(memberReferenceTree).equals(Nullness.NULLABLE)) {
+ // if the super method returns nonnull, overriding method better not return nullable
+ // Note that, for the overriding method, the permissive default is non-null,
+ // but it's nullable for the overridden one.
+ if (overriddenMethodReturnsNonNull(
+ overriddenMethod, overridingMethod.owner, memberReferenceTree, state)
+ && getMethodReturnNullness(overridingMethod, state, Nullness.NONNULL)
+ .equals(Nullness.NULLABLE)
+ && (memberReferenceTree == null
+ || getComputedNullness(memberReferenceTree).equals(Nullness.NULLABLE))) {
String message;
if (memberReferenceTree != null) {
message =
@@ -763,7 +1002,6 @@ public class NullAway extends BugChecker
+ "."
+ overriddenMethod.toString()
+ " returns @NonNull";
-
} else {
message =
"method returns @Nullable, but superclass method "
@@ -772,6 +1010,7 @@ public class NullAway extends BugChecker
+ overriddenMethod.toString()
+ " returns @NonNull";
}
+
Tree errorTree =
memberReferenceTree != null
? memberReferenceTree
@@ -779,7 +1018,8 @@ public class NullAway extends BugChecker
return errorBuilder.createErrorDescription(
new ErrorMessage(MessageTypes.WRONG_OVERRIDE_RETURN, message),
buildDescription(errorTree),
- state);
+ state,
+ overriddenMethod);
}
// if any parameter in the super method is annotated @Nullable,
// overriding method cannot assume @Nonnull
@@ -787,9 +1027,38 @@ public class NullAway extends BugChecker
overridingMethod.getParameters(), overriddenMethod, null, memberReferenceTree, state);
}
+ private boolean overriddenMethodReturnsNonNull(
+ Symbol.MethodSymbol overriddenMethod,
+ Symbol enclosingSymbol,
+ @Nullable MemberReferenceTree memberReferenceTree,
+ VisitorState state) {
+ Nullness methodReturnNullness =
+ getMethodReturnNullness(overriddenMethod, state, Nullness.NULLABLE);
+ if (!methodReturnNullness.equals(Nullness.NONNULL)) {
+ return false;
+ }
+ // In JSpecify mode, for generic methods, we additionally need to check the return nullness
+ // using the type arguments from the type enclosing the overriding method
+ if (config.isJSpecifyMode()) {
+ if (memberReferenceTree != null) {
+ // For a method reference, we get generic type arguments from javac's inferred type for the
+ // tree, which properly preserves type-use annotations
+ return GenericsChecks.getGenericMethodReturnTypeNullness(
+ overriddenMethod, ASTHelpers.getType(memberReferenceTree), state, config)
+ .equals(Nullness.NONNULL);
+ } else {
+ // Use the enclosing class of the overriding method to find generic type arguments
+ return GenericsChecks.getGenericMethodReturnTypeNullness(
+ overriddenMethod, enclosingSymbol, state, config)
+ .equals(Nullness.NONNULL);
+ }
+ }
+ return true;
+ }
+
@Override
public Description matchIdentifier(IdentifierTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
return checkForReadBeforeInit(tree, state);
@@ -826,8 +1095,9 @@ public class NullAway extends BugChecker
if (!symbol.getKind().equals(ElementKind.FIELD)) {
return Description.NO_MATCH;
}
+
// for static fields, make sure the enclosing init is a static method or block
- if (symbol.isStatic()) {
+ if (isStatic(symbol)) {
Tree enclosing = enclosingBlockPath.getLeaf();
if (enclosing instanceof MethodTree
&& !ASTHelpers.getSymbol((MethodTree) enclosing).isStatic()) {
@@ -836,13 +1106,14 @@ public class NullAway extends BugChecker
return Description.NO_MATCH;
}
}
- if (okToReadBeforeInitialized(path)) {
+ if (okToReadBeforeInitialized(path, state)) {
// writing the field, not reading it
return Description.NO_MATCH;
}
// check that the field might actually be problematic to read
- FieldInitEntities entities = class2Entities.get(enclosingClassSymbol(enclosingBlockPath));
+ FieldInitEntities entities =
+ castToNonNull(class2Entities.get(enclosingClassSymbol(enclosingBlockPath)));
if (!(entities.nonnullInstanceFields().contains(symbol)
|| entities.nonnullStaticFields().contains(symbol))) {
// field is either nullable or initialized at declaration
@@ -862,7 +1133,7 @@ public class NullAway extends BugChecker
Tree parent = enclosingBlockPath.getParentPath().getLeaf();
return ASTHelpers.getSymbol((ClassTree) parent);
} else {
- return ASTHelpers.enclosingClass(ASTHelpers.getSymbol(leaf));
+ return castToNonNull(ASTHelpers.enclosingClass(ASTHelpers.getSymbol(leaf)));
}
}
@@ -876,16 +1147,26 @@ public class NullAway extends BugChecker
if (isConstructor(methodTree) && !constructorInvokesAnother(methodTree, state)) {
return true;
}
+
+ final Symbol.ClassSymbol enclClassSymbol = enclosingClassSymbol(enclosingBlockPath);
+
+ // Checking for initialization is only meaningful if the full class is null-annotated, which
+ // might not be the case with @NullMarked methods inside @NullUnmarked classes (note that,
+ // in those cases, we won't even have a populated class2Entities map). We skip this check if
+ // we are not inside a @NullMarked/annotated *class*:
+ if (nullMarkingForTopLevelClass == NullMarking.PARTIALLY_MARKED
+ && !codeAnnotationInfo.isClassNullAnnotated(enclClassSymbol, config)) {
+ return false;
+ }
+
if (ASTHelpers.getSymbol(methodTree).isStatic()) {
Set<MethodTree> staticInitializerMethods =
- class2Entities.get(enclosingClassSymbol(enclosingBlockPath)).staticInitializerMethods();
+ castToNonNull(class2Entities.get(enclClassSymbol)).staticInitializerMethods();
return staticInitializerMethods.size() == 1
&& staticInitializerMethods.contains(methodTree);
} else {
Set<MethodTree> instanceInitializerMethods =
- class2Entities
- .get(enclosingClassSymbol(enclosingBlockPath))
- .instanceInitializerMethods();
+ castToNonNull(class2Entities.get(enclClassSymbol)).instanceInitializerMethods();
return instanceInitializerMethods.size() == 1
&& instanceInitializerMethods.contains(methodTree);
}
@@ -907,7 +1188,7 @@ public class NullAway extends BugChecker
new ErrorMessage(
MessageTypes.NONNULL_FIELD_READ_BEFORE_INIT,
"read of @NonNull field " + symbol + " before initialization");
- return errorBuilder.createErrorDescription(errorMessage, buildDescription(tree), state);
+ return errorBuilder.createErrorDescription(errorMessage, buildDescription(tree), state, null);
} else {
return Description.NO_MATCH;
}
@@ -925,7 +1206,7 @@ public class NullAway extends BugChecker
Symbol symbol, TreePath pathToRead, VisitorState state, TreePath enclosingBlockPath) {
AccessPathNullnessAnalysis nullnessAnalysis = getNullnessAnalysis(state);
Set<Element> nonnullFields;
- if (symbol.isStatic()) {
+ if (isStatic(symbol)) {
nonnullFields = nullnessAnalysis.getNonnullStaticFieldsBefore(pathToRead, state.context);
} else {
nonnullFields = new LinkedHashSet<>();
@@ -1017,12 +1298,10 @@ public class NullAway extends BugChecker
Symbol fieldSymbol, TreePath initTreePath, VisitorState state) {
TreePath enclosingClassPath = initTreePath.getParentPath();
ClassTree enclosingClass = (ClassTree) enclosingClassPath.getLeaf();
+ ClassSymbol classSymbol = ASTHelpers.getSymbol(enclosingClass);
Multimap<Tree, Element> tree2Init =
- initTree2PrevFieldInit.get(ASTHelpers.getSymbol(enclosingClass));
- if (tree2Init == null) {
- tree2Init = computeTree2Init(enclosingClassPath, state);
- initTree2PrevFieldInit.put(ASTHelpers.getSymbol(enclosingClass), tree2Init);
- }
+ initTree2PrevFieldInit.computeIfAbsent(
+ classSymbol, sym -> computeTree2Init(enclosingClassPath, state));
return tree2Init.containsEntry(initTreePath.getLeaf(), fieldSymbol);
}
@@ -1069,7 +1348,7 @@ public class NullAway extends BugChecker
// all the initializer blocks have run before any code inside a constructor
constructors.stream().forEach((c) -> builder.putAll(c, initThusFar));
Symbol.ClassSymbol classSymbol = ASTHelpers.getSymbol(enclosingClass);
- FieldInitEntities entities = class2Entities.get(classSymbol);
+ FieldInitEntities entities = castToNonNull(class2Entities.get(classSymbol));
if (entities.instanceInitializerMethods().size() == 1) {
MethodTree initMethod = entities.instanceInitializerMethods().iterator().next();
// collect the fields that may not be initialized by *some* constructor NC
@@ -1091,10 +1370,11 @@ public class NullAway extends BugChecker
/**
* @param path tree path to read operation
+ * @param state the current VisitorState
* @return true if it is permissible to perform this read before the field has been initialized,
* false otherwise
*/
- private boolean okToReadBeforeInitialized(TreePath path) {
+ private boolean okToReadBeforeInitialized(TreePath path, VisitorState state) {
TreePath parentPath = path.getParentPath();
Tree leaf = path.getLeaf();
Tree parent = parentPath.getLeaf();
@@ -1118,22 +1398,36 @@ public class NullAway extends BugChecker
Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(methodInvoke);
String qualifiedName =
ASTHelpers.enclosingClass(methodSymbol) + "." + methodSymbol.getSimpleName().toString();
- if (qualifiedName.equals(config.getCastToNonNullMethod())) {
- List<? extends ExpressionTree> arguments = methodInvoke.getArguments();
- return arguments.size() == 1 && leaf.equals(arguments.get(0));
+ List<? extends ExpressionTree> arguments = methodInvoke.getArguments();
+ Integer castToNonNullArg;
+ if (qualifiedName.equals(config.getCastToNonNullMethod())
+ && methodSymbol.getParameters().size() == 1) {
+ castToNonNullArg = 0;
+ } else {
+ castToNonNullArg =
+ handler.castToNonNullArgumentPositionsForMethod(
+ this, state, methodSymbol, arguments, null);
}
+ if (castToNonNullArg != null && leaf.equals(arguments.get(castToNonNullArg))) {
+ return true;
+ }
+ return false;
}
return false;
}
@Override
public Description matchVariable(VariableTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
VarSymbol symbol = ASTHelpers.getSymbol(tree);
+ if (tree.getInitializer() != null) {
+ GenericsChecks.checkTypeParameterNullnessForAssignability(tree, this, state);
+ }
+
if (symbol.type.isPrimitive() && tree.getInitializer() != null) {
- return doUnboxingCheck(state, tree.getInitializer());
+ doUnboxingCheck(state, tree.getInitializer());
}
if (!symbol.getKind().equals(ElementKind.FIELD)) {
return Description.NO_MATCH;
@@ -1147,15 +1441,42 @@ public class NullAway extends BugChecker
MessageTypes.ASSIGN_FIELD_NULLABLE,
"assigning @Nullable expression to @NonNull field");
return errorBuilder.createErrorDescriptionForNullAssignment(
- errorMessage, initializer, buildDescription(tree), state);
+ errorMessage, initializer, buildDescription(tree), state, symbol);
}
}
}
return Description.NO_MATCH;
}
+ /**
+ * Check if an inner class's annotation means this Compilation Unit is partially annotated.
+ *
+ * <p>Returns true iff classSymbol has a direct @NullMarked or @NullUnmarked annotation which
+ * differs from the {@link NullMarking} of the top-level class, meaning the compilation unit is
+ * itself partially marked, and we need to switch to our slower mode for detecting whether we are
+ * in unannotated code.
+ *
+ * @param classSymbol a ClassSymbol representing an inner class within the current compilation
+ * unit.
+ * @return true iff this inner class is @NullMarked and the top-level class unmarked or vice
+ * versa.
+ */
+ private boolean classAnnotationIntroducesPartialMarking(Symbol.ClassSymbol classSymbol) {
+ return (nullMarkingForTopLevelClass == NullMarking.FULLY_UNMARKED
+ && ASTHelpers.hasDirectAnnotationWithSimpleName(
+ classSymbol, NullabilityUtil.NULLMARKED_SIMPLE_NAME))
+ || (nullMarkingForTopLevelClass == NullMarking.FULLY_MARKED
+ && ASTHelpers.hasDirectAnnotationWithSimpleName(
+ classSymbol, NullabilityUtil.NULLUNMARKED_SIMPLE_NAME));
+ }
+
@Override
public Description matchClass(ClassTree tree, VisitorState state) {
+ // Ensure codeAnnotationInfo is initialized here since it requires access to the Context,
+ // which is not available in the constructor
+ if (codeAnnotationInfo == null) {
+ codeAnnotationInfo = CodeAnnotationInfo.instance(state.context);
+ }
// Check if the class is excluded according to the filter
// if so, set the flag to match within the class to false
// NOTE: for this mechanism to work, we rely on the enclosing ClassTree
@@ -1173,7 +1494,12 @@ public class NullAway extends BugChecker
Symbol.ClassSymbol classSymbol = ASTHelpers.getSymbol(tree);
NestingKind nestingKind = classSymbol.getNestingKind();
if (!nestingKind.isNested()) {
- matchWithinTopLevelClass = !isExcludedClass(classSymbol);
+ // Here we optimistically set the marking to either FULLY_UNMARKED or FULLY_MARKED. If a
+ // nested entity has a contradicting annotation, at that point we update the marking level to
+ // PARTIALLY_MARKED, which will increase checking overhead for the remainder of the top-level
+ // class
+ nullMarkingForTopLevelClass =
+ isExcludedClass(classSymbol) ? NullMarking.FULLY_UNMARKED : NullMarking.FULLY_MARKED;
// since we are processing a new top-level class, invalidate any cached
// results for previous classes
handler.onMatchTopLevelClass(this, tree, state, classSymbol);
@@ -1183,12 +1509,17 @@ public class NullAway extends BugChecker
class2ConstructorUninit.clear();
computedNullnessMap.clear();
EnclosingEnvironmentNullness.instance(state.context).clear();
+ } else if (classAnnotationIntroducesPartialMarking(classSymbol)) {
+ // Handle the case where the top-class is unannotated, but there is a @NullMarked annotation
+ // on a nested class, or, conversely the top-level is annotated but there is a @NullUnmarked
+ // annotation on a nested class.
+ nullMarkingForTopLevelClass = NullMarking.PARTIALLY_MARKED;
}
- if (matchWithinTopLevelClass) {
+ if (withinAnnotatedCode(state)) {
// we need to update the environment before checking field initialization, as the latter
// may run dataflow analysis
if (nestingKind.equals(NestingKind.LOCAL) || nestingKind.equals(NestingKind.ANONYMOUS)) {
- updateEnvironmentMapping(tree, state);
+ updateEnvironmentMapping(state.getPath(), state);
}
checkFieldInitialization(tree, state);
}
@@ -1199,72 +1530,84 @@ public class NullAway extends BugChecker
@Override
public Description matchBinary(BinaryTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
+ // Perform unboxing checks on operands if needed
+ Type binaryExprType = ASTHelpers.getType(tree);
+ // If the type of the expression is not primitive, we do not need to do unboxing checks. This
+ // handles the case of `+` used for string concatenation
+ if (binaryExprType == null || !binaryExprType.isPrimitive()) {
+ return Description.NO_MATCH;
+ }
+ Tree.Kind kind = tree.getKind();
ExpressionTree leftOperand = tree.getLeftOperand();
ExpressionTree rightOperand = tree.getRightOperand();
- Type leftType = ASTHelpers.getType(leftOperand);
- Type rightType = ASTHelpers.getType(rightOperand);
- if (leftType == null || rightType == null) {
- throw new RuntimeException();
- }
- if (leftType.isPrimitive() && !rightType.isPrimitive()) {
- return doUnboxingCheck(state, rightOperand);
- }
- if (rightType.isPrimitive() && !leftType.isPrimitive()) {
- return doUnboxingCheck(state, leftOperand);
+ if (kind.equals(Tree.Kind.EQUAL_TO) || kind.equals(Tree.Kind.NOT_EQUAL_TO)) {
+ // here we need a check if one operand is of primitive type and the other is not, as that will
+ // cause unboxing of the non-primitive operand
+ Type leftType = ASTHelpers.getType(leftOperand);
+ Type rightType = ASTHelpers.getType(rightOperand);
+ if (leftType == null || rightType == null) {
+ return Description.NO_MATCH;
+ }
+ if (leftType.isPrimitive() && !rightType.isPrimitive()) {
+ doUnboxingCheck(state, rightOperand);
+ } else if (rightType.isPrimitive() && !leftType.isPrimitive()) {
+ doUnboxingCheck(state, leftOperand);
+ }
+ } else {
+ // in all other cases, both operands should be checked
+ doUnboxingCheck(state, leftOperand, rightOperand);
}
return Description.NO_MATCH;
}
@Override
public Description matchUnary(UnaryTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
- return Description.NO_MATCH;
+ if (withinAnnotatedCode(state)) {
+ doUnboxingCheck(state, tree.getExpression());
}
- return doUnboxingCheck(state, tree.getExpression());
+ return Description.NO_MATCH;
}
@Override
public Description matchConditionalExpression(
ConditionalExpressionTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
- return Description.NO_MATCH;
+ if (withinAnnotatedCode(state)) {
+ GenericsChecks.checkTypeParameterNullnessForConditionalExpression(tree, this, state);
+ doUnboxingCheck(state, tree.getCondition());
}
- return doUnboxingCheck(state, tree.getCondition());
+ return Description.NO_MATCH;
}
@Override
public Description matchIf(IfTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
- return Description.NO_MATCH;
+ if (withinAnnotatedCode(state)) {
+ doUnboxingCheck(state, tree.getCondition());
}
- return doUnboxingCheck(state, tree.getCondition());
+ return Description.NO_MATCH;
}
@Override
public Description matchWhileLoop(WhileLoopTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
- return Description.NO_MATCH;
+ if (withinAnnotatedCode(state)) {
+ doUnboxingCheck(state, tree.getCondition());
}
- return doUnboxingCheck(state, tree.getCondition());
+ return Description.NO_MATCH;
}
@Override
public Description matchForLoop(ForLoopTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
- return Description.NO_MATCH;
- }
- if (tree.getCondition() != null) {
- return doUnboxingCheck(state, tree.getCondition());
+ if (withinAnnotatedCode(state) && tree.getCondition() != null) {
+ doUnboxingCheck(state, tree.getCondition());
}
return Description.NO_MATCH;
}
@Override
public Description matchEnhancedForLoop(EnhancedForLoopTree tree, VisitorState state) {
- if (!matchWithinTopLevelClass) {
+ if (!withinAnnotatedCode(state)) {
return Description.NO_MATCH;
}
ExpressionTree expr = tree.getExpression();
@@ -1273,19 +1616,20 @@ public class NullAway extends BugChecker
MessageTypes.DEREFERENCE_NULLABLE,
"enhanced-for expression " + state.getSourceForNode(expr) + " is @Nullable");
if (mayBeNullExpr(state, expr)) {
- return errorBuilder.createErrorDescription(errorMessage, buildDescription(expr), state);
+ return errorBuilder.createErrorDescription(errorMessage, buildDescription(expr), state, null);
}
return Description.NO_MATCH;
}
/**
- * if any expression has non-primitive type, we should check that it can't be null as it is
- * getting unboxed
+ * Checks that all given expressions cannot be null, and for those that are {@code @Nullable},
+ * reports an unboxing error.
*
+ * @param state the visitor state, used to report errors via {@link
+ * VisitorState#reportMatch(Description)}
* @param expressions expressions to check
- * @return error Description if an error is found, otherwise NO_MATCH
*/
- private Description doUnboxingCheck(VisitorState state, ExpressionTree... expressions) {
+ private void doUnboxingCheck(VisitorState state, ExpressionTree... expressions) {
for (ExpressionTree tree : expressions) {
Type type = ASTHelpers.getType(tree);
if (type == null) {
@@ -1295,11 +1639,12 @@ public class NullAway extends BugChecker
if (mayBeNullExpr(state, tree)) {
final ErrorMessage errorMessage =
new ErrorMessage(MessageTypes.UNBOX_NULLABLE, "unboxing of a @Nullable value");
- return errorBuilder.createErrorDescription(errorMessage, buildDescription(tree), state);
+ state.reportMatch(
+ errorBuilder.createErrorDescription(
+ errorMessage, buildDescription(tree), state, null));
}
}
}
- return Description.NO_MATCH;
}
/**
@@ -1316,12 +1661,6 @@ public class NullAway extends BugChecker
VisitorState state,
Symbol.MethodSymbol methodSymbol,
List<? extends ExpressionTree> actualParams) {
- ImmutableSet<Integer> nonNullPositions = null;
- if (NullabilityUtil.isUnannotated(methodSymbol, config)) {
- nonNullPositions =
- handler.onUnannotatedInvocationGetNonNullPositions(
- this, state, methodSymbol, actualParams, ImmutableSet.of());
- }
List<VarSymbol> formalParams = methodSymbol.getParameters();
if (formalParams.size() != actualParams.size()
@@ -1335,32 +1674,54 @@ public class NullAway extends BugChecker
return Description.NO_MATCH;
}
- if (nonNullPositions == null) {
- ImmutableSet.Builder<Integer> builder = ImmutableSet.builder();
+ final boolean isMethodAnnotated = !codeAnnotationInfo.isSymbolUnannotated(methodSymbol, config);
+ // If argumentPositionNullness[i] == null, parameter i is unannotated
+ Nullness[] argumentPositionNullness = new Nullness[formalParams.size()];
+
+ if (isMethodAnnotated) {
// compute which arguments are @NonNull
for (int i = 0; i < formalParams.size(); i++) {
VarSymbol param = formalParams.get(i);
if (param.type.isPrimitive()) {
- Description unboxingCheck = doUnboxingCheck(state, actualParams.get(i));
- if (unboxingCheck != Description.NO_MATCH) {
- return unboxingCheck;
- } else {
- continue;
- }
- }
- // we need to call paramHasNullableAnnotation here since the invoked method may be defined
- // in a class file
- if (!Nullness.paramHasNullableAnnotation(methodSymbol, i, config)) {
- builder.add(i);
+ doUnboxingCheck(state, actualParams.get(i));
+ argumentPositionNullness[i] = Nullness.NONNULL;
+ } else if (ASTHelpers.isSameType(
+ param.type, Suppliers.JAVA_LANG_VOID_TYPE.get(state), state)) {
+ // Temporarily treat a Void argument type as if it were @Nullable Void. Handling of Void
+ // without special-casing, as recommended by JSpecify might: a) require generics support
+ // and, b) require checking that third-party libraries considered annotated adopt
+ // JSpecify semantics.
+ // See the suppression in https://github.com/uber/NullAway/pull/608 for an example of why
+ // this is needed.
+ argumentPositionNullness[i] = Nullness.NULLABLE;
+ } else {
+ // we need to call paramHasNullableAnnotation here since the invoked method may be defined
+ // in a class file
+ argumentPositionNullness[i] =
+ Nullness.paramHasNullableAnnotation(methodSymbol, i, config)
+ ? Nullness.NULLABLE
+ : ((config.isJSpecifyMode() && tree instanceof MethodInvocationTree)
+ ? GenericsChecks.getGenericParameterNullnessAtInvocation(
+ i, methodSymbol, (MethodInvocationTree) tree, state, config)
+ : Nullness.NONNULL);
}
}
- nonNullPositions = builder.build();
+ GenericsChecks.compareGenericTypeParameterNullabilityForCall(
+ formalParams, actualParams, methodSymbol.isVarArgs(), this, state);
}
+ // Allow handlers to override the list of non-null argument positions
+ argumentPositionNullness =
+ handler.onOverrideMethodInvocationParametersNullability(
+ state.context, methodSymbol, isMethodAnnotated, argumentPositionNullness);
+
// now actually check the arguments
// NOTE: the case of an invocation on a possibly-null reference
// is handled by matchMemberSelect()
- for (int argPos : nonNullPositions) {
+ for (int argPos = 0; argPos < argumentPositionNullness.length; argPos++) {
+ if (!Objects.equals(Nullness.NONNULL, argumentPositionNullness[argPos])) {
+ continue;
+ }
ExpressionTree actual = null;
boolean mayActualBeNull = false;
if (argPos == formalParams.size() - 1 && methodSymbol.isVarArgs()) {
@@ -1387,12 +1748,10 @@ public class NullAway extends BugChecker
"passing @Nullable parameter '"
+ state.getSourceForNode(actual)
+ "' where @NonNull is required";
+ ErrorMessage errorMessage = new ErrorMessage(MessageTypes.PASS_NULLABLE, message);
state.reportMatch(
errorBuilder.createErrorDescriptionForNullAssignment(
- new ErrorMessage(MessageTypes.PASS_NULLABLE, message),
- actual,
- buildDescription(actual),
- state));
+ errorMessage, actual, buildDescription(actual), state, formalParams.get(argPos)));
}
}
// Check for @NonNull being passed to castToNonNull (if configured)
@@ -1406,12 +1765,19 @@ public class NullAway extends BugChecker
List<? extends ExpressionTree> actualParams) {
String qualifiedName =
ASTHelpers.enclosingClass(methodSymbol) + "." + methodSymbol.getSimpleName().toString();
- if (qualifiedName.equals(config.getCastToNonNullMethod())) {
- if (actualParams.size() != 1) {
- throw new RuntimeException(
- "Invalid number of parameters passed to configured CastToNonNullMethod.");
- }
- ExpressionTree actual = actualParams.get(0);
+ Integer castToNonNullPosition;
+ if (qualifiedName.equals(config.getCastToNonNullMethod())
+ && methodSymbol.getParameters().size() == 1) {
+ // castToNonNull method passed to CLI config, it acts as a cast-to-non-null on its first
+ // argument. Since this is a single argument method, we skip further querying of handlers.
+ castToNonNullPosition = 0;
+ } else {
+ castToNonNullPosition =
+ handler.castToNonNullArgumentPositionsForMethod(
+ this, state, methodSymbol, actualParams, null);
+ }
+ if (castToNonNullPosition != null) {
+ ExpressionTree actual = actualParams.get(castToNonNullPosition);
TreePath enclosingMethodOrLambda =
NullabilityUtil.findEnclosingMethodOrLambdaOrInitializer(state.getPath());
boolean isInitializer;
@@ -1432,13 +1798,18 @@ public class NullAway extends BugChecker
+ state.getSourceForNode(actual)
+ "' to CastToNonNullMethod ("
+ qualifiedName
- + "). This method should only take arguments that NullAway considers @Nullable "
+ + ") at position "
+ + castToNonNullPosition
+ + ". This method argument should only take values that NullAway considers @Nullable "
+ "at the invocation site, but which are known not to be null at runtime.";
return errorBuilder.createErrorDescription(
new ErrorMessage(MessageTypes.CAST_TO_NONNULL_ARG_NONNULL, message),
- tree,
+ // The Tree passed as suggestTree is the expression being cast
+ // to avoid recomputing the arg index:
+ actual,
buildDescription(tree),
- state);
+ state,
+ null);
}
}
return Description.NO_MATCH;
@@ -1491,7 +1862,8 @@ public class NullAway extends BugChecker
} else if (constructorInitInfo == null) {
// report it on the field, except in the case where the class is externalInit and
// we have no initializer methods
- if (!(isExternalInit(classSymbol) && entities.instanceInitializerMethods().isEmpty())) {
+ if (!(symbolHasExternalInitAnnotation(classSymbol)
+ && entities.instanceInitializerMethods().isEmpty())) {
errorBuilder.reportInitErrorOnField(
uninitField, state, buildDescription(getTreesInstance(state).getTree(uninitField)));
}
@@ -1506,11 +1878,17 @@ public class NullAway extends BugChecker
}
}
for (Element constructorElement : errorFieldsForInitializer.keySet()) {
+ ImmutableList<Symbol> fieldSymbols =
+ errorFieldsForInitializer.get(constructorElement).stream()
+ .map(element -> ASTHelpers.getSymbol(getTreesInstance(state).getTree(element)))
+ .collect(ImmutableList.toImmutableList());
+
errorBuilder.reportInitializerError(
(Symbol.MethodSymbol) constructorElement,
errMsgForInitializer(errorFieldsForInitializer.get(constructorElement), state),
state,
- buildDescription(getTreesInstance(state).getTree(constructorElement)));
+ buildDescription(getTreesInstance(state).getTree(constructorElement)),
+ fieldSymbols);
}
// For static fields
Set<Symbol> notInitializedStaticFields = notInitializedStatic(entities, state);
@@ -1592,12 +1970,14 @@ public class NullAway extends BugChecker
SetMultimap<MethodTree, Symbol> result = LinkedHashMultimap.create();
Set<Symbol> nonnullInstanceFields = entities.nonnullInstanceFields();
Trees trees = getTreesInstance(state);
- boolean isExternalInit = isExternalInit(entities.classSymbol());
+ boolean isExternalInitClass = symbolHasExternalInitAnnotation(entities.classSymbol());
for (MethodTree constructor : entities.constructors()) {
if (constructorInvokesAnother(constructor, state)) {
continue;
}
- if (constructor.getParameters().size() == 0 && isExternalInit) {
+ if (constructor.getParameters().size() == 0
+ && (isExternalInitClass
+ || symbolHasExternalInitAnnotation(ASTHelpers.getSymbol(constructor)))) {
// external framework initializes fields in this case
continue;
}
@@ -1612,8 +1992,9 @@ public class NullAway extends BugChecker
return result;
}
- private boolean isExternalInit(Symbol.ClassSymbol classSymbol) {
- return StreamSupport.stream(NullabilityUtil.getAllAnnotations(classSymbol).spliterator(), false)
+ private boolean symbolHasExternalInitAnnotation(Symbol symbol) {
+ return StreamSupport.stream(
+ NullabilityUtil.getAllAnnotations(symbol, config).spliterator(), false)
.map((anno) -> anno.getAnnotationType().toString())
.anyMatch(config::isExternalInitClassAnnotation);
}
@@ -1745,7 +2126,7 @@ public class NullAway extends BugChecker
&& !symbol.isStatic()
&& !modifiers.contains(Modifier.NATIVE)) {
// check it's the same class (could be an issue with inner classes)
- if (ASTHelpers.enclosingClass(symbol).equals(enclosingClassSymbol)) {
+ if (castToNonNull(ASTHelpers.enclosingClass(symbol)).equals(enclosingClassSymbol)) {
// make sure the receiver is 'this'
ExpressionTree receiver = ASTHelpers.getReceiver(expressionTree);
return receiver == null || isThisIdentifier(receiver);
@@ -1813,7 +2194,7 @@ public class NullAway extends BugChecker
// matchVariable()
continue;
}
- if (fieldSymbol.isStatic()) {
+ if (isStatic(fieldSymbol)) {
nonnullStaticFields.add(fieldSymbol);
} else {
nonnullInstanceFields.add(fieldSymbol);
@@ -1870,24 +2251,23 @@ public class NullAway extends BugChecker
}
private boolean skipDueToFieldAnnotation(Symbol fieldSymbol) {
- return NullabilityUtil.getAllAnnotations(fieldSymbol)
+ return NullabilityUtil.getAllAnnotations(fieldSymbol, config)
.map(anno -> anno.getAnnotationType().toString())
.anyMatch(config::isExcludedFieldAnnotation);
}
+ // classSymbol must be a top-level class
private boolean isExcludedClass(Symbol.ClassSymbol classSymbol) {
String className = classSymbol.getQualifiedName().toString();
if (config.isExcludedClass(className)) {
return true;
}
- if (!config.fromAnnotatedPackage(classSymbol)) {
+ if (!codeAnnotationInfo.isClassNullAnnotated(classSymbol, config)) {
return true;
}
// check annotations
ImmutableSet<String> excludedClassAnnotations = config.getExcludedClassAnnotations();
- return classSymbol
- .getAnnotationMirrors()
- .stream()
+ return classSymbol.getAnnotationMirrors().stream()
.map(anno -> anno.getAnnotationType().toString())
.anyMatch(excludedClassAnnotations::contains);
}
@@ -1899,18 +2279,13 @@ public class NullAway extends BugChecker
// obviously not null
return false;
}
- // the logic here is to avoid doing dataflow analysis whenever possible
- Symbol exprSymbol = ASTHelpers.getSymbol(expr);
- boolean exprMayBeNull;
+ // return early for expressions that no handler overrides and will not need dataflow analysis
switch (expr.getKind()) {
case NULL_LITERAL:
// obviously null
- exprMayBeNull = true;
- break;
+ return true;
case ARRAY_ACCESS:
// unsound! we cannot check for nullness of array contents yet
- exprMayBeNull = false;
- break;
case NEW_CLASS:
case NEW_ARRAY:
// for string concatenation, auto-boxing
@@ -1938,6 +2313,7 @@ public class NullAway extends BugChecker
case REMAINDER:
case CONDITIONAL_AND:
case CONDITIONAL_OR:
+ case BITWISE_COMPLEMENT:
case LOGICAL_COMPLEMENT:
case INSTANCE_OF:
case PREFIX_INCREMENT:
@@ -1959,60 +2335,79 @@ public class NullAway extends BugChecker
case RIGHT_SHIFT:
case UNSIGNED_RIGHT_SHIFT:
// clearly not null
- exprMayBeNull = false;
+ return false;
+ default:
break;
+ }
+ // the logic here is to avoid doing dataflow analysis whenever possible
+ Symbol exprSymbol = ASTHelpers.getSymbol(expr);
+ boolean exprMayBeNull;
+ switch (expr.getKind()) {
case MEMBER_SELECT:
if (exprSymbol == null) {
throw new IllegalStateException(
"unexpected null symbol for dereference expression " + state.getSourceForNode(expr));
}
- exprMayBeNull = mayBeNullFieldAccess(state, expr, exprSymbol);
+ exprMayBeNull =
+ NullabilityUtil.mayBeNullFieldFromType(exprSymbol, config, codeAnnotationInfo);
break;
case IDENTIFIER:
if (exprSymbol == null) {
throw new IllegalStateException(
"unexpected null symbol for identifier " + state.getSourceForNode(expr));
}
- if (exprSymbol.getKind().equals(ElementKind.FIELD)) {
- // Special case: mayBeNullFieldAccess runs handler.onOverrideMayBeNullExpr before
- // dataflow.
- return mayBeNullFieldAccess(state, expr, exprSymbol);
+ if (exprSymbol.getKind() == ElementKind.FIELD) {
+ exprMayBeNull =
+ NullabilityUtil.mayBeNullFieldFromType(exprSymbol, config, codeAnnotationInfo);
} else {
- // Check handler.onOverrideMayBeNullExpr before dataflow.
- exprMayBeNull = handler.onOverrideMayBeNullExpr(this, expr, state, true);
- return exprMayBeNull ? nullnessFromDataflow(state, expr) : false;
+ // rely on dataflow analysis for local variables
+ exprMayBeNull = true;
}
+ break;
case METHOD_INVOCATION:
- // Special case: mayBeNullMethodCall runs handler.onOverrideMayBeNullExpr before dataflow.
- return mayBeNullMethodCall(state, expr, (Symbol.MethodSymbol) exprSymbol);
+ if (!(exprSymbol instanceof Symbol.MethodSymbol)) {
+ throw new IllegalStateException(
+ "unexpected symbol "
+ + exprSymbol
+ + " for method invocation "
+ + state.getSourceForNode(expr));
+ }
+ exprMayBeNull =
+ mayBeNullMethodCall(
+ (Symbol.MethodSymbol) exprSymbol, (MethodInvocationTree) expr, state);
+ break;
case CONDITIONAL_EXPRESSION:
case ASSIGNMENT:
- exprMayBeNull = nullnessFromDataflow(state, expr);
+ exprMayBeNull = true;
break;
default:
// match switch expressions by comparing strings, so the code compiles on JDK versions < 12
if (expr.getKind().name().equals("SWITCH_EXPRESSION")) {
- exprMayBeNull = nullnessFromDataflow(state, expr);
+ exprMayBeNull = true;
} else {
throw new RuntimeException(
"whoops, better handle " + expr.getKind() + " " + state.getSourceForNode(expr));
}
}
- exprMayBeNull = handler.onOverrideMayBeNullExpr(this, expr, state, exprMayBeNull);
- return exprMayBeNull;
+ exprMayBeNull = handler.onOverrideMayBeNullExpr(this, expr, exprSymbol, state, exprMayBeNull);
+ return exprMayBeNull && nullnessFromDataflow(state, expr);
}
private boolean mayBeNullMethodCall(
- VisitorState state, ExpressionTree expr, Symbol.MethodSymbol exprSymbol) {
- boolean exprMayBeNull = true;
- if (NullabilityUtil.isUnannotated(exprSymbol, config)) {
- exprMayBeNull = false;
+ Symbol.MethodSymbol exprSymbol, MethodInvocationTree invocationTree, VisitorState state) {
+ if (codeAnnotationInfo.isSymbolUnannotated(exprSymbol, config)) {
+ return false;
}
- if (!Nullness.hasNullableAnnotation(exprSymbol, config)) {
- exprMayBeNull = false;
+ if (Nullness.hasNullableAnnotation(exprSymbol, config)) {
+ return true;
}
- exprMayBeNull = handler.onOverrideMayBeNullExpr(this, expr, state, exprMayBeNull);
- return exprMayBeNull ? nullnessFromDataflow(state, expr) : false;
+ if (config.isJSpecifyMode()
+ && GenericsChecks.getGenericReturnNullnessAtInvocation(
+ exprSymbol, invocationTree, state, config)
+ .equals(Nullness.NULLABLE)) {
+ return true;
+ }
+ return false;
}
public boolean nullnessFromDataflow(VisitorState state, ExpressionTree expr) {
@@ -2030,15 +2425,6 @@ public class NullAway extends BugChecker
return AccessPathNullnessAnalysis.instance(state, nonAnnotatedMethod, config, this.handler);
}
- private boolean mayBeNullFieldAccess(VisitorState state, ExpressionTree expr, Symbol exprSymbol) {
- boolean exprMayBeNull = true;
- if (!NullabilityUtil.mayBeNullFieldFromType(exprSymbol, config)) {
- exprMayBeNull = false;
- }
- exprMayBeNull = handler.onOverrideMayBeNullExpr(this, expr, state, exprMayBeNull);
- return exprMayBeNull ? nullnessFromDataflow(state, expr) : false;
- }
-
private Description matchDereference(
ExpressionTree baseExpression, ExpressionTree derefExpression, VisitorState state) {
Symbol baseExpressionSymbol = ASTHelpers.getSymbol(baseExpression);
@@ -2059,62 +2445,23 @@ public class NullAway extends BugChecker
ErrorMessage errorMessage = new ErrorMessage(MessageTypes.DEREFERENCE_NULLABLE, message);
return errorBuilder.createErrorDescriptionForNullAssignment(
- errorMessage, baseExpression, buildDescription(derefExpression), state);
+ errorMessage, baseExpression, buildDescription(derefExpression), state, null);
}
Optional<ErrorMessage> handlerErrorMessage =
handler.onExpressionDereference(derefExpression, baseExpression, state);
if (handlerErrorMessage.isPresent()) {
return errorBuilder.createErrorDescriptionForNullAssignment(
- handlerErrorMessage.get(), derefExpression, buildDescription(derefExpression), state);
+ handlerErrorMessage.get(),
+ derefExpression,
+ buildDescription(derefExpression),
+ state,
+ null);
}
return Description.NO_MATCH;
}
- @SuppressWarnings("unused")
- private Description.Builder changeReturnNullabilityFix(
- Tree suggestTree, Description.Builder builder, VisitorState state) {
- if (suggestTree.getKind() != Tree.Kind.METHOD) {
- throw new RuntimeException("This should be a MethodTree");
- }
- SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
- MethodTree methodTree = (MethodTree) suggestTree;
- int countNullableAnnotations = 0;
- for (AnnotationTree annotationTree : methodTree.getModifiers().getAnnotations()) {
- if (state.getSourceForNode(annotationTree.getAnnotationType()).endsWith("Nullable")) {
- fixBuilder.delete(annotationTree);
- countNullableAnnotations += 1;
- }
- }
- assert countNullableAnnotations > 1;
- return builder.addFix(fixBuilder.build());
- }
-
- @SuppressWarnings("unused")
- private Description.Builder changeParamNullabilityFix(
- Tree suggestTree, Description.Builder builder) {
- return builder.addFix(SuggestedFix.prefixWith(suggestTree, "@Nullable "));
- }
-
- @SuppressWarnings("unused")
- private int depth(ExpressionTree expression) {
- switch (expression.getKind()) {
- case MEMBER_SELECT:
- MemberSelectTree selectTree = (MemberSelectTree) expression;
- return 1 + depth(selectTree.getExpression());
- case METHOD_INVOCATION:
- MethodInvocationTree invTree = (MethodInvocationTree) expression;
- return depth(invTree.getMethodSelect());
- case IDENTIFIER:
- IdentifierTree varTree = (IdentifierTree) expression;
- Symbol symbol = ASTHelpers.getSymbol(varTree);
- return symbol.getKind().equals(ElementKind.FIELD) ? 2 : 1;
- default:
- return 0;
- }
- }
-
private static boolean isThisIdentifier(ExpressionTree expressionTree) {
return expressionTree.getKind().equals(IDENTIFIER)
&& ((IdentifierTree) expressionTree).getName().toString().equals("this");
@@ -2172,11 +2519,7 @@ public class NullAway extends BugChecker
* @return computed nullness for e, if any, else Nullable
*/
public Nullness getComputedNullness(ExpressionTree e) {
- if (computedNullnessMap.containsKey(e)) {
- return computedNullnessMap.get(e);
- } else {
- return Nullness.NULLABLE;
- }
+ return computedNullnessMap.getOrDefault(e, Nullness.NULLABLE);
}
/**
@@ -2215,46 +2558,46 @@ public class NullAway extends BugChecker
ImmutableSet.copyOf(staticInitializerMethods));
}
- /** @return symbol for class */
+ /** Returns symbol for class. */
abstract Symbol.ClassSymbol classSymbol();
/**
- * @return <code>@NonNull</code> instance fields that are not directly initialized at
- * declaration
+ * Returns <code>@NonNull</code> instance fields that are not directly initialized at
+ * declaration.
*/
abstract ImmutableSet<Symbol> nonnullInstanceFields();
/**
- * @return <code>@NonNull</code> static fields that are not directly initialized at declaration
+ * Returns <code>@NonNull</code> static fields that are not directly initialized at declaration.
*/
abstract ImmutableSet<Symbol> nonnullStaticFields();
/**
- * @return the list of instance initializer blocks (e.g. blocks of the form `class X { { //Code
- * } } ), in the order in which they appear in the class
+ * Returns the list of instance initializer blocks (e.g. blocks of the form `class X { { //Code
+ * } } ), in the order in which they appear in the class.
*/
abstract ImmutableList<BlockTree> instanceInitializerBlocks();
/**
- * @return the list of static initializer blocks (e.g. blocks of the form `class X { static {
- * //Code } } ), in the order in which they appear in the class
+ * Returns the list of static initializer blocks (e.g. blocks of the form `class X { static {
+ * //Code } } ), in the order in which they appear in the class.
*/
abstract ImmutableList<BlockTree> staticInitializerBlocks();
- /** @return the list of constructor */
+ /** Returns constructors in the class. */
abstract ImmutableSet<MethodTree> constructors();
/**
- * @return the list of non-static (instance) initializer methods. This includes methods
- * annotated @Initializer, as well as those specified by -XepOpt:NullAway:KnownInitializers
- * or annotated with annotations passed to -XepOpt:NullAway:CustomInitializerAnnotations
+ * Returns the list of non-static (instance) initializer methods. This includes methods
+ * annotated @Initializer, as well as those specified by -XepOpt:NullAway:KnownInitializers or
+ * annotated with annotations passed to -XepOpt:NullAway:CustomInitializerAnnotations.
*/
abstract ImmutableSet<MethodTree> instanceInitializerMethods();
/**
- * @return the list of static initializer methods. This includes static methods
- * annotated @Initializer, as well as those specified by -XepOpt:NullAway:KnownInitializers
- * or annotated with annotations passed to -XepOpt:NullAway:CustomInitializerAnnotations
+ * Returns the list of static initializer methods. This includes static methods
+ * annotated @Initializer, as well as those specified by -XepOpt:NullAway:KnownInitializers or
+ * annotated with annotations passed to -XepOpt:NullAway:CustomInitializerAnnotations.
*/
abstract ImmutableSet<MethodTree> staticInitializerMethods();
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java b/nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java
index 35f056a..2a04d46 100644
--- a/nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java
+++ b/nullaway/src/main/java/com/uber/nullaway/NullabilityUtil.java
@@ -41,6 +41,7 @@ import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.TargetType;
import com.sun.tools.javac.code.Type;
+import com.sun.tools.javac.code.TypeAnnotationPosition.TypePathEntry;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.util.JCDiagnostic;
@@ -57,6 +58,8 @@ import org.checkerframework.nullaway.javacutil.AnnotationUtils;
/** Helpful utility methods for nullability analysis. */
public class NullabilityUtil {
+ public static final String NULLMARKED_SIMPLE_NAME = "NullMarked";
+ public static final String NULLUNMARKED_SIMPLE_NAME = "NullUnmarked";
private static final Supplier<Type> MAP_TYPE_SUPPLIER = Suppliers.typeFromString("java.util.Map");
@@ -122,27 +125,6 @@ public class NullabilityUtil {
}
return null;
}
- /**
- * finds the symbol for the top-level class containing the given symbol
- *
- * @param symbol the given symbol
- * @return symbol for the non-nested enclosing class
- */
- public static Symbol.ClassSymbol getOutermostClassSymbol(Symbol symbol) {
- // get the symbol for the outermost enclosing class. this handles
- // the case of anonymous classes
- Symbol.ClassSymbol outermostClassSymbol = ASTHelpers.enclosingClass(symbol);
- while (outermostClassSymbol.getNestingKind().isNested()) {
- Symbol.ClassSymbol enclosingSymbol = ASTHelpers.enclosingClass(outermostClassSymbol.owner);
- if (enclosingSymbol != null) {
- outermostClassSymbol = enclosingSymbol;
- } else {
- // enclosingSymbol can be null in weird cases like for array methods
- break;
- }
- }
- return outermostClassSymbol;
- }
/**
* find the enclosing method, lambda expression or initializer block for the leaf of some tree
@@ -198,9 +180,9 @@ public class NullabilityUtil {
* @param symbol the symbol
* @return all annotations on the symbol and on the type of the symbol
*/
- public static Stream<? extends AnnotationMirror> getAllAnnotations(Symbol symbol) {
+ public static Stream<? extends AnnotationMirror> getAllAnnotations(Symbol symbol, Config config) {
// for methods, we care about annotations on the return type, not on the method type itself
- Stream<? extends AnnotationMirror> typeUseAnnotations = getTypeUseAnnotations(symbol);
+ Stream<? extends AnnotationMirror> typeUseAnnotations = getTypeUseAnnotations(symbol, config);
return Stream.concat(symbol.getAnnotationMirrors().stream(), typeUseAnnotations);
}
@@ -284,39 +266,99 @@ public class NullabilityUtil {
Symbol.VarSymbol varSymbol = symbol.getParameters().get(paramInd);
return Stream.concat(
varSymbol.getAnnotationMirrors().stream(),
- symbol
- .getRawTypeAttributes()
- .stream()
+ symbol.getRawTypeAttributes().stream()
.filter(
t ->
t.position.type.equals(TargetType.METHOD_FORMAL_PARAMETER)
&& t.position.parameter_index == paramInd));
}
- private static Stream<? extends AnnotationMirror> getTypeUseAnnotations(Symbol symbol) {
+ /**
+ * Gets the type use annotations on a symbol, ignoring annotations on components of the type (type
+ * arguments, wildcards, etc.)
+ */
+ private static Stream<? extends AnnotationMirror> getTypeUseAnnotations(
+ Symbol symbol, Config config) {
Stream<Attribute.TypeCompound> rawTypeAttributes = symbol.getRawTypeAttributes().stream();
if (symbol instanceof Symbol.MethodSymbol) {
- // for methods, we want the type-use annotations on the return type
- return rawTypeAttributes.filter((t) -> t.position.type.equals(TargetType.METHOD_RETURN));
+ // for methods, we want annotations on the return type
+ return rawTypeAttributes.filter(
+ (t) ->
+ t.position.type.equals(TargetType.METHOD_RETURN)
+ && isDirectTypeUseAnnotation(t, config));
+ } else {
+ // filter for annotations directly on the type
+ return rawTypeAttributes.filter(t -> NullabilityUtil.isDirectTypeUseAnnotation(t, config));
+ }
+ }
+
+ /**
+ * Check whether a type-use annotation should be treated as applying directly to the top-level
+ * type
+ *
+ * <p>For example {@code @Nullable List<T> lst} is a direct type use annotation of {@code lst},
+ * but {@code List<@Nullable T> lst} is not.
+ *
+ * @param t the annotation and its position in the type
+ * @param config NullAway configuration
+ * @return {@code true} if the annotation should be treated as applying directly to the top-level
+ * type, false otherwise
+ */
+ private static boolean isDirectTypeUseAnnotation(Attribute.TypeCompound t, Config config) {
+ // location is a list of TypePathEntry objects, indicating whether the annotation is
+ // on an array, inner type, wildcard, or type argument. If it's empty, then the
+ // annotation is directly on the type.
+ // We care about both annotations directly on the outer type and also those directly
+ // on an inner type or array dimension, but wish to discard annotations on wildcards,
+ // or type arguments.
+ // For arrays, outside JSpecify mode, we treat annotations on the outer type and on any
+ // dimension of the array as applying to the nullability of the array itself, not the elements.
+ // In JSpecify mode, annotations on array dimensions are *not* treated as applying to the
+ // top-level type, consistent with the JSpecify spec.
+ // We don't allow mixing of inner types and array dimensions in the same location
+ // (i.e. `Foo.@Nullable Bar []` is meaningless).
+ // These aren't correct semantics for type use annotations, but a series of hacky
+ // compromises to keep some semblance of backwards compatibility until we can do a
+ // proper deprecation of the incorrect behaviors for type use annotations when their
+ // semantics don't match those of a declaration annotation in the same position.
+ // See https://github.com/uber/NullAway/issues/708
+ boolean locationHasInnerTypes = false;
+ boolean locationHasArray = false;
+ for (TypePathEntry entry : t.position.location) {
+ switch (entry.tag) {
+ case INNER_TYPE:
+ locationHasInnerTypes = true;
+ break;
+ case ARRAY:
+ if (config.isJSpecifyMode()) {
+ // In JSpecify mode, annotations on array element types do not apply to the top-level
+ // type
+ return false;
+ }
+ locationHasArray = true;
+ break;
+ default:
+ // Wildcard or type argument!
+ return false;
+ }
}
- return rawTypeAttributes;
+ // Make sure it's not a mix of inner types and arrays for this annotation's location
+ return !(locationHasInnerTypes && locationHasArray);
}
/**
* Check if a field might be null, based on the type.
*
- * @param symbol symbol for field; must be non-null
+ * @param symbol symbol for field
* @param config NullAway config
* @return true if based on the type, package, and name of the field, the analysis should assume
* the field might be null; false otherwise
- * @throws NullPointerException if {@code symbol} is null
*/
- public static boolean mayBeNullFieldFromType(Symbol symbol, Config config) {
- Preconditions.checkNotNull(
- symbol, "mayBeNullFieldFromType should never be called with a null symbol");
+ public static boolean mayBeNullFieldFromType(
+ Symbol symbol, Config config, CodeAnnotationInfo codeAnnotationInfo) {
return !(symbol.getSimpleName().toString().equals("class")
|| symbol.isEnum()
- || isUnannotated(symbol, config))
+ || codeAnnotationInfo.isSymbolUnannotated(symbol, config))
&& Nullness.hasNullableAnnotation(symbol, config);
}
@@ -342,31 +384,6 @@ public class NullabilityUtil {
}
/**
- * Check if a symbol comes from unannotated code.
- *
- * @param symbol symbol for entity
- * @param config NullAway config
- * @return true if symbol represents an entity from a class that is unannotated; false otherwise
- */
- public static boolean isUnannotated(Symbol symbol, Config config) {
- Symbol.ClassSymbol outermostClassSymbol = getOutermostClassSymbol(symbol);
- return !config.fromAnnotatedPackage(outermostClassSymbol)
- || config.isUnannotatedClass(outermostClassSymbol);
- }
-
- /**
- * Check if a symbol comes from generated code.
- *
- * @param symbol symbol for entity
- * @return true if symbol represents an entity from a class annotated with {@code @Generated};
- * false otherwise
- */
- public static boolean isGenerated(Symbol symbol) {
- Symbol.ClassSymbol outermostClassSymbol = getOutermostClassSymbol(symbol);
- return ASTHelpers.hasDirectAnnotationWithSimpleName(outermostClassSymbol, "Generated");
- }
-
- /**
* Checks if {@code symbol} is a method on {@code java.util.Map} (or a subtype) with name {@code
* methodName} and {@code numParams} parameters
*/
@@ -381,4 +398,16 @@ public class NullabilityUtil {
Symbol owner = symbol.owner;
return ASTHelpers.isSubtype(owner.type, MAP_TYPE_SUPPLIER.get(state), state);
}
+
+ /**
+ * Downcasts a {@code @Nullable} argument to {@code NonNull}, returning the argument
+ *
+ * @throws NullPointerException if argument is {@code null}
+ */
+ public static <T> T castToNonNull(@Nullable T obj) {
+ if (obj == null) {
+ throw new NullPointerException("castToNonNull failed!");
+ }
+ return obj;
+ }
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/Nullness.java b/nullaway/src/main/java/com/uber/nullaway/Nullness.java
index d8a042a..845d547 100644
--- a/nullaway/src/main/java/com/uber/nullaway/Nullness.java
+++ b/nullaway/src/main/java/com/uber/nullaway/Nullness.java
@@ -19,6 +19,8 @@
package com.uber.nullaway;
import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.code.Type;
+import com.sun.tools.javac.util.List;
import java.util.stream.Stream;
import javax.lang.model.element.AnnotationMirror;
import org.checkerframework.nullaway.dataflow.analysis.AbstractValue;
@@ -128,14 +130,14 @@ public enum Nullness implements AbstractValue<Nullness> {
return displayName;
}
- private static boolean hasNullableAnnotation(
+ public static boolean hasNullableAnnotation(
Stream<? extends AnnotationMirror> annotations, Config config) {
return annotations
.map(anno -> anno.getAnnotationType().toString())
.anyMatch(anno -> isNullableAnnotation(anno, config));
}
- private static boolean hasNonNullAnnotation(
+ public static boolean hasNonNullAnnotation(
Stream<? extends AnnotationMirror> annotations, Config config) {
return annotations
.map(anno -> anno.getAnnotationType().toString())
@@ -156,6 +158,12 @@ public enum Nullness implements AbstractValue<Nullness> {
|| annotName.endsWith(".checkerframework.checker.nullness.compatqual.NullableDecl")
// matches javax.annotation.CheckForNull and edu.umd.cs.findbugs.annotations.CheckForNull
|| annotName.endsWith(".CheckForNull")
+ // matches any of the multiple @ParametricNullness annotations used within Guava
+ // (see https://github.com/google/guava/issues/6126)
+ // We check the simple name first and the package prefix second for boolean short
+ // circuiting, as Guava includes
+ // many annotations
+ || (annotName.endsWith(".ParametricNullness") && annotName.startsWith("com.google.common."))
|| (config.acknowledgeAndroidRecent()
&& annotName.equals("androidx.annotation.RecentlyNullable"))
|| config.isCustomNullableAnnotation(annotName);
@@ -184,7 +192,7 @@ public enum Nullness implements AbstractValue<Nullness> {
* Config)}
*/
public static boolean hasNonNullAnnotation(Symbol symbol, Config config) {
- return hasNonNullAnnotation(NullabilityUtil.getAllAnnotations(symbol), config);
+ return hasNonNullAnnotation(NullabilityUtil.getAllAnnotations(symbol, config), config);
}
/**
@@ -195,7 +203,7 @@ public enum Nullness implements AbstractValue<Nullness> {
* Config)}
*/
public static boolean hasNullableAnnotation(Symbol symbol, Config config) {
- return hasNullableAnnotation(NullabilityUtil.getAllAnnotations(symbol), config);
+ return hasNullableAnnotation(NullabilityUtil.getAllAnnotations(symbol, config), config);
}
/**
@@ -204,10 +212,36 @@ public enum Nullness implements AbstractValue<Nullness> {
*/
public static boolean paramHasNullableAnnotation(
Symbol.MethodSymbol symbol, int paramInd, Config config) {
+ // We treat the (generated) equals() method of record types to have a @Nullable parameter, as
+ // the generated implementation handles null (as required by the contract of Object.equals())
+ if (isRecordEqualsParam(symbol, paramInd)) {
+ return true;
+ }
return hasNullableAnnotation(
NullabilityUtil.getAllAnnotationsForParameter(symbol, paramInd), config);
}
+ private static boolean isRecordEqualsParam(Symbol.MethodSymbol symbol, int paramInd) {
+ // Here we compare with toString() to preserve compatibility with JDK 11 (records only
+ // introduced in JDK 16)
+ if (!symbol.owner.getKind().toString().equals("RECORD")) {
+ return false;
+ }
+ if (!symbol.getSimpleName().contentEquals("equals")) {
+ return false;
+ }
+ // Check for a boolean return type and a single parameter of type java.lang.Object
+ Type type = symbol.type;
+ List<Type> parameterTypes = type.getParameterTypes();
+ if (!(type.getReturnType().toString().equals("boolean")
+ && parameterTypes != null
+ && parameterTypes.size() == 1
+ && parameterTypes.get(0).toString().equals("java.lang.Object"))) {
+ return false;
+ }
+ return paramInd == 0;
+ }
+
/**
* Does the parameter of {@code symbol} at {@code paramInd} have a {@code @NonNull} declaration or
* type-use annotation? This method works for methods defined in either source or class files.
diff --git a/nullaway/src/main/java/com/uber/nullaway/annotations/JacocoIgnoreGenerated.java b/nullaway/src/main/java/com/uber/nullaway/annotations/JacocoIgnoreGenerated.java
new file mode 100644
index 0000000..ca06d27
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/annotations/JacocoIgnoreGenerated.java
@@ -0,0 +1,14 @@
+package com.uber.nullaway.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to indicate to Jacoco that code coverage of a method or constructor should be ignored.
+ * Jacoco requires such annotations to have "Generated" in their name.
+ */
+@Retention(RetentionPolicy.CLASS)
+@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
+public @interface JacocoIgnoreGenerated {}
diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPath.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPath.java
index 96f4141..d4fa677 100644
--- a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPath.java
+++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPath.java
@@ -22,6 +22,8 @@
package com.uber.nullaway.dataflow;
+import static com.uber.nullaway.NullabilityUtil.castToNonNull;
+
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
@@ -33,9 +35,11 @@ import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import com.uber.nullaway.NullabilityUtil;
+import com.uber.nullaway.annotations.JacocoIgnoreGenerated;
+import java.util.ArrayDeque;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
import javax.lang.model.element.Element;
@@ -96,7 +100,8 @@ public final class AccessPath implements MapKey {
return IMMUTABLE_FIELD_PREFIX + fieldFQN;
}
- private final Root root;
+ /** Root of the access path. If {@code null}, the root is the receiver argument */
+ @Nullable private final Element root;
private final ImmutableList<AccessPathElement> elements;
@@ -105,15 +110,16 @@ public final class AccessPath implements MapKey {
*/
@Nullable private final MapKey mapGetArg;
- AccessPath(Root root, List<AccessPathElement> elements) {
- this.root = root;
- this.elements = ImmutableList.copyOf(elements);
- this.mapGetArg = null;
+ private AccessPath(@Nullable Element root, ImmutableList<AccessPathElement> elements) {
+ this(root, elements, null);
}
- private AccessPath(Root root, List<AccessPathElement> elements, MapKey mapGetArg) {
+ private AccessPath(
+ @Nullable Element root,
+ ImmutableList<AccessPathElement> elements,
+ @Nullable MapKey mapGetArg) {
this.root = root;
- this.elements = ImmutableList.copyOf(elements);
+ this.elements = elements;
this.mapGetArg = mapGetArg;
}
@@ -124,7 +130,7 @@ public final class AccessPath implements MapKey {
* @return access path representing the local
*/
public static AccessPath fromLocal(LocalVariableNode node) {
- return new AccessPath(new Root(node.getElement()), ImmutableList.of());
+ return new AccessPath(node.getElement(), ImmutableList.of());
}
/**
@@ -135,7 +141,7 @@ public final class AccessPath implements MapKey {
*/
static AccessPath fromVarDecl(VariableDeclarationNode node) {
Element elem = TreeUtils.elementFromDeclaration(node.getTree());
- return new AccessPath(new Root(elem), ImmutableList.of());
+ return new AccessPath(elem, ImmutableList.of());
}
/**
@@ -148,9 +154,12 @@ public final class AccessPath implements MapKey {
*/
@Nullable
static AccessPath fromFieldAccess(FieldAccessNode node, AccessPathContext apContext) {
- List<AccessPathElement> elements = new ArrayList<>();
- Root root = populateElementsRec(node, elements, apContext);
- return (root != null) ? new AccessPath(root, elements) : null;
+ return fromNodeAndContext(node, apContext);
+ }
+
+ @Nullable
+ private static AccessPath fromNodeAndContext(Node node, AccessPathContext apContext) {
+ return buildAccessPathRecursive(node, new ArrayDeque<>(), apContext, null);
}
/**
@@ -163,9 +172,9 @@ public final class AccessPath implements MapKey {
*/
@Nullable
static AccessPath fromMethodCall(
- MethodInvocationNode node, @Nullable VisitorState state, AccessPathContext apContext) {
- if (state != null && isMapGet(ASTHelpers.getSymbol(node.getTree()), state)) {
- return fromMapGetCall(node, apContext);
+ MethodInvocationNode node, VisitorState state, AccessPathContext apContext) {
+ if (isMapGet(ASTHelpers.getSymbol(node.getTree()), state)) {
+ return fromMapGetCall(node, state, apContext);
}
return fromVanillaMethodCall(node, apContext);
}
@@ -173,9 +182,15 @@ public final class AccessPath implements MapKey {
@Nullable
private static AccessPath fromVanillaMethodCall(
MethodInvocationNode node, AccessPathContext apContext) {
- List<AccessPathElement> elements = new ArrayList<>();
- Root root = populateElementsRec(node, elements, apContext);
- return (root != null) ? new AccessPath(root, elements) : null;
+ return fromNodeAndContext(node, apContext);
+ }
+
+ /**
+ * Returns an access path rooted at {@code newRoot} with the same elements and map-get argument as
+ * {@code origAP}
+ */
+ static AccessPath switchRoot(AccessPath origAP, Element newRoot) {
+ return new AccessPath(newRoot, origAP.elements, origAP.mapGetArg);
}
/**
@@ -190,13 +205,15 @@ public final class AccessPath implements MapKey {
@Nullable
public static AccessPath fromBaseAndElement(
Node base, Element element, AccessPathContext apContext) {
- List<AccessPathElement> elements = new ArrayList<>();
- Root root = populateElementsRec(base, elements, apContext);
- if (root == null) {
- return null;
- }
- elements.add(new AccessPathElement(element));
- return new AccessPath(root, elements);
+ return fromNodeElementAndContext(base, new AccessPathElement(element), apContext);
+ }
+
+ @Nullable
+ private static AccessPath fromNodeElementAndContext(
+ Node base, AccessPathElement apElement, AccessPathContext apContext) {
+ ArrayDeque<AccessPathElement> elements = new ArrayDeque<>();
+ elements.push(apElement);
+ return buildAccessPathRecursive(base, elements, apContext, null);
}
/**
@@ -205,7 +222,7 @@ public final class AccessPath implements MapKey {
* <p>IMPORTANT: Be careful with this method, the argument list is not the variable names of the
* method arguments, but rather the string representation of primitive-type compile-time constants
* or the name of static final fields of structurally immutable types (see {@link
- * #populateElementsRec(Node, List, AccessPathContext)}).
+ * #buildAccessPathRecursive(Node, ArrayDeque, AccessPathContext, MapKey)}).
*
* <p>This is used by a few specialized Handlers to set nullability around particular paths
* involving constants.
@@ -220,13 +237,8 @@ public final class AccessPath implements MapKey {
@Nullable
public static AccessPath fromBaseMethodAndConstantArgs(
Node base, Element method, List<String> constantArguments, AccessPathContext apContext) {
- List<AccessPathElement> elements = new ArrayList<>();
- Root root = populateElementsRec(base, elements, apContext);
- if (root == null) {
- return null;
- }
- elements.add(new AccessPathElement(method, constantArguments));
- return new AccessPath(root, elements);
+ return fromNodeElementAndContext(
+ base, new AccessPathElement(method, constantArguments), apContext);
}
/**
@@ -241,14 +253,12 @@ public final class AccessPath implements MapKey {
*/
@Nullable
public static AccessPath getForMapInvocation(
- MethodInvocationNode node, AccessPathContext apContext) {
+ MethodInvocationNode node, VisitorState state, AccessPathContext apContext) {
// For the receiver type for get, use the declared type of the receiver of the containsKey()
- // call.
- // Note that this may differ from the containing class of the resolved containsKey() method,
- // which
- // can be in a superclass (e.g., LinkedHashMap does not override containsKey())
+ // call. Note that this may differ from the containing class of the resolved containsKey()
+ // method, which can be in a superclass (e.g., LinkedHashMap does not override containsKey())
// assumption: map type will not both override containsKey() and inherit get()
- return fromMapGetCall(node, apContext);
+ return fromMapGetCall(node, state, apContext);
}
private static Node stripCasts(Node node) {
@@ -259,13 +269,14 @@ public final class AccessPath implements MapKey {
}
@Nullable
- private static MapKey argumentToMapKeySpecifier(Node argument, AccessPathContext apContext) {
+ private static MapKey argumentToMapKeySpecifier(
+ Node argument, VisitorState state, AccessPathContext apContext) {
// Required to have Node type match Tree type in some instances.
if (argument instanceof WideningConversionNode) {
argument = ((WideningConversionNode) argument).getOperand();
}
// A switch at the Tree level should be faster than multiple if checks at the Node level.
- switch (argument.getTree().getKind()) {
+ switch (castToNonNull(argument.getTree()).getKind()) {
case STRING_LITERAL:
return new StringMapKey(((StringLiteralNode) argument).getValue());
case INT_LITERAL:
@@ -279,46 +290,28 @@ public final class AccessPath implements MapKey {
// Check for int/long boxing.
if (target.getMethod().getSimpleName().toString().equals("valueOf")
&& arguments.size() == 1
- && receiver.getTree().getKind().equals(Tree.Kind.IDENTIFIER)
+ && castToNonNull(receiver.getTree()).getKind().equals(Tree.Kind.IDENTIFIER)
&& (receiver.toString().equals("Integer") || receiver.toString().equals("Long"))) {
- return argumentToMapKeySpecifier(arguments.get(0), apContext);
+ return argumentToMapKeySpecifier(arguments.get(0), state, apContext);
}
// Fine to fallthrough:
default:
// Every other type of expression, including variables, field accesses, new A(...), etc.
- return getAccessPathForNodeNoMapGet(argument, apContext); // Every AP is a MapKey too
+ return getAccessPathForNode(argument, state, apContext); // Every AP is a MapKey too
}
}
@Nullable
- private static AccessPath fromMapGetCall(MethodInvocationNode node, AccessPathContext apContext) {
+ private static AccessPath fromMapGetCall(
+ MethodInvocationNode node, VisitorState state, AccessPathContext apContext) {
Node argument = node.getArgument(0);
- MapKey mapKey = argumentToMapKeySpecifier(argument, apContext);
+ MapKey mapKey = argumentToMapKeySpecifier(argument, state, apContext);
if (mapKey == null) {
return null;
}
MethodAccessNode target = node.getTarget();
Node receiver = stripCasts(target.getReceiver());
- List<AccessPathElement> elements = new ArrayList<>();
- Root root = populateElementsRec(receiver, elements, apContext);
- if (root == null) {
- return null;
- }
- return new AccessPath(root, elements, mapKey);
- }
-
- /**
- * Gets corresponding AccessPath for node, if it exists. Does <emph>not</emph> handle calls to
- * <code>Map.get()</code>
- *
- * @param node AST node
- * @param apContext the current access path context information (see {@link
- * AccessPath.AccessPathContext}).
- * @return corresponding AccessPath if it exists; <code>null</code> otherwise
- */
- @Nullable
- public static AccessPath getAccessPathForNodeNoMapGet(Node node, AccessPathContext apContext) {
- return getAccessPathForNodeWithMapGet(node, null, apContext);
+ return buildAccessPathRecursive(receiver, new ArrayDeque<>(), apContext, mapKey);
}
/**
@@ -332,8 +325,8 @@ public final class AccessPath implements MapKey {
* @return corresponding AccessPath if it exists; <code>null</code> otherwise
*/
@Nullable
- public static AccessPath getAccessPathForNodeWithMapGet(
- Node node, @Nullable VisitorState state, AccessPathContext apContext) {
+ public static AccessPath getAccessPathForNode(
+ Node node, VisitorState state, AccessPathContext apContext) {
if (node instanceof LocalVariableNode) {
return fromLocal((LocalVariableNode) node);
} else if (node instanceof FieldAccessNode) {
@@ -356,36 +349,57 @@ public final class AccessPath implements MapKey {
Preconditions.checkArgument(
element.getKind().isField(),
"element must be of type: FIELD but received: " + element.getKind());
- Root root = new Root();
- return new AccessPath(root, Collections.singletonList(new AccessPathElement(element)));
+ return new AccessPath(null, ImmutableList.of(new AccessPathElement(element)));
}
private static boolean isBoxingMethod(Symbol.MethodSymbol methodSymbol) {
- return methodSymbol.isStatic()
- && methodSymbol.getSimpleName().contentEquals("valueOf")
- && methodSymbol.enclClass().packge().fullname.contentEquals("java.lang");
+ if (methodSymbol.isStatic() && methodSymbol.getSimpleName().contentEquals("valueOf")) {
+ Symbol.PackageSymbol enclosingPackage = ASTHelpers.enclosingPackage(methodSymbol.enclClass());
+ return enclosingPackage != null && enclosingPackage.fullname.contentEquals("java.lang");
+ }
+ return false;
}
+ /**
+ * A helper function that recursively builds an AccessPath from a CFG node.
+ *
+ * @param node the CFG node
+ * @param elements elements to append to the final access path.
+ * @param apContext context information, used to handle cases with constant arguments
+ * @param mapKey map key to be used as the map-get argument, or {@code null} if there is no key
+ * @return the final access path
+ */
@Nullable
- private static Root populateElementsRec(
- Node node, List<AccessPathElement> elements, AccessPathContext apContext) {
- Root result;
+ private static AccessPath buildAccessPathRecursive(
+ Node node,
+ ArrayDeque<AccessPathElement> elements,
+ AccessPathContext apContext,
+ @Nullable MapKey mapKey) {
+ AccessPath result;
if (node instanceof FieldAccessNode) {
FieldAccessNode fieldAccess = (FieldAccessNode) node;
if (fieldAccess.isStatic()) {
// this is the root
- result = new Root(fieldAccess.getElement());
+ result = new AccessPath(fieldAccess.getElement(), ImmutableList.copyOf(elements), mapKey);
} else {
// instance field access
- result = populateElementsRec(stripCasts(fieldAccess.getReceiver()), elements, apContext);
- elements.add(new AccessPathElement(fieldAccess.getElement()));
+ elements.push(new AccessPathElement(fieldAccess.getElement()));
+ result =
+ buildAccessPathRecursive(
+ stripCasts(fieldAccess.getReceiver()), elements, apContext, mapKey);
}
} else if (node instanceof MethodInvocationNode) {
MethodInvocationNode invocation = (MethodInvocationNode) node;
AccessPathElement accessPathElement;
MethodAccessNode accessNode = invocation.getTarget();
if (invocation.getArguments().size() == 0) {
- accessPathElement = new AccessPathElement(accessNode.getMethod());
+ Symbol.MethodSymbol symbol = ASTHelpers.getSymbol(invocation.getTree());
+ if (symbol.isStatic()) {
+ // a zero-argument static method call can be the root of an access path
+ return new AccessPath(symbol, ImmutableList.copyOf(elements), mapKey);
+ } else {
+ accessPathElement = new AccessPathElement(accessNode.getMethod());
+ }
} else {
List<String> constantArgumentValues = new ArrayList<>();
for (Node argumentNode : invocation.getArguments()) {
@@ -417,7 +431,8 @@ public final class AccessPath implements MapKey {
case IDENTIFIER: // check for CONST
// Check for a constant field (static final)
Symbol symbol = ASTHelpers.getSymbol(tree);
- if (symbol.getKind().equals(ElementKind.FIELD)) {
+ if (symbol instanceof Symbol.VarSymbol
+ && symbol.getKind().equals(ElementKind.FIELD)) {
Symbol.VarSymbol varSymbol = (Symbol.VarSymbol) symbol;
// From docs: getConstantValue() returns the value of this variable if this is a
// static final field initialized to a compile-time constant. Returns null
@@ -454,14 +469,16 @@ public final class AccessPath implements MapKey {
}
accessPathElement = new AccessPathElement(accessNode.getMethod(), constantArgumentValues);
}
- result = populateElementsRec(stripCasts(accessNode.getReceiver()), elements, apContext);
- elements.add(accessPathElement);
+ elements.push(accessPathElement);
+ result =
+ buildAccessPathRecursive(
+ stripCasts(accessNode.getReceiver()), elements, apContext, mapKey);
} else if (node instanceof LocalVariableNode) {
- result = new Root(((LocalVariableNode) node).getElement());
- } else if (node instanceof ThisNode) {
- result = new Root();
- } else if (node instanceof SuperNode) {
- result = new Root();
+ result =
+ new AccessPath(
+ ((LocalVariableNode) node).getElement(), ImmutableList.copyOf(elements), mapKey);
+ } else if (node instanceof ThisNode || node instanceof SuperNode) {
+ result = new AccessPath(null, ImmutableList.copyOf(elements), mapKey);
} else {
// don't handle any other cases
result = null;
@@ -483,13 +500,9 @@ public final class AccessPath implements MapKey {
@Nullable
public static AccessPath mapWithIteratorContentsKey(
Node mapNode, LocalVariableNode iterVar, AccessPathContext apContext) {
- List<AccessPathElement> elems = new ArrayList<>();
- Root root = populateElementsRec(mapNode, elems, apContext);
- if (root != null) {
- return new AccessPath(
- root, elems, new IteratorContentsKey((VariableElement) iterVar.getElement()));
- }
- return null;
+ IteratorContentsKey iterContentsKey =
+ new IteratorContentsKey((VariableElement) iterVar.getElement());
+ return buildAccessPathRecursive(mapNode, new ArrayDeque<>(), apContext, iterContentsKey);
}
/**
@@ -505,32 +518,30 @@ public final class AccessPath implements MapKey {
if (this == o) {
return true;
}
- if (!(o instanceof AccessPath)) {
+ if (o == null || getClass() != o.getClass()) {
return false;
}
-
AccessPath that = (AccessPath) o;
-
- if (!root.equals(that.root)) {
- return false;
- }
- if (!elements.equals(that.elements)) {
- return false;
- }
- return mapGetArg != null
- ? (that.mapGetArg != null && mapGetArg.equals(that.mapGetArg))
- : that.mapGetArg == null;
+ return Objects.equals(root, that.root)
+ && elements.equals(that.elements)
+ && Objects.equals(mapGetArg, that.mapGetArg);
}
@Override
public int hashCode() {
- int result = root.hashCode();
+ int result = 1;
+ result = 31 * result + (root != null ? root.hashCode() : 0);
result = 31 * result + elements.hashCode();
result = 31 * result + (mapGetArg != null ? mapGetArg.hashCode() : 0);
return result;
}
- public Root getRoot() {
+ /**
+ * Returns the root element of the access path. If the root is the receiver argument, returns
+ * {@code null}.
+ */
+ @Nullable
+ public Element getRoot() {
return root;
}
@@ -545,7 +556,14 @@ public final class AccessPath implements MapKey {
@Override
public String toString() {
- return "AccessPath{" + "root=" + root + ", elements=" + elements + '}';
+ return "AccessPath{"
+ + "root="
+ + (root == null ? "this" : root)
+ + ", elements="
+ + elements
+ + ", mapGetArg="
+ + mapGetArg
+ + '}';
}
private static boolean isMapGet(Symbol.MethodSymbol symbol, VisitorState state) {
@@ -557,77 +575,12 @@ public final class AccessPath implements MapKey {
}
public static boolean isMapPut(Symbol.MethodSymbol symbol, VisitorState state) {
- return NullabilityUtil.isMapMethod(symbol, state, "put", 2);
+ return NullabilityUtil.isMapMethod(symbol, state, "put", 2)
+ || NullabilityUtil.isMapMethod(symbol, state, "putIfAbsent", 2);
}
- /**
- * root of an access path; either a variable {@link javax.lang.model.element.Element} or <code>
- * this</code> (enclosing method receiver)
- */
- public static final class Root {
-
- /** does this represent the receiver? */
- private final boolean isMethodReceiver;
-
- @Nullable private final Element varElement;
-
- Root(Element varElement) {
- this.isMethodReceiver = false;
- this.varElement = Preconditions.checkNotNull(varElement);
- }
-
- /** for case when it represents the receiver */
- Root() {
- this.isMethodReceiver = true;
- this.varElement = null;
- }
-
- /**
- * Get the variable element of this access path root, if not representing <code>this</code>.
- *
- * @return the variable, if not representing 'this'
- */
- public Element getVarElement() {
- return Preconditions.checkNotNull(varElement);
- }
-
- /**
- * Check whether this access path root represents the receiver (i.e. <code>this</code>). s
- *
- * @return <code>true</code> if representing 'this', <code>false</code> otherwise
- */
- public boolean isReceiver() {
- return isMethodReceiver;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- Root root = (Root) o;
-
- if (isMethodReceiver != root.isMethodReceiver) {
- return false;
- }
- return varElement != null ? varElement.equals(root.varElement) : root.varElement == null;
- }
-
- @Override
- public int hashCode() {
- int result = (isMethodReceiver ? 1 : 0);
- result = 31 * result + (varElement != null ? varElement.hashCode() : 0);
- return result;
- }
-
- @Override
- public String toString() {
- return "Root{" + "isMethodReceiver=" + isMethodReceiver + ", varElement=" + varElement + '}';
- }
+ public static boolean isMapComputeIfAbsent(Symbol.MethodSymbol symbol, VisitorState state) {
+ return NullabilityUtil.isMapMethod(symbol, state, "computeIfAbsent", 2);
}
private static final class StringMapKey implements MapKey {
@@ -665,7 +618,14 @@ public final class AccessPath implements MapKey {
return Long.hashCode(this.key);
}
+ /**
+ * We ignore this method for code coverage since there is non-determinism somewhere deep in a
+ * Map implementation such that, depending on how AccessPaths get bucketed in the Map (which
+ * depends on non-deterministic hash codes), sometimes this method is called and sometimes it is
+ * not.
+ */
@Override
+ @JacocoIgnoreGenerated
public boolean equals(Object obj) {
if (obj instanceof NumericMapKey) {
return this.key == ((NumericMapKey) obj).key;
@@ -696,7 +656,14 @@ public final class AccessPath implements MapKey {
return iteratorVarElement;
}
+ /**
+ * We ignore this method for code coverage since there is non-determinism somewhere deep in a
+ * Map implementation such that, depending on how AccessPaths get bucketed in the Map (which
+ * depends on non-deterministic hash codes), sometimes this method is called and sometimes it is
+ * not.
+ */
@Override
+ @JacocoIgnoreGenerated
public boolean equals(Object o) {
if (this == o) {
return true;
@@ -745,7 +712,7 @@ public final class AccessPath implements MapKey {
/**
* Passes the set of structurally immutable types registered into this AccessPathContext.
*
- * <p>See {@link com.uber.nullaway.handlers.Handler.onRegisterImmutableTypes} for more info.
+ * <p>See {@link com.uber.nullaway.handlers.Handler#onRegisterImmutableTypes} for more info.
*
* @param immutableTypes the immutable types known to our dataflow analysis.
*/
@@ -760,6 +727,9 @@ public final class AccessPath implements MapKey {
* @return an access path context constructed from everything added to the builder
*/
public AccessPathContext build() {
+ if (immutableTypes == null) {
+ throw new IllegalStateException("must set immutable types before building");
+ }
return new AccessPathContext(immutableTypes);
}
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathElement.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathElement.java
index d30cd75..1111d79 100644
--- a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathElement.java
+++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathElement.java
@@ -3,6 +3,7 @@ package com.uber.nullaway.dataflow;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
import javax.annotation.Nullable;
import javax.lang.model.element.Element;
@@ -31,18 +32,12 @@ public final class AccessPathElement {
return this.javaElement;
}
- public ImmutableList<String> getConstantArguments() {
- return this.constantArguments;
- }
-
@Override
public boolean equals(Object obj) {
if (obj instanceof AccessPathElement) {
AccessPathElement otherNode = (AccessPathElement) obj;
return this.javaElement.equals(otherNode.javaElement)
- && (constantArguments == null
- ? otherNode.constantArguments == null
- : constantArguments.equals(otherNode.constantArguments));
+ && Objects.equals(constantArguments, otherNode.constantArguments);
} else {
return false;
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessAnalysis.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessAnalysis.java
index 6576472..e68f05e 100644
--- a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessAnalysis.java
+++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessAnalysis.java
@@ -18,6 +18,8 @@
package com.uber.nullaway.dataflow;
+import static com.uber.nullaway.NullabilityUtil.castToNonNull;
+
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.VisitorState;
@@ -35,6 +37,7 @@ import java.util.function.Predicate;
import javax.annotation.Nullable;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.Modifier;
import javax.lang.model.element.VariableElement;
import org.checkerframework.nullaway.dataflow.analysis.AnalysisResult;
import org.checkerframework.nullaway.dataflow.cfg.node.MethodAccessNode;
@@ -78,7 +81,7 @@ public final class AccessPathNullnessAnalysis {
config,
handler,
new CoreNullnessStoreInitializer());
- this.dataFlow = new DataFlow(config.assertsEnabled());
+ this.dataFlow = new DataFlow(config.assertsEnabled(), handler);
if (config.checkContracts()) {
this.contractNullnessPropagation =
@@ -138,7 +141,8 @@ public final class AccessPathNullnessAnalysis {
*/
@Nullable
public Nullness getNullnessForContractDataflow(TreePath exprPath, Context context) {
- return dataFlow.expressionDataflow(exprPath, context, contractNullnessPropagation);
+ return dataFlow.expressionDataflow(
+ exprPath, context, castToNonNull(contractNullnessPropagation));
}
/**
@@ -162,7 +166,8 @@ public final class AccessPathNullnessAnalysis {
Set<AccessPath> nonnullAccessPaths = nullnessResult.getAccessPathsWithValue(Nullness.NONNULL);
Set<Element> result = new LinkedHashSet<>();
for (AccessPath ap : nonnullAccessPaths) {
- if (ap.getRoot().isReceiver()) {
+ // A null root represents the receiver
+ if (ap.getRoot() == null) {
ImmutableList<AccessPathElement> elements = ap.getElements();
if (elements.size() == 1) {
Element elem = elements.get(0).getJavaElement();
@@ -206,7 +211,7 @@ public final class AccessPathNullnessAnalysis {
}
/**
- * Get nullness info for local variables before some node
+ * Get nullness info for local variables (and final fields) before some node
*
* @param path tree path to some AST node within a method / lambda / initializer
* @param state visitor state
@@ -220,14 +225,23 @@ public final class AccessPathNullnessAnalysis {
}
return store.filterAccessPaths(
(ap) -> {
- if (ap.getElements().size() == 0) {
- AccessPath.Root root = ap.getRoot();
- if (!root.isReceiver()) {
- Element e = root.getVarElement();
- return e.getKind().equals(ElementKind.PARAMETER)
- || e.getKind().equals(ElementKind.LOCAL_VARIABLE);
+ boolean allAPNonRootElementsAreFinalFields = true;
+ for (AccessPathElement ape : ap.getElements()) {
+ Element e = ape.getJavaElement();
+ if (!e.getKind().equals(ElementKind.FIELD)
+ || !e.getModifiers().contains(Modifier.FINAL)) {
+ allAPNonRootElementsAreFinalFields = false;
+ break;
}
}
+ if (allAPNonRootElementsAreFinalFields) {
+ Element e = ap.getRoot();
+ return e == null // This is the case for: this(.f)* where each f is a final field.
+ || e.getKind().equals(ElementKind.PARAMETER)
+ || e.getKind().equals(ElementKind.LOCAL_VARIABLE)
+ || (e.getKind().equals(ElementKind.FIELD)
+ && e.getModifiers().contains(Modifier.FINAL));
+ }
return handler.includeApInfoInSavedContext(ap, state);
});
@@ -303,10 +317,9 @@ public final class AccessPathNullnessAnalysis {
Set<AccessPath> nonnullAccessPaths = nullnessResult.getAccessPathsWithValue(Nullness.NONNULL);
Set<Element> result = new LinkedHashSet<>();
for (AccessPath ap : nonnullAccessPaths) {
- assert !ap.getRoot().isReceiver();
- Element varElement = ap.getRoot().getVarElement();
- if (varElement.getKind().equals(ElementKind.FIELD)) {
- result.add(varElement);
+ Element element = ap.getRoot();
+ if (element != null && element.getKind().equals(ElementKind.FIELD)) {
+ result.add(element);
}
}
return result;
@@ -325,6 +338,7 @@ public final class AccessPathNullnessAnalysis {
* @param context Javac context
* @return the final NullnessStore on exit from the method.
*/
+ @Nullable
public NullnessStore forceRunOnMethod(TreePath methodPath, Context context) {
return dataFlow.finalResult(methodPath, context, nullnessPropagation);
}
@@ -343,9 +357,10 @@ public final class AccessPathNullnessAnalysis {
// We use the CFG to get the Node corresponding to the expression
Set<Node> exprNodes =
- dataFlow
- .getControlFlowGraph(exprPath, context, nullnessPropagation)
- .getNodesCorrespondingToTree(exprPath.getLeaf());
+ castToNonNull(
+ dataFlow
+ .getControlFlowGraph(exprPath, context, nullnessPropagation)
+ .getNodesCorrespondingToTree(exprPath.getLeaf()));
if (exprNodes.size() != 1) {
// Since the expression must have a single corresponding node
@@ -357,9 +372,7 @@ public final class AccessPathNullnessAnalysis {
AccessPath.fromBaseAndElement(exprNodes.iterator().next(), variableElement, apContext);
if (store != null && ap != null) {
- if (store
- .getAccessPathsWithValue(Nullness.NONNULL)
- .stream()
+ if (store.getAccessPathsWithValue(Nullness.NONNULL).stream()
.anyMatch(accessPath -> accessPath.equals(ap))) {
return Nullness.NONNULL;
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java
index 3cf5a79..36f35e4 100644
--- a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java
+++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java
@@ -16,6 +16,8 @@
package com.uber.nullaway.dataflow;
import static com.google.common.base.Preconditions.checkNotNull;
+import static com.uber.nullaway.ASTHelpersBackports.isStatic;
+import static com.uber.nullaway.NullabilityUtil.castToNonNull;
import static com.uber.nullaway.Nullness.BOTTOM;
import static com.uber.nullaway.Nullness.NONNULL;
import static com.uber.nullaway.Nullness.NULLABLE;
@@ -27,10 +29,13 @@ import com.google.errorprone.VisitorState;
import com.google.errorprone.suppliers.Supplier;
import com.google.errorprone.suppliers.Suppliers;
import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.MethodInvocationTree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.TypeTag;
+import com.uber.nullaway.CodeAnnotationInfo;
import com.uber.nullaway.Config;
+import com.uber.nullaway.GenericsChecks;
import com.uber.nullaway.NullabilityUtil;
import com.uber.nullaway.Nullness;
import com.uber.nullaway.handlers.Handler;
@@ -43,6 +48,7 @@ import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.TypeKind;
import org.checkerframework.nullaway.dataflow.analysis.ConditionalTransferResult;
import org.checkerframework.nullaway.dataflow.analysis.ForwardTransferFunction;
import org.checkerframework.nullaway.dataflow.analysis.RegularTransferResult;
@@ -66,9 +72,11 @@ import org.checkerframework.nullaway.dataflow.cfg.node.ClassNameNode;
import org.checkerframework.nullaway.dataflow.cfg.node.ConditionalAndNode;
import org.checkerframework.nullaway.dataflow.cfg.node.ConditionalNotNode;
import org.checkerframework.nullaway.dataflow.cfg.node.ConditionalOrNode;
+import org.checkerframework.nullaway.dataflow.cfg.node.DeconstructorPatternNode;
import org.checkerframework.nullaway.dataflow.cfg.node.DoubleLiteralNode;
import org.checkerframework.nullaway.dataflow.cfg.node.EqualToNode;
import org.checkerframework.nullaway.dataflow.cfg.node.ExplicitThisNode;
+import org.checkerframework.nullaway.dataflow.cfg.node.ExpressionStatementNode;
import org.checkerframework.nullaway.dataflow.cfg.node.FieldAccessNode;
import org.checkerframework.nullaway.dataflow.cfg.node.FloatLiteralNode;
import org.checkerframework.nullaway.dataflow.cfg.node.FloatingDivisionNode;
@@ -107,7 +115,6 @@ import org.checkerframework.nullaway.dataflow.cfg.node.PrimitiveTypeNode;
import org.checkerframework.nullaway.dataflow.cfg.node.ReturnNode;
import org.checkerframework.nullaway.dataflow.cfg.node.ShortLiteralNode;
import org.checkerframework.nullaway.dataflow.cfg.node.SignedRightShiftNode;
-import org.checkerframework.nullaway.dataflow.cfg.node.StringConcatenateAssignmentNode;
import org.checkerframework.nullaway.dataflow.cfg.node.StringConcatenateNode;
import org.checkerframework.nullaway.dataflow.cfg.node.StringConversionNode;
import org.checkerframework.nullaway.dataflow.cfg.node.StringLiteralNode;
@@ -171,7 +178,12 @@ public class AccessPathNullnessPropagation
}
private static SubNodeValues values(final TransferInput<Nullness, NullnessStore> input) {
- return input::getValueOfSubNode;
+ return new SubNodeValues() {
+ @Override
+ public Nullness valueOfSubNode(Node node) {
+ return castToNonNull(input.getValueOfSubNode(node));
+ }
+ };
}
/**
@@ -366,13 +378,6 @@ public class AccessPathNullnessPropagation
}
@Override
- public TransferResult<Nullness, NullnessStore> visitStringConcatenateAssignment(
- StringConcatenateAssignmentNode stringConcatenateAssignmentNode,
- TransferInput<Nullness, NullnessStore> input) {
- return noStoreChanges(NULLABLE, input);
- }
-
- @Override
public TransferResult<Nullness, NullnessStore> visitLessThan(
LessThanNode lessThanNode, TransferInput<Nullness, NullnessStore> input) {
return noStoreChanges(NONNULL, input);
@@ -448,14 +453,14 @@ public class AccessPathNullnessPropagation
Node realLeftNode = unwrapAssignExpr(leftNode);
Node realRightNode = unwrapAssignExpr(rightNode);
- AccessPath leftAP = AccessPath.getAccessPathForNodeWithMapGet(realLeftNode, state, apContext);
+ AccessPath leftAP = AccessPath.getAccessPathForNode(realLeftNode, state, apContext);
if (leftAP != null) {
equalBranchUpdates.set(leftAP, equalBranchValue);
notEqualBranchUpdates.set(
leftAP, leftVal.greatestLowerBound(rightVal.deducedValueWhenNotEqual()));
}
- AccessPath rightAP = AccessPath.getAccessPathForNodeWithMapGet(realRightNode, state, apContext);
+ AccessPath rightAP = AccessPath.getAccessPathForNode(realRightNode, state, apContext);
if (rightAP != null) {
equalBranchUpdates.set(rightAP, equalBranchValue);
notEqualBranchUpdates.set(
@@ -486,12 +491,10 @@ public class AccessPathNullnessPropagation
@Override
public TransferResult<Nullness, NullnessStore> visitTernaryExpression(
TernaryExpressionNode node, TransferInput<Nullness, NullnessStore> input) {
- SubNodeValues inputs = values(input);
- Nullness result =
- inputs
- .valueOfSubNode(node.getThenOperand())
- .leastUpperBound(inputs.valueOfSubNode(node.getElseOperand()));
- return new RegularTransferResult<>(result, input.getRegularStore());
+ // The cfg includes assignments of the value of the "then" and "else" sub-expressions to the
+ // synthetic variable for the ternary expression. So, the dataflow result for the ternary
+ // expression is just the result for the synthetic variable
+ return visitLocalVariable(node.getTernaryExpressionVar(), input);
}
@Override
@@ -512,7 +515,7 @@ public class AccessPathNullnessPropagation
Node target = node.getTarget();
if (target instanceof LocalVariableNode
- && !ASTHelpers.getType(target.getTree()).isPrimitive()) {
+ && !castToNonNull(ASTHelpers.getType(target.getTree())).isPrimitive()) {
LocalVariableNode localVariableNode = (LocalVariableNode) target;
updates.set(localVariableNode, value);
handleEnhancedForOverKeySet(localVariableNode, rhs, input, updates);
@@ -523,15 +526,19 @@ public class AccessPathNullnessPropagation
}
if (target instanceof FieldAccessNode) {
- // we don't allow arbitrary access paths to be tracked from assignments
- // here we still require an access of a field of this, or a static field
FieldAccessNode fieldAccessNode = (FieldAccessNode) target;
Node receiver = fieldAccessNode.getReceiver();
setNonnullIfAnalyzeable(updates, receiver);
- if ((receiver instanceof ThisNode || fieldAccessNode.isStatic())
- && fieldAccessNode.getElement().getKind().equals(ElementKind.FIELD)
- && !ASTHelpers.getType(target.getTree()).isPrimitive()) {
- updates.set(fieldAccessNode, value);
+ if (fieldAccessNode.getElement().getKind().equals(ElementKind.FIELD)
+ && !castToNonNull(ASTHelpers.getType(target.getTree())).isPrimitive()) {
+ if (receiver instanceof ThisNode || fieldAccessNode.isStatic()) {
+ // Guaranteed to produce a valid access path, we call updates.set
+ updates.set(fieldAccessNode, value);
+ } else {
+ // Might not be a valid access path, e.g. it might ultimately be rooted at (new Foo).f or
+ // some other expression that's not a valid AP root.
+ updates.tryAndSet(fieldAccessNode, value);
+ }
}
}
@@ -655,7 +662,7 @@ public class AccessPathNullnessPropagation
* the updates
*/
private void setNonnullIfAnalyzeable(Updates updates, Node node) {
- AccessPath ap = AccessPath.getAccessPathForNodeWithMapGet(node, state, apContext);
+ AccessPath ap = AccessPath.getAccessPathForNode(node, state, apContext);
if (ap != null) {
updates.set(ap, NONNULL);
}
@@ -666,8 +673,8 @@ public class AccessPathNullnessPropagation
}
private static boolean hasNonNullConstantValue(LocalVariableNode node) {
- if (node.getElement() instanceof VariableElement) {
- VariableElement element = (VariableElement) node.getElement();
+ VariableElement element = node.getElement();
+ if (element != null) {
return (element.getConstantValue() != null);
}
return false;
@@ -685,7 +692,8 @@ public class AccessPathNullnessPropagation
}
private static boolean isCatchVariable(VariableDeclarationNode node) {
- return elementFromDeclaration(node.getTree()).getKind() == EXCEPTION_PARAMETER;
+ VariableElement variableElement = elementFromDeclaration(node.getTree());
+ return variableElement != null && variableElement.getKind() == EXCEPTION_PARAMETER;
}
@Override
@@ -707,10 +715,33 @@ public class AccessPathNullnessPropagation
public TransferResult<Nullness, NullnessStore> visitFieldAccess(
FieldAccessNode fieldAccessNode, TransferInput<Nullness, NullnessStore> input) {
ReadableUpdates updates = new ReadableUpdates();
- Symbol symbol = ASTHelpers.getSymbol(fieldAccessNode.getTree());
+ Symbol symbol = Preconditions.checkNotNull(ASTHelpers.getSymbol(fieldAccessNode.getTree()));
setReceiverNonnull(updates, fieldAccessNode.getReceiver(), symbol);
Nullness nullness = NULLABLE;
- if (!NullabilityUtil.mayBeNullFieldFromType(symbol, config)) {
+ boolean fieldMayBeNull;
+ switch (handler.onDataflowVisitFieldAccess(
+ fieldAccessNode,
+ symbol,
+ state.getTypes(),
+ state.context,
+ apContext,
+ values(input),
+ updates)) {
+ case HINT_NULLABLE:
+ fieldMayBeNull = true;
+ break;
+ case FORCE_NONNULL:
+ fieldMayBeNull = false;
+ break;
+ case UNKNOWN:
+ fieldMayBeNull =
+ NullabilityUtil.mayBeNullFieldFromType(symbol, config, getCodeAnnotationInfo(state));
+ break;
+ default:
+ // Should be unreachable unless NullnessHint changes, cases above are exhaustive!
+ throw new RuntimeException("Unexpected NullnessHint from handler!");
+ }
+ if (!fieldMayBeNull) {
nullness = NONNULL;
} else {
nullness = input.getRegularStore().valueOfField(fieldAccessNode, nullness, apContext);
@@ -718,9 +749,18 @@ public class AccessPathNullnessPropagation
return updateRegularStore(nullness, input, updates);
}
+ @Nullable private CodeAnnotationInfo codeAnnotationInfo;
+
+ private CodeAnnotationInfo getCodeAnnotationInfo(VisitorState state) {
+ if (codeAnnotationInfo == null) {
+ codeAnnotationInfo = CodeAnnotationInfo.instance(state.context);
+ }
+ return codeAnnotationInfo;
+ }
+
private void setReceiverNonnull(
AccessPathNullnessPropagation.ReadableUpdates updates, Node receiver, Symbol symbol) {
- if (symbol != null && !symbol.isStatic()) {
+ if ((symbol != null) && !isStatic(symbol)) {
setNonnullIfAnalyzeable(updates, receiver);
}
}
@@ -841,8 +881,8 @@ public class AccessPathNullnessPropagation
}
AccessPath accessPath =
- AccessPath.getAccessPathForNodeNoMapGet(
- ((NotEqualNode) condition).getLeftOperand(), apContext);
+ AccessPath.getAccessPathForNode(
+ ((NotEqualNode) condition).getLeftOperand(), state, apContext);
if (accessPath == null) {
return noStoreChanges(NULLABLE, input);
@@ -873,22 +913,16 @@ public class AccessPathNullnessPropagation
ReadableUpdates elseUpdates = new ReadableUpdates();
ReadableUpdates bothUpdates = new ReadableUpdates();
Symbol.MethodSymbol callee = ASTHelpers.getSymbol(node.getTree());
- Preconditions.checkNotNull(callee);
+ Preconditions.checkNotNull(
+ callee); // this could be null before https://github.com/google/error-prone/pull/2902
setReceiverNonnull(bothUpdates, node.getTarget().getReceiver(), callee);
setNullnessForMapCalls(
node, callee, node.getArguments(), values(input), thenUpdates, bothUpdates);
NullnessHint nullnessHint =
handler.onDataflowVisitMethodInvocation(
- node,
- state.getTypes(),
- state.context,
- apContext,
- values(input),
- thenUpdates,
- elseUpdates,
- bothUpdates);
+ node, callee, state, apContext, values(input), thenUpdates, elseUpdates, bothUpdates);
Nullness nullness = returnValueNullness(node, input, nullnessHint);
- if (booleanReturnType(node)) {
+ if (booleanReturnType(callee)) {
ResultingStore thenStore = updateStore(input.getThenStore(), thenUpdates, bothUpdates);
ResultingStore elseStore = updateStore(input.getElseStore(), elseUpdates, bothUpdates);
return conditionalResult(
@@ -906,7 +940,7 @@ public class AccessPathNullnessPropagation
AccessPathNullnessPropagation.Updates bothUpdates) {
if (AccessPath.isContainsKey(callee, state)) {
// make sure argument is a variable, and get its element
- AccessPath getAccessPath = AccessPath.getForMapInvocation(node, apContext);
+ AccessPath getAccessPath = AccessPath.getForMapInvocation(node, state, apContext);
if (getAccessPath != null) {
// in the then branch, we want the get() call with the same argument to be non-null
// we assume that the declared target of the get() method will be in the same class
@@ -914,17 +948,41 @@ public class AccessPathNullnessPropagation
thenUpdates.set(getAccessPath, NONNULL);
}
} else if (AccessPath.isMapPut(callee, state)) {
- AccessPath getAccessPath = AccessPath.getForMapInvocation(node, apContext);
+ AccessPath getAccessPath = AccessPath.getForMapInvocation(node, state, apContext);
if (getAccessPath != null) {
Nullness value = inputs.valueOfSubNode(arguments.get(1));
bothUpdates.set(getAccessPath, value);
}
+ } else if (AccessPath.isMapComputeIfAbsent(callee, state)) {
+ AccessPath getAccessPath = AccessPath.getForMapInvocation(node, state, apContext);
+ if (getAccessPath != null) {
+ // TODO: For now, Function<K, V> implies a @NonNull V. We need to revisit this once we
+ // support generics, but we do include a couple defensive tests below.
+ if (arguments.size() < 2) {
+ return;
+ }
+ Node funcNode = arguments.get(1);
+ if (!funcNode.getType().getKind().equals(TypeKind.DECLARED)) {
+ return;
+ }
+ Type.ClassType classType = (Type.ClassType) funcNode.getType();
+ if (classType.getTypeArguments().size() != 2) {
+ return;
+ }
+ Type functionReturnType = classType.getTypeArguments().get(1);
+ // Unfortunately, functionReturnType.tsym seems to elide annotation info, so we can't call
+ // the Nullness.* methods that deal with Symbol. We might have better APIs for this kind of
+ // check once we have real generics support.
+ if (!Nullness.hasNullableAnnotation(
+ functionReturnType.getAnnotationMirrors().stream(), config)) {
+ bothUpdates.set(getAccessPath, NONNULL);
+ }
+ }
}
}
- private boolean booleanReturnType(MethodInvocationNode node) {
- Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(node.getTree());
- return methodSymbol != null && methodSymbol.getReturnType().getTag() == TypeTag.BOOLEAN;
+ private static boolean booleanReturnType(Symbol.MethodSymbol methodSymbol) {
+ return methodSymbol.getReturnType().getTag() == TypeTag.BOOLEAN;
}
Nullness returnValueNullness(
@@ -946,7 +1004,8 @@ public class AccessPathNullnessPropagation
nullness = input.getRegularStore().valueOfMethodCall(node, state, NULLABLE, apContext);
} else if (node == null
|| methodReturnsNonNull.test(node)
- || !Nullness.hasNullableAnnotation((Symbol) node.getTarget().getMethod(), config)) {
+ || (!Nullness.hasNullableAnnotation((Symbol) node.getTarget().getMethod(), config)
+ && !genericReturnIsNullable(node))) {
// definite non-null return
nullness = NONNULL;
} else {
@@ -956,6 +1015,27 @@ public class AccessPathNullnessPropagation
return nullness;
}
+ /**
+ * Computes the nullability of a generic return type in the context of some receiver type at an
+ * invocation.
+ *
+ * @param node the invocation node
+ * @return nullability of the return type in the context of the type of the receiver argument at
+ * {@code node}
+ */
+ private boolean genericReturnIsNullable(MethodInvocationNode node) {
+ if (node != null && config.isJSpecifyMode()) {
+ MethodInvocationTree tree = node.getTree();
+ if (tree != null) {
+ Nullness nullness =
+ GenericsChecks.getGenericReturnNullnessAtInvocation(
+ ASTHelpers.getSymbol(tree), tree, state, config);
+ return nullness.equals(NULLABLE);
+ }
+ }
+ return false;
+ }
+
@Override
public TransferResult<Nullness, NullnessStore> visitObjectCreation(
ObjectCreationNode objectCreationNode, TransferInput<Nullness, NullnessStore> input) {
@@ -1000,6 +1080,20 @@ public class AccessPathNullnessPropagation
}
@Override
+ public TransferResult<Nullness, NullnessStore> visitExpressionStatement(
+ ExpressionStatementNode expressionStatementNode,
+ TransferInput<Nullness, NullnessStore> input) {
+ return noStoreChanges(NULLABLE, input);
+ }
+
+ @Override
+ public TransferResult<Nullness, NullnessStore> visitDeconstructorPattern(
+ DeconstructorPatternNode deconstructorPatternNode,
+ TransferInput<Nullness, NullnessStore> input) {
+ return noStoreChanges(NULLABLE, input);
+ }
+
+ @Override
public TransferResult<Nullness, NullnessStore> visitPackageName(
PackageNameNode packageNameNode, TransferInput<Nullness, NullnessStore> input) {
return noStoreChanges(NULLABLE, input);
@@ -1062,6 +1156,9 @@ public class AccessPathNullnessPropagation
void set(FieldAccessNode node, Nullness value);
+ /** Like set, but ignore if node does not produce a valid access path */
+ void tryAndSet(FieldAccessNode node, Nullness value);
+
void set(MethodInvocationNode node, Nullness value);
void set(AccessPath ap, Nullness value);
@@ -1072,18 +1169,27 @@ public class AccessPathNullnessPropagation
@Override
public void set(LocalVariableNode node, Nullness value) {
- values.put(AccessPath.fromLocal(node), checkNotNull(value));
+ values.put(AccessPath.fromLocal(node), value);
}
@Override
public void set(VariableDeclarationNode node, Nullness value) {
- values.put(AccessPath.fromVarDecl(node), checkNotNull(value));
+ values.put(AccessPath.fromVarDecl(node), value);
}
@Override
public void set(FieldAccessNode node, Nullness value) {
AccessPath accessPath = AccessPath.fromFieldAccess(node, apContext);
- values.put(Preconditions.checkNotNull(accessPath), checkNotNull(value));
+ values.put(checkNotNull(accessPath), value);
+ }
+
+ @Override
+ public void tryAndSet(FieldAccessNode node, Nullness value) {
+ AccessPath accessPath = AccessPath.fromFieldAccess(node, apContext);
+ if (accessPath == null) {
+ return;
+ }
+ values.put(accessPath, value);
}
@Override
diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java
index c408940..2dfce9f 100644
--- a/nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java
+++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/CoreNullnessStoreInitializer.java
@@ -3,19 +3,22 @@ package com.uber.nullaway.dataflow;
import static com.uber.nullaway.Nullness.NONNULL;
import static com.uber.nullaway.Nullness.NULLABLE;
-import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.util.Context;
+import com.uber.nullaway.CodeAnnotationInfo;
import com.uber.nullaway.Config;
import com.uber.nullaway.NullabilityUtil;
import com.uber.nullaway.Nullness;
import com.uber.nullaway.handlers.Handler;
import java.util.List;
import java.util.Objects;
+import javax.annotation.Nullable;
import javax.lang.model.element.Element;
import org.checkerframework.nullaway.dataflow.cfg.UnderlyingAST;
import org.checkerframework.nullaway.dataflow.cfg.node.LocalVariableNode;
@@ -38,7 +41,13 @@ class CoreNullnessStoreInitializer extends NullnessStoreInitializer {
boolean isLambda = underlyingAST.getKind().equals(UnderlyingAST.Kind.LAMBDA);
if (isLambda) {
return lambdaInitialStore(
- (UnderlyingAST.CFGLambda) underlyingAST, parameters, handler, context, types, config);
+ (UnderlyingAST.CFGLambda) underlyingAST,
+ parameters,
+ handler,
+ context,
+ types,
+ config,
+ getCodeAnnotationInfo(context));
} else {
return methodInitialStore(
(UnderlyingAST.CFGMethod) underlyingAST, parameters, handler, context, config);
@@ -70,7 +79,8 @@ class CoreNullnessStoreInitializer extends NullnessStoreInitializer {
Handler handler,
Context context,
Types types,
- Config config) {
+ Config config,
+ CodeAnnotationInfo codeAnnotationInfo) {
// include nullness info for locals from enclosing environment
EnclosingEnvironmentNullness environmentNullness =
EnclosingEnvironmentNullness.instance(context);
@@ -84,9 +94,32 @@ class CoreNullnessStoreInitializer extends NullnessStoreInitializer {
Symbol.MethodSymbol fiMethodSymbol = NullabilityUtil.getFunctionalInterfaceMethod(code, types);
com.sun.tools.javac.util.List<Symbol.VarSymbol> fiMethodParameters =
fiMethodSymbol.getParameters();
- ImmutableSet<Integer> nullableParamsFromHandler =
- handler.onUnannotatedInvocationGetExplicitlyNullablePositions(
- context, fiMethodSymbol, ImmutableSet.of());
+ // This obtains the types of the functional interface method parameters with preserved
+ // annotations in case of generic type arguments. Only used in JSpecify mode.
+ List<Type> overridenMethodParamTypeList =
+ types.memberType(ASTHelpers.getType(code), fiMethodSymbol).getParameterTypes();
+ // If fiArgumentPositionNullness[i] == null, parameter position i is unannotated
+ Nullness[] fiArgumentPositionNullness = new Nullness[fiMethodParameters.size()];
+ final boolean isFIAnnotated = !codeAnnotationInfo.isSymbolUnannotated(fiMethodSymbol, config);
+ if (isFIAnnotated) {
+ for (int i = 0; i < fiMethodParameters.size(); i++) {
+ if (Nullness.hasNullableAnnotation(fiMethodParameters.get(i), config)) {
+ // Get the Nullness if the Annotation is directly written with the parameter
+ fiArgumentPositionNullness[i] = NULLABLE;
+ } else if (config.isJSpecifyMode()
+ && Nullness.hasNullableAnnotation(
+ overridenMethodParamTypeList.get(i).getAnnotationMirrors().stream(), config)) {
+ // Get the Nullness if the Annotation is indirectly applied through a generic type if we
+ // are in JSpecify mode
+ fiArgumentPositionNullness[i] = NULLABLE;
+ } else {
+ fiArgumentPositionNullness[i] = NONNULL;
+ }
+ }
+ }
+ fiArgumentPositionNullness =
+ handler.onOverrideMethodInvocationParametersNullability(
+ context, fiMethodSymbol, isFIAnnotated, fiArgumentPositionNullness);
for (int i = 0; i < parameters.size(); i++) {
LocalVariableNode param = parameters.get(i);
@@ -102,19 +135,20 @@ class CoreNullnessStoreInitializer extends NullnessStoreInitializer {
// treat as non-null
assumed = NONNULL;
} else {
- if (NullabilityUtil.isUnannotated(fiMethodSymbol, config)) {
- // assume parameter is non-null unless handler tells us otherwise
- assumed = nullableParamsFromHandler.contains(i) ? NULLABLE : NONNULL;
- } else {
- assumed =
- Nullness.hasNullableAnnotation(fiMethodParameters.get(i), config)
- ? NULLABLE
- : NONNULL;
- }
+ assumed = fiArgumentPositionNullness[i] == null ? NONNULL : fiArgumentPositionNullness[i];
}
result.setInformation(AccessPath.fromLocal(param), assumed);
}
result = handler.onDataflowInitialStore(underlyingAST, parameters, result);
return result.build();
}
+
+ @Nullable private CodeAnnotationInfo codeAnnotationInfo;
+
+ private CodeAnnotationInfo getCodeAnnotationInfo(Context context) {
+ if (codeAnnotationInfo == null) {
+ codeAnnotationInfo = CodeAnnotationInfo.instance(context);
+ }
+ return codeAnnotationInfo;
+ }
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/DataFlow.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/DataFlow.java
index e3bf0f9..c4035da 100644
--- a/nullaway/src/main/java/com/uber/nullaway/dataflow/DataFlow.java
+++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/DataFlow.java
@@ -18,6 +18,7 @@
package com.uber.nullaway.dataflow;
+import static com.uber.nullaway.NullabilityUtil.castToNonNull;
import static com.uber.nullaway.NullabilityUtil.findEnclosingMethodOrLambdaOrInitializer;
import com.google.auto.value.AutoValue;
@@ -37,6 +38,8 @@ import com.sun.source.util.TreePath;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.util.Context;
import com.uber.nullaway.NullabilityUtil;
+import com.uber.nullaway.dataflow.cfg.NullAwayCFGBuilder;
+import com.uber.nullaway.handlers.Handler;
import javax.annotation.Nullable;
import javax.annotation.processing.ProcessingEnvironment;
import org.checkerframework.nullaway.dataflow.analysis.AbstractValue;
@@ -48,7 +51,6 @@ import org.checkerframework.nullaway.dataflow.analysis.Store;
import org.checkerframework.nullaway.dataflow.analysis.TransferFunction;
import org.checkerframework.nullaway.dataflow.cfg.ControlFlowGraph;
import org.checkerframework.nullaway.dataflow.cfg.UnderlyingAST;
-import org.checkerframework.nullaway.dataflow.cfg.builder.CFGBuilder;
/**
* Provides a wrapper around {@link org.checkerframework.nullaway.dataflow.analysis.Analysis}.
@@ -69,8 +71,11 @@ public final class DataFlow {
private final boolean assertsEnabled;
- DataFlow(boolean assertsEnabled) {
+ private final Handler handler;
+
+ DataFlow(boolean assertsEnabled, Handler handler) {
this.assertsEnabled = assertsEnabled;
+ this.handler = handler;
}
private final LoadingCache<AnalysisParams, Analysis<?, ?, ?>> analysisCache =
@@ -106,12 +111,14 @@ public final class DataFlow {
(LambdaExpressionTree) codePath.getLeaf();
MethodTree enclMethod =
ASTHelpers.findEnclosingNode(codePath, MethodTree.class);
- ClassTree enclClass = ASTHelpers.findEnclosingNode(codePath, ClassTree.class);
+ ClassTree enclClass =
+ castToNonNull(ASTHelpers.findEnclosingNode(codePath, ClassTree.class));
ast = new UnderlyingAST.CFGLambda(lambdaExpressionTree, enclClass, enclMethod);
bodyPath = new TreePath(codePath, lambdaExpressionTree.getBody());
} else if (codePath.getLeaf() instanceof MethodTree) {
MethodTree method = (MethodTree) codePath.getLeaf();
- ClassTree enclClass = ASTHelpers.findEnclosingNode(codePath, ClassTree.class);
+ ClassTree enclClass =
+ castToNonNull(ASTHelpers.findEnclosingNode(codePath, ClassTree.class));
ast = new UnderlyingAST.CFGMethod(method, enclClass);
BlockTree body = method.getBody();
if (body == null) {
@@ -127,7 +134,8 @@ public final class DataFlow {
bodyPath = codePath;
}
- return CFGBuilder.build(bodyPath, ast, assertsEnabled, !assertsEnabled, env);
+ return NullAwayCFGBuilder.build(
+ bodyPath, ast, assertsEnabled, !assertsEnabled, env, handler);
}
});
@@ -174,8 +182,12 @@ public final class DataFlow {
*/
<A extends AbstractValue<A>, S extends Store<S>, T extends ForwardTransferFunction<A, S>>
ControlFlowGraph getControlFlowGraph(TreePath path, Context context, T transfer) {
- return dataflow(findEnclosingMethodOrLambdaOrInitializer(path), context, transfer)
- .getControlFlowGraph();
+ TreePath enclosingMethodOrLambdaOrInitializer = findEnclosingMethodOrLambdaOrInitializer(path);
+ if (enclosingMethodOrLambdaOrInitializer == null) {
+ throw new IllegalArgumentException(
+ "Cannot get CFG for node outside a method, lambda, or initializer");
+ }
+ return dataflow(enclosingMethodOrLambdaOrInitializer, context, transfer).getControlFlowGraph();
}
/**
@@ -208,6 +220,7 @@ public final class DataFlow {
* @param <T> transfer function type
* @return dataflow result at exit of method
*/
+ @Nullable
public <A extends AbstractValue<A>, S extends Store<S>, T extends ForwardTransferFunction<A, S>>
S finalResult(TreePath path, Context context, T transfer) {
final Tree leaf = path.getLeaf();
@@ -253,7 +266,8 @@ public final class DataFlow {
return resultFor(exprPath, context, transfer);
}
- private <A extends AbstractValue<A>, S extends Store<S>, T extends ForwardTransferFunction<A, S>>
+ private @Nullable <
+ A extends AbstractValue<A>, S extends Store<S>, T extends ForwardTransferFunction<A, S>>
AnalysisResult<A, S> resultFor(TreePath exprPath, Context context, T transfer) {
final TreePath enclosingPath =
NullabilityUtil.findEnclosingMethodOrLambdaOrInitializer(exprPath);
@@ -285,7 +299,7 @@ public final class DataFlow {
@AutoValue
abstract static class CfgParams {
// Should not be used for hashCode or equals
- private ProcessingEnvironment environment;
+ private @Nullable ProcessingEnvironment environment;
private static CfgParams create(TreePath codePath, ProcessingEnvironment environment) {
CfgParams cp = new AutoValue_DataFlow_CfgParams(codePath);
@@ -294,7 +308,7 @@ public final class DataFlow {
}
ProcessingEnvironment environment() {
- return environment;
+ return castToNonNull(environment);
}
abstract TreePath codePath();
diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/NullnessStore.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/NullnessStore.java
index 3d53556..732ed01 100644
--- a/nullaway/src/main/java/com/uber/nullaway/dataflow/NullnessStore.java
+++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/NullnessStore.java
@@ -50,6 +50,7 @@ public class NullnessStore implements Store<NullnessStore> {
private NullnessStore(Map<AccessPath, Nullness> contents) {
this.contents = ImmutableMap.copyOf(contents);
}
+
/**
* Produce an empty store.
*
@@ -67,8 +68,7 @@ public class NullnessStore implements Store<NullnessStore> {
* @return fact associated with local
*/
public Nullness valueOfLocalVariable(LocalVariableNode node, Nullness defaultValue) {
- Nullness result = contents.get(AccessPath.fromLocal(node));
- return result != null ? result : defaultValue;
+ return contents.getOrDefault(AccessPath.fromLocal(node), defaultValue);
}
/**
@@ -84,8 +84,7 @@ public class NullnessStore implements Store<NullnessStore> {
if (path == null) {
return defaultValue;
}
- Nullness result = contents.get(path);
- return result != null ? result : defaultValue;
+ return contents.getOrDefault(path, defaultValue);
}
/**
@@ -104,8 +103,7 @@ public class NullnessStore implements Store<NullnessStore> {
if (accessPath == null) {
return defaultValue;
}
- Nullness result = contents.get(accessPath);
- return result != null ? result : defaultValue;
+ return contents.getOrDefault(accessPath, defaultValue);
}
/**
@@ -142,6 +140,7 @@ public class NullnessStore implements Store<NullnessStore> {
}
return null;
}
+
/**
* Gets the {@link Nullness} value of an access path.
*
@@ -152,8 +151,7 @@ public class NullnessStore implements Store<NullnessStore> {
if (contents == null) {
return Nullness.NULLABLE;
}
- Nullness nullness = contents.get(accessPath);
- return (nullness == null) ? Nullness.NULLABLE : nullness;
+ return contents.getOrDefault(accessPath, Nullness.NULLABLE);
}
public Builder toBuilder() {
@@ -188,7 +186,7 @@ public class NullnessStore implements Store<NullnessStore> {
}
@Override
- public boolean equals(Object o) {
+ public boolean equals(@Nullable Object o) {
if (!(o instanceof NullnessStore)) {
return false;
}
@@ -235,15 +233,15 @@ public class NullnessStore implements Store<NullnessStore> {
Map<LocalVariableNode, LocalVariableNode> localVarTranslations) {
NullnessStore.Builder nullnessBuilder = NullnessStore.empty().toBuilder();
for (AccessPath ap : contents.keySet()) {
- if (ap.getRoot().isReceiver()) {
+ Element element = ap.getRoot();
+ if (element == null) {
+ // Access path is rooted at the receiver, so we don't need to uproot it
continue;
}
- Element varElement = ap.getRoot().getVarElement();
for (LocalVariableNode fromVar : localVarTranslations.keySet()) {
- if (varElement.equals(fromVar.getElement())) {
+ if (element.equals(fromVar.getElement())) {
LocalVariableNode toVar = localVarTranslations.get(fromVar);
- AccessPath newAP =
- new AccessPath(new AccessPath.Root(toVar.getElement()), ap.getElements());
+ AccessPath newAP = AccessPath.switchRoot(ap, toVar.getElement());
nullnessBuilder.setInformation(newAP, contents.get(ap));
}
}
@@ -259,9 +257,7 @@ public class NullnessStore implements Store<NullnessStore> {
*/
public NullnessStore filterAccessPaths(Predicate<AccessPath> pred) {
return new NullnessStore(
- contents
- .entrySet()
- .stream()
+ contents.entrySet().stream()
.filter(e -> pred.test(e.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/cfg/NullAwayCFGBuilder.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/cfg/NullAwayCFGBuilder.java
new file mode 100644
index 0000000..2f955b7
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/cfg/NullAwayCFGBuilder.java
@@ -0,0 +1,202 @@
+package com.uber.nullaway.dataflow.cfg;
+
+import com.google.common.base.Preconditions;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.source.tree.ThrowTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.TreeVisitor;
+import com.sun.source.util.TreePath;
+import com.uber.nullaway.handlers.Handler;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.type.TypeMirror;
+import org.checkerframework.nullaway.dataflow.cfg.ControlFlowGraph;
+import org.checkerframework.nullaway.dataflow.cfg.UnderlyingAST;
+import org.checkerframework.nullaway.dataflow.cfg.builder.CFGBuilder;
+import org.checkerframework.nullaway.dataflow.cfg.builder.CFGTranslationPhaseOne;
+import org.checkerframework.nullaway.dataflow.cfg.builder.CFGTranslationPhaseThree;
+import org.checkerframework.nullaway.dataflow.cfg.builder.CFGTranslationPhaseTwo;
+import org.checkerframework.nullaway.dataflow.cfg.builder.ConditionalJump;
+import org.checkerframework.nullaway.dataflow.cfg.builder.ExtendedNode;
+import org.checkerframework.nullaway.dataflow.cfg.builder.Label;
+import org.checkerframework.nullaway.dataflow.cfg.builder.PhaseOneResult;
+import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode;
+import org.checkerframework.nullaway.dataflow.cfg.node.Node;
+import org.checkerframework.nullaway.dataflow.cfg.node.ThrowNode;
+import org.checkerframework.nullaway.javacutil.AnnotationProvider;
+import org.checkerframework.nullaway.javacutil.BasicAnnotationProvider;
+import org.checkerframework.nullaway.javacutil.trees.TreeBuilder;
+
+/**
+ * A NullAway specific CFGBuilder subclass, which allows to more directly control the AST to CFG
+ * translation performed by the checker framework.
+ *
+ * <p>This holds the static method {@link #build(TreePath, UnderlyingAST, boolean, boolean,
+ * ProcessingEnvironment, Handler)}, called to perform the CFG translation, and the class {@link
+ * NullAwayCFGTranslationPhaseOne}, which extends {@link CFGTranslationPhaseOne} and adds hooks for
+ * the NullAway handlers mechanism and some utility methods.
+ */
+public final class NullAwayCFGBuilder extends CFGBuilder {
+
+ /** This class should never be instantiated. */
+ private NullAwayCFGBuilder() {}
+
+ /**
+ * This static method produces a new CFG representation given a method's (or lambda/initializer)
+ * body.
+ *
+ * <p>It is analogous to {@link CFGBuilder#build(TreePath, UnderlyingAST, boolean, boolean,
+ * ProcessingEnvironment)}, but it also takes a handler to be called at specific extention points
+ * during the CFG translation.
+ *
+ * @param bodyPath the TreePath to the body of the method, lambda, or initializer.
+ * @param underlyingAST the AST that underlies the control frow graph
+ * @param assumeAssertionsEnabled can assertions be assumed to be disabled?
+ * @param assumeAssertionsDisabled can assertions be assumed to be enabled?
+ * @param env annotation processing environment containing type utilities
+ * @param handler a NullAway handler or chain of handlers (through {@link
+ * com.uber.nullaway.handlers.CompositeHandler}
+ * @return a control flow graph
+ */
+ public static ControlFlowGraph build(
+ TreePath bodyPath,
+ UnderlyingAST underlyingAST,
+ boolean assumeAssertionsEnabled,
+ boolean assumeAssertionsDisabled,
+ ProcessingEnvironment env,
+ Handler handler) {
+ TreeBuilder builder = new TreeBuilder(env);
+ AnnotationProvider annotationProvider = new BasicAnnotationProvider();
+ CFGTranslationPhaseOne phase1translator =
+ new NullAwayCFGTranslationPhaseOne(
+ builder,
+ annotationProvider,
+ assumeAssertionsEnabled,
+ assumeAssertionsDisabled,
+ env,
+ handler);
+ PhaseOneResult phase1result = phase1translator.process(bodyPath, underlyingAST);
+ ControlFlowGraph phase2result = CFGTranslationPhaseTwo.process(phase1result);
+ ControlFlowGraph phase3result = CFGTranslationPhaseThree.process(phase2result);
+ return phase3result;
+ }
+
+ /**
+ * A NullAway specific subclass of the Checker Framework's {@link CFGTranslationPhaseOne},
+ * augmented with handler extension points and some utility methods meant to be called from
+ * handlers to customize the AST to CFG translation.
+ */
+ public static class NullAwayCFGTranslationPhaseOne extends CFGTranslationPhaseOne {
+
+ private final Handler handler;
+
+ /**
+ * Create a new NullAway phase one translation visitor.
+ *
+ * @param builder a TreeBuilder object (used to create synthetic AST nodes to feed to the
+ * translation process)
+ * @param annotationProvider an {@link AnnotationProvider}.
+ * @param assumeAssertionsEnabled can assertions be assumed to be disabled?
+ * @param assumeAssertionsDisabled can assertions be assumed to be enabled?
+ * @param env annotation processing environment containing type utilities
+ * @param handler a NullAway handler or chain of handlers (through {@link
+ * com.uber.nullaway.handlers.CompositeHandler}
+ */
+ public NullAwayCFGTranslationPhaseOne(
+ TreeBuilder builder,
+ AnnotationProvider annotationProvider,
+ boolean assumeAssertionsEnabled,
+ boolean assumeAssertionsDisabled,
+ ProcessingEnvironment env,
+ Handler handler) {
+ super(builder, annotationProvider, assumeAssertionsEnabled, assumeAssertionsDisabled, env);
+ this.handler = handler;
+ }
+
+ /**
+ * Obtain the type mirror for a given class, used for exception throwing.
+ *
+ * <p>We use this method to expose the otherwise protected method {@link #getTypeMirror(Class)}
+ * to handlers.
+ *
+ * @param klass a Java class
+ * @return the corresponding type mirror
+ */
+ public TypeMirror classToErrorType(Class<?> klass) {
+ return this.getTypeMirror(klass);
+ }
+
+ /**
+ * Extend the CFG to throw an exception if the passed expression node evaluates to {@code
+ * false}.
+ *
+ * @param booleanExpressionNode a CFG Node representing a boolean expression.
+ * @param errorType the type of the exception to throw if booleanExpressionNode evaluates to
+ * {@code false}.
+ */
+ public void insertThrowOnFalse(Node booleanExpressionNode, TypeMirror errorType) {
+ insertThrowOn(false, booleanExpressionNode, errorType);
+ }
+
+ /**
+ * Extend the CFG to throw an exception if the passed expression node evaluates to {@code true}.
+ *
+ * @param booleanExpressionNode a CFG Node representing a boolean expression.
+ * @param errorType the type of the exception to throw if booleanExpressionNode evaluates to
+ * {@code true}.
+ */
+ public void insertThrowOnTrue(Node booleanExpressionNode, TypeMirror errorType) {
+ insertThrowOn(true, booleanExpressionNode, errorType);
+ }
+
+ private void insertThrowOn(boolean throwOn, Node booleanExpressionNode, TypeMirror errorType) {
+ Tree tree = booleanExpressionNode.getTree();
+ Preconditions.checkArgument(
+ tree instanceof ExpressionTree,
+ "Argument booleanExpressionNode must represent a boolean expression");
+ ExpressionTree booleanExpressionTree = (ExpressionTree) booleanExpressionNode.getTree();
+ Preconditions.checkNotNull(booleanExpressionTree);
+ Label preconditionEntry = new Label();
+ Label endPrecondition = new Label();
+ this.scan(booleanExpressionTree, (Void) null);
+ ConditionalJump cjump =
+ new ConditionalJump(
+ throwOn ? preconditionEntry : endPrecondition,
+ throwOn ? endPrecondition : preconditionEntry);
+ this.extendWithExtendedNode(cjump);
+ this.addLabelForNextNode(preconditionEntry);
+ ExtendedNode exNode =
+ this.extendWithNodeWithException(
+ new ThrowNode(
+ new ThrowTree() {
+ // Dummy throw tree, unused. We could use null here, but that violates nullness
+ // typing.
+ @Override
+ public ExpressionTree getExpression() {
+ return booleanExpressionTree;
+ }
+
+ @Override
+ public Kind getKind() {
+ return Kind.THROW;
+ }
+
+ @Override
+ public <R, D> R accept(TreeVisitor<R, D> visitor, D data) {
+ return visitor.visitThrow(this, data);
+ }
+ },
+ booleanExpressionNode,
+ this.env.getTypeUtils()),
+ errorType);
+ exNode.setTerminatesExecution(true);
+ this.addLabelForNextNode(endPrecondition);
+ }
+
+ @Override
+ public MethodInvocationNode visitMethodInvocation(MethodInvocationTree tree, Void p) {
+ MethodInvocationNode originalNode = super.visitMethodInvocation(tree, p);
+ return handler.onCFGBuildPhase1AfterVisitMethodInvocation(this, tree, originalNode);
+ }
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/FixSerializationConfig.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/FixSerializationConfig.java
new file mode 100644
index 0000000..5ffca63
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/FixSerializationConfig.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization;
+
+import com.google.common.base.Preconditions;
+import com.uber.nullaway.fixserialization.adapters.SerializationAdapter;
+import com.uber.nullaway.fixserialization.adapters.SerializationV1Adapter;
+import com.uber.nullaway.fixserialization.adapters.SerializationV3Adapter;
+import com.uber.nullaway.fixserialization.out.SuggestedNullableFixInfo;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import javax.annotation.Nullable;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import org.w3c.dom.Document;
+import org.xml.sax.SAXException;
+
+/** Config class for Fix Serialization package. */
+public class FixSerializationConfig {
+
+ /**
+ * If enabled, the corresponding output file will be cleared and for all reported errors, NullAway
+ * will serialize information and suggest type changes to resolve them, in case these errors could
+ * be fixed by adding a {@code @Nullable} annotation. These type change suggestions are in form of
+ * {@link SuggestedNullableFixInfo} instances and will be serialized at output directory. If
+ * deactivated, no {@code SuggestedFixInfo} will be created and the output file will remain
+ * untouched.
+ */
+ public final boolean suggestEnabled;
+
+ /**
+ * If enabled, serialized information of a fix suggest will also include the enclosing method and
+ * class of the element involved in error. Finding enclosing elements is costly and will only be
+ * computed at request.
+ */
+ public final boolean suggestEnclosing;
+
+ /**
+ * If enabled, NullAway will serialize information about methods that initialize a field and leave
+ * it {@code @NonNull} at exit point.
+ */
+ public final boolean fieldInitInfoEnabled;
+
+ /** The directory where all files generated/read by Fix Serialization package resides. */
+ @Nullable public final String outputDirectory;
+
+ @Nullable private final Serializer serializer;
+
+ /** Default Constructor, all features are disabled with this config. */
+ public FixSerializationConfig() {
+ suggestEnabled = false;
+ suggestEnclosing = false;
+ fieldInitInfoEnabled = false;
+ outputDirectory = null;
+ serializer = null;
+ }
+
+ public FixSerializationConfig(
+ boolean suggestEnabled,
+ boolean suggestEnclosing,
+ boolean fieldInitInfoEnabled,
+ @Nullable String outputDirectory) {
+ this.suggestEnabled = suggestEnabled;
+ this.suggestEnclosing = suggestEnclosing;
+ this.fieldInitInfoEnabled = fieldInitInfoEnabled;
+ this.outputDirectory = outputDirectory;
+ serializer = new Serializer(this, initializeAdapter(SerializationAdapter.LATEST_VERSION));
+ }
+
+ /**
+ * Sets all flags based on their values in the configuration file.
+ *
+ * @param configFilePath Path to the serialization config file written in xml.
+ * @param serializationVersion Requested serialization version, this value is configurable via
+ * ErrorProne flags ("SerializeFixMetadataVersion"). If not defined by the user {@link
+ * SerializationAdapter#LATEST_VERSION} will be used.
+ */
+ public FixSerializationConfig(String configFilePath, int serializationVersion) {
+ Document document;
+ try {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ document = builder.parse(Files.newInputStream(Paths.get(configFilePath)));
+ document.normalize();
+ } catch (IOException | SAXException | ParserConfigurationException e) {
+ throw new RuntimeException("Error in reading/parsing config at path: " + configFilePath, e);
+ }
+ this.outputDirectory =
+ XMLUtil.getValueFromTag(document, "/serialization/path", String.class).orElse(null);
+ Preconditions.checkNotNull(
+ this.outputDirectory, "Error in FixSerialization Config: Output path cannot be null");
+ suggestEnabled =
+ XMLUtil.getValueFromAttribute(document, "/serialization/suggest", "active", Boolean.class)
+ .orElse(false);
+ suggestEnclosing =
+ XMLUtil.getValueFromAttribute(
+ document, "/serialization/suggest", "enclosing", Boolean.class)
+ .orElse(false);
+ if (suggestEnclosing && !suggestEnabled) {
+ throw new IllegalStateException(
+ "Error in the fix serialization configuration, suggest flag must be enabled to activate enclosing method and class serialization.");
+ }
+ fieldInitInfoEnabled =
+ XMLUtil.getValueFromAttribute(
+ document, "/serialization/fieldInitInfo", "active", Boolean.class)
+ .orElse(false);
+ SerializationAdapter serializationAdapter = initializeAdapter(serializationVersion);
+ serializer = new Serializer(this, serializationAdapter);
+ }
+
+ /**
+ * Initializes NullAway serialization adapter according to the requested serialization version.
+ */
+ private SerializationAdapter initializeAdapter(int version) {
+ switch (version) {
+ case 1:
+ return new SerializationV1Adapter();
+ case 2:
+ throw new RuntimeException(
+ "Serialization version v2 is skipped and was used for an alpha version of the auto-annotator tool. Please use version 3 instead.");
+ case 3:
+ return new SerializationV3Adapter();
+ default:
+ throw new RuntimeException(
+ "Unrecognized NullAway serialization version: "
+ + version
+ + ". Supported versions: 1 to "
+ + SerializationAdapter.LATEST_VERSION
+ + ".");
+ }
+ }
+
+ @Nullable
+ public Serializer getSerializer() {
+ return serializer;
+ }
+
+ /** Builder class for Serialization Config */
+ public static class Builder {
+
+ private boolean suggestEnabled;
+ private boolean suggestEnclosing;
+ private boolean fieldInitInfo;
+ @Nullable private String outputDir;
+
+ public Builder() {
+ suggestEnabled = false;
+ suggestEnclosing = false;
+ fieldInitInfo = false;
+ }
+
+ public Builder setSuggest(boolean value, boolean withEnclosing) {
+ this.suggestEnabled = value;
+ this.suggestEnclosing = withEnclosing && suggestEnabled;
+ return this;
+ }
+
+ public Builder setFieldInitInfo(boolean enabled) {
+ this.fieldInitInfo = enabled;
+ return this;
+ }
+
+ public Builder setOutputDirectory(String outputDir) {
+ this.outputDir = outputDir;
+ return this;
+ }
+
+ /**
+ * Builds and writes the config with the state in builder at the given path as XML.
+ *
+ * @param path path to write the config file.
+ */
+ public void writeAsXML(String path) {
+ FixSerializationConfig config = this.build();
+ XMLUtil.writeInXMLFormat(config, path);
+ }
+
+ public FixSerializationConfig build() {
+ if (outputDir == null) {
+ throw new IllegalStateException("did not set mandatory output directory");
+ }
+ return new FixSerializationConfig(suggestEnabled, suggestEnclosing, fieldInitInfo, outputDir);
+ }
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/SerializationService.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/SerializationService.java
new file mode 100644
index 0000000..eefe7b6
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/SerializationService.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import com.google.errorprone.VisitorState;
+import com.sun.source.tree.Tree;
+import com.sun.source.util.TreePath;
+import com.sun.source.util.Trees;
+import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.processing.JavacProcessingEnvironment;
+import com.uber.nullaway.Config;
+import com.uber.nullaway.ErrorMessage;
+import com.uber.nullaway.Nullness;
+import com.uber.nullaway.fixserialization.location.SymbolLocation;
+import com.uber.nullaway.fixserialization.out.ErrorInfo;
+import com.uber.nullaway.fixserialization.out.SuggestedNullableFixInfo;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.Nullable;
+
+/** A facade class to interact with fix serialization package. */
+public class SerializationService {
+
+ /** Special characters that need to be escaped in TSV files. */
+ private static final ImmutableMap<Character, Character> escapes =
+ ImmutableMap.of(
+ '\n', 'n',
+ '\t', 't',
+ '\f', 'f',
+ '\b', 'b',
+ '\r', 'r');
+
+ /**
+ * Escapes special characters in string to conform with TSV file formats. The most common
+ * convention for lossless conversion is to escape special characters with a backslash according
+ * to <a
+ * href="https://en.wikipedia.org/wiki/Tab-separated_values#Conventions_for_lossless_conversion_to_TSV">
+ * Conventions for lossless conversion to TSV</a>
+ *
+ * @param str String to process.
+ * @return returns modified str which its special characters are escaped.
+ */
+ public static String escapeSpecialCharacters(String str) {
+ // regex needs "\\" to match character '\', each must also be escaped in string to create "\\",
+ // therefore we need four "\".
+ // escape existing backslashes
+ str = str.replaceAll(Pattern.quote("\\"), Matcher.quoteReplacement("\\\\"));
+ // escape special characters
+ for (Map.Entry<Character, Character> entry : escapes.entrySet()) {
+ str =
+ str.replaceAll(
+ String.valueOf(entry.getKey()), Matcher.quoteReplacement("\\" + entry.getValue()));
+ }
+ return str;
+ }
+
+ /**
+ * Serializes the suggested type change of an element in the source code that can resolve the
+ * error. We do not want suggested fix changes to override explicit annotations in the code,
+ * therefore, if the target element has an explicit {@code @Nonnull} annotation, no type change is
+ * suggested.
+ *
+ * @param config NullAway config.
+ * @param state Visitor state.
+ * @param target Target element to alternate it's type.
+ * @param errorMessage Error caused by the target.
+ */
+ public static void serializeFixSuggestion(
+ Config config, VisitorState state, Symbol target, ErrorMessage errorMessage) {
+ FixSerializationConfig serializationConfig = config.getSerializationConfig();
+ if (!serializationConfig.suggestEnabled) {
+ return;
+ }
+ // Skip if the element has an explicit @Nonnull annotation.
+ if (Nullness.hasNonNullAnnotation(target, config)) {
+ return;
+ }
+ Trees trees = Trees.instance(JavacProcessingEnvironment.instance(state.context));
+ // Skip if the element is received as bytecode.
+ if (trees.getPath(target) == null) {
+ return;
+ }
+ SymbolLocation location = SymbolLocation.createLocationFromSymbol(target);
+ SuggestedNullableFixInfo suggestedNullableFixInfo =
+ buildFixMetadata(state.getPath(), errorMessage, location);
+ Serializer serializer = serializationConfig.getSerializer();
+ Preconditions.checkNotNull(
+ serializer, "Serializer shouldn't be null at this point, error in configuration setting!");
+ serializer.serializeSuggestedFixInfo(
+ suggestedNullableFixInfo, serializationConfig.suggestEnclosing);
+ }
+
+ /**
+ * Serializes the reporting error.
+ *
+ * @param config NullAway config.
+ * @param state Visitor state.
+ * @param errorTree Tree of the element involved in the reporting error.
+ * @param errorMessage Error caused by the target.
+ */
+ public static void serializeReportingError(
+ Config config,
+ VisitorState state,
+ Tree errorTree,
+ @Nullable Symbol target,
+ ErrorMessage errorMessage) {
+ Serializer serializer = config.getSerializationConfig().getSerializer();
+ Preconditions.checkNotNull(
+ serializer, "Serializer shouldn't be null at this point, error in configuration setting!");
+ serializer.serializeErrorInfo(new ErrorInfo(state.getPath(), errorTree, errorMessage, target));
+ }
+
+ /**
+ * Builds the {@link SuggestedNullableFixInfo} instance based on the {@link ErrorMessage} type.
+ */
+ private static SuggestedNullableFixInfo buildFixMetadata(
+ TreePath path, ErrorMessage errorMessage, SymbolLocation location) {
+ SuggestedNullableFixInfo suggestedNullableFixInfo;
+ switch (errorMessage.getMessageType()) {
+ case RETURN_NULLABLE:
+ case WRONG_OVERRIDE_RETURN:
+ case WRONG_OVERRIDE_PARAM:
+ case PASS_NULLABLE:
+ case FIELD_NO_INIT:
+ case ASSIGN_FIELD_NULLABLE:
+ case METHOD_NO_INIT:
+ suggestedNullableFixInfo = new SuggestedNullableFixInfo(path, location, errorMessage);
+ break;
+ default:
+ throw new IllegalStateException(
+ "Cannot suggest a type to resolve error of type: " + errorMessage.getMessageType());
+ }
+ return suggestedNullableFixInfo;
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/Serializer.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/Serializer.java
new file mode 100644
index 0000000..3adb94b
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/Serializer.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization;
+
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.ErrorMessage;
+import com.uber.nullaway.fixserialization.adapters.SerializationAdapter;
+import com.uber.nullaway.fixserialization.out.ErrorInfo;
+import com.uber.nullaway.fixserialization.out.FieldInitializationInfo;
+import com.uber.nullaway.fixserialization.out.SuggestedNullableFixInfo;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import javax.annotation.Nullable;
+
+/**
+ * Serializer class where all generated files in Fix Serialization package is created through APIs
+ * of this class.
+ */
+public class Serializer {
+ /** Path to write errors. */
+ private final Path errorOutputPath;
+
+ /** Path to write suggested fix metadata. */
+ private final Path suggestedFixesOutputPath;
+
+ /** Path to write suggested fix metadata. */
+ private final Path fieldInitializationOutputPath;
+
+ /**
+ * Adapter used to serialize outputs. This adapter is capable of serializing outputs according to
+ * the requested serilization version and maintaining backward compatibility with previous
+ * versions of NullAway.
+ */
+ private final SerializationAdapter serializationAdapter;
+
+ public Serializer(FixSerializationConfig config, SerializationAdapter serializationAdapter) {
+ String outputDirectory = config.outputDirectory;
+ this.errorOutputPath = Paths.get(outputDirectory, "errors.tsv");
+ this.suggestedFixesOutputPath = Paths.get(outputDirectory, "fixes.tsv");
+ this.fieldInitializationOutputPath = Paths.get(outputDirectory, "field_init.tsv");
+ this.serializationAdapter = serializationAdapter;
+ serializeVersion(outputDirectory);
+ initializeOutputFiles(config);
+ }
+
+ /**
+ * Appends the string representation of the {@link SuggestedNullableFixInfo}.
+ *
+ * @param suggestedNullableFixInfo SuggestedFixInfo object.
+ * @param enclosing Flag to control if enclosing method and class should be included.
+ */
+ public void serializeSuggestedFixInfo(
+ SuggestedNullableFixInfo suggestedNullableFixInfo, boolean enclosing) {
+ if (enclosing) {
+ suggestedNullableFixInfo.initEnclosing();
+ }
+ appendToFile(
+ suggestedNullableFixInfo.tabSeparatedToString(serializationAdapter),
+ suggestedFixesOutputPath);
+ }
+
+ /**
+ * Appends the string representation of the {@link ErrorMessage}.
+ *
+ * @param errorInfo ErrorMessage object.
+ */
+ public void serializeErrorInfo(ErrorInfo errorInfo) {
+ errorInfo.initEnclosing();
+ appendToFile(serializationAdapter.serializeError(errorInfo), errorOutputPath);
+ }
+
+ public void serializeFieldInitializationInfo(FieldInitializationInfo info) {
+ appendToFile(info.tabSeparatedToString(serializationAdapter), fieldInitializationOutputPath);
+ }
+
+ /** Cleared the content of the file if exists and writes the header in the first line. */
+ private void initializeFile(Path path, String header) {
+ try {
+ Files.deleteIfExists(path);
+ } catch (IOException e) {
+ throw new RuntimeException("Could not clear file at: " + path, e);
+ }
+ try (OutputStream os = new FileOutputStream(path.toFile())) {
+ header += "\n";
+ os.write(header.getBytes(Charset.defaultCharset()), 0, header.length());
+ os.flush();
+ } catch (IOException e) {
+ throw new RuntimeException("Could not finish resetting File at Path: " + path, e);
+ }
+ }
+
+ /**
+ * Returns the serialization version.
+ *
+ * @return The serialization version.
+ */
+ public int getSerializationVersion() {
+ return serializationAdapter.getSerializationVersion();
+ }
+
+ /**
+ * Serializes the using {@link SerializationAdapter} version as {@code string} in
+ * <b>serialization_version.txt</b> file under root output directory for all serialized outputs.
+ *
+ * @param outputDirectory Path to root directory for all serialized outputs.
+ */
+ private void serializeVersion(@Nullable String outputDirectory) {
+ Path versionOutputPath = Paths.get(outputDirectory).resolve("serialization_version.txt");
+ try (Writer fileWriter =
+ Files.newBufferedWriter(versionOutputPath.toFile().toPath(), Charset.defaultCharset())) {
+ fileWriter.write(Integer.toString(serializationAdapter.getSerializationVersion()));
+ } catch (IOException exception) {
+ throw new RuntimeException("Could not serialize output version", exception);
+ }
+ }
+
+ /** Initializes every file which will be re-generated in the new run of NullAway. */
+ private void initializeOutputFiles(FixSerializationConfig config) {
+ try {
+ Files.createDirectories(Paths.get(config.outputDirectory));
+ if (config.suggestEnabled) {
+ initializeFile(suggestedFixesOutputPath, SuggestedNullableFixInfo.header());
+ }
+ if (config.fieldInitInfoEnabled) {
+ initializeFile(fieldInitializationOutputPath, FieldInitializationInfo.header());
+ }
+ initializeFile(errorOutputPath, serializationAdapter.getErrorsOutputFileHeader());
+ } catch (IOException e) {
+ throw new RuntimeException("Could not finish resetting serializer", e);
+ }
+ }
+
+ private void appendToFile(String row, Path path) {
+ // Since there is no method available in API of either javac or errorprone to inform NullAway
+ // that the analysis is finished, we cannot open a single stream and flush it within a finalize
+ // method. Must open and close a new stream everytime we are appending a new line to a file.
+ if (row == null || row.equals("")) {
+ return;
+ }
+ row = row + "\n";
+ try (OutputStream os = new FileOutputStream(path.toFile(), true)) {
+ os.write(row.getBytes(Charset.defaultCharset()), 0, row.length());
+ os.flush();
+ } catch (IOException e) {
+ throw new RuntimeException("Error happened for writing at file: " + path, e);
+ }
+ }
+
+ /**
+ * Converts the given uri to the real path. Note, in NullAway CI tests, source files exists in
+ * memory and there is no real path leading to those files. Instead, we just serialize the path
+ * from uri as the full paths are not checked in tests.
+ *
+ * @param uri Given uri.
+ * @return Real path for the give uri.
+ */
+ @Nullable
+ public static Path pathToSourceFileFromURI(@Nullable URI uri) {
+ if (uri == null) {
+ return null;
+ }
+ if ("jimfs".equals(uri.getScheme())) {
+ // In NullAway unit tests, files are stored in memory and have this scheme.
+ return Paths.get(uri);
+ }
+ if (!"file".equals(uri.getScheme())) {
+ return null;
+ }
+ Path path = Paths.get(uri);
+ try {
+ return path.toRealPath();
+ } catch (IOException e) {
+ // In this case, we still would like to continue the serialization instead of returning null
+ // and not serializing anything.
+ return path;
+ }
+ }
+
+ /**
+ * Serializes the given {@link Symbol} to a string.
+ *
+ * @param symbol The symbol to serialize.
+ * @param adapter adapter used to serialize symbols.
+ * @return The serialized symbol.
+ */
+ public static String serializeSymbol(@Nullable Symbol symbol, SerializationAdapter adapter) {
+ if (symbol == null) {
+ return "null";
+ }
+ switch (symbol.getKind()) {
+ case FIELD:
+ case PARAMETER:
+ return symbol.name.toString();
+ case METHOD:
+ case CONSTRUCTOR:
+ return adapter.serializeMethodSignature((Symbol.MethodSymbol) symbol);
+ default:
+ return symbol.flatName().toString();
+ }
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/XMLUtil.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/XMLUtil.java
new file mode 100644
index 0000000..91d45cd
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/XMLUtil.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization;
+
+import java.io.File;
+import javax.annotation.Nullable;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import org.jetbrains.annotations.Contract;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+/** Helper for class for parsing/writing xml files. */
+public class XMLUtil {
+
+ /**
+ * Helper method for reading attributes of node located at /key_1/key_2/.../key_n (in the form of
+ * {@code Xpath} query) from a {@link Document}.
+ *
+ * @param doc XML object to read values from.
+ * @param key Key to locate the value, can be nested in the form of {@code Xpath} query (e.g.
+ * /key1/key2:.../key_n).
+ * @param klass Class type of the value in doc.
+ * @return The value in the specified keychain cast to the class type given in parameter.
+ */
+ public static <T> DefaultXMLValueProvider<T> getValueFromAttribute(
+ Document doc, String key, String attr, Class<T> klass) {
+ try {
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ Node node = (Node) xPath.compile(key).evaluate(doc, XPathConstants.NODE);
+ if (node != null && node.getNodeType() == Node.ELEMENT_NODE) {
+ Element eElement = (Element) node;
+ return new DefaultXMLValueProvider<>(eElement.getAttribute(attr), klass);
+ }
+ } catch (XPathExpressionException ignored) {
+ return new DefaultXMLValueProvider<>(null, klass);
+ }
+ return new DefaultXMLValueProvider<>(null, klass);
+ }
+
+ /**
+ * Helper method for reading value of a node located at /key_1/key_2/.../key_n (in the form of
+ * {@code Xpath} query) from a {@link Document}.
+ *
+ * @param doc XML object to read values from.
+ * @param key Key to locate the value, can be nested in the form of {@code Xpath} query (e.g.
+ * /key1/key2/.../key_n).
+ * @param klass Class type of the value in doc.
+ * @return The value in the specified keychain cast to the class type given in parameter.
+ */
+ public static <T> DefaultXMLValueProvider<T> getValueFromTag(
+ Document doc, String key, Class<T> klass) {
+ try {
+ XPath xPath = XPathFactory.newInstance().newXPath();
+ Node node = (Node) xPath.compile(key).evaluate(doc, XPathConstants.NODE);
+ if (node != null && node.getNodeType() == Node.ELEMENT_NODE) {
+ Element eElement = (Element) node;
+ return new DefaultXMLValueProvider<>(eElement.getTextContent(), klass);
+ }
+ } catch (XPathExpressionException ignored) {
+ return new DefaultXMLValueProvider<>(null, klass);
+ }
+ return new DefaultXMLValueProvider<>(null, klass);
+ }
+
+ /**
+ * Writes the {@link FixSerializationConfig} in {@code XML} format.
+ *
+ * @param config Config file to write.
+ * @param path Path to write the config at.
+ */
+ public static void writeInXMLFormat(FixSerializationConfig config, String path) {
+ DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
+ try {
+ DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
+ Document doc = docBuilder.newDocument();
+
+ // Root
+ Element rootElement = doc.createElement("serialization");
+ doc.appendChild(rootElement);
+
+ // Suggest
+ Element suggestElement = doc.createElement("suggest");
+ suggestElement.setAttribute("active", String.valueOf(config.suggestEnabled));
+ suggestElement.setAttribute("enclosing", String.valueOf(config.suggestEnclosing));
+ rootElement.appendChild(suggestElement);
+
+ // Field Initialization
+ Element fieldInitInfoEnabled = doc.createElement("fieldInitInfo");
+ fieldInitInfoEnabled.setAttribute("active", String.valueOf(config.fieldInitInfoEnabled));
+ rootElement.appendChild(fieldInitInfoEnabled);
+
+ // Output dir
+ Element outputDir = doc.createElement("path");
+ outputDir.setTextContent(config.outputDirectory);
+ rootElement.appendChild(outputDir);
+
+ // Serialization version
+ if (config.getSerializer() != null) {
+ Element serializationVersion = doc.createElement("version");
+ serializationVersion.setTextContent(
+ String.valueOf(config.getSerializer().getSerializationVersion()));
+ rootElement.appendChild(serializationVersion);
+ }
+
+ // Writings
+ TransformerFactory transformerFactory = TransformerFactory.newInstance();
+ Transformer transformer = transformerFactory.newTransformer();
+ DOMSource source = new DOMSource(doc);
+ StreamResult result = new StreamResult(new File(path));
+ transformer.transform(source, result);
+ } catch (ParserConfigurationException | TransformerException e) {
+ throw new RuntimeException("Error happened in writing config.", e);
+ }
+ }
+
+ /** Helper class for setting default values when the key is not found. */
+ static class DefaultXMLValueProvider<T> {
+ @Nullable final Object value;
+ final Class<T> klass;
+
+ DefaultXMLValueProvider(@Nullable Object value, Class<T> klass) {
+ this.klass = klass;
+ if (value == null) {
+ this.value = null;
+ } else {
+ String content = value.toString();
+ switch (klass.getSimpleName()) {
+ case "Integer":
+ this.value = Integer.valueOf(content);
+ break;
+ case "Boolean":
+ this.value = Boolean.valueOf(content);
+ break;
+ case "String":
+ this.value = String.valueOf(content);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Cannot extract values of type: "
+ + klass
+ + ", only Double|Boolean|String accepted.");
+ }
+ }
+ }
+
+ @Contract("!null -> !null")
+ @Nullable
+ T orElse(@Nullable T other) {
+ return value == null ? other : klass.cast(this.value);
+ }
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/adapters/SerializationAdapter.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/adapters/SerializationAdapter.java
new file mode 100644
index 0000000..2f3bc5f
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/adapters/SerializationAdapter.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization.adapters;
+
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.fixserialization.out.ErrorInfo;
+
+/**
+ * Adapter for serialization service to provide its output according to the requested serialization
+ * version. Outputs are currently produced in TSV format and columns in these files may change
+ * future releases. Subclasses of this interface are used to maintain backward compatibility and
+ * produce the exact output of previous NullAway versions.
+ */
+public interface SerializationAdapter {
+
+ /**
+ * Latest version number. If version is not defined by the user, NullAway will use the
+ * corresponding adapter to this version in its serialization.
+ */
+ int LATEST_VERSION = 3;
+
+ /**
+ * Returns header of "errors.tsv" which contains all serialized {@link ErrorInfo} reported by
+ * NullAway.
+ *
+ * @return Header of "errors.tsv".
+ */
+ String getErrorsOutputFileHeader();
+
+ /**
+ * Serializes contents of the given {@link ErrorInfo} according to the defined header into a
+ * string with each field separated by a tab.
+ *
+ * @param errorInfo Given errorInfo to serialize.
+ * @return String representation of the given {@link ErrorInfo}. The returned string should be
+ * ready to get appended to a tsv file as a row.
+ */
+ String serializeError(ErrorInfo errorInfo);
+
+ /**
+ * Returns the associated version number with this adapter.
+ *
+ * @return Supporting serialization version number.
+ */
+ int getSerializationVersion();
+
+ /**
+ * Serializes the signature of the given {@link Symbol.MethodSymbol} to a string.
+ *
+ * @param methodSymbol The method symbol to serialize.
+ * @return The serialized method symbol.
+ */
+ String serializeMethodSignature(Symbol.MethodSymbol methodSymbol);
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/adapters/SerializationV1Adapter.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/adapters/SerializationV1Adapter.java
new file mode 100644
index 0000000..b0ad0cc
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/adapters/SerializationV1Adapter.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization.adapters;
+
+import static com.uber.nullaway.fixserialization.out.ErrorInfo.EMPTY_NONNULL_TARGET_LOCATION_STRING;
+
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.fixserialization.SerializationService;
+import com.uber.nullaway.fixserialization.Serializer;
+import com.uber.nullaway.fixserialization.location.SymbolLocation;
+import com.uber.nullaway.fixserialization.out.ErrorInfo;
+
+/** Adapter for version 1. Base version for serializations. */
+public class SerializationV1Adapter implements SerializationAdapter {
+
+ @Override
+ public String getErrorsOutputFileHeader() {
+ return String.join(
+ "\t",
+ "message_type",
+ "message",
+ "enc_class",
+ "enc_member",
+ "target_kind",
+ "target_class",
+ "target_method",
+ "param",
+ "index",
+ "uri");
+ }
+
+ @Override
+ public String serializeError(ErrorInfo errorInfo) {
+ return String.join(
+ "\t",
+ errorInfo.getErrorMessage().getMessageType().toString(),
+ SerializationService.escapeSpecialCharacters(errorInfo.getErrorMessage().getMessage()),
+ Serializer.serializeSymbol(errorInfo.getRegionClass(), this),
+ Serializer.serializeSymbol(errorInfo.getRegionMember(), this),
+ (errorInfo.getNonnullTarget() != null
+ ? SymbolLocation.createLocationFromSymbol(errorInfo.getNonnullTarget())
+ .tabSeparatedToString(this)
+ : EMPTY_NONNULL_TARGET_LOCATION_STRING));
+ }
+
+ @Override
+ public int getSerializationVersion() {
+ return 1;
+ }
+
+ @Override
+ public String serializeMethodSignature(Symbol.MethodSymbol methodSymbol) {
+ return methodSymbol.toString();
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/adapters/SerializationV3Adapter.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/adapters/SerializationV3Adapter.java
new file mode 100644
index 0000000..a9aa74e
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/adapters/SerializationV3Adapter.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization.adapters;
+
+import static com.uber.nullaway.fixserialization.out.ErrorInfo.EMPTY_NONNULL_TARGET_LOCATION_STRING;
+import static java.util.stream.Collectors.joining;
+
+import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.code.Type;
+import com.sun.tools.javac.util.Name;
+import com.uber.nullaway.fixserialization.SerializationService;
+import com.uber.nullaway.fixserialization.Serializer;
+import com.uber.nullaway.fixserialization.location.SymbolLocation;
+import com.uber.nullaway.fixserialization.out.ErrorInfo;
+
+/**
+ * Adapter for serialization version 3.
+ *
+ * <p>Updates to previous version (version 1):
+ *
+ * <ul>
+ * <li>Serialized errors contain an extra column indicating the offset of the program point where
+ * the error is reported.
+ * <li>Serialized errors contain an extra column indicating the path to the containing source file
+ * where the error is reported
+ * <li>Type arguments and Type use annotations are excluded from the serialized method signatures.
+ * </ul>
+ */
+public class SerializationV3Adapter implements SerializationAdapter {
+
+ @Override
+ public String getErrorsOutputFileHeader() {
+ return String.join(
+ "\t",
+ "message_type",
+ "message",
+ "enc_class",
+ "enc_member",
+ "offset",
+ "path",
+ "target_kind",
+ "target_class",
+ "target_method",
+ "target_param",
+ "target_index",
+ "target_path");
+ }
+
+ @Override
+ public String serializeError(ErrorInfo errorInfo) {
+ return String.join(
+ "\t",
+ errorInfo.getErrorMessage().getMessageType().toString(),
+ SerializationService.escapeSpecialCharacters(errorInfo.getErrorMessage().getMessage()),
+ Serializer.serializeSymbol(errorInfo.getRegionClass(), this),
+ Serializer.serializeSymbol(errorInfo.getRegionMember(), this),
+ String.valueOf(errorInfo.getOffset()),
+ errorInfo.getPath() != null ? errorInfo.getPath().toString() : "null",
+ (errorInfo.getNonnullTarget() != null
+ ? SymbolLocation.createLocationFromSymbol(errorInfo.getNonnullTarget())
+ .tabSeparatedToString(this)
+ : EMPTY_NONNULL_TARGET_LOCATION_STRING));
+ }
+
+ @Override
+ public int getSerializationVersion() {
+ return 3;
+ }
+
+ @Override
+ public String serializeMethodSignature(Symbol.MethodSymbol methodSymbol) {
+ StringBuilder sb = new StringBuilder();
+ if (methodSymbol.isConstructor()) {
+ // For constructors, method's simple name is <init> and not the enclosing class, need to
+ // locate the enclosing class.
+ Symbol.ClassSymbol encClass = methodSymbol.owner.enclClass();
+ Name name = encClass.getSimpleName();
+ if (name.isEmpty()) {
+ // An anonymous class cannot declare its own constructor. Based on this assumption and our
+ // use case, we should not serialize the method signature.
+ throw new RuntimeException(
+ "Did not expect method serialization for anonymous class constructor: "
+ + methodSymbol
+ + ", in anonymous class: "
+ + encClass);
+ }
+ sb.append(name);
+ } else {
+ // For methods, we use the name of the method.
+ sb.append(methodSymbol.getSimpleName());
+ }
+ sb.append(
+ methodSymbol.getParameters().stream()
+ .map(
+ parameter ->
+ // check if array
+ (parameter.type instanceof Type.ArrayType)
+ // if is array, get the element type and append "[]"
+ ? ((Type.ArrayType) parameter.type).elemtype.tsym + "[]"
+ // else, just get the type
+ : parameter.type.tsym.toString())
+ .collect(joining(",", "(", ")")));
+ return sb.toString();
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/AbstractSymbolLocation.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/AbstractSymbolLocation.java
new file mode 100644
index 0000000..319c636
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/AbstractSymbolLocation.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization.location;
+
+import static com.uber.nullaway.NullabilityUtil.castToNonNull;
+
+import com.google.common.base.Preconditions;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.fixserialization.Serializer;
+import java.net.URI;
+import java.nio.file.Path;
+import javax.annotation.Nullable;
+import javax.lang.model.element.ElementKind;
+
+/** abstract base class for {@link SymbolLocation}. */
+public abstract class AbstractSymbolLocation implements SymbolLocation {
+
+ /** Element kind of the targeted symbol */
+ protected final ElementKind type;
+
+ /** Path of the file containing the symbol, if available. */
+ @Nullable protected final Path path;
+
+ /** Enclosing class of the symbol. */
+ protected final Symbol.ClassSymbol enclosingClass;
+
+ public AbstractSymbolLocation(ElementKind type, Symbol target) {
+ Preconditions.checkArgument(
+ type.equals(target.getKind()),
+ "Cannot instantiate element of type: "
+ + target.getKind()
+ + " with location type of: "
+ + type
+ + ".");
+ this.type = type;
+ this.enclosingClass = castToNonNull(ASTHelpers.enclosingClass(target));
+ URI pathInURI = enclosingClass.sourcefile != null ? enclosingClass.sourcefile.toUri() : null;
+ this.path = Serializer.pathToSourceFileFromURI(pathInURI);
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/FieldLocation.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/FieldLocation.java
new file mode 100644
index 0000000..19ca23d
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/FieldLocation.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization.location;
+
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.fixserialization.Serializer;
+import com.uber.nullaway.fixserialization.adapters.SerializationAdapter;
+import javax.lang.model.element.ElementKind;
+
+/** subtype of {@link AbstractSymbolLocation} targeting class fields. */
+public class FieldLocation extends AbstractSymbolLocation {
+
+ /** Symbol of targeted class field */
+ protected final Symbol.VarSymbol variableSymbol;
+
+ public FieldLocation(Symbol target) {
+ super(ElementKind.FIELD, target);
+ variableSymbol = (Symbol.VarSymbol) target;
+ }
+
+ @Override
+ public String tabSeparatedToString(SerializationAdapter adapter) {
+ return String.join(
+ "\t",
+ type.toString(),
+ Serializer.serializeSymbol(enclosingClass, adapter),
+ "null",
+ Serializer.serializeSymbol(variableSymbol, adapter),
+ "null",
+ path != null ? path.toString() : "null");
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/MethodLocation.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/MethodLocation.java
new file mode 100644
index 0000000..f3c3022
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/MethodLocation.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization.location;
+
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.fixserialization.Serializer;
+import com.uber.nullaway.fixserialization.adapters.SerializationAdapter;
+import javax.lang.model.element.ElementKind;
+
+/** subtype of {@link AbstractSymbolLocation} targeting methods. */
+public class MethodLocation extends AbstractSymbolLocation {
+
+ /** Symbol of the targeted method. */
+ protected final Symbol.MethodSymbol enclosingMethod;
+
+ public MethodLocation(Symbol target) {
+ super(ElementKind.METHOD, target);
+ enclosingMethod = (Symbol.MethodSymbol) target;
+ }
+
+ @Override
+ public String tabSeparatedToString(SerializationAdapter adapter) {
+ return String.join(
+ "\t",
+ type.toString(),
+ Serializer.serializeSymbol(enclosingClass, adapter),
+ Serializer.serializeSymbol(enclosingMethod, adapter),
+ "null",
+ "null",
+ path != null ? path.toString() : "null");
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/MethodParameterLocation.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/MethodParameterLocation.java
new file mode 100644
index 0000000..7a1c22f
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/MethodParameterLocation.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization.location;
+
+import com.google.common.base.Preconditions;
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.fixserialization.Serializer;
+import com.uber.nullaway.fixserialization.adapters.SerializationAdapter;
+import javax.lang.model.element.ElementKind;
+
+/** subtype of {@link AbstractSymbolLocation} targeting a method parameter. */
+public class MethodParameterLocation extends AbstractSymbolLocation {
+
+ /** Symbol of the targeted method. */
+ private final Symbol.MethodSymbol enclosingMethod;
+
+ /** Symbol of the targeted method parameter. */
+ private final Symbol.VarSymbol paramSymbol;
+
+ /** Index of the method parameter in the containing method's argument list. */
+ private final int index;
+
+ public MethodParameterLocation(Symbol target) {
+ super(ElementKind.PARAMETER, target);
+ this.paramSymbol = (Symbol.VarSymbol) target;
+ Symbol cursor = target;
+ // Look for the enclosing method.
+ while (cursor != null
+ && cursor.getKind() != ElementKind.CONSTRUCTOR
+ && cursor.getKind() != ElementKind.METHOD) {
+ cursor = cursor.owner;
+ }
+ Preconditions.checkNotNull(cursor);
+ this.enclosingMethod = (Symbol.MethodSymbol) cursor;
+ int i;
+ for (i = 0; i < this.enclosingMethod.getParameters().size(); i++) {
+ if (this.enclosingMethod.getParameters().get(i).equals(target)) {
+ break;
+ }
+ }
+ index = i;
+ }
+
+ @Override
+ public String tabSeparatedToString(SerializationAdapter adapter) {
+ return String.join(
+ "\t",
+ type.toString(),
+ Serializer.serializeSymbol(enclosingClass, adapter),
+ Serializer.serializeSymbol(enclosingMethod, adapter),
+ Serializer.serializeSymbol(paramSymbol, adapter),
+ String.valueOf(index),
+ path != null ? path.toString() : "null");
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/SymbolLocation.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/SymbolLocation.java
new file mode 100644
index 0000000..262df71
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/location/SymbolLocation.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization.location;
+
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.fixserialization.adapters.SerializationAdapter;
+
+/** Provides method for symbol locations. */
+public interface SymbolLocation {
+
+ /**
+ * returns string representation of contents of the instance. It must have the format below: kind
+ * of the element, symbol of the containing class, symbol of the enclosing method, symbol of the
+ * variable, index of the element and uri to containing file.
+ *
+ * @param adapter adapter used to serialize symbols.
+ * @return string representation of contents in a line seperated by tabs.
+ */
+ String tabSeparatedToString(SerializationAdapter adapter);
+
+ /**
+ * Creates header of an output file containing all {@link SymbolLocation} written in string which
+ * values are separated tabs.
+ *
+ * @return string representation of the header separated by tabs.
+ */
+ static String header() {
+ return String.join("\t", "kind", "class", "method", "param", "index", "uri");
+ }
+
+ /**
+ * returns the appropriate subtype of {@link SymbolLocation} based on the target kind.
+ *
+ * @param target Target element.
+ * @return subtype of {@link SymbolLocation} matching target's type.
+ */
+ static SymbolLocation createLocationFromSymbol(Symbol target) {
+ switch (target.getKind()) {
+ case PARAMETER:
+ return new MethodParameterLocation(target);
+ case METHOD:
+ return new MethodLocation(target);
+ case FIELD:
+ return new FieldLocation(target);
+ default:
+ throw new IllegalArgumentException("Cannot locate node: " + target);
+ }
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ClassAndMemberInfo.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ClassAndMemberInfo.java
new file mode 100644
index 0000000..258830d
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ClassAndMemberInfo.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization.out;
+
+import com.google.common.base.Preconditions;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.MethodTree;
+import com.sun.source.tree.Tree;
+import com.sun.source.tree.VariableTree;
+import com.sun.source.util.TreePath;
+import com.sun.tools.javac.code.Symbol;
+import javax.annotation.Nullable;
+import javax.lang.model.element.ElementKind;
+
+/** Class and member corresponding to a program point at which an error / fix was reported. */
+public class ClassAndMemberInfo {
+ /** Path to the program point of the reported error / fix */
+ @Nullable public TreePath path;
+
+ // Finding values for these properties is costly and are not needed by default, hence, they are
+ // not final and are only initialized at request.
+ @Nullable private Symbol member;
+
+ @Nullable private Symbol.ClassSymbol clazz;
+
+ public ClassAndMemberInfo(TreePath path) {
+ Preconditions.checkNotNull(path);
+ this.path = path;
+ }
+
+ public ClassAndMemberInfo(Tree regionTree) {
+ // regionTree should either represent a field or a method
+ Symbol symbol = ASTHelpers.getSymbol(regionTree);
+ if (!(regionTree instanceof MethodTree
+ || (regionTree instanceof VariableTree
+ && symbol != null
+ && symbol.getKind().equals(ElementKind.FIELD)))) {
+ throw new RuntimeException(
+ "not expecting a region tree " + regionTree + " of type " + regionTree.getClass());
+ }
+ this.member = symbol;
+ this.clazz = ASTHelpers.enclosingClass(this.member);
+ }
+
+ /** Finds the class and member where the error / fix is reported according to {@code path}. */
+ public void findValues() {
+ if (this.member != null || path == null) {
+ // Values are already computed.
+ return;
+ }
+ MethodTree enclosingMethod;
+ // If the error is reported on a method, that method itself is the relevant program point.
+ // Otherwise, use the enclosing method (if present).
+ enclosingMethod =
+ path.getLeaf() instanceof MethodTree
+ ? (MethodTree) path.getLeaf()
+ : ASTHelpers.findEnclosingNode(path, MethodTree.class);
+ // If the error is reported on a class, that class itself is the relevant program point.
+ // Otherwise, use the enclosing class.
+ ClassTree classTree =
+ path.getLeaf() instanceof ClassTree
+ ? (ClassTree) path.getLeaf()
+ : ASTHelpers.findEnclosingNode(path, ClassTree.class);
+ if (classTree != null) {
+ clazz = ASTHelpers.getSymbol(classTree);
+ if (enclosingMethod != null) {
+ // It is possible that the computed method is not enclosed by the computed class, e.g., for
+ // the following case:
+ // class C {
+ // void foo() {
+ // class Local {
+ // Object f = null; // error
+ // }
+ // }
+ // }
+ // Here the above code will compute clazz to be Local and method as foo(). In such cases,
+ // set method to null, we always want the corresponding method to be nested in the
+ // corresponding class.
+ Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(enclosingMethod);
+ if (!methodSymbol.isEnclosedBy(clazz)) {
+ enclosingMethod = null;
+ }
+ }
+ if (enclosingMethod != null) {
+ member = ASTHelpers.getSymbol(enclosingMethod);
+ } else {
+ // Node is not enclosed by any method, can be a field declaration or enclosed by it.
+ Symbol sym = ASTHelpers.getSymbol(path.getLeaf());
+ Symbol.VarSymbol fieldSymbol = null;
+ if (sym != null && sym.getKind().isField() && sym.isEnclosedBy(clazz)) {
+ // Directly on a field declaration.
+ fieldSymbol = (Symbol.VarSymbol) sym;
+ } else {
+ // Can be enclosed by a field declaration tree.
+ VariableTree fieldDeclTree = ASTHelpers.findEnclosingNode(path, VariableTree.class);
+ if (fieldDeclTree != null) {
+ fieldSymbol = ASTHelpers.getSymbol(fieldDeclTree);
+ }
+ }
+ if (fieldSymbol != null && fieldSymbol.isEnclosedBy(clazz)) {
+ member = fieldSymbol;
+ }
+ }
+ }
+ }
+
+ @Nullable
+ public Symbol getMember() {
+ return member;
+ }
+
+ @Nullable
+ public Symbol.ClassSymbol getClazz() {
+ return clazz;
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ErrorInfo.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ErrorInfo.java
new file mode 100644
index 0000000..eb4b21f
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/ErrorInfo.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization.out;
+
+import static com.uber.nullaway.ErrorMessage.MessageTypes.FIELD_NO_INIT;
+import static com.uber.nullaway.ErrorMessage.MessageTypes.METHOD_NO_INIT;
+
+import com.sun.source.tree.Tree;
+import com.sun.source.util.TreePath;
+import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.util.JCDiagnostic;
+import com.uber.nullaway.ErrorMessage;
+import com.uber.nullaway.fixserialization.Serializer;
+import java.nio.file.Path;
+import javax.annotation.Nullable;
+
+/** Stores information regarding an error which will be reported by NullAway. */
+public class ErrorInfo {
+
+ private final ErrorMessage errorMessage;
+ private final ClassAndMemberInfo classAndMemberInfo;
+
+ /**
+ * if non-null, this error involved a pseudo-assignment of a @Nullable expression into a @NonNull
+ * target, and this field is the Symbol for that target.
+ */
+ @Nullable private final Symbol nonnullTarget;
+
+ /**
+ * In cases where {@link ErrorInfo#nonnullTarget} is {@code null}, we serialize this value at its
+ * placeholder in the output tsv file.
+ */
+ public static final String EMPTY_NONNULL_TARGET_LOCATION_STRING =
+ "null\tnull\tnull\tnull\tnull\tnull";
+
+ /** Offset of program point where this error is reported. */
+ private final int offset;
+
+ /** Path to the containing source file where this error is reported. */
+ @Nullable private final Path path;
+
+ public ErrorInfo(
+ TreePath path, Tree errorTree, ErrorMessage errorMessage, @Nullable Symbol nonnullTarget) {
+ this.classAndMemberInfo =
+ (errorMessage.getMessageType().equals(FIELD_NO_INIT)
+ || errorMessage.getMessageType().equals(METHOD_NO_INIT))
+ ? new ClassAndMemberInfo(errorTree)
+ : new ClassAndMemberInfo(path);
+ this.errorMessage = errorMessage;
+ this.nonnullTarget = nonnullTarget;
+ JCDiagnostic.DiagnosticPosition treePosition = (JCDiagnostic.DiagnosticPosition) errorTree;
+ this.offset = treePosition.getStartPosition();
+ this.path =
+ Serializer.pathToSourceFileFromURI(path.getCompilationUnit().getSourceFile().toUri());
+ }
+
+ /**
+ * Getter for error message.
+ *
+ * @return Error message.
+ */
+ public ErrorMessage getErrorMessage() {
+ return errorMessage;
+ }
+
+ /**
+ * Region member where this error is reported by NullAway.
+ *
+ * @return Enclosing region member. Returns {@code null} if the values are not computed yet.
+ */
+ @Nullable
+ public Symbol getRegionMember() {
+ return classAndMemberInfo.getMember();
+ }
+
+ /**
+ * Region class where this error is reported by NullAway.
+ *
+ * @return Enclosing region class. Returns {@code null} if the values are not computed yet.
+ */
+ @Nullable
+ public Symbol getRegionClass() {
+ return classAndMemberInfo.getClazz();
+ }
+
+ /**
+ * Returns the symbol of a {@code @Nonnull} element which was involved in a pseudo-assignment of a
+ * {@code @Nullable} expression into a {@code @Nonnull} target and caused this error to be
+ * reported if such element exists, otherwise, it will return {@code null}.
+ *
+ * @return The symbol of the {@code @Nonnull} element if exists, and {@code null} otherwise.
+ */
+ @Nullable
+ public Symbol getNonnullTarget() {
+ return nonnullTarget;
+ }
+
+ /**
+ * Returns offset of program point where this error is reported.
+ *
+ * @return Offset of program point where this error is reported.
+ */
+ public int getOffset() {
+ return offset;
+ }
+
+ /**
+ * Returns Path to the containing source file where this error is reported.
+ *
+ * @return Path to the containing source file where this error is reported.
+ */
+ @Nullable
+ public Path getPath() {
+ return path;
+ }
+
+ /** Finds the class and member of program point where the error is reported. */
+ public void initEnclosing() {
+ classAndMemberInfo.findValues();
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/FieldInitializationInfo.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/FieldInitializationInfo.java
new file mode 100644
index 0000000..1a23c81
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/FieldInitializationInfo.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization.out;
+
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.fixserialization.Serializer;
+import com.uber.nullaway.fixserialization.adapters.SerializationAdapter;
+import com.uber.nullaway.fixserialization.location.SymbolLocation;
+
+/**
+ * Stores information regarding a method that initializes a class field and leaves it
+ * {@code @NonNull} at exit point.
+ */
+public class FieldInitializationInfo {
+
+ /** Symbol of the initializer method. */
+ private final SymbolLocation initializerMethodLocation;
+
+ /** Symbol of the initialized class field. */
+ private final Symbol field;
+
+ public FieldInitializationInfo(Symbol.MethodSymbol initializerMethod, Symbol field) {
+ this.initializerMethodLocation = SymbolLocation.createLocationFromSymbol(initializerMethod);
+ this.field = field;
+ }
+
+ /**
+ * Returns string representation of content of an object.
+ *
+ * @param adapter adapter used to serialize symbols.
+ * @return string representation of contents of an object in a line seperated by tabs.
+ */
+ public String tabSeparatedToString(SerializationAdapter adapter) {
+ return initializerMethodLocation.tabSeparatedToString(adapter)
+ + '\t'
+ + Serializer.serializeSymbol(field, adapter);
+ }
+
+ /**
+ * Creates header of an output file containing all {@link FieldInitializationInfo} written in
+ * string which values are separated by tabs.
+ *
+ * @return string representation of the header separated by tabs.
+ */
+ public static String header() {
+ return SymbolLocation.header() + '\t' + "field";
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/SuggestedNullableFixInfo.java b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/SuggestedNullableFixInfo.java
new file mode 100644
index 0000000..3c5c661
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/fixserialization/out/SuggestedNullableFixInfo.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.fixserialization.out;
+
+import com.sun.source.util.TreePath;
+import com.uber.nullaway.ErrorMessage;
+import com.uber.nullaway.fixserialization.Serializer;
+import com.uber.nullaway.fixserialization.adapters.SerializationAdapter;
+import com.uber.nullaway.fixserialization.location.SymbolLocation;
+import java.util.Objects;
+
+/** Stores information suggesting adding @Nullable on an element in source code. */
+public class SuggestedNullableFixInfo {
+
+ /** SymbolLocation of the target element in source code. */
+ private final SymbolLocation symbolLocation;
+
+ /** Error which will be resolved by this type change. */
+ private final ErrorMessage errorMessage;
+
+ private final ClassAndMemberInfo classAndMemberInfo;
+
+ public SuggestedNullableFixInfo(
+ TreePath path, SymbolLocation symbolLocation, ErrorMessage errorMessage) {
+ this.symbolLocation = symbolLocation;
+ this.errorMessage = errorMessage;
+ this.classAndMemberInfo = new ClassAndMemberInfo(path);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SuggestedNullableFixInfo)) {
+ return false;
+ }
+ SuggestedNullableFixInfo suggestedNullableFixInfo = (SuggestedNullableFixInfo) o;
+ return Objects.equals(symbolLocation, suggestedNullableFixInfo.symbolLocation)
+ && Objects.equals(
+ errorMessage.getMessageType().toString(),
+ suggestedNullableFixInfo.errorMessage.getMessageType().toString());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(symbolLocation, errorMessage.getMessageType().toString());
+ }
+
+ /**
+ * returns string representation of content of an object.
+ *
+ * @param adapter adapter used to serialize symbols.
+ * @return string representation of contents of an object in a line separated by tabs.
+ */
+ public String tabSeparatedToString(SerializationAdapter adapter) {
+ return String.join(
+ "\t",
+ symbolLocation.tabSeparatedToString(adapter),
+ errorMessage.getMessageType().toString(),
+ "nullable",
+ Serializer.serializeSymbol(classAndMemberInfo.getClazz(), adapter),
+ Serializer.serializeSymbol(classAndMemberInfo.getMember(), adapter));
+ }
+
+ /** Finds the class and member of program point where triggered this type change. */
+ public void initEnclosing() {
+ classAndMemberInfo.findValues();
+ }
+
+ /**
+ * Creates header of an output file containing all {@link SuggestedNullableFixInfo} written in
+ * string which values are separated by tabs.
+ *
+ * @return string representation of the header separated by tabs.
+ */
+ public static String header() {
+ return String.join(
+ "\t", SymbolLocation.header(), "reason", "annotation", "rootClass", "rootMethod");
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/AbstractFieldContractHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/AbstractFieldContractHandler.java
index 9189259..d90cc80 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/AbstractFieldContractHandler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/AbstractFieldContractHandler.java
@@ -22,6 +22,9 @@
package com.uber.nullaway.handlers;
+import static com.uber.nullaway.ASTHelpersBackports.getEnclosedElements;
+import static com.uber.nullaway.NullabilityUtil.castToNonNull;
+
import com.google.common.base.Preconditions;
import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
@@ -46,6 +49,7 @@ import javax.lang.model.element.VariableElement;
public abstract class AbstractFieldContractHandler extends BaseNoOpHandler {
protected static final String THIS_NOTATION = "this.";
+
/** Simple name of the annotation in {@code String} */
protected final String annotName;
@@ -69,7 +73,8 @@ public abstract class AbstractFieldContractHandler extends BaseNoOpHandler {
boolean isAnnotated = annotationContent != null;
boolean isValid =
isAnnotated
- && validateAnnotationSyntax(annotationContent, analysis, tree, state, methodSymbol)
+ && validateAnnotationSyntax(
+ castToNonNull(annotationContent), analysis, tree, state, methodSymbol)
&& validateAnnotationSemantics(analysis, state, tree, methodSymbol);
if (isAnnotated && !isValid) {
return;
@@ -81,7 +86,7 @@ public abstract class AbstractFieldContractHandler extends BaseNoOpHandler {
}
Set<String> fieldNames;
if (isAnnotated) {
- fieldNames = ContractUtils.trimReceivers(annotationContent);
+ fieldNames = ContractUtils.trimReceivers(castToNonNull(annotationContent));
} else {
fieldNames = Collections.emptySet();
}
@@ -153,7 +158,8 @@ public abstract class AbstractFieldContractHandler extends BaseNoOpHandler {
new ErrorMessage(ErrorMessage.MessageTypes.ANNOTATION_VALUE_INVALID, message),
tree,
analysis.buildDescription(tree),
- state));
+ state,
+ null));
return false;
} else {
for (String fieldName : content) {
@@ -174,13 +180,14 @@ public abstract class AbstractFieldContractHandler extends BaseNoOpHandler {
ErrorMessage.MessageTypes.ANNOTATION_VALUE_INVALID, message),
tree,
analysis.buildDescription(tree),
- state));
+ state,
+ null));
return false;
} else {
fieldName = fieldName.substring(fieldName.lastIndexOf(".") + 1);
}
}
- Symbol.ClassSymbol classSymbol = ASTHelpers.enclosingClass(methodSymbol);
+ Symbol.ClassSymbol classSymbol = castToNonNull(ASTHelpers.enclosingClass(methodSymbol));
VariableElement field = getInstanceFieldOfClass(classSymbol, fieldName);
if (field == null) {
message =
@@ -197,7 +204,8 @@ public abstract class AbstractFieldContractHandler extends BaseNoOpHandler {
new ErrorMessage(ErrorMessage.MessageTypes.ANNOTATION_VALUE_INVALID, message),
tree,
analysis.buildDescription(tree),
- state));
+ state,
+ null));
return false;
}
}
@@ -215,7 +223,7 @@ public abstract class AbstractFieldContractHandler extends BaseNoOpHandler {
public static @Nullable VariableElement getInstanceFieldOfClass(
Symbol.ClassSymbol classSymbol, String name) {
Preconditions.checkNotNull(classSymbol);
- for (Element member : classSymbol.getEnclosedElements()) {
+ for (Element member : getEnclosedElements(classSymbol)) {
if (member.getKind().isField() && !member.getModifiers().contains(Modifier.STATIC)) {
if (member.getSimpleName().toString().equals(name)) {
return (VariableElement) member;
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/ApacheThriftIsSetHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/ApacheThriftIsSetHandler.java
index 0a76585..802cedd 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/ApacheThriftIsSetHandler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/ApacheThriftIsSetHandler.java
@@ -21,27 +21,27 @@
*/
package com.uber.nullaway.handlers;
-import com.google.common.base.Preconditions;
+import static com.uber.nullaway.ASTHelpersBackports.getEnclosedElements;
+
import com.google.errorprone.VisitorState;
import com.google.errorprone.suppliers.Supplier;
import com.google.errorprone.suppliers.Suppliers;
-import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ClassTree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
-import com.sun.tools.javac.util.Context;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.Nullness;
+import com.uber.nullaway.annotations.Initializer;
import com.uber.nullaway.dataflow.AccessPath;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
+import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode;
import org.checkerframework.nullaway.dataflow.cfg.node.Node;
-import org.checkerframework.nullaway.javacutil.Pair;
/**
* Handler to better handle {@code isSetXXXX()} methods in code generated by Apache Thrift. With
@@ -54,8 +54,13 @@ public class ApacheThriftIsSetHandler extends BaseNoOpHandler {
private static final Supplier<Type> TBASE_TYPE_SUPPLIER = Suppliers.typeFromString(TBASE_NAME);
- @Nullable private Optional<Type> tbaseType;
+ private Optional<Type> tbaseType;
+ /**
+ * This method is annotated {@code @Initializer} since it will be invoked when the first class is
+ * processed, before any other handler methods
+ */
+ @Initializer
@Override
public void onMatchTopLevelClass(
NullAway analysis, ClassTree tree, VisitorState state, Symbol.ClassSymbol classSymbol) {
@@ -68,25 +73,24 @@ public class ApacheThriftIsSetHandler extends BaseNoOpHandler {
@Override
public NullnessHint onDataflowVisitMethodInvocation(
MethodInvocationNode node,
- Types types,
- Context context,
+ Symbol.MethodSymbol symbol,
+ VisitorState state,
AccessPath.AccessPathContext apContext,
AccessPathNullnessPropagation.SubNodeValues inputs,
AccessPathNullnessPropagation.Updates thenUpdates,
AccessPathNullnessPropagation.Updates elseUpdates,
AccessPathNullnessPropagation.Updates bothUpdates) {
- Symbol.MethodSymbol symbol = ASTHelpers.getSymbol(node.getTree());
- if (thriftIsSetCall(symbol, types)) {
+ if (thriftIsSetCall(symbol, state.getTypes())) {
String methodName = symbol.getSimpleName().toString();
// remove "isSet"
String capPropName = methodName.substring(5);
if (capPropName.length() > 0) {
// build access paths for the getter and the field access, and
// make them nonnull in the thenUpdates
- Pair<Element, Element> fieldAndGetter = getFieldAndSetterForProperty(symbol, capPropName);
+ FieldAndGetterElements fieldAndGetter = getFieldAndGetterForProperty(symbol, capPropName);
Node base = node.getTarget().getReceiver();
- updateNonNullAPsForElement(thenUpdates, fieldAndGetter.first, base, apContext);
- updateNonNullAPsForElement(thenUpdates, fieldAndGetter.second, base, apContext);
+ updateNonNullAPsForElement(thenUpdates, fieldAndGetter.fieldElem, base, apContext);
+ updateNonNullAPsForElement(thenUpdates, fieldAndGetter.getterElem, base, apContext);
}
}
return NullnessHint.UNKNOWN;
@@ -105,17 +109,47 @@ public class ApacheThriftIsSetHandler extends BaseNoOpHandler {
}
}
+ private static final class FieldAndGetterElements {
+
+ @Nullable final Element fieldElem;
+
+ @Nullable final Element getterElem;
+
+ public FieldAndGetterElements(@Nullable Element fieldElem, @Nullable Element getterElem) {
+ this.fieldElem = fieldElem;
+ this.getterElem = getterElem;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ FieldAndGetterElements that = (FieldAndGetterElements) o;
+ return Objects.equals(fieldElem, that.fieldElem)
+ && Objects.equals(getterElem, that.getterElem);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(fieldElem, getterElem);
+ }
+ }
+
/**
- * Returns the field (if it exists and is visible) and the setter for a property. If the field is
- * not available, the first element of the returned pair is {@code null}.
+ * Returns the field (if it exists and is visible) and the getter for a property. If the field is
+ * not available, returns {@code null}.
*/
- private Pair<Element, Element> getFieldAndSetterForProperty(
+ private FieldAndGetterElements getFieldAndGetterForProperty(
Symbol.MethodSymbol symbol, String capPropName) {
Element field = null;
Element getter = null;
String fieldName = decapitalize(capPropName);
String getterName = "get" + capPropName;
- for (Symbol elem : symbol.owner.getEnclosedElements()) {
+ for (Symbol elem : getEnclosedElements(symbol.owner)) {
if (elem.getKind().isField() && elem.getSimpleName().toString().equals(fieldName)) {
if (field != null) {
throw new RuntimeException("already found field " + fieldName);
@@ -131,9 +165,9 @@ public class ApacheThriftIsSetHandler extends BaseNoOpHandler {
}
if (field != null && field.asType().getKind().isPrimitive()) {
// ignore primitive properties
- return Pair.of(null, null);
+ return new FieldAndGetterElements(null, null);
}
- return Pair.of(field, getter);
+ return new FieldAndGetterElements(field, getter);
}
private static String decapitalize(String str) {
@@ -144,7 +178,6 @@ public class ApacheThriftIsSetHandler extends BaseNoOpHandler {
}
private boolean thriftIsSetCall(Symbol.MethodSymbol symbol, Types types) {
- Preconditions.checkNotNull(tbaseType);
// noinspection ConstantConditions
return tbaseType.isPresent()
&& symbol.getSimpleName().toString().startsWith("isSet")
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/AssertionHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/AssertionHandler.java
index c2b650e..2a24923 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/AssertionHandler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/AssertionHandler.java
@@ -24,10 +24,9 @@ package com.uber.nullaway.handlers;
import static com.uber.nullaway.Nullness.NONNULL;
+import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
import com.sun.tools.javac.code.Symbol;
-import com.sun.tools.javac.code.Types;
-import com.sun.tools.javac.util.Context;
import com.uber.nullaway.dataflow.AccessPath;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
import java.util.List;
@@ -46,32 +45,28 @@ public class AssertionHandler extends BaseNoOpHandler {
@Override
public NullnessHint onDataflowVisitMethodInvocation(
MethodInvocationNode node,
- Types types,
- Context context,
+ Symbol.MethodSymbol callee,
+ VisitorState state,
AccessPath.AccessPathContext apContext,
AccessPathNullnessPropagation.SubNodeValues inputs,
AccessPathNullnessPropagation.Updates thenUpdates,
AccessPathNullnessPropagation.Updates elseUpdates,
AccessPathNullnessPropagation.Updates bothUpdates) {
- Symbol.MethodSymbol callee = ASTHelpers.getSymbol(node.getTree());
- if (callee == null) {
- return NullnessHint.UNKNOWN;
- }
-
if (!methodNameUtil.isUtilInitialized()) {
methodNameUtil.initializeMethodNames(callee.name.table);
}
- // Look for statements of the form: assertThat(A).isNotNull()
+ // Look for statements of the form: assertThat(A).isNotNull() or
+ // assertThat(A).isInstanceOf(Foo.class)
// A will not be NULL after this statement.
- if (methodNameUtil.isMethodIsNotNull(callee)) {
+ if (methodNameUtil.isMethodIsNotNull(callee) || methodNameUtil.isMethodIsInstanceOf(callee)) {
Node receiver = node.getTarget().getReceiver();
if (receiver instanceof MethodInvocationNode) {
MethodInvocationNode receiver_method = (MethodInvocationNode) receiver;
Symbol.MethodSymbol receiver_symbol = ASTHelpers.getSymbol(receiver_method.getTree());
if (methodNameUtil.isMethodAssertThat(receiver_symbol)) {
Node arg = receiver_method.getArgument(0);
- AccessPath ap = AccessPath.getAccessPathForNodeNoMapGet(arg, apContext);
+ AccessPath ap = AccessPath.getAccessPathForNode(arg, state, apContext);
if (ap != null) {
bothUpdates.set(ap, NONNULL);
}
@@ -82,11 +77,15 @@ public class AssertionHandler extends BaseNoOpHandler {
// Look for statements of the form:
// * assertThat(A, is(not(nullValue())))
// * assertThat(A, is(notNullValue()))
+ // * assertThat(A, is(instanceOf(Foo.class)))
+ // * assertThat(A, isA(Foo.class))
if (methodNameUtil.isMethodHamcrestAssertThat(callee)
|| methodNameUtil.isMethodJunitAssertThat(callee)) {
List<Node> args = node.getArguments();
- if (args.size() == 2 && methodNameUtil.isMatcherIsNotNull(args.get(1))) {
- AccessPath ap = AccessPath.getAccessPathForNodeNoMapGet(args.get(0), apContext);
+ if (args.size() == 2
+ && (methodNameUtil.isMatcherIsNotNull(args.get(1))
+ || methodNameUtil.isMatcherIsInstanceOf(args.get(1)))) {
+ AccessPath ap = AccessPath.getAccessPathForNode(args.get(0), state, apContext);
if (ap != null) {
bothUpdates.set(ap, NONNULL);
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/BaseNoOpHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/BaseNoOpHandler.java
index 74ea3da..242a96a 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/BaseNoOpHandler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/BaseNoOpHandler.java
@@ -36,12 +36,17 @@ import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.util.Context;
import com.uber.nullaway.ErrorMessage;
import com.uber.nullaway.NullAway;
+import com.uber.nullaway.Nullness;
import com.uber.nullaway.dataflow.AccessPath;
+import com.uber.nullaway.dataflow.AccessPathNullnessAnalysis;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
import com.uber.nullaway.dataflow.NullnessStore;
+import com.uber.nullaway.dataflow.cfg.NullAwayCFGBuilder;
import java.util.List;
import java.util.Optional;
+import javax.annotation.Nullable;
import org.checkerframework.nullaway.dataflow.cfg.UnderlyingAST;
+import org.checkerframework.nullaway.dataflow.cfg.node.FieldAccessNode;
import org.checkerframework.nullaway.dataflow.cfg.node.LocalVariableNode;
import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode;
@@ -104,35 +109,32 @@ public abstract class BaseNoOpHandler implements Handler {
}
@Override
- public ImmutableSet<Integer> onUnannotatedInvocationGetExplicitlyNullablePositions(
- Context context,
+ public Nullness onOverrideMethodReturnNullability(
Symbol.MethodSymbol methodSymbol,
- ImmutableSet<Integer> explicitlyNullablePositions) {
- // NoOp
- return explicitlyNullablePositions;
- }
-
- @Override
- public boolean onUnannotatedInvocationGetExplicitlyNonNullReturn(
- Symbol.MethodSymbol methodSymbol, boolean explicitlyNonNullReturn) {
+ VisitorState state,
+ boolean isAnnotated,
+ Nullness returnNullness) {
// NoOp
- return explicitlyNonNullReturn;
+ return returnNullness;
}
@Override
- public ImmutableSet<Integer> onUnannotatedInvocationGetNonNullPositions(
- NullAway analysis,
- VisitorState state,
+ public Nullness[] onOverrideMethodInvocationParametersNullability(
+ Context context,
Symbol.MethodSymbol methodSymbol,
- List<? extends ExpressionTree> actualParams,
- ImmutableSet<Integer> nonNullPositions) {
+ boolean isAnnotated,
+ Nullness[] argumentPositionNullness) {
// NoOp
- return nonNullPositions;
+ return argumentPositionNullness;
}
@Override
public boolean onOverrideMayBeNullExpr(
- NullAway analysis, ExpressionTree expr, VisitorState state, boolean exprMayBeNull) {
+ NullAway analysis,
+ ExpressionTree expr,
+ @Nullable Symbol exprSymbol,
+ VisitorState state,
+ boolean exprMayBeNull) {
// NoOp
return exprMayBeNull;
}
@@ -148,8 +150,8 @@ public abstract class BaseNoOpHandler implements Handler {
@Override
public NullnessHint onDataflowVisitMethodInvocation(
MethodInvocationNode node,
- Types types,
- Context context,
+ Symbol.MethodSymbol symbol,
+ VisitorState state,
AccessPath.AccessPathContext apContext,
AccessPathNullnessPropagation.SubNodeValues inputs,
AccessPathNullnessPropagation.Updates thenUpdates,
@@ -160,6 +162,19 @@ public abstract class BaseNoOpHandler implements Handler {
}
@Override
+ public NullnessHint onDataflowVisitFieldAccess(
+ FieldAccessNode node,
+ Symbol symbol,
+ Types types,
+ Context context,
+ AccessPath.AccessPathContext apContext,
+ AccessPathNullnessPropagation.SubNodeValues inputs,
+ AccessPathNullnessPropagation.Updates updates) {
+ // NoOp
+ return NullnessHint.UNKNOWN;
+ }
+
+ @Override
public void onDataflowVisitReturn(
ReturnTree tree, NullnessStore thenStore, NullnessStore elseStore) {
// NoOp
@@ -186,4 +201,30 @@ public abstract class BaseNoOpHandler implements Handler {
public ImmutableSet<String> onRegisterImmutableTypes() {
return ImmutableSet.of();
}
+
+ @Override
+ public void onNonNullFieldAssignment(
+ Symbol field, AccessPathNullnessAnalysis analysis, VisitorState state) {
+ // NoOp
+ }
+
+ @Override
+ public MethodInvocationNode onCFGBuildPhase1AfterVisitMethodInvocation(
+ NullAwayCFGBuilder.NullAwayCFGTranslationPhaseOne phase,
+ MethodInvocationTree tree,
+ MethodInvocationNode originalNode) {
+ return originalNode;
+ }
+
+ @Override
+ @Nullable
+ public Integer castToNonNullArgumentPositionsForMethod(
+ NullAway analysis,
+ VisitorState state,
+ Symbol.MethodSymbol methodSymbol,
+ List<? extends ExpressionTree> actualParams,
+ @Nullable Integer previousArgumentPosition) {
+ // NoOp
+ return previousArgumentPosition;
+ }
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/CompositeHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/CompositeHandler.java
index 0ad99fb..31617da 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/CompositeHandler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/CompositeHandler.java
@@ -37,12 +37,17 @@ import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.util.Context;
import com.uber.nullaway.ErrorMessage;
import com.uber.nullaway.NullAway;
+import com.uber.nullaway.Nullness;
import com.uber.nullaway.dataflow.AccessPath;
+import com.uber.nullaway.dataflow.AccessPathNullnessAnalysis;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
import com.uber.nullaway.dataflow.NullnessStore;
+import com.uber.nullaway.dataflow.cfg.NullAwayCFGBuilder;
import java.util.List;
import java.util.Optional;
+import javax.annotation.Nullable;
import org.checkerframework.nullaway.dataflow.cfg.UnderlyingAST;
+import org.checkerframework.nullaway.dataflow.cfg.node.FieldAccessNode;
import org.checkerframework.nullaway.dataflow.cfg.node.LocalVariableNode;
import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode;
@@ -118,49 +123,41 @@ class CompositeHandler implements Handler {
}
@Override
- public ImmutableSet<Integer> onUnannotatedInvocationGetExplicitlyNullablePositions(
- Context context,
+ public Nullness onOverrideMethodReturnNullability(
Symbol.MethodSymbol methodSymbol,
- ImmutableSet<Integer> explicitlyNullablePositions) {
- for (Handler h : handlers) {
- explicitlyNullablePositions =
- h.onUnannotatedInvocationGetExplicitlyNullablePositions(
- context, methodSymbol, explicitlyNullablePositions);
- }
- return explicitlyNullablePositions;
- }
-
- @Override
- public boolean onUnannotatedInvocationGetExplicitlyNonNullReturn(
- Symbol.MethodSymbol methodSymbol, boolean explicitlyNonNullReturn) {
+ VisitorState state,
+ boolean isAnnotated,
+ Nullness returnNullness) {
for (Handler h : handlers) {
- explicitlyNonNullReturn =
- h.onUnannotatedInvocationGetExplicitlyNonNullReturn(
- methodSymbol, explicitlyNonNullReturn);
+ returnNullness =
+ h.onOverrideMethodReturnNullability(methodSymbol, state, isAnnotated, returnNullness);
}
- return explicitlyNonNullReturn;
+ return returnNullness;
}
@Override
- public ImmutableSet<Integer> onUnannotatedInvocationGetNonNullPositions(
- NullAway analysis,
- VisitorState state,
+ public Nullness[] onOverrideMethodInvocationParametersNullability(
+ Context context,
Symbol.MethodSymbol methodSymbol,
- List<? extends ExpressionTree> actualParams,
- ImmutableSet<Integer> nonNullPositions) {
+ boolean isAnnotated,
+ Nullness[] argumentPositionNullness) {
for (Handler h : handlers) {
- nonNullPositions =
- h.onUnannotatedInvocationGetNonNullPositions(
- analysis, state, methodSymbol, actualParams, nonNullPositions);
+ argumentPositionNullness =
+ h.onOverrideMethodInvocationParametersNullability(
+ context, methodSymbol, isAnnotated, argumentPositionNullness);
}
- return nonNullPositions;
+ return argumentPositionNullness;
}
@Override
public boolean onOverrideMayBeNullExpr(
- NullAway analysis, ExpressionTree expr, VisitorState state, boolean exprMayBeNull) {
+ NullAway analysis,
+ ExpressionTree expr,
+ @Nullable Symbol exprSymbol,
+ VisitorState state,
+ boolean exprMayBeNull) {
for (Handler h : handlers) {
- exprMayBeNull = h.onOverrideMayBeNullExpr(analysis, expr, state, exprMayBeNull);
+ exprMayBeNull = h.onOverrideMayBeNullExpr(analysis, expr, exprSymbol, state, exprMayBeNull);
}
return exprMayBeNull;
}
@@ -179,8 +176,8 @@ class CompositeHandler implements Handler {
@Override
public NullnessHint onDataflowVisitMethodInvocation(
MethodInvocationNode node,
- Types types,
- Context context,
+ Symbol.MethodSymbol symbol,
+ VisitorState state,
AccessPath.AccessPathContext apContext,
AccessPathNullnessPropagation.SubNodeValues inputs,
AccessPathNullnessPropagation.Updates thenUpdates,
@@ -190,7 +187,25 @@ class CompositeHandler implements Handler {
for (Handler h : handlers) {
NullnessHint n =
h.onDataflowVisitMethodInvocation(
- node, types, context, apContext, inputs, thenUpdates, elseUpdates, bothUpdates);
+ node, symbol, state, apContext, inputs, thenUpdates, elseUpdates, bothUpdates);
+ nullnessHint = nullnessHint.merge(n);
+ }
+ return nullnessHint;
+ }
+
+ @Override
+ public NullnessHint onDataflowVisitFieldAccess(
+ FieldAccessNode node,
+ Symbol symbol,
+ Types types,
+ Context context,
+ AccessPath.AccessPathContext apContext,
+ AccessPathNullnessPropagation.SubNodeValues inputs,
+ AccessPathNullnessPropagation.Updates updates) {
+ NullnessHint nullnessHint = NullnessHint.UNKNOWN;
+ for (Handler h : handlers) {
+ NullnessHint n =
+ h.onDataflowVisitFieldAccess(node, symbol, types, context, apContext, inputs, updates);
nullnessHint = nullnessHint.merge(n);
}
return nullnessHint;
@@ -242,4 +257,40 @@ class CompositeHandler implements Handler {
}
return builder.build();
}
+
+ @Override
+ public void onNonNullFieldAssignment(
+ Symbol field, AccessPathNullnessAnalysis analysis, VisitorState state) {
+ for (Handler h : handlers) {
+ h.onNonNullFieldAssignment(field, analysis, state);
+ }
+ }
+
+ @Override
+ public MethodInvocationNode onCFGBuildPhase1AfterVisitMethodInvocation(
+ NullAwayCFGBuilder.NullAwayCFGTranslationPhaseOne phase,
+ MethodInvocationTree tree,
+ MethodInvocationNode originalNode) {
+ MethodInvocationNode currentNode = originalNode;
+ for (Handler h : handlers) {
+ currentNode = h.onCFGBuildPhase1AfterVisitMethodInvocation(phase, tree, currentNode);
+ }
+ return currentNode;
+ }
+
+ @Override
+ @Nullable
+ public Integer castToNonNullArgumentPositionsForMethod(
+ NullAway analysis,
+ VisitorState state,
+ Symbol.MethodSymbol methodSymbol,
+ List<? extends ExpressionTree> actualParams,
+ @Nullable Integer previousArgumentPosition) {
+ for (Handler h : handlers) {
+ previousArgumentPosition =
+ h.castToNonNullArgumentPositionsForMethod(
+ analysis, state, methodSymbol, actualParams, previousArgumentPosition);
+ }
+ return previousArgumentPosition;
+ }
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/FieldInitializationSerializationHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/FieldInitializationSerializationHandler.java
new file mode 100644
index 0000000..be058c9
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/FieldInitializationSerializationHandler.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.handlers;
+
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.Tree;
+import com.sun.source.util.TreePath;
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.NullabilityUtil;
+import com.uber.nullaway.dataflow.AccessPathNullnessAnalysis;
+import com.uber.nullaway.fixserialization.FixSerializationConfig;
+import com.uber.nullaway.fixserialization.out.FieldInitializationInfo;
+import javax.lang.model.element.ElementKind;
+
+/**
+ * This handler is used to serialize information regarding methods that initialize a class field.
+ *
+ * <p>It uses the following heuristic: if a method assigns a {@code @NonNull} value to a field, and
+ * furthermore guarantees that said field is {@code @NonNull} on return from the method, then we
+ * serialize information for this method as a potential initializer for that field. This information
+ * can be used by an inference tool to detect initializer methods in classes.
+ *
+ * <p>Note that the above two conditions are both needed to eliminate the cases below:
+ *
+ * <ul>
+ * <li>Methods that initialize a field conditionally through only some of their execution paths
+ * <li>Methods which only check that the field is already initialized and terminate exceptionally
+ * otherwise (these methods guarantee the field is initialized on return, but are rarely what
+ * we are looking for when we look for candidates for {@code @Initializer})
+ * </ul>
+ */
+public class FieldInitializationSerializationHandler extends BaseNoOpHandler {
+
+ private final FixSerializationConfig config;
+
+ FieldInitializationSerializationHandler(FixSerializationConfig config) {
+ this.config = config;
+ }
+
+ @Override
+ public void onNonNullFieldAssignment(
+ Symbol field, AccessPathNullnessAnalysis analysis, VisitorState state) {
+ // Traversing AST is costly, therefore we access the initializer method through the leaf node in
+ // state parameter
+ TreePath pathToMethod =
+ NullabilityUtil.findEnclosingMethodOrLambdaOrInitializer(state.getPath());
+ if (pathToMethod == null || pathToMethod.getLeaf().getKind() != Tree.Kind.METHOD) {
+ return;
+ }
+ Symbol symbol = ASTHelpers.getSymbol(pathToMethod.getLeaf());
+ if (!(symbol instanceof Symbol.MethodSymbol)) {
+ return;
+ }
+ Symbol.MethodSymbol methodSymbol = (Symbol.MethodSymbol) symbol;
+ // Check if the method and the field are in the same class.
+ if (!field.enclClass().equals(methodSymbol.enclClass())) {
+ // We don't want m in A.m() to be a candidate initializer for B.f unless A == B
+ return;
+ }
+ // We are only looking for non-constructor methods that initialize a class field.
+ if (methodSymbol.getKind() == ElementKind.CONSTRUCTOR) {
+ return;
+ }
+ final String fieldName = field.getSimpleName().toString();
+ boolean leavesNonNullAtExitPoint =
+ analysis.getNonnullFieldsOfReceiverAtExit(pathToMethod, state.context).stream()
+ .anyMatch(element -> element.getSimpleName().toString().equals(fieldName));
+ if (!leavesNonNullAtExitPoint) {
+ // Method does not keep the field @NonNull at exit point and fails the post condition to be an
+ // Initializer.
+ return;
+ }
+ NullabilityUtil.castToNonNull(config.getSerializer())
+ .serializeFieldInitializationInfo(new FieldInitializationInfo(methodSymbol, field));
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/GrpcHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/GrpcHandler.java
index 8a4124c..82b436f 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/GrpcHandler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/GrpcHandler.java
@@ -21,7 +21,9 @@
*/
package com.uber.nullaway.handlers;
-import com.google.common.base.Preconditions;
+import static com.uber.nullaway.ASTHelpersBackports.getEnclosedElements;
+import static com.uber.nullaway.NullabilityUtil.castToNonNull;
+
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.VisitorState;
import com.google.errorprone.suppliers.Supplier;
@@ -32,9 +34,9 @@ import com.sun.source.tree.MethodInvocationTree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
-import com.sun.tools.javac.util.Context;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.Nullness;
+import com.uber.nullaway.annotations.Initializer;
import com.uber.nullaway.dataflow.AccessPath;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
import java.util.ArrayList;
@@ -59,9 +61,14 @@ public class GrpcHandler extends BaseNoOpHandler {
private static final Supplier<Type> GRPC_METADATA_KEY_TYPE_SUPPLIER =
Suppliers.typeFromString(GRPC_METADATA_KEY_TNAME);
- @Nullable private Optional<Type> grpcMetadataType;
- @Nullable private Optional<Type> grpcKeyType;
+ private Optional<Type> grpcMetadataType;
+ private Optional<Type> grpcKeyType;
+ /**
+ * This method is annotated {@code @Initializer} since it will be invoked when the first class is
+ * processed, before any other handler methods
+ */
+ @Initializer
@Override
public void onMatchTopLevelClass(
NullAway analysis, ClassTree tree, VisitorState state, Symbol.ClassSymbol classSymbol) {
@@ -78,22 +85,24 @@ public class GrpcHandler extends BaseNoOpHandler {
@Override
public NullnessHint onDataflowVisitMethodInvocation(
MethodInvocationNode node,
- Types types,
- Context context,
+ Symbol.MethodSymbol symbol,
+ VisitorState state,
AccessPath.AccessPathContext apContext,
AccessPathNullnessPropagation.SubNodeValues inputs,
AccessPathNullnessPropagation.Updates thenUpdates,
AccessPathNullnessPropagation.Updates elseUpdates,
AccessPathNullnessPropagation.Updates bothUpdates) {
- MethodInvocationTree tree = node.getTree();
- Symbol.MethodSymbol symbol = ASTHelpers.getSymbol(tree);
+ MethodInvocationTree tree = castToNonNull(node.getTree());
+ Types types = state.getTypes();
if (grpcIsMetadataContainsKeyCall(symbol, types)) {
// On seeing o.containsKey(k), set AP for o.get(k) to @NonNull
Element getter = getGetterForMetadataSubtype(symbol.enclClass(), types);
Node base = node.getTarget().getReceiver();
// Argument list and types should be already checked by grpcIsMetadataContainsKeyCall
Symbol keyArgSymbol = ASTHelpers.getSymbol(tree.getArguments().get(0));
- if (getter != null && keyArgSymbol.getKind().equals(ElementKind.FIELD)) {
+ if (getter != null
+ && keyArgSymbol instanceof Symbol.VarSymbol
+ && keyArgSymbol.getKind().equals(ElementKind.FIELD)) {
Symbol.VarSymbol varSymbol = (Symbol.VarSymbol) keyArgSymbol;
String immutableFieldFQN =
varSymbol.enclClass().flatName().toString() + "." + varSymbol.flatName().toString();
@@ -119,7 +128,7 @@ public class GrpcHandler extends BaseNoOpHandler {
private Symbol.MethodSymbol getGetterForMetadataSubtype(
Symbol.ClassSymbol classSymbol, Types types) {
// Is there a better way than iteration?
- for (Symbol elem : classSymbol.getEnclosedElements()) {
+ for (Symbol elem : getEnclosedElements(classSymbol)) {
if (elem.getKind().equals(ElementKind.METHOD)) {
Symbol.MethodSymbol methodSymbol = (Symbol.MethodSymbol) elem;
if (grpcIsMetadataGetCall(methodSymbol, types)) {
@@ -131,8 +140,6 @@ public class GrpcHandler extends BaseNoOpHandler {
}
private boolean grpcIsMetadataContainsKeyCall(Symbol.MethodSymbol symbol, Types types) {
- Preconditions.checkNotNull(grpcMetadataType);
- Preconditions.checkNotNull(grpcKeyType);
// noinspection ConstantConditions
return grpcMetadataType.isPresent()
&& grpcKeyType.isPresent()
@@ -145,8 +152,6 @@ public class GrpcHandler extends BaseNoOpHandler {
}
private boolean grpcIsMetadataGetCall(Symbol.MethodSymbol symbol, Types types) {
- Preconditions.checkNotNull(grpcMetadataType);
- Preconditions.checkNotNull(grpcKeyType);
// noinspection ConstantConditions
return grpcMetadataType.isPresent()
&& grpcKeyType.isPresent()
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/GuavaAssertionsHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/GuavaAssertionsHandler.java
new file mode 100644
index 0000000..5356171
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/GuavaAssertionsHandler.java
@@ -0,0 +1,75 @@
+package com.uber.nullaway.handlers;
+
+import com.google.common.base.Preconditions;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.dataflow.cfg.NullAwayCFGBuilder;
+import javax.annotation.Nullable;
+import javax.lang.model.element.Name;
+import javax.lang.model.type.TypeMirror;
+import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode;
+
+/**
+ * Handler to expose semantics of Guava routines like {@code checkState}, {@code checkArgument}, and
+ * {@code verify} that check a boolean condition and fail with an exception if it is false.
+ */
+public class GuavaAssertionsHandler extends BaseNoOpHandler {
+
+ private static final String PRECONDITIONS_CLASS_NAME = "com.google.common.base.Preconditions";
+ private static final String CHECK_ARGUMENT_METHOD_NAME = "checkArgument";
+ private static final String CHECK_STATE_METHOD_NAME = "checkState";
+ private static final String VERIFY_CLASS_NAME = "com.google.common.base.Verify";
+ private static final String VERIFY_METHOD_NAME = "verify";
+
+ @Nullable private Name preconditionsClass;
+ @Nullable private Name verifyClass;
+ @Nullable private Name checkArgumentMethod;
+ @Nullable private Name checkStateMethod;
+ @Nullable private Name verifyMethod;
+ @Nullable TypeMirror preconditionCheckArgumentErrorType;
+ @Nullable TypeMirror preconditionCheckStateErrorType;
+ @Nullable TypeMirror verifyErrorType;
+
+ @Override
+ public MethodInvocationNode onCFGBuildPhase1AfterVisitMethodInvocation(
+ NullAwayCFGBuilder.NullAwayCFGTranslationPhaseOne phase,
+ MethodInvocationTree tree,
+ MethodInvocationNode originalNode) {
+ Symbol.MethodSymbol callee = ASTHelpers.getSymbol(tree);
+ if (preconditionsClass == null) {
+ preconditionsClass = callee.name.table.fromString(PRECONDITIONS_CLASS_NAME);
+ verifyClass = callee.name.table.fromString(VERIFY_CLASS_NAME);
+ checkArgumentMethod = callee.name.table.fromString(CHECK_ARGUMENT_METHOD_NAME);
+ checkStateMethod = callee.name.table.fromString(CHECK_STATE_METHOD_NAME);
+ verifyMethod = callee.name.table.fromString(VERIFY_METHOD_NAME);
+ preconditionCheckArgumentErrorType = phase.classToErrorType(IllegalArgumentException.class);
+ preconditionCheckStateErrorType = phase.classToErrorType(IllegalStateException.class);
+ // We treat the Verify.* APIs as throwing a RuntimeException to avoid any issues with
+ // the VerifyException that they actually throw not being in the classpath (this will not
+ // affect the analysis result)
+ verifyErrorType = phase.classToErrorType(RuntimeException.class);
+ }
+ Preconditions.checkNotNull(preconditionCheckArgumentErrorType);
+ Preconditions.checkNotNull(preconditionCheckStateErrorType);
+ Preconditions.checkNotNull(verifyErrorType);
+ if (callee.enclClass().getQualifiedName().equals(preconditionsClass)
+ && !callee.getParameters().isEmpty()) {
+ // Attempt to match Precondition check methods to the expected exception type, providing as
+ // much context as possible for static analysis.
+ // In practice this may not be strictly necessary because the conditional throw is inserted
+ // after the method invocation, thus analysis must assume that the preconditions call is
+ // capable of throwing any unchecked throwable.
+ if (callee.name.equals(checkArgumentMethod)) {
+ phase.insertThrowOnFalse(originalNode.getArgument(0), preconditionCheckArgumentErrorType);
+ } else if (callee.name.equals(checkStateMethod)) {
+ phase.insertThrowOnFalse(originalNode.getArgument(0), preconditionCheckStateErrorType);
+ }
+ } else if (callee.enclClass().getQualifiedName().equals(verifyClass)
+ && !callee.getParameters().isEmpty()
+ && callee.name.equals(verifyMethod)) {
+ phase.insertThrowOnFalse(originalNode.getArgument(0), verifyErrorType);
+ }
+ return originalNode;
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/Handler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/Handler.java
index 52a5a1a..835c01f 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/Handler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/Handler.java
@@ -35,14 +35,19 @@ import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.util.Context;
import com.uber.nullaway.ErrorMessage;
+import com.uber.nullaway.LibraryModels;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.Nullness;
import com.uber.nullaway.dataflow.AccessPath;
+import com.uber.nullaway.dataflow.AccessPathNullnessAnalysis;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
import com.uber.nullaway.dataflow.NullnessStore;
+import com.uber.nullaway.dataflow.cfg.NullAwayCFGBuilder;
import java.util.List;
import java.util.Optional;
+import javax.annotation.Nullable;
import org.checkerframework.nullaway.dataflow.cfg.UnderlyingAST;
+import org.checkerframework.nullaway.dataflow.cfg.node.FieldAccessNode;
import org.checkerframework.nullaway.dataflow.cfg.node.LocalVariableNode;
import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode;
@@ -131,72 +136,69 @@ public interface Handler {
void onMatchReturn(NullAway analysis, ReturnTree tree, VisitorState state);
/**
- * Called when NullAway encounters an unannotated method and asks for params explicitly
- * marked @Nullable
- *
- * <p>This is mostly used to force overriding methods to also use @Nullable, e.g. for callbacks
- * that can get passed null values.
- *
- * <p>Note that this should be only used for parameters EXPLICLTY marked as @Nullable (e.g. by
- * library models) rather than those considered @Nullable by NullAway's default optimistic
- * assumptions.
- *
- * @param methodSymbol The method symbol for the unannotated method in question.
- * @param explicitlyNullablePositions Parameter nullability computed by upstream handlers (the
- * core analysis supplies the empty set to the first handler in the chain).
- * @return Updated parameter nullability computed by this handler.
- */
- ImmutableSet<Integer> onUnannotatedInvocationGetExplicitlyNullablePositions(
- Context context,
- Symbol.MethodSymbol methodSymbol,
- ImmutableSet<Integer> explicitlyNullablePositions);
-
- /**
- * Called when NullAway encounters an unannotated method and asks if return value is explicitly
- * marked @Nullable
- *
- * <p>Note that this should be only used for return values EXPLICLTY marked as @NonNull (e.g. by
- * library models) rather than those considered @Nullable by NullAway's default optimistic
- * assumptions.
+ * Called after the analysis determines if a expression can be null or not, allowing handlers to
+ * override.
*
- * @param methodSymbol The method symbol for the unannotated method in question.
- * @param explicitlyNonNullReturn return nullability computed by upstream handlers.
- * @return Updated return nullability computed by this handler.
+ * @param analysis A reference to the running NullAway analysis.
+ * @param expr The expression in question.
+ * @param exprSymbol The symbol of the expression, might be null
+ * @param state The current visitor state.
+ * @param exprMayBeNull Whether or not the expression may be null according to the base analysis
+ * or upstream handlers.
+ * @return Whether or not the expression may be null, as updated by this handler.
*/
- boolean onUnannotatedInvocationGetExplicitlyNonNullReturn(
- Symbol.MethodSymbol methodSymbol, boolean explicitlyNonNullReturn);
+ boolean onOverrideMayBeNullExpr(
+ NullAway analysis,
+ ExpressionTree expr,
+ @Nullable Symbol exprSymbol,
+ VisitorState state,
+ boolean exprMayBeNull);
/**
- * Called when NullAway encounters an unannotated method and asks for params default nullability
+ * Called to potentially override the nullability of an annotated or unannotated method's return,
+ * when only the method symbol (and not a full invocation tree) is available. This is used
+ * primarily for checking subtyping / method overrides.
*
- * @param analysis A reference to the running NullAway analysis.
+ * @param methodSymbol The method symbol for the method in question.
* @param state The current visitor state.
- * @param methodSymbol The method symbol for the unannotated method in question.
- * @param actualParams The actual parameters from the invocation node
- * @param nonNullPositions Parameter nullability computed by upstream handlers (the core analysis
- * supplies the empty set to the first handler in the chain).
- * @return Updated parameter nullability computed by this handler.
+ * @param isAnnotated A boolean flag indicating whether the called method is considered to be
+ * within annotated or unannotated code, used to avoid querying for this information multiple
+ * times within the same handler chain.
+ * @param returnNullness return nullness computed by upstream handlers or NullAway core.
+ * @return Updated return nullability computed by this handler.
*/
- ImmutableSet<Integer> onUnannotatedInvocationGetNonNullPositions(
- NullAway analysis,
- VisitorState state,
+ Nullness onOverrideMethodReturnNullability(
Symbol.MethodSymbol methodSymbol,
- List<? extends ExpressionTree> actualParams,
- ImmutableSet<Integer> nonNullPositions);
+ VisitorState state,
+ boolean isAnnotated,
+ Nullness returnNullness);
/**
- * Called after the analysis determines if a expression can be null or not, allowing handlers to
- * override.
+ * Called after the analysis determines the nullability of a method's arguments, allowing handlers
+ * to override.
*
- * @param analysis A reference to the running NullAway analysis.
- * @param expr The expression in question.
- * @param state The current visitor state.
- * @param exprMayBeNull Whether or not the expression may be null according to the base analysis
- * or upstream handlers.
- * @return Whether or not the expression may be null, as updated by this handler.
+ * <p>The passed Map object maps argument positions to nullness information and is sparse, where
+ * the nullness of missing indexes is determined by base analysis and depends on if the code is
+ * considered isAnnotated or not. We use a mutable map for performance, but it should not outlive
+ * the chain of handler invocations.
+ *
+ * @param context The current context.
+ * @param methodSymbol The method symbol for the method in question.
+ * @param isAnnotated A boolean flag indicating whether the called method is considered to be
+ * within isAnnotated or unannotated code, used to avoid querying for this information
+ * multiple times within the same handler chain.
+ * @param argumentPositionNullness Nullness info for each argument position as computed by
+ * upstream handlers and/or the base analysis. Some entries may be {@code null}, indicating
+ * upstream handlers and the base analysis consider the parameter to be nullness-unknown,
+ * usually since the parameter is from unannotated code.
+ * @return The updated nullness info for each argument position, as computed by the current
+ * handler.
*/
- boolean onOverrideMayBeNullExpr(
- NullAway analysis, ExpressionTree expr, VisitorState state, boolean exprMayBeNull);
+ Nullness[] onOverrideMethodInvocationParametersNullability(
+ Context context,
+ Symbol.MethodSymbol methodSymbol,
+ boolean isAnnotated,
+ Nullness[] argumentPositionNullness);
/**
* Called when the Dataflow analysis generates the initial NullnessStore for a method or lambda.
@@ -220,7 +222,8 @@ public interface Handler {
* Called when the Dataflow analysis visits each method invocation.
*
* @param node The AST node for the method callsite.
- * @param types {@link Types} for the current compilation
+ * @param symbol The symbol of the called method
+ * @param state The current visitor state.
* @param apContext the current access path context information (see {@link
* AccessPath.AccessPathContext}).
* @param inputs NullnessStore information known before the method invocation.
@@ -236,8 +239,8 @@ public interface Handler {
*/
NullnessHint onDataflowVisitMethodInvocation(
MethodInvocationNode node,
- Types types,
- Context context,
+ Symbol.MethodSymbol symbol,
+ VisitorState state,
AccessPath.AccessPathContext apContext,
AccessPathNullnessPropagation.SubNodeValues inputs,
AccessPathNullnessPropagation.Updates thenUpdates,
@@ -245,6 +248,30 @@ public interface Handler {
AccessPathNullnessPropagation.Updates bothUpdates);
/**
+ * Called when the Dataflow analysis visits each field access.
+ *
+ * @param node The AST node for the field access.
+ * @param symbol The {@link Symbol} object for the above node, provided for convenience.
+ * @param types {@link Types} for the current compilation
+ * @param context the javac Context object (or Error Prone SubContext)
+ * @param apContext the current access path context information (see {@link
+ * AccessPath.AccessPathContext}).
+ * @param inputs NullnessStore information known before the method invocation.
+ * @param updates NullnessStore updates to be added, handlers can add via the set() method.
+ * @return The Nullness information for this field computed by this handler. See NullnessHint and
+ * CompositeHandler for more information about how this values get merged into a final
+ * Nullness value.
+ */
+ NullnessHint onDataflowVisitFieldAccess(
+ FieldAccessNode node,
+ Symbol symbol,
+ Types types,
+ Context context,
+ AccessPath.AccessPathContext apContext,
+ AccessPathNullnessPropagation.SubNodeValues inputs,
+ AccessPathNullnessPropagation.Updates updates);
+
+ /**
* Called when the Dataflow analysis visits a return statement.
*
* @param tree The AST node for the return statement being matched.
@@ -315,8 +342,59 @@ public interface Handler {
ImmutableSet<String> onRegisterImmutableTypes();
/**
+ * Called when a method writes a {@code @NonNull} value to a class field.
+ *
+ * @param field Symbol of the initialized class field.
+ * @param analysis nullness dataflow analysis
+ * @param state VisitorState.
+ */
+ void onNonNullFieldAssignment(
+ Symbol field, AccessPathNullnessAnalysis analysis, VisitorState state);
+
+ /**
+ * Called during AST to CFG translation (CFGTranslationPhaseOne) immediately after translating a
+ * MethodInvocationTree.
+ *
+ * @param phase a reference to the NullAwayCFGTranslationPhaseOne object and its utility
+ * functions.
+ * @param tree the MethodInvocationTree being translated.
+ * @param originalNode the resulting MethodInvocationNode right before this handler is called.
+ * @return a MethodInvocationNode which might be originalNode or a modified version, this is
+ * passed to the next handler in the chain.
+ */
+ MethodInvocationNode onCFGBuildPhase1AfterVisitMethodInvocation(
+ NullAwayCFGBuilder.NullAwayCFGTranslationPhaseOne phase,
+ MethodInvocationTree tree,
+ MethodInvocationNode originalNode);
+
+ /**
+ * Called to determine when a method acts as a cast-to-non-null operation on its parameters.
+ *
+ * <p>See {@link LibraryModels#castToNonNullMethods()} for more information about general
+ * configuration of <code>castToNonNull</code> methods.
+ *
+ * @param analysis A reference to the running NullAway analysis.
+ * @param state The current visitor state.
+ * @param methodSymbol The method symbol for the potential castToNonNull method.
+ * @param actualParams The actual parameters from the invocation node
+ * @param previousArgumentPosition The result computed by the previous handler in the chain, if
+ * any.
+ * @return The index of the parameter for which the method should act as a cast (if any). This
+ * value can be set only once through the full chain of handlers, with each handler deciding
+ * whether to propagate or override the value previousArgumentPosition passed by the previous
+ * handler in the chain.
+ */
+ @Nullable
+ Integer castToNonNullArgumentPositionsForMethod(
+ NullAway analysis,
+ VisitorState state,
+ Symbol.MethodSymbol methodSymbol,
+ List<? extends ExpressionTree> actualParams,
+ @Nullable Integer previousArgumentPosition);
+
+ /**
* A three value enum for handlers implementing onDataflowVisitMethodInvocation to communicate
- * their knowledge of the method return nullability to the the rest of NullAway.
+ * their knowledge of the method return nullability to the rest of NullAway.
*/
public enum NullnessHint {
/**
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/Handlers.java b/nullaway/src/main/java/com/uber/nullaway/handlers/Handlers.java
index a1dfe51..22792dd 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/Handlers.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/Handlers.java
@@ -28,6 +28,7 @@ import com.uber.nullaway.handlers.contract.ContractCheckHandler;
import com.uber.nullaway.handlers.contract.ContractHandler;
import com.uber.nullaway.handlers.contract.fieldcontract.EnsuresNonNullHandler;
import com.uber.nullaway.handlers.contract.fieldcontract.RequiresNonNullHandler;
+import com.uber.nullaway.handlers.temporary.FluentFutureHandler;
/** Utility static methods for the handlers package. */
public class Handlers {
@@ -55,20 +56,31 @@ public class Handlers {
if (config.handleTestAssertionLibraries()) {
handlerListBuilder.add(new AssertionHandler(methodNameUtil));
}
- handlerListBuilder.add(new LibraryModelsHandler(config));
+ handlerListBuilder.add(new GuavaAssertionsHandler());
+ LibraryModelsHandler libraryModelsHandler = new LibraryModelsHandler(config);
+ handlerListBuilder.add(libraryModelsHandler);
handlerListBuilder.add(StreamNullabilityPropagatorFactory.getRxStreamNullabilityPropagator());
handlerListBuilder.add(StreamNullabilityPropagatorFactory.getJavaStreamNullabilityPropagator());
+ handlerListBuilder.add(
+ StreamNullabilityPropagatorFactory.fromSpecs(
+ libraryModelsHandler.getStreamNullabilitySpecs()));
handlerListBuilder.add(new ContractHandler(config));
handlerListBuilder.add(new ApacheThriftIsSetHandler());
handlerListBuilder.add(new GrpcHandler());
handlerListBuilder.add(new RequiresNonNullHandler());
handlerListBuilder.add(new EnsuresNonNullHandler());
+ if (config.serializationIsActive() && config.getSerializationConfig().fieldInitInfoEnabled) {
+ handlerListBuilder.add(
+ new FieldInitializationSerializationHandler(config.getSerializationConfig()));
+ }
if (config.checkOptionalEmptiness()) {
handlerListBuilder.add(new OptionalEmptinessHandler(config, methodNameUtil));
}
if (config.checkContracts()) {
handlerListBuilder.add(new ContractCheckHandler(config));
}
+ handlerListBuilder.add(new LombokHandler(config));
+ handlerListBuilder.add(new FluentFutureHandler());
return new CompositeHandler(handlerListBuilder.build());
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java
index 3e34e3a..bd70059 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/InferredJARModelsHandler.java
@@ -23,19 +23,15 @@
package com.uber.nullaway.handlers;
import com.google.common.base.Preconditions;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
import com.google.errorprone.VisitorState;
-import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
-import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Type;
-import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.util.Context;
import com.uber.nullaway.Config;
import com.uber.nullaway.NullAway;
+import com.uber.nullaway.Nullness;
import com.uber.nullaway.dataflow.AccessPath;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
import com.uber.nullaway.jarinfer.JarInferStubxProvider;
@@ -44,10 +40,10 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
-import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
+import javax.annotation.Nullable;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.TypeKind;
import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode;
@@ -125,12 +121,25 @@ public class InferredJARModelsHandler extends BaseNoOpHandler {
}
@Override
- public ImmutableSet<Integer> onUnannotatedInvocationGetNonNullPositions(
- NullAway analysis,
- VisitorState state,
+ public Nullness[] onOverrideMethodInvocationParametersNullability(
+ Context context,
Symbol.MethodSymbol methodSymbol,
- List<? extends ExpressionTree> actualParams,
- ImmutableSet<Integer> nonNullPositions) {
+ boolean isAnnotated,
+ Nullness[] argumentPositionNullness) {
+ if (isAnnotated) {
+ // We currently do not load JarInfer models for code marked as annotated.
+ // This is unlikely to change, as the behavior of JarInfer on arguments is to explicitly mark
+ // as @NonNull those arguments that are shallowly dereferenced within the analyzed method. By
+ // convention, annotated code has no use for explicit @NonNull annotations, since `T` already
+ // means `@NonNull T` within annotated code. The only case where we would want to enable this
+ // for annotated code is if we expect/want JarInfer results to override the results of another
+ // handler, such as restrictive annotations, but a library models is a safer place to perform
+ // such an override.
+ // Additionally, by default, InferredJARModelsHandler is used only to load our Android SDK
+ // JarInfer models (i.e. `com.uber.nullaway:JarInferAndroidModelsSDK##`), since the default
+ // model of JarInfer on a normal jar/aar is to add bytecode annotations.
+ return argumentPositionNullness;
+ }
Symbol.ClassSymbol classSymbol = methodSymbol.enclClass();
String className = classSymbol.getQualifiedName().toString();
if (methodSymbol.getModifiers().contains(Modifier.ABSTRACT)) {
@@ -138,41 +147,43 @@ public class InferredJARModelsHandler extends BaseNoOpHandler {
VERBOSE,
"Warn",
"Skipping abstract method: " + className + " : " + methodSymbol.getQualifiedName());
- return nonNullPositions;
+ return argumentPositionNullness;
}
if (!argAnnotCache.containsKey(className)) {
- return nonNullPositions;
+ return argumentPositionNullness;
}
String methodSign = getMethodSignature(methodSymbol);
Map<Integer, Set<String>> methodArgAnnotations = lookupMethodInCache(className, methodSign);
if (methodArgAnnotations == null) {
- return nonNullPositions;
+ return argumentPositionNullness;
}
Set<Integer> jiNonNullParams = new LinkedHashSet<>();
for (Map.Entry<Integer, Set<String>> annotationEntry : methodArgAnnotations.entrySet()) {
if (annotationEntry.getKey() != RETURN
&& annotationEntry.getValue().contains("javax.annotation.Nonnull")) {
// Skip 'this' param for non-static methods
- jiNonNullParams.add(annotationEntry.getKey() - (methodSymbol.isStatic() ? 0 : 1));
+ int nonNullPosition = annotationEntry.getKey() - (methodSymbol.isStatic() ? 0 : 1);
+ jiNonNullParams.add(nonNullPosition);
+ argumentPositionNullness[nonNullPosition] = Nullness.NONNULL;
}
}
if (!jiNonNullParams.isEmpty()) {
LOG(DEBUG, "DEBUG", "Nonnull params: " + jiNonNullParams.toString() + " for " + methodSign);
}
- return Sets.union(nonNullPositions, jiNonNullParams).immutableCopy();
+ return argumentPositionNullness;
}
@Override
public NullnessHint onDataflowVisitMethodInvocation(
MethodInvocationNode node,
- Types types,
- Context context,
+ Symbol.MethodSymbol symbol,
+ VisitorState state,
AccessPath.AccessPathContext apContext,
AccessPathNullnessPropagation.SubNodeValues inputs,
AccessPathNullnessPropagation.Updates thenUpdates,
AccessPathNullnessPropagation.Updates elseUpdates,
AccessPathNullnessPropagation.Updates bothUpdates) {
- if (isReturnAnnotatedNullable(ASTHelpers.getSymbol(node.getTree()))) {
+ if (isReturnAnnotatedNullable(symbol)) {
return NullnessHint.HINT_NULLABLE;
}
return NullnessHint.UNKNOWN;
@@ -180,12 +191,20 @@ public class InferredJARModelsHandler extends BaseNoOpHandler {
@Override
public boolean onOverrideMayBeNullExpr(
- NullAway analysis, ExpressionTree expr, VisitorState state, boolean exprMayBeNull) {
- if (expr.getKind().equals(Tree.Kind.METHOD_INVOCATION)) {
- return exprMayBeNull
- || isReturnAnnotatedNullable(ASTHelpers.getSymbol((MethodInvocationTree) expr));
+ NullAway analysis,
+ ExpressionTree expr,
+ @Nullable Symbol exprSymbol,
+ VisitorState state,
+ boolean exprMayBeNull) {
+ if (exprMayBeNull) {
+ return true;
+ }
+ if (expr.getKind() == Tree.Kind.METHOD_INVOCATION
+ && exprSymbol instanceof Symbol.MethodSymbol
+ && isReturnAnnotatedNullable((Symbol.MethodSymbol) exprSymbol)) {
+ return true;
}
- return exprMayBeNull;
+ return false;
}
private boolean isReturnAnnotatedNullable(Symbol.MethodSymbol methodSymbol) {
@@ -210,6 +229,7 @@ public class InferredJARModelsHandler extends BaseNoOpHandler {
return false;
}
+ @Nullable
private Map<Integer, Set<String>> lookupMethodInCache(String className, String methodSign) {
if (!argAnnotCache.containsKey(className)) {
return null;
@@ -329,15 +349,12 @@ public class InferredJARModelsHandler extends BaseNoOpHandler {
private void cacheAnnotation(String methodSig, Integer argNum, String annotation) {
// TODO: handle inner classes properly
String className = methodSig.split(":")[0].replace('$', '.');
- if (!argAnnotCache.containsKey(className)) {
- argAnnotCache.put(className, new LinkedHashMap<>());
- }
- if (!argAnnotCache.get(className).containsKey(methodSig)) {
- argAnnotCache.get(className).put(methodSig, new LinkedHashMap<>());
- }
- if (!argAnnotCache.get(className).get(methodSig).containsKey(argNum)) {
- argAnnotCache.get(className).get(methodSig).put(argNum, new LinkedHashSet<>());
- }
- argAnnotCache.get(className).get(methodSig).get(argNum).add(annotation);
+ Map<String, Map<Integer, Set<String>>> cacheForClass =
+ argAnnotCache.computeIfAbsent(className, s -> new LinkedHashMap<>());
+ Map<Integer, Set<String>> cacheForMethod =
+ cacheForClass.computeIfAbsent(methodSig, s -> new LinkedHashMap<>());
+ Set<String> cacheForArgument =
+ cacheForMethod.computeIfAbsent(argNum, s -> new LinkedHashSet<>());
+ cacheForArgument.add(annotation);
}
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java
index a404495..e000c4f 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java
@@ -24,11 +24,12 @@ package com.uber.nullaway.handlers;
import static com.uber.nullaway.LibraryModels.MethodRef.methodRef;
import static com.uber.nullaway.Nullness.NONNULL;
+import static com.uber.nullaway.Nullness.NULLABLE;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
-import com.google.common.collect.Sets;
import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
@@ -38,14 +39,17 @@ import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Names;
+import com.uber.nullaway.CodeAnnotationInfo;
import com.uber.nullaway.Config;
import com.uber.nullaway.LibraryModels;
import com.uber.nullaway.LibraryModels.MethodRef;
import com.uber.nullaway.NullAway;
-import com.uber.nullaway.NullabilityUtil;
+import com.uber.nullaway.Nullness;
import com.uber.nullaway.dataflow.AccessPath;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
+import com.uber.nullaway.handlers.stream.StreamTypeRecord;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -73,77 +77,138 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
public LibraryModelsHandler(Config config) {
super();
this.config = config;
- libraryModels = loadLibraryModels();
+ libraryModels = loadLibraryModels(config);
}
@Override
- public ImmutableSet<Integer> onUnannotatedInvocationGetNonNullPositions(
- NullAway analysis,
- VisitorState state,
+ public Nullness[] onOverrideMethodInvocationParametersNullability(
+ Context context,
Symbol.MethodSymbol methodSymbol,
- List<? extends ExpressionTree> actualParams,
- ImmutableSet<Integer> nonNullPositions) {
- return Sets.union(
- nonNullPositions, getOptLibraryModels(state.context).nonNullParameters(methodSymbol))
- .immutableCopy();
+ boolean isAnnotated,
+ Nullness[] argumentPositionNullness) {
+ OptimizedLibraryModels optimizedLibraryModels = getOptLibraryModels(context);
+ ImmutableSet<Integer> nullableParamsFromModel =
+ optimizedLibraryModels.explicitlyNullableParameters(methodSymbol);
+ ImmutableSet<Integer> nonNullParamsFromModel =
+ optimizedLibraryModels.nonNullParameters(methodSymbol);
+ // For sanity check: $ nonNullParamsFromModel \cap nullableParamsFromModel $ should be empty
+ Set<Integer> allPositions = new HashSet<>();
+ for (Integer nullParam : nullableParamsFromModel) {
+ allPositions.add(nullParam);
+ argumentPositionNullness[nullParam] = NULLABLE;
+ }
+ for (Integer nonNullParam : nonNullParamsFromModel) {
+ if (!allPositions.add(nonNullParam)) {
+ // position was already marked as nullable
+ throw new IllegalStateException(
+ String.format(
+ "Library models give conflicting nullability for the following parameter of method %s: %s",
+ methodSymbol.getQualifiedName().toString(), nonNullParam.toString()));
+ }
+ argumentPositionNullness[nonNullParam] = NONNULL;
+ }
+ return argumentPositionNullness;
}
@Override
- public ImmutableSet<Integer> onUnannotatedInvocationGetExplicitlyNullablePositions(
- Context context,
+ public Nullness onOverrideMethodReturnNullability(
Symbol.MethodSymbol methodSymbol,
- ImmutableSet<Integer> explicitlyNullablePositions) {
- return Sets.union(
- Sets.difference(
- explicitlyNullablePositions,
- getOptLibraryModels(context).nonNullParameters(methodSymbol)),
- getOptLibraryModels(context).explicitlyNullableParameters(methodSymbol))
- .immutableCopy();
+ VisitorState state,
+ boolean isAnnotated,
+ Nullness returnNullness) {
+ OptimizedLibraryModels optLibraryModels = getOptLibraryModels(state.context);
+ if (optLibraryModels.hasNonNullReturn(methodSymbol, state.getTypes(), !isAnnotated)) {
+ return NONNULL;
+ } else if (optLibraryModels.hasNullableReturn(methodSymbol, state.getTypes(), !isAnnotated)) {
+ return NULLABLE;
+ }
+ return returnNullness;
}
@Override
public boolean onOverrideMayBeNullExpr(
- NullAway analysis, ExpressionTree expr, VisitorState state, boolean exprMayBeNull) {
- if (expr.getKind() == Tree.Kind.METHOD_INVOCATION) {
- OptimizedLibraryModels optLibraryModels = getOptLibraryModels(state.context);
- Symbol.MethodSymbol methodSymbol = (Symbol.MethodSymbol) ASTHelpers.getSymbol(expr);
- if (!NullabilityUtil.isUnannotated(methodSymbol, this.config)) {
- // We only look at library models for unannotated (i.e. third-party) code.
- return exprMayBeNull;
- } else if (optLibraryModels.hasNullableReturn(methodSymbol, state.getTypes())
- || !optLibraryModels.nullImpliesNullParameters(methodSymbol).isEmpty()) {
- // These mean the method might be null, depending on dataflow and arguments. We force
- // dataflow to run.
- return analysis.nullnessFromDataflow(state, expr) || exprMayBeNull;
- } else if (optLibraryModels.hasNonNullReturn(methodSymbol, state.getTypes())) {
- // This means the method can't be null, so we return false outright.
- return false;
- }
+ NullAway analysis,
+ ExpressionTree expr,
+ @Nullable Symbol exprSymbol,
+ VisitorState state,
+ boolean exprMayBeNull) {
+ if (!(expr.getKind() == Tree.Kind.METHOD_INVOCATION
+ && exprSymbol instanceof Symbol.MethodSymbol)) {
+ return exprMayBeNull;
+ }
+ OptimizedLibraryModels optLibraryModels = getOptLibraryModels(state.context);
+ Symbol.MethodSymbol methodSymbol = (Symbol.MethodSymbol) exprSymbol;
+ // When looking up library models of annotated code, we match the exact method signature only;
+ // overriding methods in subclasses must be explicitly given their own library model.
+ // When dealing with unannotated code, we default to generality: a model applies to a method
+ // and any of its overriding implementations.
+ // see https://github.com/uber/NullAway/issues/445 for why this is needed.
+ boolean isMethodUnannotated =
+ getCodeAnnotationInfo(state.context).isSymbolUnannotated(methodSymbol, this.config);
+ if (exprMayBeNull) {
+ // This is the only case in which we may switch the result from @Nullable to @NonNull:
+ return !optLibraryModels.hasNonNullReturn(
+ methodSymbol, state.getTypes(), isMethodUnannotated);
+ }
+ if (optLibraryModels.hasNullableReturn(methodSymbol, state.getTypes(), isMethodUnannotated)) {
+ return true;
+ }
+ if (!optLibraryModels.nullImpliesNullParameters(methodSymbol).isEmpty()) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ @Nullable
+ public Integer castToNonNullArgumentPositionsForMethod(
+ NullAway analysis,
+ VisitorState state,
+ Symbol.MethodSymbol methodSymbol,
+ List<? extends ExpressionTree> actualParams,
+ @Nullable Integer previousArgumentPosition) {
+ OptimizedLibraryModels optLibraryModels = getOptLibraryModels(state.context);
+ ImmutableSet<Integer> newPositions = optLibraryModels.castToNonNullMethod(methodSymbol);
+ if (newPositions.size() > 1) {
+ // Library models sanity check
+ String qualifiedName =
+ ASTHelpers.enclosingClass(methodSymbol) + "." + methodSymbol.getSimpleName().toString();
+ throw new IllegalStateException(
+ "Found multiple applicable castToNonNull library models for the same method signature: "
+ + qualifiedName);
+ }
+ // Override if an argument position was found from the library models, otherwise propagate
+ // previousArgumentPosition
+ return newPositions.stream().findAny().orElse(previousArgumentPosition);
+ }
+
+ @Nullable private CodeAnnotationInfo codeAnnotationInfo;
+
+ private CodeAnnotationInfo getCodeAnnotationInfo(Context context) {
+ if (codeAnnotationInfo == null) {
+ codeAnnotationInfo = CodeAnnotationInfo.instance(context);
}
- return exprMayBeNull;
+ return codeAnnotationInfo;
}
@Override
public NullnessHint onDataflowVisitMethodInvocation(
MethodInvocationNode node,
- Types types,
- Context context,
+ Symbol.MethodSymbol callee,
+ VisitorState state,
AccessPath.AccessPathContext apContext,
AccessPathNullnessPropagation.SubNodeValues inputs,
AccessPathNullnessPropagation.Updates thenUpdates,
AccessPathNullnessPropagation.Updates elseUpdates,
AccessPathNullnessPropagation.Updates bothUpdates) {
- Symbol.MethodSymbol callee = ASTHelpers.getSymbol(node.getTree());
- Preconditions.checkNotNull(callee);
- if (!NullabilityUtil.isUnannotated(callee, this.config)) {
- // Ignore annotated methods, library models should only apply to "unannotated" code.
- return NullnessHint.UNKNOWN;
- }
- setUnconditionalArgumentNullness(bothUpdates, node.getArguments(), callee, context, apContext);
+ boolean isMethodAnnotated =
+ !getCodeAnnotationInfo(state.context).isSymbolUnannotated(callee, this.config);
+ setUnconditionalArgumentNullness(bothUpdates, node.getArguments(), callee, state, apContext);
setConditionalArgumentNullness(
- thenUpdates, elseUpdates, node.getArguments(), callee, context, apContext);
+ thenUpdates, elseUpdates, node.getArguments(), callee, state, apContext);
+ OptimizedLibraryModels optLibraryModels = getOptLibraryModels(state.context);
ImmutableSet<Integer> nullImpliesNullIndexes =
- getOptLibraryModels(context).nullImpliesNullParameters(callee);
+ optLibraryModels.nullImpliesNullParameters(callee);
if (!nullImpliesNullIndexes.isEmpty()) {
// If the method is marked as having argument dependent nullability and any of the
// corresponding arguments is null, then the return is nullable. If the method is
@@ -153,13 +218,15 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
for (int idx : nullImpliesNullIndexes) {
if (!inputs.valueOfSubNode(node.getArgument(idx)).equals(NONNULL)) {
anyNull = true;
+ break;
}
}
return anyNull ? NullnessHint.HINT_NULLABLE : NullnessHint.FORCE_NONNULL;
}
- if (getOptLibraryModels(context).hasNonNullReturn(callee, types)) {
+ Types types = state.getTypes();
+ if (optLibraryModels.hasNonNullReturn(callee, types, !isMethodAnnotated)) {
return NullnessHint.FORCE_NONNULL;
- } else if (getOptLibraryModels(context).hasNullableReturn(callee, types)) {
+ } else if (optLibraryModels.hasNullableReturn(callee, types, !isMethodAnnotated)) {
return NullnessHint.HINT_NULLABLE;
} else {
return NullnessHint.UNKNOWN;
@@ -171,30 +238,32 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
AccessPathNullnessPropagation.Updates elseUpdates,
List<Node> arguments,
Symbol.MethodSymbol callee,
- Context context,
+ VisitorState state,
AccessPath.AccessPathContext apContext) {
- Set<Integer> nullImpliesTrueParameters =
- getOptLibraryModels(context).nullImpliesTrueParameters(callee);
- Set<Integer> nullImpliesFalseParameters =
- getOptLibraryModels(context).nullImpliesFalseParameters(callee);
+ OptimizedLibraryModels optLibraryModels = getOptLibraryModels(state.context);
+ Set<Integer> nullImpliesTrueParameters = optLibraryModels.nullImpliesTrueParameters(callee);
for (AccessPath accessPath :
- accessPathsAtIndexes(nullImpliesTrueParameters, arguments, apContext)) {
+ accessPathsAtIndexes(nullImpliesTrueParameters, arguments, state, apContext)) {
elseUpdates.set(accessPath, NONNULL);
}
+ Set<Integer> nullImpliesFalseParameters = optLibraryModels.nullImpliesFalseParameters(callee);
for (AccessPath accessPath :
- accessPathsAtIndexes(nullImpliesFalseParameters, arguments, apContext)) {
+ accessPathsAtIndexes(nullImpliesFalseParameters, arguments, state, apContext)) {
thenUpdates.set(accessPath, NONNULL);
}
}
private static Iterable<AccessPath> accessPathsAtIndexes(
- Set<Integer> indexes, List<Node> arguments, AccessPath.AccessPathContext apContext) {
+ Set<Integer> indexes,
+ List<Node> arguments,
+ VisitorState state,
+ AccessPath.AccessPathContext apContext) {
List<AccessPath> result = new ArrayList<>();
for (Integer i : indexes) {
Preconditions.checkArgument(i >= 0 && i < arguments.size(), "Invalid argument index: " + i);
if (i >= 0 && i < arguments.size()) {
Node argument = arguments.get(i);
- AccessPath ap = AccessPath.getAccessPathForNodeNoMapGet(argument, apContext);
+ AccessPath ap = AccessPath.getAccessPathForNode(argument, state, apContext);
if (ap != null) {
result.add(ap);
}
@@ -214,22 +283,41 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
AccessPathNullnessPropagation.Updates bothUpdates,
List<Node> arguments,
Symbol.MethodSymbol callee,
- Context context,
+ VisitorState state,
AccessPath.AccessPathContext apContext) {
Set<Integer> requiredNonNullParameters =
- getOptLibraryModels(context).failIfNullParameters(callee);
+ getOptLibraryModels(state.context).failIfNullParameters(callee);
for (AccessPath accessPath :
- accessPathsAtIndexes(requiredNonNullParameters, arguments, apContext)) {
+ accessPathsAtIndexes(requiredNonNullParameters, arguments, state, apContext)) {
bothUpdates.set(accessPath, NONNULL);
}
}
- private static LibraryModels loadLibraryModels() {
+ /**
+ * Get all the stream specifications loaded from any of our library models.
+ *
+ * <p>This is used in Handlers.java to create a StreamNullabilityPropagator handler, which gets
+ * registered independently of this LibraryModelsHandler itself.
+ *
+ * <p>LibraryModelsHandler is responsible from reading the library models for stream specs, but
+ * beyond that, checking of the specs falls under the responsibility of the generated
+ * StreamNullabilityPropagator handler.
+ *
+ * @return The list of all stream specifications loaded from any of our library models.
+ */
+ public ImmutableList<StreamTypeRecord> getStreamNullabilitySpecs() {
+ // Note: Currently, OptimizedLibraryModels doesn't carry the information about stream type
+ // records, it is not clear what it means to "optimize" lookup for those and they get accessed
+ // only once by calling this method during handler setup in Handlers.java.
+ return libraryModels.customStreamNullabilitySpecs();
+ }
+
+ private static LibraryModels loadLibraryModels(Config config) {
Iterable<LibraryModels> externalLibraryModels =
ServiceLoader.load(LibraryModels.class, LibraryModels.class.getClassLoader());
ImmutableSet.Builder<LibraryModels> libModelsBuilder = new ImmutableSet.Builder<>();
libModelsBuilder.add(new DefaultLibraryModels()).addAll(externalLibraryModels);
- return new CombinedLibraryModels(libModelsBuilder.build());
+ return new CombinedLibraryModels(libModelsBuilder.build(), config);
}
private static class DefaultLibraryModels implements LibraryModels {
@@ -356,6 +444,12 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
"com.google.common.base.Preconditions",
"<T>checkNotNull(T,java.lang.String,java.lang.Object,java.lang.Object,java.lang.Object,java.lang.Object)"),
0)
+ .put(methodRef("com.google.common.base.Verify", "<T>verifyNotNull(T)"), 0)
+ .put(
+ methodRef(
+ "com.google.common.base.Verify",
+ "<T>verifyNotNull(T,java.lang.String,java.lang.Object...)"),
+ 0)
.put(methodRef("java.util.Objects", "<T>requireNonNull(T)"), 0)
.put(methodRef("java.util.Objects", "<T>requireNonNull(T,java.lang.String)"), 0)
.put(
@@ -379,6 +473,54 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
"org.junit.jupiter.api.Assertions",
"assertNotNull(java.lang.Object,java.util.function.Supplier<java.lang.String>)"),
0)
+ .put(methodRef("org.apache.commons.lang3.Validate", "<T>notNull(T)"), 0)
+ .put(
+ methodRef(
+ "org.apache.commons.lang3.Validate",
+ "<T>notNull(T,java.lang.String,java.lang.Object...)"),
+ 0)
+ .put(
+ methodRef(
+ "org.apache.commons.lang3.Validate",
+ "<T>notEmpty(T[],java.lang.String,java.lang.Object...)"),
+ 0)
+ .put(methodRef("org.apache.commons.lang3.Validate", "<T>notEmpty(T[])"), 0)
+ .put(
+ methodRef(
+ "org.apache.commons.lang3.Validate",
+ "<T>notEmpty(T,java.lang.String,java.lang.Object...)"),
+ 0)
+ .put(methodRef("org.apache.commons.lang3.Validate", "<T>notEmpty(T)"), 0)
+ .put(
+ methodRef(
+ "org.apache.commons.lang3.Validate",
+ "<T>notBlank(T,java.lang.String,java.lang.Object...)"),
+ 0)
+ .put(methodRef("org.apache.commons.lang3.Validate", "<T>notBlank(T)"), 0)
+ .put(
+ methodRef(
+ "org.apache.commons.lang3.Validate",
+ "<T>noNullElements(T[],java.lang.String,java.lang.Object...)"),
+ 0)
+ .put(methodRef("org.apache.commons.lang3.Validate", "<T>noNullElements(T[])"), 0)
+ .put(
+ methodRef(
+ "org.apache.commons.lang3.Validate",
+ "<T>noNullElements(T,java.lang.String,java.lang.Object...)"),
+ 0)
+ .put(methodRef("org.apache.commons.lang3.Validate", "<T>noNullElements(T)"), 0)
+ .put(
+ methodRef(
+ "org.apache.commons.lang3.Validate",
+ "<T>validIndex(T[],int,java.lang.String,java.lang.Object...)"),
+ 0)
+ .put(methodRef("org.apache.commons.lang3.Validate", "<T>validIndex(T[],int)"), 0)
+ .put(
+ methodRef(
+ "org.apache.commons.lang3.Validate",
+ "<T>validIndex(T,int,java.lang.String,java.lang.Object...)"),
+ 0)
+ .put(methodRef("org.apache.commons.lang3.Validate", "<T>validIndex(T,int)"), 0)
.build();
private static final ImmutableSetMultimap<MethodRef, Integer> EXPLICITLY_NULLABLE_PARAMETERS =
@@ -483,6 +625,9 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
private static final ImmutableSetMultimap<MethodRef, Integer> NULL_IMPLIES_TRUE_PARAMETERS =
new ImmutableSetMultimap.Builder<MethodRef, Integer>()
.put(methodRef("com.google.common.base.Strings", "isNullOrEmpty(java.lang.String)"), 0)
+ .put(
+ methodRef("com.google.api.client.util.Strings", "isNullOrEmpty(java.lang.String)"),
+ 0)
.put(methodRef("java.util.Objects", "isNull(java.lang.Object)"), 0)
.put(
methodRef("org.springframework.util.ObjectUtils", "isEmpty(java.lang.Object[])"), 0)
@@ -512,6 +657,7 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
private static final ImmutableSetMultimap<MethodRef, Integer> NULL_IMPLIES_FALSE_PARAMETERS =
new ImmutableSetMultimap.Builder<MethodRef, Integer>()
+ .put(methodRef("java.lang.Class", "isInstance(java.lang.Object)"), 0)
.put(methodRef("java.util.Objects", "nonNull(java.lang.Object)"), 0)
.put(
methodRef("org.springframework.util.StringUtils", "hasLength(java.lang.String)"), 0)
@@ -544,13 +690,25 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
private static final ImmutableSetMultimap<MethodRef, Integer> NULL_IMPLIES_NULL_PARAMETERS =
new ImmutableSetMultimap.Builder<MethodRef, Integer>()
+ .put(methodRef("java.lang.Class", "cast(java.lang.Object)"), 0)
.put(methodRef("java.util.Optional", "orElse(T)"), 0)
+ .put(methodRef("com.google.common.io.Closer", "<C>register(C)"), 0)
+ .put(methodRef("java.util.Map", "getOrDefault(java.lang.Object,V)"), 1)
+ // We add ImmutableMap.getOrDefault explicitly, since when
+ // AcknowledgeRestrictiveAnnotations is enabled, the explicit annotations in the code
+ // override the inherited library model
+ .put(
+ methodRef(
+ "com.google.common.collect.ImmutableMap", "getOrDefault(java.lang.Object,V)"),
+ 1)
.build();
private static final ImmutableSet<MethodRef> NULLABLE_RETURNS =
new ImmutableSet.Builder<MethodRef>()
.add(methodRef("com.sun.source.tree.CompilationUnitTree", "getPackageName()"))
.add(methodRef("java.lang.Throwable", "getMessage()"))
+ .add(methodRef("java.lang.Throwable", "getLocalizedMessage()"))
+ .add(methodRef("java.lang.Throwable", "getCause()"))
.add(methodRef("java.lang.ref.Reference", "get()"))
.add(methodRef("java.lang.ref.PhantomReference", "get()"))
.add(methodRef("java.lang.ref.SoftReference", "get()"))
@@ -567,6 +725,7 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
.add(methodRef("android.view.View", "getHandler()"))
.add(methodRef("android.webkit.WebView", "getUrl()"))
.add(methodRef("android.widget.TextView", "getLayout()"))
+ .add(methodRef("java.lang.System", "console()"))
.build();
private static final ImmutableSet<MethodRef> NONNULL_RETURNS =
@@ -618,6 +777,9 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
.add(methodRef("android.support.design.widget.TextInputLayout", "getEditText()"))
.build();
+ private static final ImmutableSetMultimap<MethodRef, Integer> CAST_TO_NONNULL_METHODS =
+ new ImmutableSetMultimap.Builder<MethodRef, Integer>().build();
+
@Override
public ImmutableSetMultimap<MethodRef, Integer> failIfNullParameters() {
return FAIL_IF_NULL_PARAMETERS;
@@ -657,10 +819,17 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
public ImmutableSet<MethodRef> nonNullReturns() {
return NONNULL_RETURNS;
}
+
+ @Override
+ public ImmutableSetMultimap<MethodRef, Integer> castToNonNullMethods() {
+ return CAST_TO_NONNULL_METHODS;
+ }
}
private static class CombinedLibraryModels implements LibraryModels {
+ private final Config config;
+
private final ImmutableSetMultimap<MethodRef, Integer> failIfNullParameters;
private final ImmutableSetMultimap<MethodRef, Integer> explicitlyNullableParameters;
@@ -677,7 +846,12 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
private final ImmutableSet<MethodRef> nonNullReturns;
- public CombinedLibraryModels(Iterable<LibraryModels> models) {
+ private final ImmutableSetMultimap<MethodRef, Integer> castToNonNullMethods;
+
+ private final ImmutableList<StreamTypeRecord> customStreamNullabilitySpecs;
+
+ public CombinedLibraryModels(Iterable<LibraryModels> models, Config config) {
+ this.config = config;
ImmutableSetMultimap.Builder<MethodRef, Integer> failIfNullParametersBuilder =
new ImmutableSetMultimap.Builder<>();
ImmutableSetMultimap.Builder<MethodRef, Integer> explicitlyNullableParametersBuilder =
@@ -692,35 +866,72 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
new ImmutableSetMultimap.Builder<>();
ImmutableSet.Builder<MethodRef> nullableReturnsBuilder = new ImmutableSet.Builder<>();
ImmutableSet.Builder<MethodRef> nonNullReturnsBuilder = new ImmutableSet.Builder<>();
+ ImmutableSetMultimap.Builder<MethodRef, Integer> castToNonNullMethodsBuilder =
+ new ImmutableSetMultimap.Builder<>();
+ ImmutableList.Builder<StreamTypeRecord> customStreamNullabilitySpecsBuilder =
+ new ImmutableList.Builder<>();
for (LibraryModels libraryModels : models) {
for (Map.Entry<MethodRef, Integer> entry : libraryModels.failIfNullParameters().entries()) {
+ if (shouldSkipModel(entry.getKey())) {
+ continue;
+ }
failIfNullParametersBuilder.put(entry);
}
for (Map.Entry<MethodRef, Integer> entry :
libraryModels.explicitlyNullableParameters().entries()) {
+ if (shouldSkipModel(entry.getKey())) {
+ continue;
+ }
explicitlyNullableParametersBuilder.put(entry);
}
for (Map.Entry<MethodRef, Integer> entry : libraryModels.nonNullParameters().entries()) {
+ if (shouldSkipModel(entry.getKey())) {
+ continue;
+ }
nonNullParametersBuilder.put(entry);
}
for (Map.Entry<MethodRef, Integer> entry :
libraryModels.nullImpliesTrueParameters().entries()) {
+ if (shouldSkipModel(entry.getKey())) {
+ continue;
+ }
nullImpliesTrueParametersBuilder.put(entry);
}
for (Map.Entry<MethodRef, Integer> entry :
libraryModels.nullImpliesFalseParameters().entries()) {
+ if (shouldSkipModel(entry.getKey())) {
+ continue;
+ }
nullImpliesFalseParametersBuilder.put(entry);
}
for (Map.Entry<MethodRef, Integer> entry :
libraryModels.nullImpliesNullParameters().entries()) {
+ if (shouldSkipModel(entry.getKey())) {
+ continue;
+ }
nullImpliesNullParametersBuilder.put(entry);
}
for (MethodRef name : libraryModels.nullableReturns()) {
+ if (shouldSkipModel(name)) {
+ continue;
+ }
nullableReturnsBuilder.add(name);
}
for (MethodRef name : libraryModels.nonNullReturns()) {
+ if (shouldSkipModel(name)) {
+ continue;
+ }
nonNullReturnsBuilder.add(name);
}
+ for (Map.Entry<MethodRef, Integer> entry : libraryModels.castToNonNullMethods().entries()) {
+ if (shouldSkipModel(entry.getKey())) {
+ continue;
+ }
+ castToNonNullMethodsBuilder.put(entry);
+ }
+ for (StreamTypeRecord streamTypeRecord : libraryModels.customStreamNullabilitySpecs()) {
+ customStreamNullabilitySpecsBuilder.add(streamTypeRecord);
+ }
}
failIfNullParameters = failIfNullParametersBuilder.build();
explicitlyNullableParameters = explicitlyNullableParametersBuilder.build();
@@ -730,6 +941,12 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
nullImpliesNullParameters = nullImpliesNullParametersBuilder.build();
nullableReturns = nullableReturnsBuilder.build();
nonNullReturns = nonNullReturnsBuilder.build();
+ castToNonNullMethods = castToNonNullMethodsBuilder.build();
+ customStreamNullabilitySpecs = customStreamNullabilitySpecsBuilder.build();
+ }
+
+ private boolean shouldSkipModel(MethodRef key) {
+ return config.isSkippedLibraryModel(key.enclosingClass + "." + key.methodName);
}
@Override
@@ -771,6 +988,16 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
public ImmutableSet<MethodRef> nonNullReturns() {
return nonNullReturns;
}
+
+ @Override
+ public ImmutableSetMultimap<MethodRef, Integer> castToNonNullMethods() {
+ return castToNonNullMethods;
+ }
+
+ @Override
+ public ImmutableList<StreamTypeRecord> customStreamNullabilitySpecs() {
+ return customStreamNullabilitySpecs;
+ }
}
/**
@@ -817,6 +1044,7 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
private final NameIndexedMap<ImmutableSet<Integer>> nullImpliesNullParams;
private final NameIndexedMap<Boolean> nullableRet;
private final NameIndexedMap<Boolean> nonNullRet;
+ private final NameIndexedMap<ImmutableSet<Integer>> castToNonNullMethods;
public OptimizedLibraryModels(LibraryModels models, Context context) {
Names names = Names.instance(context);
@@ -830,14 +1058,15 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
nullImpliesNullParams = makeOptimizedIntSetLookup(names, models.nullImpliesNullParameters());
nullableRet = makeOptimizedBoolLookup(names, models.nullableReturns());
nonNullRet = makeOptimizedBoolLookup(names, models.nonNullReturns());
+ castToNonNullMethods = makeOptimizedIntSetLookup(names, models.castToNonNullMethods());
}
- public boolean hasNonNullReturn(Symbol.MethodSymbol symbol, Types types) {
- return lookupHandlingOverrides(symbol, types, nonNullRet) != null;
+ public boolean hasNonNullReturn(Symbol.MethodSymbol symbol, Types types, boolean checkSuper) {
+ return lookupHandlingOverrides(symbol, types, nonNullRet, checkSuper) != null;
}
- public boolean hasNullableReturn(Symbol.MethodSymbol symbol, Types types) {
- return lookupHandlingOverrides(symbol, types, nullableRet) != null;
+ public boolean hasNullableReturn(Symbol.MethodSymbol symbol, Types types, boolean checkSuper) {
+ return lookupHandlingOverrides(symbol, types, nullableRet, checkSuper) != null;
}
ImmutableSet<Integer> failIfNullParameters(Symbol.MethodSymbol symbol) {
@@ -864,6 +1093,10 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
return lookupImmutableSet(symbol, nullImpliesNullParams);
}
+ ImmutableSet<Integer> castToNonNullMethod(Symbol.MethodSymbol symbol) {
+ return lookupImmutableSet(symbol, castToNonNullMethods);
+ }
+
private ImmutableSet<Integer> lookupImmutableSet(
Symbol.MethodSymbol symbol, NameIndexedMap<ImmutableSet<Integer>> lookup) {
ImmutableSet<Integer> result = lookup.get(symbol);
@@ -885,11 +1118,8 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
Map<Name, Map<MethodRef, T>> nameMapping = new LinkedHashMap<>();
for (MethodRef ref : refs) {
Name methodName = names.fromString(ref.methodName);
- Map<MethodRef, T> mapForName = nameMapping.get(methodName);
- if (mapForName == null) {
- mapForName = new LinkedHashMap<>();
- nameMapping.put(methodName, mapForName);
- }
+ Map<MethodRef, T> mapForName =
+ nameMapping.computeIfAbsent(methodName, k -> new LinkedHashMap<>());
mapForName.put(ref, getValForRef.apply(ref));
}
return new NameIndexedMap<>(nameMapping);
@@ -901,7 +1131,10 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
*/
@Nullable
private static Symbol.MethodSymbol lookupHandlingOverrides(
- Symbol.MethodSymbol symbol, Types types, NameIndexedMap<Boolean> optLookup) {
+ Symbol.MethodSymbol symbol,
+ Types types,
+ NameIndexedMap<Boolean> optLookup,
+ boolean checkSuperTypes) {
if (optLookup.nameNotPresent(symbol)) {
// no model matching the method name, so we don't need to check for overridden methods
return null;
@@ -909,6 +1142,12 @@ public class LibraryModelsHandler extends BaseNoOpHandler {
if (optLookup.get(symbol) != null) {
return symbol;
}
+ if (checkSuperTypes == false) {
+ // Consider only a model on the exact class and method, used when checking annotated code
+ return null;
+ }
+ // For unannotated code, we allow a single model to cover all overriding implementations /
+ // subtypes
for (Symbol.MethodSymbol superSymbol : ASTHelpers.findSuperMethods(symbol, types)) {
if (optLookup.get(superSymbol) != null) {
return superSymbol;
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/LombokHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/LombokHandler.java
new file mode 100644
index 0000000..7069497
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/LombokHandler.java
@@ -0,0 +1,89 @@
+package com.uber.nullaway.handlers;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.ExpressionTree;
+import com.sun.source.tree.Tree;
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.Config;
+import com.uber.nullaway.NullAway;
+import com.uber.nullaway.Nullness;
+import java.util.stream.StreamSupport;
+import javax.annotation.Nullable;
+import javax.lang.model.element.ElementKind;
+
+/**
+ * A general handler for Lombok generated code and its internal semantics.
+ *
+ * <p>Currently used to propagate @Nullable in cases where the Lombok annotation processor fails to
+ * do so consistently.
+ */
+public class LombokHandler extends BaseNoOpHandler {
+
+ private static String LOMBOK_GENERATED_ANNOTATION_NAME = "lombok.Generated";
+ private static String LOMBOK_BUILDER_DEFAULT_METHOD_PREFIX = "$default$";
+
+ private final Config config;
+
+ public LombokHandler(Config config) {
+ this.config = config;
+ }
+
+ private boolean isLombokMethodWithMissingNullableAnnotation(
+ Symbol.MethodSymbol methodSymbol, VisitorState state) {
+ if (!ASTHelpers.hasAnnotation(methodSymbol, LOMBOK_GENERATED_ANNOTATION_NAME, state)) {
+ return false;
+ }
+ String methodNameString = methodSymbol.name.toString();
+ if (!methodNameString.startsWith(LOMBOK_BUILDER_DEFAULT_METHOD_PREFIX)) {
+ return false;
+ }
+ String originalFieldName =
+ methodNameString.substring(LOMBOK_BUILDER_DEFAULT_METHOD_PREFIX.length());
+ ImmutableList<Symbol> matchingMembers =
+ StreamSupport.stream(methodSymbol.enclClass().members().getSymbols().spliterator(), false)
+ .filter(
+ sym ->
+ sym.name.contentEquals(originalFieldName)
+ && sym.getKind().equals(ElementKind.FIELD))
+ .collect(ImmutableList.toImmutableList());
+ Preconditions.checkArgument(
+ matchingMembers.size() == 1,
+ String.format(
+ "Found %d fields matching Lombok generated builder default method %s",
+ matchingMembers.size(), methodNameString));
+ return Nullness.hasNullableAnnotation(matchingMembers.get(0), config);
+ }
+
+ @Override
+ public boolean onOverrideMayBeNullExpr(
+ NullAway analysis,
+ ExpressionTree expr,
+ @Nullable Symbol exprSymbol,
+ VisitorState state,
+ boolean exprMayBeNull) {
+ if (exprMayBeNull) {
+ return true;
+ }
+ Tree.Kind exprKind = expr.getKind();
+ if (exprSymbol != null && exprKind == Tree.Kind.METHOD_INVOCATION) {
+ Symbol.MethodSymbol methodSymbol = (Symbol.MethodSymbol) exprSymbol;
+ return isLombokMethodWithMissingNullableAnnotation(methodSymbol, state);
+ }
+ return false;
+ }
+
+ @Override
+ public Nullness onOverrideMethodReturnNullability(
+ Symbol.MethodSymbol methodSymbol,
+ VisitorState state,
+ boolean isAnnotated,
+ Nullness returnNullness) {
+ if (isLombokMethodWithMissingNullableAnnotation(methodSymbol, state)) {
+ return Nullness.NULLABLE;
+ }
+ return returnNullness;
+ }
+}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/MethodNameUtil.java b/nullaway/src/main/java/com/uber/nullaway/handlers/MethodNameUtil.java
index 84ec4ca..51e9cd9 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/MethodNameUtil.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/MethodNameUtil.java
@@ -25,6 +25,7 @@ package com.uber.nullaway.handlers;
import com.google.errorprone.util.ASTHelpers;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.util.Name;
+import com.uber.nullaway.annotations.Initializer;
import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode;
import org.checkerframework.nullaway.dataflow.cfg.node.Node;
@@ -39,80 +40,175 @@ class MethodNameUtil {
// Strings corresponding to the names of the methods (and their owners) used to identify
// assertions in this handler.
private static final String IS_NOT_NULL_METHOD = "isNotNull";
- private static final String IS_NOT_NULL_OWNER = "com.google.common.truth.Subject";
+ private static final String IS_OWNER_TRUTH_SUBJECT = "com.google.common.truth.Subject";
+ private static final String IS_OWNER_ASSERTJ_ABSTRACT_ASSERT =
+ "org.assertj.core.api.AbstractAssert";
+ private static final String IS_INSTANCE_OF_METHOD = "isInstanceOf";
+ private static final String IS_INSTANCE_OF_ANY_METHOD = "isInstanceOfAny";
private static final String IS_TRUE_METHOD = "isTrue";
- private static final String IS_TRUE_OWNER = "com.google.common.truth.BooleanSubject";
+ private static final String IS_FALSE_METHOD = "isFalse";
+ private static final String IS_TRUE_OWNER_TRUTH = "com.google.common.truth.BooleanSubject";
+ private static final String IS_TRUE_OWNER_ASSERTJ = "org.assertj.core.api.AbstractBooleanAssert";
+ private static final String BOOLEAN_VALUE_OF_METHOD = "valueOf";
+ private static final String BOOLEAN_VALUE_OF_OWNER = "java.lang.Boolean";
+ private static final String IS_PRESENT_METHOD = "isPresent";
+ private static final String IS_NOT_EMPTY_METHOD = "isNotEmpty";
+ private static final String IS_PRESENT_OWNER_ASSERTJ =
+ "org.assertj.core.api.AbstractOptionalAssert";
private static final String ASSERT_THAT_METHOD = "assertThat";
- private static final String ASSERT_THAT_OWNER = "com.google.common.truth.Truth";
+ private static final String ASSERT_THAT_OWNER_TRUTH = "com.google.common.truth.Truth";
+ private static final String ASSERT_THAT_OWNER_ASSERTJ = "org.assertj.core.api.Assertions";
private static final String HAMCREST_ASSERT_CLASS = "org.hamcrest.MatcherAssert";
private static final String JUNIT_ASSERT_CLASS = "org.junit.Assert";
+ private static final String JUNIT5_ASSERTION_CLASS = "org.junit.jupiter.api.Assertions";
+
+ private static final String ASSERT_TRUE_METHOD = "assertTrue";
+ private static final String ASSERT_FALSE_METHOD = "assertFalse";
private static final String MATCHERS_CLASS = "org.hamcrest.Matchers";
private static final String CORE_MATCHERS_CLASS = "org.hamcrest.CoreMatchers";
private static final String CORE_IS_NULL_CLASS = "org.hamcrest.core.IsNull";
private static final String IS_MATCHER = "is";
+ private static final String IS_A_MATCHER = "isA";
private static final String NOT_MATCHER = "not";
private static final String NOT_NULL_VALUE_MATCHER = "notNullValue";
private static final String NULL_VALUE_MATCHER = "nullValue";
+ private static final String INSTANCE_OF_MATCHER = "instanceOf";
// Names of the methods (and their owners) used to identify assertions in this handler. Name used
// here refers to com.sun.tools.javac.util.Name. Comparing methods using Names is faster than
// comparing using strings.
private Name isNotNull;
- private Name isNotNullOwner;
+
+ private Name isInstanceOf;
+ private Name isInstanceOfAny;
+ private Name isOwnerTruthSubject;
+ private Name isOwnerAssertJAbstractAssert;
private Name isTrue;
- private Name isTrueOwner;
+ private Name isFalse;
+ private Name isTrueOwnerTruth;
+ private Name isTrueOwnerAssertJ;
+ private Name isPresent;
+ private Name isNotEmpty;
+ private Name isPresentOwnerAssertJ;
+
+ private Name isBooleanValueOfMethod;
+ private Name isBooleanValueOfOwner;
private Name assertThat;
- private Name assertThatOwner;
+ private Name assertThatOwnerTruth;
+ private Name assertThatOwnerAssertJ;
// Names for junit assertion libraries.
private Name hamcrestAssertClass;
private Name junitAssertClass;
+ private Name junit5AssertionClass;
+
+ private Name assertTrue;
+ private Name assertFalse;
// Names for hamcrest matchers.
private Name matchersClass;
private Name coreMatchersClass;
private Name coreIsNullClass;
private Name isMatcher;
+ private Name isAMatcher;
private Name notMatcher;
private Name notNullValueMatcher;
private Name nullValueMatcher;
+ private Name instanceOfMatcher;
+ @Initializer
void initializeMethodNames(Name.Table table) {
isNotNull = table.fromString(IS_NOT_NULL_METHOD);
- isNotNullOwner = table.fromString(IS_NOT_NULL_OWNER);
+ isOwnerTruthSubject = table.fromString(IS_OWNER_TRUTH_SUBJECT);
+ isOwnerAssertJAbstractAssert = table.fromString(IS_OWNER_ASSERTJ_ABSTRACT_ASSERT);
+
+ isInstanceOf = table.fromString(IS_INSTANCE_OF_METHOD);
+ isInstanceOfAny = table.fromString(IS_INSTANCE_OF_ANY_METHOD);
isTrue = table.fromString(IS_TRUE_METHOD);
- isTrueOwner = table.fromString(IS_TRUE_OWNER);
+ isFalse = table.fromString(IS_FALSE_METHOD);
+ isTrueOwnerTruth = table.fromString(IS_TRUE_OWNER_TRUTH);
+ isTrueOwnerAssertJ = table.fromString(IS_TRUE_OWNER_ASSERTJ);
+
+ isBooleanValueOfMethod = table.fromString(BOOLEAN_VALUE_OF_METHOD);
+ isBooleanValueOfOwner = table.fromString(BOOLEAN_VALUE_OF_OWNER);
assertThat = table.fromString(ASSERT_THAT_METHOD);
- assertThatOwner = table.fromString(ASSERT_THAT_OWNER);
+ assertThatOwnerTruth = table.fromString(ASSERT_THAT_OWNER_TRUTH);
+ assertThatOwnerAssertJ = table.fromString(ASSERT_THAT_OWNER_ASSERTJ);
+
+ isPresent = table.fromString(IS_PRESENT_METHOD);
+ isNotEmpty = table.fromString(IS_NOT_EMPTY_METHOD);
+ isPresentOwnerAssertJ = table.fromString(IS_PRESENT_OWNER_ASSERTJ);
hamcrestAssertClass = table.fromString(HAMCREST_ASSERT_CLASS);
junitAssertClass = table.fromString(JUNIT_ASSERT_CLASS);
+ junit5AssertionClass = table.fromString(JUNIT5_ASSERTION_CLASS);
+
+ assertTrue = table.fromString(ASSERT_TRUE_METHOD);
+ assertFalse = table.fromString(ASSERT_FALSE_METHOD);
matchersClass = table.fromString(MATCHERS_CLASS);
coreMatchersClass = table.fromString(CORE_MATCHERS_CLASS);
coreIsNullClass = table.fromString(CORE_IS_NULL_CLASS);
isMatcher = table.fromString(IS_MATCHER);
+ isAMatcher = table.fromString(IS_A_MATCHER);
notMatcher = table.fromString(NOT_MATCHER);
notNullValueMatcher = table.fromString(NOT_NULL_VALUE_MATCHER);
nullValueMatcher = table.fromString(NULL_VALUE_MATCHER);
+ instanceOfMatcher = table.fromString(INSTANCE_OF_MATCHER);
}
boolean isMethodIsNotNull(Symbol.MethodSymbol methodSymbol) {
- return matchesMethod(methodSymbol, isNotNull, isNotNullOwner);
+ return matchesMethod(methodSymbol, isNotNull, isOwnerTruthSubject)
+ || matchesMethod(methodSymbol, isNotNull, isOwnerAssertJAbstractAssert);
+ }
+
+ boolean isMethodIsInstanceOf(Symbol.MethodSymbol methodSymbol) {
+ return matchesMethod(methodSymbol, isInstanceOf, isOwnerTruthSubject)
+ || matchesMethod(methodSymbol, isInstanceOf, isOwnerAssertJAbstractAssert)
+ // Truth doesn't seem to have isInstanceOfAny
+ || matchesMethod(methodSymbol, isInstanceOfAny, isOwnerAssertJAbstractAssert);
+ }
+
+ boolean isMethodAssertTrue(Symbol.MethodSymbol methodSymbol) {
+ return matchesMethod(methodSymbol, assertTrue, junitAssertClass)
+ || matchesMethod(methodSymbol, assertTrue, junit5AssertionClass);
+ }
+
+ boolean isMethodAssertFalse(Symbol.MethodSymbol methodSymbol) {
+ return matchesMethod(methodSymbol, assertFalse, junitAssertClass)
+ || matchesMethod(methodSymbol, assertFalse, junit5AssertionClass);
+ }
+
+ boolean isMethodThatEnsuresOptionalPresent(Symbol.MethodSymbol methodSymbol) {
+ // same owner
+ return matchesMethod(methodSymbol, isPresent, isPresentOwnerAssertJ)
+ || matchesMethod(methodSymbol, isNotEmpty, isPresentOwnerAssertJ);
}
boolean isMethodIsTrue(Symbol.MethodSymbol methodSymbol) {
- return matchesMethod(methodSymbol, isTrue, isTrueOwner);
+ return matchesMethod(methodSymbol, isTrue, isTrueOwnerTruth)
+ || matchesMethod(methodSymbol, isTrue, isTrueOwnerAssertJ);
+ }
+
+ boolean isMethodIsFalse(Symbol.MethodSymbol methodSymbol) {
+ // same owners as isTrue
+ return matchesMethod(methodSymbol, isFalse, isTrueOwnerTruth)
+ || matchesMethod(methodSymbol, isFalse, isTrueOwnerAssertJ);
+ }
+
+ boolean isMethodBooleanValueOf(Symbol.MethodSymbol methodSymbol) {
+ return matchesMethod(methodSymbol, isBooleanValueOfMethod, isBooleanValueOfOwner);
}
boolean isMethodAssertThat(Symbol.MethodSymbol methodSymbol) {
- return matchesMethod(methodSymbol, assertThat, assertThatOwner);
+ return matchesMethod(methodSymbol, assertThat, assertThatOwnerTruth)
+ || matchesMethod(methodSymbol, assertThat, assertThatOwnerAssertJ);
}
boolean isMethodHamcrestAssertThat(Symbol.MethodSymbol methodSymbol) {
@@ -156,6 +252,21 @@ class MethodNameUtil {
|| matchesMatcherMethod(node, nullValueMatcher, coreIsNullClass);
}
+ boolean isMatcherIsInstanceOf(Node node) {
+ // Matches with
+ // * is(instanceOf(Some.class))
+ // * isA(Some.class)
+ if (matchesMatcherMethod(node, isMatcher, matchersClass)
+ || matchesMatcherMethod(node, isMatcher, coreMatchersClass)) {
+ // All overloads of `is` method have exactly one argument.
+ Node inner = ((MethodInvocationNode) node).getArgument(0);
+ return matchesMatcherMethod(inner, instanceOfMatcher, matchersClass)
+ || matchesMatcherMethod(inner, instanceOfMatcher, coreMatchersClass);
+ }
+ return (matchesMatcherMethod(node, isAMatcher, matchersClass)
+ || matchesMatcherMethod(node, isAMatcher, coreMatchersClass));
+ }
+
private boolean matchesMatcherMethod(Node node, Name matcherName, Name matcherClass) {
if (node instanceof MethodInvocationNode) {
MethodInvocationNode methodInvocationNode = (MethodInvocationNode) node;
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/OptionalEmptinessHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/OptionalEmptinessHandler.java
index 919ef33..e581885 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/OptionalEmptinessHandler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/OptionalEmptinessHandler.java
@@ -21,6 +21,7 @@
*/
package com.uber.nullaway.handlers;
+import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
@@ -29,9 +30,11 @@ import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.code.Symbol;
+import com.sun.tools.javac.code.Symtab;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.util.Context;
+import com.sun.tools.javac.util.Names;
import com.uber.nullaway.Config;
import com.uber.nullaway.ErrorMessage;
import com.uber.nullaway.NullAway;
@@ -40,10 +43,13 @@ import com.uber.nullaway.dataflow.AccessPath;
import com.uber.nullaway.dataflow.AccessPathNullnessAnalysis;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
import java.lang.annotation.Annotation;
+import java.lang.reflect.Array;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.function.Consumer;
import javax.annotation.Nullable;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
@@ -52,6 +58,7 @@ import javax.lang.model.element.ElementVisitor;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode;
import org.checkerframework.nullaway.dataflow.cfg.node.Node;
@@ -63,13 +70,11 @@ import org.checkerframework.nullaway.dataflow.cfg.node.Node;
public class OptionalEmptinessHandler extends BaseNoOpHandler {
@Nullable private ImmutableSet<Type> optionalTypes;
- private NullAway analysis;
+ private @Nullable NullAway analysis;
private final Config config;
private final MethodNameUtil methodNameUtil;
- public static final VariableElement OPTIONAL_CONTENT = getOptionalContentElement();
-
OptionalEmptinessHandler(Config config, MethodNameUtil methodNameUtil) {
this.config = config;
this.methodNameUtil = methodNameUtil;
@@ -77,12 +82,20 @@ public class OptionalEmptinessHandler extends BaseNoOpHandler {
@Override
public boolean onOverrideMayBeNullExpr(
- NullAway analysis, ExpressionTree expr, VisitorState state, boolean exprMayBeNull) {
+ NullAway analysis,
+ ExpressionTree expr,
+ @Nullable Symbol exprSymbol,
+ VisitorState state,
+ boolean exprMayBeNull) {
+ if (exprMayBeNull) {
+ return true;
+ }
if (expr.getKind() == Tree.Kind.METHOD_INVOCATION
- && optionalIsGetCall((Symbol.MethodSymbol) ASTHelpers.getSymbol(expr), state.getTypes())) {
+ && exprSymbol instanceof Symbol.MethodSymbol
+ && optionalIsGetCall((Symbol.MethodSymbol) exprSymbol, state.getTypes())) {
return true;
}
- return exprMayBeNull;
+ return false;
}
@Override
@@ -91,33 +104,35 @@ public class OptionalEmptinessHandler extends BaseNoOpHandler {
this.analysis = analysis;
- optionalTypes =
- config
- .getOptionalClassPaths()
- .stream()
- .map(state::getTypeFromString)
- .filter(Objects::nonNull)
- .map(state.getTypes()::erasure)
- .collect(ImmutableSet.toImmutableSet());
+ if (optionalTypes == null) {
+ optionalTypes =
+ config.getOptionalClassPaths().stream()
+ .map(state::getTypeFromString)
+ .filter(Objects::nonNull)
+ .map(state.getTypes()::erasure)
+ .collect(ImmutableSet.toImmutableSet());
+ }
}
@Override
public NullnessHint onDataflowVisitMethodInvocation(
MethodInvocationNode node,
- Types types,
- Context context,
+ Symbol.MethodSymbol symbol,
+ VisitorState state,
AccessPath.AccessPathContext apContext,
AccessPathNullnessPropagation.SubNodeValues inputs,
AccessPathNullnessPropagation.Updates thenUpdates,
AccessPathNullnessPropagation.Updates elseUpdates,
AccessPathNullnessPropagation.Updates bothUpdates) {
- Symbol.MethodSymbol symbol = ASTHelpers.getSymbol(node.getTree());
-
+ Types types = state.getTypes();
if (optionalIsPresentCall(symbol, types)) {
- updateNonNullAPsForOptionalContent(thenUpdates, node.getTarget().getReceiver(), apContext);
- } else if (config.handleTestAssertionLibraries() && methodNameUtil.isMethodIsTrue(symbol)) {
- // we check for instance of AssertThat(optionalFoo.isPresent()).isTrue()
- updateIfAssertIsPresentTrueOnOptional(node, types, apContext, bothUpdates);
+ updateNonNullAPsForOptionalContent(
+ state.context, thenUpdates, node.getTarget().getReceiver(), apContext);
+ } else if (optionalIsEmptyCall(symbol, types)) {
+ updateNonNullAPsForOptionalContent(
+ state.context, elseUpdates, node.getTarget().getReceiver(), apContext);
+ } else if (config.handleTestAssertionLibraries()) {
+ handleTestAssertions(state, apContext, bothUpdates, node, symbol);
}
return NullnessHint.UNKNOWN;
}
@@ -125,9 +140,10 @@ public class OptionalEmptinessHandler extends BaseNoOpHandler {
@Override
public Optional<ErrorMessage> onExpressionDereference(
ExpressionTree expr, ExpressionTree baseExpr, VisitorState state) {
-
- if (ASTHelpers.getSymbol(expr) instanceof Symbol.MethodSymbol
- && optionalIsGetCall((Symbol.MethodSymbol) ASTHelpers.getSymbol(expr), state.getTypes())
+ Preconditions.checkNotNull(analysis);
+ Symbol symbol = ASTHelpers.getSymbol(expr);
+ if (symbol instanceof Symbol.MethodSymbol
+ && optionalIsGetCall((Symbol.MethodSymbol) symbol, state.getTypes())
&& isOptionalContentNullable(state, baseExpr, analysis.getNullnessAnalysis(state))) {
final String message = "Invoking get() on possibly empty Optional " + baseExpr;
return Optional.of(
@@ -137,9 +153,13 @@ public class OptionalEmptinessHandler extends BaseNoOpHandler {
}
private boolean isOptionalContentNullable(
- VisitorState state, ExpressionTree baseExpr, AccessPathNullnessAnalysis analysis) {
- return analysis.getNullnessOfExpressionNamedField(
- new TreePath(state.getPath(), baseExpr), state.context, OPTIONAL_CONTENT)
+ VisitorState state,
+ ExpressionTree baseExpr,
+ AccessPathNullnessAnalysis accessPathNullnessAnalysis) {
+ return accessPathNullnessAnalysis.getNullnessOfExpressionNamedField(
+ new TreePath(state.getPath(), baseExpr),
+ state.context,
+ OptionalContentVariableElement.instance(state.context))
== Nullness.NULLABLE;
}
@@ -147,61 +167,144 @@ public class OptionalEmptinessHandler extends BaseNoOpHandler {
public boolean includeApInfoInSavedContext(AccessPath accessPath, VisitorState state) {
if (accessPath.getElements().size() == 1) {
- AccessPath.Root root = accessPath.getRoot();
- if (!root.isReceiver()) {
- final Element e = root.getVarElement();
+ final Element e = accessPath.getRoot();
+ if (e != null) {
return e.getKind().equals(ElementKind.LOCAL_VARIABLE)
- && accessPath.getElements().get(0).getJavaElement().equals(OPTIONAL_CONTENT);
+ && accessPath.getElements().get(0).getJavaElement()
+ instanceof OptionalContentVariableElement;
}
}
return false;
}
- private void updateIfAssertIsPresentTrueOnOptional(
+ private void handleTestAssertions(
+ VisitorState state,
+ AccessPath.AccessPathContext apContext,
+ AccessPathNullnessPropagation.Updates bothUpdates,
MethodInvocationNode node,
+ Symbol.MethodSymbol symbol) {
+
+ Consumer<Node> nonNullMarker =
+ nonNullNode ->
+ updateNonNullAPsForOptionalContent(state.context, bothUpdates, nonNullNode, apContext);
+
+ boolean isAssertTrueMethod = methodNameUtil.isMethodAssertTrue(symbol);
+ boolean isAssertFalseMethod = methodNameUtil.isMethodAssertFalse(symbol);
+ boolean isTrueMethod = methodNameUtil.isMethodIsTrue(symbol);
+ boolean isFalseMethod = methodNameUtil.isMethodIsFalse(symbol);
+ if (isAssertTrueMethod || isAssertFalseMethod) {
+ // assertTrue(optionalFoo.isPresent())
+ // assertFalse("optional was empty", optionalFoo.isEmpty())
+ // note: in junit4 the optional string message comes first, but in junit5 it comes last
+ Optional<MethodInvocationNode> assertedOnMethod =
+ node.getArguments().stream()
+ .filter(n -> TypeKind.BOOLEAN.equals(n.getType().getKind()))
+ .filter(n -> n instanceof MethodInvocationNode)
+ .map(n -> (MethodInvocationNode) n)
+ .findFirst();
+ if (assertedOnMethod.isPresent()) {
+ handleBooleanAssertionOnMethod(
+ nonNullMarker,
+ state.getTypes(),
+ assertedOnMethod.get(),
+ isAssertTrueMethod,
+ isAssertFalseMethod);
+ }
+ } else if (isTrueMethod || isFalseMethod) {
+ // asertThat(optionalFoo.isPresent()).isTrue()
+ // asertThat(optionalFoo.isEmpty()).isFalse()
+ Optional<MethodInvocationNode> wrappedMethod =
+ getNodeWrappedByAssertThat(node)
+ .filter(n -> n instanceof MethodInvocationNode)
+ .map(n -> (MethodInvocationNode) n)
+ .map(this::maybeUnwrapBooleanValueOf);
+ if (wrappedMethod.isPresent()) {
+ handleBooleanAssertionOnMethod(
+ nonNullMarker, state.getTypes(), wrappedMethod.get(), isTrueMethod, isFalseMethod);
+ }
+ } else if (methodNameUtil.isMethodThatEnsuresOptionalPresent(symbol)) {
+ // assertThat(optionalRef).isPresent()
+ // assertThat(methodReturningOptional()).isNotEmpty()
+ // assertThat(mapWithOptionalValues.get("key")).isNotEmpty()
+ getNodeWrappedByAssertThat(node).ifPresent(nonNullMarker);
+ }
+ }
+
+ private void handleBooleanAssertionOnMethod(
+ Consumer<Node> nonNullMarker,
Types types,
- AccessPath.AccessPathContext apContext,
- AccessPathNullnessPropagation.Updates bothUpdates) {
+ MethodInvocationNode node,
+ boolean assertsTrue,
+ boolean assertsFalse) {
+ Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(node.getTree());
+ boolean ensuresIsPresent = assertsTrue && optionalIsPresentCall(methodSymbol, types);
+ boolean ensuresNotEmpty = assertsFalse && optionalIsEmptyCall(methodSymbol, types);
+ if (ensuresIsPresent || ensuresNotEmpty) {
+ nonNullMarker.accept(node.getTarget().getReceiver());
+ }
+ }
+
+ private Optional<Node> getNodeWrappedByAssertThat(MethodInvocationNode node) {
Node receiver = node.getTarget().getReceiver();
if (receiver instanceof MethodInvocationNode) {
MethodInvocationNode receiverMethod = (MethodInvocationNode) receiver;
- Symbol.MethodSymbol receiverSymbol = ASTHelpers.getSymbol(receiverMethod.getTree());
- if (methodNameUtil.isMethodAssertThat(receiverSymbol)) {
- // assertThat will always have at least one argument, So safe to extract from the arguments
- Node arg = receiverMethod.getArgument(0);
- if (arg instanceof MethodInvocationNode) {
- // Since assertThat(a.isPresent()) changes to
- // Truth.assertThat(Boolean.valueOf(a.isPresent()))
- // need to be unwrapped from Boolean.valueOf
- Node unwrappedArg = ((MethodInvocationNode) arg).getArgument(0);
- if (unwrappedArg instanceof MethodInvocationNode) {
- MethodInvocationNode argMethod = (MethodInvocationNode) unwrappedArg;
- Symbol.MethodSymbol argSymbol = ASTHelpers.getSymbol(argMethod.getTree());
- if (optionalIsPresentCall(argSymbol, types)) {
- updateNonNullAPsForOptionalContent(
- bothUpdates, argMethod.getTarget().getReceiver(), apContext);
- }
- }
+ if (receiverMethod.getArguments().size() == 1) {
+ Symbol.MethodSymbol receiverSymbol = ASTHelpers.getSymbol(receiverMethod.getTree());
+ if (methodNameUtil.isMethodAssertThat(receiverSymbol)) {
+ return Optional.of(receiverMethod.getArgument(0));
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ private MethodInvocationNode maybeUnwrapBooleanValueOf(MethodInvocationNode node) {
+ // Due to autoboxing in the java compiler
+ // Truth.assertThat(a.isPresent()) changes to
+ // Truth.assertThat(Boolean.valueOf(a.isPresent()))
+ // and we need to unwrap Boolean.valueOf here
+ if (node.getArguments().size() == 1) {
+ Symbol.MethodSymbol symbol = ASTHelpers.getSymbol(node.getTree());
+ if (methodNameUtil.isMethodBooleanValueOf(symbol)) {
+ Node unwrappedArg = node.getArgument(0);
+ if (unwrappedArg instanceof MethodInvocationNode) {
+ return (MethodInvocationNode) unwrappedArg;
}
}
}
+ return node;
}
private void updateNonNullAPsForOptionalContent(
+ Context context,
AccessPathNullnessPropagation.Updates updates,
Node base,
AccessPath.AccessPathContext apContext) {
- AccessPath ap = AccessPath.fromBaseAndElement(base, OPTIONAL_CONTENT, apContext);
+ AccessPath ap =
+ AccessPath.fromBaseAndElement(
+ base, OptionalContentVariableElement.instance(context), apContext);
if (ap != null && base.getTree() != null) {
updates.set(ap, Nullness.NONNULL);
}
}
private boolean optionalIsPresentCall(Symbol.MethodSymbol symbol, Types types) {
+ return isZeroArgOptionalMethod("isPresent", symbol, types);
+ }
+
+ private boolean optionalIsEmptyCall(Symbol.MethodSymbol symbol, Types types) {
+ return isZeroArgOptionalMethod("isEmpty", symbol, types);
+ }
+
+ private boolean isZeroArgOptionalMethod(
+ String methodName, Symbol.MethodSymbol symbol, Types types) {
+ Preconditions.checkNotNull(optionalTypes);
+ if (!(symbol.getSimpleName().toString().equals(methodName)
+ && symbol.getParameters().length() == 0)) {
+ return false;
+ }
for (Type optionalType : optionalTypes) {
- if (symbol.getSimpleName().toString().equals("isPresent")
- && symbol.getParameters().length() == 0
- && types.isSubtype(symbol.owner.type, optionalType)) {
+ if (types.isSubtype(symbol.owner.type, optionalType)) {
return true;
}
}
@@ -209,77 +312,106 @@ public class OptionalEmptinessHandler extends BaseNoOpHandler {
}
private boolean optionalIsGetCall(Symbol.MethodSymbol symbol, Types types) {
- for (Type optionalType : optionalTypes) {
- if (symbol.getSimpleName().toString().equals("get")
- && symbol.getParameters().length() == 0
- && types.isSubtype(symbol.owner.type, optionalType)) {
- return true;
- }
- }
- return false;
+ return isZeroArgOptionalMethod("get", symbol, types);
}
- private static VariableElement getOptionalContentElement() {
- return new VariableElement() {
- @Override
- public Object getConstantValue() {
- return null;
+ /**
+ * A {@link VariableElement} for a dummy "field" holding the contents of an Optional object, used
+ * in dataflow analysis to track whether the Optional content is present.
+ *
+ * <p>Instances of this type should be accessed using {@link #instance(Context)}, not instantiated
+ * directly.
+ */
+ private static final class OptionalContentVariableElement implements VariableElement {
+ public static final Context.Key<OptionalContentVariableElement> contextKey =
+ new Context.Key<>();
+
+ private static final Set<Modifier> MODIFIERS = ImmutableSet.of(Modifier.PUBLIC, Modifier.FINAL);
+ private final Name name;
+ private final TypeMirror asType;
+
+ static synchronized VariableElement instance(Context context) {
+ OptionalContentVariableElement instance = context.get(contextKey);
+ if (instance == null) {
+ instance =
+ new OptionalContentVariableElement(
+ Names.instance(context).fromString("value"), Symtab.instance(context).objectType);
+ context.put(contextKey, instance);
}
+ return instance;
+ }
- @Override
- public Name getSimpleName() {
- return null;
- }
+ private OptionalContentVariableElement(Name name, TypeMirror asType) {
+ this.name = name;
+ this.asType = asType;
+ }
- @Override
- public Element getEnclosingElement() {
- return null;
- }
+ @Override
+ @Nullable
+ public Object getConstantValue() {
+ return null;
+ }
- @Override
- public List<? extends Element> getEnclosedElements() {
- return null;
- }
+ @Override
+ public Name getSimpleName() {
+ return name;
+ }
- @Override
- public List<? extends AnnotationMirror> getAnnotationMirrors() {
- return null;
- }
+ @Override
+ @Nullable
+ public Element getEnclosingElement() {
+ // A field would have an enclosing element, however this method isn't guaranteed to
+ // return non-null in all cases. It may be beneficial to implement this in a future
+ // improvement, but that will require tracking an instance per supported optional
+ // type (e.g. java.util.Optional and guava Optional).
+ return null;
+ }
- @Override
- public <A extends Annotation> A getAnnotation(Class<A> aClass) {
- return null;
- }
+ @Override
+ public List<? extends Element> getEnclosedElements() {
+ return Collections.emptyList();
+ }
- @Override
- public <A extends Annotation> A[] getAnnotationsByType(Class<A> aClass) {
- return null;
- }
+ @Override
+ public List<? extends AnnotationMirror> getAnnotationMirrors() {
+ return Collections.emptyList();
+ }
- @Override
- public <R, P> R accept(ElementVisitor<R, P> elementVisitor, P p) {
- return null;
- }
+ @Override
+ @Nullable
+ public <A extends Annotation> A getAnnotation(Class<A> aClass) {
+ return null;
+ }
- @Override
- public TypeMirror asType() {
- return null;
- }
+ @Override
+ @SuppressWarnings("unchecked")
+ public <A extends Annotation> A[] getAnnotationsByType(Class<A> aClass) {
+ return (A[]) Array.newInstance(aClass, 0);
+ }
- @Override
- public ElementKind getKind() {
- return null;
- }
+ @Override
+ public <R, P> R accept(ElementVisitor<R, P> elementVisitor, P p) {
+ return elementVisitor.visitVariable(this, p);
+ }
- @Override
- public Set<Modifier> getModifiers() {
- return null;
- }
+ @Override
+ public TypeMirror asType() {
+ return asType;
+ }
- @Override
- public String toString() {
- return "OPTIONAL_CONTENT";
- }
- };
+ @Override
+ public ElementKind getKind() {
+ return ElementKind.FIELD;
+ }
+
+ @Override
+ public Set<Modifier> getModifiers() {
+ return MODIFIERS;
+ }
+
+ @Override
+ public String toString() {
+ return "OPTIONAL_CONTENT";
+ }
}
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/RestrictiveAnnotationHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/RestrictiveAnnotationHandler.java
index ddece84..561ac6e 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/RestrictiveAnnotationHandler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/RestrictiveAnnotationHandler.java
@@ -22,106 +22,162 @@
package com.uber.nullaway.handlers;
-import com.google.common.collect.ImmutableSet;
import com.google.errorprone.VisitorState;
-import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ExpressionTree;
-import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.util.Context;
+import com.uber.nullaway.CodeAnnotationInfo;
import com.uber.nullaway.Config;
import com.uber.nullaway.NullAway;
-import com.uber.nullaway.NullabilityUtil;
import com.uber.nullaway.Nullness;
import com.uber.nullaway.dataflow.AccessPath;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
-import java.util.HashSet;
-import java.util.List;
+import javax.annotation.Nullable;
+import org.checkerframework.nullaway.dataflow.cfg.node.FieldAccessNode;
import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode;
public class RestrictiveAnnotationHandler extends BaseNoOpHandler {
+ private static final String JETBRAINS_NOT_NULL = "org.jetbrains.annotations.NotNull";
+
private final Config config;
RestrictiveAnnotationHandler(Config config) {
this.config = config;
}
+ /**
+ * Returns true iff the symbol is considered unannotated but restrictively annotated
+ * {@code @Nullable} under {@code AcknowledgeRestrictiveAnnotations=true} logic.
+ *
+ * <p>In particular, this means the symbol is explicitly annotated as {@code @Nullable} and, if
+ * {@code TreatGeneratedAsUnannotated=true}, it is not within generated code.
+ *
+ * @param symbol the symbol being checked
+ * @param context Javac Context or Error Prone SubContext
+ * @return whether this handler would generally override the nullness of {@code symbol} as
+ * nullable.
+ */
+ private boolean isSymbolRestrictivelyNullable(Symbol symbol, Context context) {
+ CodeAnnotationInfo codeAnnotationInfo = getCodeAnnotationInfo(context);
+ return (codeAnnotationInfo.isSymbolUnannotated(symbol, config)
+ // with the generated-as-unannotated option enabled, we want to ignore annotations in
+ // generated code no matter what
+ && !(config.treatGeneratedAsUnannotated() && codeAnnotationInfo.isGenerated(symbol, config))
+ && Nullness.hasNullableAnnotation(symbol, config));
+ }
+
@Override
- public ImmutableSet<Integer> onUnannotatedInvocationGetNonNullPositions(
+ public boolean onOverrideMayBeNullExpr(
NullAway analysis,
+ ExpressionTree expr,
+ @Nullable Symbol exprSymbol,
VisitorState state,
- Symbol.MethodSymbol methodSymbol,
- List<? extends ExpressionTree> actualParams,
- ImmutableSet<Integer> nonNullPositions) {
- HashSet<Integer> positions = new HashSet<Integer>();
- positions.addAll(nonNullPositions);
- for (int i = 0; i < methodSymbol.getParameters().size(); ++i) {
- if (Nullness.paramHasNonNullAnnotation(methodSymbol, i, config)) {
- positions.add(i);
- }
+ boolean exprMayBeNull) {
+ if (exprMayBeNull) {
+ return true;
+ }
+ Tree.Kind exprKind = expr.getKind();
+ if (exprSymbol != null
+ && (exprKind == Tree.Kind.METHOD_INVOCATION || exprKind == Tree.Kind.IDENTIFIER)
+ && isSymbolRestrictivelyNullable(exprSymbol, state.context)) {
+ return true;
}
- return ImmutableSet.copyOf(positions);
+ return false;
}
- @Override
- public boolean onOverrideMayBeNullExpr(
- NullAway analysis, ExpressionTree expr, VisitorState state, boolean exprMayBeNull) {
- if (expr.getKind().equals(Tree.Kind.METHOD_INVOCATION)) {
- Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol((MethodInvocationTree) expr);
- if (NullabilityUtil.isUnannotated(methodSymbol, config)) {
- // with the generated-as-unannotated option enabled, we want to ignore
- // annotations in generated code
- if (config.treatGeneratedAsUnannotated() && NullabilityUtil.isGenerated(methodSymbol)) {
- return exprMayBeNull;
- } else {
- return Nullness.hasNullableAnnotation(methodSymbol, config) || exprMayBeNull;
- }
- } else {
- return exprMayBeNull;
- }
+ @Nullable private CodeAnnotationInfo codeAnnotationInfo;
+
+ private CodeAnnotationInfo getCodeAnnotationInfo(Context context) {
+ if (codeAnnotationInfo == null) {
+ codeAnnotationInfo = CodeAnnotationInfo.instance(context);
}
- return exprMayBeNull;
+ return codeAnnotationInfo;
}
@Override
- public ImmutableSet<Integer> onUnannotatedInvocationGetExplicitlyNullablePositions(
+ public Nullness[] onOverrideMethodInvocationParametersNullability(
Context context,
Symbol.MethodSymbol methodSymbol,
- ImmutableSet<Integer> explicitlyNullablePositions) {
- HashSet<Integer> positions = new HashSet<Integer>();
- positions.addAll(explicitlyNullablePositions);
+ boolean isAnnotated,
+ Nullness[] argumentPositionNullness) {
+ if (isAnnotated) {
+ // We ignore isAnnotated code here, since annotations in code considered isAnnotated are
+ // already handled by NullAway's core algorithm.
+ return argumentPositionNullness;
+ }
for (int i = 0; i < methodSymbol.getParameters().size(); ++i) {
- if (Nullness.paramHasNullableAnnotation(methodSymbol, i, config)) {
- positions.add(i);
+ if (Nullness.paramHasNonNullAnnotation(methodSymbol, i, config)) {
+ if (methodSymbol.isVarArgs() && i == methodSymbol.getParameters().size() - 1) {
+ // Special handling: ignore org.jetbrains.annotations.NotNull on varargs parameters
+ // to handle kotlinc generated jars (see #720)
+ // We explicitly ignore type-use annotations here, looking for @NotNull used as a
+ // declaration annotation, which is why this logic is simpler than e.g.
+ // NullabilityUtil.getAllAnnotationsForParameter.
+ boolean jetBrainsNotNullAnnotated =
+ methodSymbol.getParameters().get(i).getAnnotationMirrors().stream()
+ .map(a -> a.getAnnotationType().toString())
+ .anyMatch(annotName -> annotName.equals(JETBRAINS_NOT_NULL));
+ if (jetBrainsNotNullAnnotated) {
+ continue;
+ }
+ }
+ argumentPositionNullness[i] = Nullness.NONNULL;
+ } else if (Nullness.paramHasNullableAnnotation(methodSymbol, i, config)) {
+ argumentPositionNullness[i] = Nullness.NULLABLE;
}
}
- return ImmutableSet.copyOf(positions);
+ return argumentPositionNullness;
}
@Override
- public boolean onUnannotatedInvocationGetExplicitlyNonNullReturn(
- Symbol.MethodSymbol methodSymbol, boolean explicitlyNonNullReturn) {
- return Nullness.hasNonNullAnnotation(methodSymbol, config) || explicitlyNonNullReturn;
+ public Nullness onOverrideMethodReturnNullability(
+ Symbol.MethodSymbol methodSymbol,
+ VisitorState state,
+ boolean isAnnotated,
+ Nullness returnNullness) {
+ // Note that, for the purposes of overriding/subtyping, either @Nullable or @NonNull
+ // can be considered restrictive annotations, depending on whether the unannotated method
+ // is overriding or being overridden.
+ if (isAnnotated) {
+ return returnNullness;
+ }
+ if (Nullness.hasNullableAnnotation(methodSymbol, config)) {
+ return Nullness.NULLABLE;
+ } else if (Nullness.hasNonNullAnnotation(methodSymbol, config)) {
+ return Nullness.NONNULL;
+ }
+ return returnNullness;
}
@Override
public NullnessHint onDataflowVisitMethodInvocation(
MethodInvocationNode node,
- Types types,
- Context context,
+ Symbol.MethodSymbol methodSymbol,
+ VisitorState state,
AccessPath.AccessPathContext apContext,
AccessPathNullnessPropagation.SubNodeValues inputs,
AccessPathNullnessPropagation.Updates thenUpdates,
AccessPathNullnessPropagation.Updates elseUpdates,
AccessPathNullnessPropagation.Updates bothUpdates) {
- Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(node.getTree());
- if (NullabilityUtil.isUnannotated(methodSymbol, config)
- && Nullness.hasNullableAnnotation(methodSymbol, config)) {
- return NullnessHint.HINT_NULLABLE;
- }
- return NullnessHint.UNKNOWN;
+ return isSymbolRestrictivelyNullable(methodSymbol, state.context)
+ ? NullnessHint.HINT_NULLABLE
+ : NullnessHint.UNKNOWN;
+ }
+
+ @Override
+ public NullnessHint onDataflowVisitFieldAccess(
+ FieldAccessNode node,
+ Symbol symbol,
+ Types types,
+ Context context,
+ AccessPath.AccessPathContext apContext,
+ AccessPathNullnessPropagation.SubNodeValues inputs,
+ AccessPathNullnessPropagation.Updates updates) {
+ return isSymbolRestrictivelyNullable(symbol, context)
+ ? NullnessHint.HINT_NULLABLE
+ : NullnessHint.UNKNOWN;
}
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/StreamNullabilityPropagator.java b/nullaway/src/main/java/com/uber/nullaway/handlers/StreamNullabilityPropagator.java
index fdb4af9..0b5afe2 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/StreamNullabilityPropagator.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/StreamNullabilityPropagator.java
@@ -325,14 +325,23 @@ class StreamNullabilityPropagator extends BaseNoOpHandler {
MemberReferenceTree tree,
VisitorState state,
Symbol.MethodSymbol methodSymbol) {
- if (mapToFilterMap.containsKey(tree) && ((JCTree.JCMemberReference) tree).kind.isUnbound()) {
+ MaplikeToFilterInstanceRecord callInstanceRecord = mapToFilterMap.get(tree);
+ if (callInstanceRecord != null && ((JCTree.JCMemberReference) tree).kind.isUnbound()) {
// Unbound method reference, check if we know the corresponding path to be NonNull from the
// previous filter.
- MaplikeToFilterInstanceRecord callInstanceRecord = mapToFilterMap.get(tree);
Tree filterTree = callInstanceRecord.getFilter();
- assert (filterTree instanceof MethodTree || filterTree instanceof LambdaExpressionTree);
+ if (!(filterTree instanceof MethodTree || filterTree instanceof LambdaExpressionTree)) {
+ throw new IllegalStateException(
+ "unexpected filterTree type "
+ + filterTree.getClass()
+ + " "
+ + state.getSourceForNode(filterTree));
+ }
NullnessStore filterNullnessStore = filterToNSMap.get(filterTree);
- assert filterNullnessStore != null;
+ if (filterNullnessStore == null) {
+ throw new IllegalStateException(
+ "null filterNullStore for tree " + state.getSourceForNode(filterTree));
+ }
for (AccessPath ap : filterNullnessStore.getAccessPathsWithValue(Nullness.NONNULL)) {
// Find the access path corresponding to the current unbound method reference after binding
ImmutableList<AccessPathElement> elements = ap.getElements();
@@ -412,9 +421,9 @@ class StreamNullabilityPropagator extends BaseNoOpHandler {
return nullnessBuilder;
}
assert (tree instanceof MethodTree || tree instanceof LambdaExpressionTree);
- if (mapToFilterMap.containsKey(tree)) {
+ MaplikeToFilterInstanceRecord callInstanceRecord = mapToFilterMap.get(tree);
+ if (callInstanceRecord != null) {
// Plug Nullness info from filter method into entry to map method.
- MaplikeToFilterInstanceRecord callInstanceRecord = mapToFilterMap.get(tree);
Tree filterTree = callInstanceRecord.getFilter();
assert (filterTree instanceof MethodTree || filterTree instanceof LambdaExpressionTree);
MaplikeMethodRecord mapMR = callInstanceRecord.getMaplikeMethodRecord();
@@ -434,7 +443,9 @@ class StreamNullabilityPropagator extends BaseNoOpHandler {
new LocalVariableNode(((LambdaExpressionTree) tree).getParameters().get(argIdx));
}
NullnessStore filterNullnessStore = filterToNSMap.get(filterTree);
- assert filterNullnessStore != null;
+ if (filterNullnessStore == null) {
+ throw new IllegalStateException("null filterNullStore for tree");
+ }
NullnessStore renamedRootsNullnessStore =
filterNullnessStore.uprootAccessPaths(ImmutableMap.of(filterLocalName, mapLocalName));
for (AccessPath ap : renamedRootsNullnessStore.getAccessPathsWithValue(Nullness.NONNULL)) {
@@ -448,16 +459,14 @@ class StreamNullabilityPropagator extends BaseNoOpHandler {
@Override
public void onDataflowVisitReturn(
ReturnTree tree, NullnessStore thenStore, NullnessStore elseStore) {
- if (returnToEnclosingMethodOrLambda.containsKey(tree)) {
- Tree filterTree = returnToEnclosingMethodOrLambda.get(tree);
+ Tree filterTree = returnToEnclosingMethodOrLambda.get(tree);
+ if (filterTree != null) {
assert (filterTree instanceof MethodTree || filterTree instanceof LambdaExpressionTree);
ExpressionTree retExpression = tree.getExpression();
if (canBooleanExpressionEvalToTrue(retExpression)) {
- if (filterToNSMap.containsKey(filterTree)) {
- filterToNSMap.put(filterTree, filterToNSMap.get(filterTree).leastUpperBound(thenStore));
- } else {
- filterToNSMap.put(filterTree, thenStore);
- }
+ filterToNSMap.compute(
+ filterTree,
+ (key, value) -> value == null ? thenStore : value.leastUpperBound(thenStore));
}
}
}
@@ -465,8 +474,8 @@ class StreamNullabilityPropagator extends BaseNoOpHandler {
@Override
public void onDataflowVisitLambdaResultExpression(
ExpressionTree tree, NullnessStore thenStore, NullnessStore elseStore) {
- if (expressionBodyToFilterLambda.containsKey(tree)) {
- LambdaExpressionTree filterTree = expressionBodyToFilterLambda.get(tree);
+ LambdaExpressionTree filterTree = expressionBodyToFilterLambda.get(tree);
+ if (filterTree != null) {
if (canBooleanExpressionEvalToTrue(tree)) {
filterToNSMap.put(filterTree, thenStore);
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/StreamNullabilityPropagatorFactory.java b/nullaway/src/main/java/com/uber/nullaway/handlers/StreamNullabilityPropagatorFactory.java
index 2fb0e5a..a3695c6 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/StreamNullabilityPropagatorFactory.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/StreamNullabilityPropagatorFactory.java
@@ -1,4 +1,5 @@
package com.uber.nullaway.handlers;
+
/*
* Copyright (c) 2017 Uber Technologies, Inc.
*
@@ -28,6 +29,8 @@ import com.uber.nullaway.handlers.stream.StreamModelBuilder;
import com.uber.nullaway.handlers.stream.StreamTypeRecord;
public class StreamNullabilityPropagatorFactory {
+
+ /** Returns a handler for the standard Java 8 stream APIs. */
public static StreamNullabilityPropagator getJavaStreamNullabilityPropagator() {
ImmutableList<StreamTypeRecord> streamModels =
StreamModelBuilder.start()
@@ -77,6 +80,7 @@ public class StreamNullabilityPropagatorFactory {
return new StreamNullabilityPropagator(streamModels);
}
+ /** Returns a handler for io.reactivex.* stream APIs */
public static StreamNullabilityPropagator getRxStreamNullabilityPropagator() {
ImmutableList<StreamTypeRecord> rxModels =
StreamModelBuilder.start()
@@ -136,4 +140,19 @@ public class StreamNullabilityPropagatorFactory {
return new StreamNullabilityPropagator(rxModels);
}
+
+ /**
+ * Create a new StreamNullabilityPropagator from a list of StreamTypeRecord specs.
+ *
+ * <p>This is used to create a new StreamNullabilityPropagator based on stream API specs provided
+ * by library models.
+ *
+ * @param streamNullabilitySpecs the list of StreamTypeRecord objects defining one or more stream
+ * APIs (from {@link StreamModelBuilder}).
+ * @return A handler corresponding to the stream APIs defined by the given specs.
+ */
+ public static StreamNullabilityPropagator fromSpecs(
+ ImmutableList<StreamTypeRecord> streamNullabilitySpecs) {
+ return new StreamNullabilityPropagator(streamNullabilitySpecs);
+ }
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractCheckHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractCheckHandler.java
index bdfb786..bfbadf7 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractCheckHandler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractCheckHandler.java
@@ -153,7 +153,8 @@ public class ContractCheckHandler extends BaseNoOpHandler {
ErrorMessage.MessageTypes.ANNOTATION_VALUE_INVALID, errorMessage),
returnTree,
analysis.buildDescription(returnTree),
- returnState));
+ returnState,
+ null));
}
return super.visitReturn(returnTree, unused);
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractHandler.java
index 4a6c9e5..939c1e0 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractHandler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractHandler.java
@@ -22,6 +22,7 @@
package com.uber.nullaway.handlers.contract;
+import static com.uber.nullaway.NullabilityUtil.castToNonNull;
import static com.uber.nullaway.handlers.contract.ContractUtils.getAntecedent;
import static com.uber.nullaway.handlers.contract.ContractUtils.getConsequent;
@@ -29,18 +30,25 @@ import com.google.common.base.Preconditions;
import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.ClassTree;
+import com.sun.source.tree.MethodInvocationTree;
import com.sun.tools.javac.code.Symbol;
-import com.sun.tools.javac.code.Types;
-import com.sun.tools.javac.util.Context;
import com.uber.nullaway.Config;
import com.uber.nullaway.ErrorMessage;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.Nullness;
import com.uber.nullaway.dataflow.AccessPath;
import com.uber.nullaway.dataflow.AccessPathNullnessPropagation;
+import com.uber.nullaway.dataflow.cfg.NullAwayCFGBuilder;
import com.uber.nullaway.handlers.BaseNoOpHandler;
+import java.util.Optional;
import javax.annotation.Nullable;
+import javax.lang.model.type.TypeMirror;
+import org.checkerframework.nullaway.dataflow.cfg.node.AbstractNodeVisitor;
+import org.checkerframework.nullaway.dataflow.cfg.node.BinaryOperationNode;
+import org.checkerframework.nullaway.dataflow.cfg.node.EqualToNode;
import org.checkerframework.nullaway.dataflow.cfg.node.MethodInvocationNode;
+import org.checkerframework.nullaway.dataflow.cfg.node.Node;
+import org.checkerframework.nullaway.dataflow.cfg.node.NotEqualNode;
/**
* This Handler parses the jetbrains @Contract annotation and honors the nullness spec defined there
@@ -78,7 +86,12 @@ public class ContractHandler extends BaseNoOpHandler {
private final Config config;
private @Nullable NullAway analysis;
- private @Nullable VisitorState state;
+
+ // Cached on visiting the top-level class and used for onCFGBuildPhase1AfterVisitMethodInvocation,
+ // where no VisitorState is otherwise available.
+ private @Nullable VisitorState storedVisitorState;
+
+ private @Nullable TypeMirror runtimeExceptionType;
public ContractHandler(Config config) {
this.config = config;
@@ -88,124 +101,291 @@ public class ContractHandler extends BaseNoOpHandler {
public void onMatchTopLevelClass(
NullAway analysis, ClassTree tree, VisitorState state, Symbol.ClassSymbol classSymbol) {
this.analysis = analysis;
- this.state = state;
+ this.storedVisitorState = state;
+ }
+
+ @Override
+ public MethodInvocationNode onCFGBuildPhase1AfterVisitMethodInvocation(
+ NullAwayCFGBuilder.NullAwayCFGTranslationPhaseOne phase,
+ MethodInvocationTree tree,
+ MethodInvocationNode originalNode) {
+ Preconditions.checkNotNull(storedVisitorState);
+ Preconditions.checkNotNull(analysis);
+ Symbol.MethodSymbol callee = ASTHelpers.getSymbol(tree);
+ Preconditions.checkNotNull(callee);
+ for (String clause : ContractUtils.getContractClauses(callee, config)) {
+ // This method currently handles contracts of the form `(true|false) -> fail`, other
+ // contracts are handled by 'onDataflowVisitMethodInvocation' which has access to more
+ // dataflow information.
+ if (!"fail".equals(getConsequent(clause, tree, analysis, storedVisitorState, callee))) {
+ continue;
+ }
+ String[] antecedent =
+ getAntecedent(
+ clause,
+ tree,
+ analysis,
+ storedVisitorState,
+ callee,
+ originalNode.getArguments().size());
+ // Find a single value constraint that is not already known. If more than one argument with
+ // unknown nullness affects the method's result, then ignore this clause.
+ Node arg = null;
+ // Set to false if the rule is detected to be one we don't yet support
+ boolean supported = true;
+ boolean booleanConstraint = false;
+
+ for (int i = 0; i < antecedent.length; ++i) {
+ String valueConstraint = antecedent[i].trim();
+ if ("false".equals(valueConstraint) || "true".equals(valueConstraint)) {
+ if (arg != null) {
+ // We don't currently support contracts depending on the boolean value of more than one
+ // argument using the node-insertion method.
+ supported = false;
+ break;
+ }
+ booleanConstraint = Boolean.parseBoolean(valueConstraint);
+ arg = originalNode.getArgument(i);
+ } else if (!valueConstraint.equals("_")) {
+ // Found an unsupported type of constraint, only true, false, and '_' (wildcard) are
+ // supported.
+ // No need to implement complex handling here, 'onDataflowVisitMethodInvocation' will
+ // validate the contract.
+ supported = false;
+ break;
+ }
+ }
+ if (arg != null && supported) {
+ if (runtimeExceptionType == null) {
+ runtimeExceptionType = phase.classToErrorType(RuntimeException.class);
+ }
+ // In practice the failure may not be RuntimeException, however the conditional
+ // throw is inserted after the method invocation where we must assume that
+ // any invocation is capable of throwing an unchecked throwable.
+ Preconditions.checkNotNull(runtimeExceptionType);
+ if (booleanConstraint) {
+ phase.insertThrowOnTrue(arg, runtimeExceptionType);
+ } else {
+ phase.insertThrowOnFalse(arg, runtimeExceptionType);
+ }
+ }
+ }
+ return originalNode;
}
@Override
public NullnessHint onDataflowVisitMethodInvocation(
MethodInvocationNode node,
- Types types,
- Context context,
+ Symbol.MethodSymbol callee,
+ VisitorState state,
AccessPath.AccessPathContext apContext,
AccessPathNullnessPropagation.SubNodeValues inputs,
AccessPathNullnessPropagation.Updates thenUpdates,
AccessPathNullnessPropagation.Updates elseUpdates,
AccessPathNullnessPropagation.Updates bothUpdates) {
- Symbol.MethodSymbol callee = ASTHelpers.getSymbol(node.getTree());
- Preconditions.checkNotNull(callee);
- // Check to see if this method has an @Contract annotation
- String contractString = ContractUtils.getContractString(callee, config);
- if (contractString != null && contractString.trim().length() > 0) {
- // Found a contract, lets parse it.
- String[] clauses = contractString.split(";");
- for (String clause : clauses) {
-
- String[] antecedent =
- getAntecedent(
- clause, node.getTree(), analysis, state, callee, node.getArguments().size());
- String consequent = getConsequent(clause, node.getTree(), analysis, state, callee);
-
- // Find a single value constraint that is not already known. If more than one arguments with
- // unknown
- // nullness affect the method's result, then ignore this clause.
- int argIdx = -1;
- Nullness argAntecedentNullness = null;
- boolean supported =
- true; // Set to false if the rule is detected to be one we don't yet support
-
- for (int i = 0; i < antecedent.length; ++i) {
- String valueConstraint = antecedent[i].trim();
- if (valueConstraint.equals("_")) {
- continue;
- } else if (valueConstraint.equals("false") || valueConstraint.equals("true")) {
+ Preconditions.checkNotNull(analysis);
+ MethodInvocationTree tree = castToNonNull(node.getTree());
+ for (String clause : ContractUtils.getContractClauses(callee, config)) {
+
+ String[] antecedent =
+ getAntecedent(clause, tree, analysis, state, callee, node.getArguments().size());
+ String consequent = getConsequent(clause, tree, analysis, state, callee);
+
+ // Find a single value constraint that is not already known. If more than one argument with
+ // unknown nullness affects the method's result, then ignore this clause.
+ Node arg = null;
+ Nullness argAntecedentNullness = null;
+ // Set to false if the rule is detected to be one we don't yet support
+ boolean supported = true;
+
+ for (int i = 0; i < antecedent.length; ++i) {
+ String valueConstraint = antecedent[i].trim();
+ if (valueConstraint.equals("_")) {
+ continue;
+ } else if (valueConstraint.equals("false") || valueConstraint.equals("true")) {
+ // We handle boolean constraints in the case that the boolean argument is the result
+ // of a null or not-null check. For example,
+ // '@Contract("true -> true") boolean func(boolean v)'
+ // called with 'func(obj == null)'
+ // can be interpreted as equivalent to
+ // '@Contract("null -> true") boolean func(@Nullable Object v)'
+ // called with 'func(obj)'
+ // This path unwraps null reference equality and inequality checks
+ // to pass the target (in the above example, 'obj') as arg.
+ Node argument = node.getArgument(i);
+ // isNullTarget is the variable side of a null check. For example, both 'e == null'
+ // and 'null == e' would return the node representing 'e'.
+ Optional<Node> isNullTarget = argument.accept(NullEqualityVisitor.IS_NULL, inputs);
+ // notNullTarget is the variable side of a not-null check. For example, both 'e != null'
+ // and 'null != e' would return the node representing 'e'.
+ Optional<Node> notNullTarget = argument.accept(NullEqualityVisitor.NOT_NULL, inputs);
+ // It is possible for at most one of isNullTarget and notNullTarget to be present.
+ Node nullTestTarget = isNullTarget.orElse(notNullTarget.orElse(null));
+ if (nullTestTarget == null) {
supported = false;
break;
- } else if (valueConstraint.equals("!null")
- && inputs.valueOfSubNode(node.getArgument(i)).equals(Nullness.NONNULL)) {
- // We already know this argument can't be null, so we can treat it as not part of the
- // clause
- // for the purpose of deciding the non-nullness of the other arguments.
+ }
+ // isNullTarget is equivalent to 'null ->' while notNullTarget is equivalent
+ // to '!null ->'. However, the valueConstraint may reverse the check.
+ // The following table illustrates expected antecedentNullness based on
+ // null comparison direction and constraint:
+ // | (obj == null) | (obj != null)
+ // Constraint 'true' | NULL | NONNULL
+ // Constraint 'false' | NONNULL | NULL
+ boolean booleanConstraintValue = valueConstraint.equals("true");
+ Nullness antecedentNullness =
+ isNullTarget.isPresent()
+ ? (booleanConstraintValue ? Nullness.NULL : Nullness.NONNULL)
+ :
+ // !isNullTarget.isPresent() -> notNullTarget.present() must be true
+ (booleanConstraintValue ? Nullness.NONNULL : Nullness.NULL);
+ Nullness targetNullness = inputs.valueOfSubNode(nullTestTarget);
+ if (antecedentNullness.equals(targetNullness)) {
+ // We already know this argument is satisfied so we can treat it as part of the
+ // clause for the purpose of deciding the nullness of the other arguments.
continue;
- } else if (valueConstraint.equals("null") || valueConstraint.equals("!null")) {
- if (argIdx != -1) {
- // More than one argument involved in the antecedent, ignore this rule
- supported = false;
- break;
- }
- argIdx = i;
- argAntecedentNullness =
- valueConstraint.equals("null") ? Nullness.NULLABLE : Nullness.NONNULL;
- } else {
- Preconditions.checkNotNull(state);
- Preconditions.checkNotNull(analysis);
- String errorMessage =
- "Invalid @Contract annotation detected for method "
- + callee
- + ". It contains the following uparseable clause: "
- + clause
- + " (unknown value constraint: "
- + valueConstraint
- + ", see https://www.jetbrains.com/help/idea/contract-annotations.html).";
- state.reportMatch(
- analysis
- .getErrorBuilder()
- .createErrorDescription(
- new ErrorMessage(
- ErrorMessage.MessageTypes.ANNOTATION_VALUE_INVALID, errorMessage),
- node.getTree(),
- analysis.buildDescription(node.getTree()),
- state));
+ }
+ if (arg != null) {
+ // More than one argument involved in the antecedent, ignore this rule
supported = false;
break;
}
- }
- if (!supported) {
- // Too many arguments involved, or unsupported @Contract features. On to next clause in
- // the
- // contract expression
+ arg = nullTestTarget;
+ argAntecedentNullness = antecedentNullness;
+ } else if (valueConstraint.equals("!null")
+ && inputs.valueOfSubNode(node.getArgument(i)).equals(Nullness.NONNULL)) {
+ // We already know this argument can't be null, so we can treat it as not part of the
+ // clause for the purpose of deciding the non-nullness of the other arguments.
continue;
- }
- if (argIdx == -1) {
- // The antecedent is unconditionally true. Check for the ... -> !null case and set the
- // return nullness accordingly
- if (consequent.equals("!null")) {
- return NullnessHint.FORCE_NONNULL;
+ } else if (valueConstraint.equals("null") || valueConstraint.equals("!null")) {
+ if (arg != null) {
+ // More than one argument involved in the antecedent, ignore this rule
+ supported = false;
+ break;
}
- continue;
+ arg = node.getArgument(i);
+ argAntecedentNullness = valueConstraint.equals("null") ? Nullness.NULL : Nullness.NONNULL;
+ } else {
+ String errorMessage =
+ "Invalid @Contract annotation detected for method "
+ + callee
+ + ". It contains the following uparseable clause: "
+ + clause
+ + " (unknown value constraint: "
+ + valueConstraint
+ + ", see https://www.jetbrains.com/help/idea/contract-annotations.html).";
+ state.reportMatch(
+ analysis
+ .getErrorBuilder()
+ .createErrorDescription(
+ new ErrorMessage(
+ ErrorMessage.MessageTypes.ANNOTATION_VALUE_INVALID, errorMessage),
+ tree,
+ analysis.buildDescription(tree),
+ state,
+ null));
+ supported = false;
+ break;
}
- assert argAntecedentNullness != null;
- // The nullness of one argument is all that matters for the antecedent, let's negate the
- // consequent to fix the nullness of this argument.
- AccessPath accessPath =
- AccessPath.getAccessPathForNodeNoMapGet(node.getArgument(argIdx), apContext);
- if (accessPath == null) {
- continue;
- }
- if (consequent.equals("false") && argAntecedentNullness.equals(Nullness.NULLABLE)) {
- // If argIdx being null implies the return of the method being false, then the return
- // being true implies argIdx is not null and we must mark it as such in the then update.
- thenUpdates.set(accessPath, Nullness.NONNULL);
- } else if (consequent.equals("true") && argAntecedentNullness.equals(Nullness.NULLABLE)) {
- // If argIdx being null implies the return of the method being true, then the return being
- // false implies argIdx is not null and we must mark it as such in the else update.
- elseUpdates.set(accessPath, Nullness.NONNULL);
- } else if (consequent.equals("fail") && argAntecedentNullness.equals(Nullness.NULLABLE)) {
- // If argIdx being null implies the method throws an exception, then we can mark it as
- // non-null on both non-exceptional exits from the method
- bothUpdates.set(accessPath, Nullness.NONNULL);
+ }
+ if (!supported) {
+ // Too many arguments involved, or unsupported @Contract features. On to next clause in the
+ // contract expression
+ continue;
+ }
+ if (arg == null) {
+ // The antecedent is unconditionally true. Check for the ... -> !null case and set the
+ // return nullness accordingly
+ if (consequent.equals("!null")) {
+ return NullnessHint.FORCE_NONNULL;
}
+ continue;
+ }
+ Preconditions.checkState(
+ argAntecedentNullness != null, "argAntecedentNullness should have been set");
+ // The nullness of one argument is all that matters for the antecedent, let's negate the
+ // consequent to fix the nullness of this argument.
+ AccessPath accessPath = AccessPath.getAccessPathForNode(arg, state, apContext);
+ if (accessPath == null) {
+ continue;
+ }
+ if (consequent.equals("false") && argAntecedentNullness.equals(Nullness.NULL)) {
+ // If arg being null implies the return of the method being false, then the return
+ // being true implies arg is not null and we must mark it as such in the then update.
+ thenUpdates.set(accessPath, Nullness.NONNULL);
+ } else if (consequent.equals("true") && argAntecedentNullness.equals(Nullness.NULL)) {
+ // If arg being null implies the return of the method being true, then the return being
+ // false implies arg is not null and we must mark it as such in the else update.
+ elseUpdates.set(accessPath, Nullness.NONNULL);
+ } else if (consequent.equals("fail") && argAntecedentNullness.equals(Nullness.NULL)) {
+ // If arg being null implies the method throws an exception, then we can mark it as
+ // non-null on both non-exceptional exits from the method
+ bothUpdates.set(accessPath, Nullness.NONNULL);
}
}
return NullnessHint.UNKNOWN;
}
+
+ /**
+ * This visitor returns an {@link Optional<Node>} representing the non-null side of a null
+ * comparison check.
+ *
+ * <p>When the visited node is not an equality check (either {@link EqualToNode} or {@link
+ * NotEqualNode} based on {@link #equals} being {@code true} or {@code false} respectively)
+ * against a null value, {@link Optional#empty()} is returned. For example, visiting {@code e ==
+ * null} with {@code new NullEqualityVisitor(true)} would return an {@link Optional} of node
+ * {@code e}.
+ */
+ private static final class NullEqualityVisitor
+ extends AbstractNodeVisitor<Optional<Node>, AccessPathNullnessPropagation.SubNodeValues> {
+
+ private static final NullEqualityVisitor IS_NULL = new NullEqualityVisitor(true);
+ private static final NullEqualityVisitor NOT_NULL = new NullEqualityVisitor(false);
+
+ private final boolean equals;
+
+ NullEqualityVisitor(boolean equals) {
+ this.equals = equals;
+ }
+
+ @Override
+ public Optional<Node> visitNode(Node node, AccessPathNullnessPropagation.SubNodeValues unused) {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<Node> visitNotEqual(
+ NotEqualNode notEqualNode, AccessPathNullnessPropagation.SubNodeValues inputs) {
+ if (equals) {
+ return Optional.empty();
+ } else {
+ return visit(notEqualNode, inputs);
+ }
+ }
+
+ @Override
+ public Optional<Node> visitEqualTo(
+ EqualToNode equalToNode, AccessPathNullnessPropagation.SubNodeValues inputs) {
+ if (equals) {
+ return visit(equalToNode, inputs);
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ private Optional<Node> visit(
+ BinaryOperationNode comparison, AccessPathNullnessPropagation.SubNodeValues inputs) {
+ Node lhs = comparison.getLeftOperand();
+ Node rhs = comparison.getRightOperand();
+ Nullness lhsNullness = inputs.valueOfSubNode(lhs);
+ Nullness rhsNullness = inputs.valueOfSubNode(rhs);
+ if (Nullness.NULL.equals(lhsNullness)) {
+ return Optional.of(rhs);
+ }
+ if (Nullness.NULL.equals(rhsNullness)) {
+ return Optional.of(lhs);
+ }
+ return Optional.empty();
+ }
+ }
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractNullnessStoreInitializer.java b/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractNullnessStoreInitializer.java
index 7b35f03..41b25d5 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractNullnessStoreInitializer.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractNullnessStoreInitializer.java
@@ -41,7 +41,9 @@ public class ContractNullnessStoreInitializer extends NullnessStoreInitializer {
final Symbol.MethodSymbol callee = ASTHelpers.getSymbol(methodTree);
final String contractString = ContractUtils.getContractString(callee, config);
- assert contractString != null;
+ if (contractString == null) {
+ throw new IllegalStateException("expected non-null contractString");
+ }
String[] clauses = contractString.split(";");
String[] parts = clauses[0].split("->");
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractUtils.java b/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractUtils.java
index e15061a..ac54e11 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractUtils.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractUtils.java
@@ -17,6 +17,8 @@ import org.checkerframework.nullaway.javacutil.AnnotationUtils;
/** An utility class for {@link ContractHandler} and {@link ContractCheckHandler}. */
public class ContractUtils {
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
/**
* Returns a set of field names excluding their receivers (e.g. "this.a" will be "a")
*
@@ -24,8 +26,7 @@ public class ContractUtils {
* @return A set of trimmed field names.
*/
public static Set<String> trimReceivers(Set<String> fieldNames) {
- return fieldNames
- .stream()
+ return fieldNames.stream()
.map((Function<String, String>) input -> input.substring(input.lastIndexOf(".") + 1))
.collect(Collectors.toSet());
}
@@ -58,7 +59,8 @@ public class ContractUtils {
new ErrorMessage(ErrorMessage.MessageTypes.ANNOTATION_VALUE_INVALID, message),
tree,
analysis.buildDescription(tree),
- state));
+ state,
+ null));
}
return parts[1].trim();
}
@@ -84,7 +86,7 @@ public class ContractUtils {
String[] parts = clause.split("->");
- String[] antecedent = parts[0].split(",");
+ String[] antecedent = parts[0].trim().isEmpty() ? new String[0] : parts[0].split(",");
if (antecedent.length != numOfArguments) {
String message =
@@ -105,7 +107,8 @@ public class ContractUtils {
new ErrorMessage(ErrorMessage.MessageTypes.ANNOTATION_VALUE_INVALID, message),
tree,
analysis.buildDescription(tree),
- state));
+ state,
+ null));
}
return antecedent;
}
@@ -127,4 +130,16 @@ public class ContractUtils {
}
return null;
}
+
+ static String[] getContractClauses(Symbol.MethodSymbol callee, Config config) {
+ // Check to see if this method has an @Contract annotation
+ String contractString = getContractString(callee, config);
+ if (contractString != null) {
+ String trimmedContractString = contractString.trim();
+ if (!trimmedContractString.isEmpty()) {
+ return trimmedContractString.split(";");
+ }
+ }
+ return EMPTY_STRING_ARRAY;
+ }
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/contract/fieldcontract/EnsuresNonNullHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/contract/fieldcontract/EnsuresNonNullHandler.java
index a5b6328..bf2767c 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/contract/fieldcontract/EnsuresNonNullHandler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/contract/fieldcontract/EnsuresNonNullHandler.java
@@ -22,16 +22,14 @@
package com.uber.nullaway.handlers.contract.fieldcontract;
+import static com.uber.nullaway.NullabilityUtil.castToNonNull;
import static com.uber.nullaway.NullabilityUtil.getAnnotationValueArray;
-import com.google.common.base.Preconditions;
import com.google.errorprone.VisitorState;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.MethodTree;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.code.Symbol;
-import com.sun.tools.javac.code.Types;
-import com.sun.tools.javac.util.Context;
import com.uber.nullaway.ErrorMessage;
import com.uber.nullaway.NullAway;
import com.uber.nullaway.Nullness;
@@ -106,7 +104,8 @@ public class EnsuresNonNullHandler extends AbstractFieldContractHandler {
new ErrorMessage(ErrorMessage.MessageTypes.POSTCONDITION_NOT_SATISFIED, message),
tree,
analysis.buildDescription(tree),
- state));
+ state,
+ null));
return false;
}
return true;
@@ -140,7 +139,7 @@ public class EnsuresNonNullHandler extends AbstractFieldContractHandler {
errorMessage
.append(
"postcondition inheritance is violated, this method must guarantee that all fields written in the @EnsuresNonNull annotation of overridden method ")
- .append(ASTHelpers.enclosingClass(overriddenMethod).getSimpleName())
+ .append(castToNonNull(ASTHelpers.enclosingClass(overriddenMethod)).getSimpleName())
.append(".")
.append(overriddenMethod.getSimpleName())
.append(" are @NonNull at exit point as well. Fields [");
@@ -162,7 +161,8 @@ public class EnsuresNonNullHandler extends AbstractFieldContractHandler {
errorMessage.toString()),
tree,
analysis.buildDescription(tree),
- state));
+ state,
+ null));
}
/**
@@ -173,8 +173,8 @@ public class EnsuresNonNullHandler extends AbstractFieldContractHandler {
@Override
public NullnessHint onDataflowVisitMethodInvocation(
MethodInvocationNode node,
- Types types,
- Context context,
+ Symbol.MethodSymbol methodSymbol,
+ VisitorState state,
AccessPath.AccessPathContext apContext,
AccessPathNullnessPropagation.SubNodeValues inputs,
AccessPathNullnessPropagation.Updates thenUpdates,
@@ -184,16 +184,15 @@ public class EnsuresNonNullHandler extends AbstractFieldContractHandler {
// A synthetic node might be inserted by the Checker Framework during CFG construction, it is
// safer to do a null check here.
return super.onDataflowVisitMethodInvocation(
- node, types, context, apContext, inputs, thenUpdates, elseUpdates, bothUpdates);
+ node, methodSymbol, state, apContext, inputs, thenUpdates, elseUpdates, bothUpdates);
}
- Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(node.getTree());
- Preconditions.checkNotNull(methodSymbol);
Set<String> fieldNames = getAnnotationValueArray(methodSymbol, annotName, false);
if (fieldNames != null) {
fieldNames = ContractUtils.trimReceivers(fieldNames);
for (String fieldName : fieldNames) {
VariableElement field =
- getInstanceFieldOfClass(ASTHelpers.enclosingClass(methodSymbol), fieldName);
+ getInstanceFieldOfClass(
+ castToNonNull(ASTHelpers.enclosingClass(methodSymbol)), fieldName);
if (field == null) {
// Invalid annotation, will result in an error during validation. For now, skip field.
continue;
@@ -207,6 +206,6 @@ public class EnsuresNonNullHandler extends AbstractFieldContractHandler {
}
}
return super.onDataflowVisitMethodInvocation(
- node, types, context, apContext, inputs, thenUpdates, elseUpdates, bothUpdates);
+ node, methodSymbol, state, apContext, inputs, thenUpdates, elseUpdates, bothUpdates);
}
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/contract/fieldcontract/RequiresNonNullHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/contract/fieldcontract/RequiresNonNullHandler.java
index 7068b30..9c6e020 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/contract/fieldcontract/RequiresNonNullHandler.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/contract/fieldcontract/RequiresNonNullHandler.java
@@ -119,7 +119,8 @@ public class RequiresNonNullHandler extends AbstractFieldContractHandler {
ErrorMessage.MessageTypes.WRONG_OVERRIDE_PRECONDITION, errorMessage.toString()),
tree,
analysis.buildDescription(tree),
- state));
+ state,
+ null));
}
/**
@@ -163,7 +164,8 @@ public class RequiresNonNullHandler extends AbstractFieldContractHandler {
new ErrorMessage(ErrorMessage.MessageTypes.PRECONDITION_NOT_SATISFIED, message),
tree,
analysis.buildDescription(tree),
- state));
+ state,
+ null));
}
}
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/stream/MaplikeMethodRecord.java b/nullaway/src/main/java/com/uber/nullaway/handlers/stream/MaplikeMethodRecord.java
index a2abfd0..cb2a34e 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/stream/MaplikeMethodRecord.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/stream/MaplikeMethodRecord.java
@@ -1,4 +1,5 @@
package com.uber.nullaway.handlers.stream;
+
/*
* Copyright (c) 2017 Uber Technologies, Inc.
*
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/stream/MaplikeToFilterInstanceRecord.java b/nullaway/src/main/java/com/uber/nullaway/handlers/stream/MaplikeToFilterInstanceRecord.java
index 908931c..74c932e 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/stream/MaplikeToFilterInstanceRecord.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/stream/MaplikeToFilterInstanceRecord.java
@@ -1,4 +1,5 @@
package com.uber.nullaway.handlers.stream;
+
/*
* Copyright (c) 2017 Uber Technologies, Inc.
*
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/stream/StreamModelBuilder.java b/nullaway/src/main/java/com/uber/nullaway/handlers/stream/StreamModelBuilder.java
index d945db2..94e0b1a 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/stream/StreamModelBuilder.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/stream/StreamModelBuilder.java
@@ -1,4 +1,5 @@
package com.uber.nullaway.handlers.stream;
+
/*
* Copyright (c) 2017 Uber Technologies, Inc.
*
@@ -24,8 +25,11 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.predicates.TypePredicate;
+import com.google.errorprone.predicates.type.DescendantOf;
+import com.google.errorprone.suppliers.Suppliers;
import java.util.ArrayList;
import java.util.List;
+import javax.annotation.Nullable;
/**
* Used to produce a new list of StreamTypeRecord models, where each model represents a class from a
@@ -39,7 +43,7 @@ import java.util.List;
public class StreamModelBuilder {
private final List<StreamTypeRecord> typeRecords = new ArrayList<>();
- private TypePredicate tp = null;
+ private @Nullable TypePredicate tp = null;
private ImmutableSet.Builder<String> filterMethodSigs;
private ImmutableSet.Builder<String> filterMethodSimpleNames;
private ImmutableMap.Builder<String, MaplikeMethodRecord> mapMethodSigToRecord;
@@ -47,7 +51,10 @@ public class StreamModelBuilder {
private ImmutableSet.Builder<String> passthroughMethodSigs;
private ImmutableSet.Builder<String> passthroughMethodSimpleNames;
- private StreamModelBuilder() {}
+ private StreamModelBuilder() {
+ // initialize here to avoid having the fields be @Nullable
+ initializeBuilders();
+ }
/**
* Get an empty StreamModelBuilder.
@@ -81,13 +88,27 @@ public class StreamModelBuilder {
public StreamModelBuilder addStreamType(TypePredicate tp) {
finalizeOpenStreamTypeRecord();
this.tp = tp;
+ initializeBuilders();
+ return this;
+ }
+
+ /**
+ * Add a stream type to our models based on the type's fully qualified name.
+ *
+ * @param fullyQualifiedName the FQN of the class/interface in our stream-based API.
+ * @return This builder (for chaining).
+ */
+ public StreamModelBuilder addStreamTypeFromName(String fullyQualifiedName) {
+ return this.addStreamType(new DescendantOf(Suppliers.typeFromString(fullyQualifiedName)));
+ }
+
+ private void initializeBuilders() {
this.filterMethodSigs = ImmutableSet.builder();
this.filterMethodSimpleNames = ImmutableSet.builder();
this.mapMethodSigToRecord = ImmutableMap.builder();
this.mapMethodSimpleNameToRecord = ImmutableMap.builder();
this.passthroughMethodSigs = ImmutableSet.builder();
this.passthroughMethodSimpleNames = ImmutableSet.builder();
- return this;
}
/**
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/stream/StreamTypeRecord.java b/nullaway/src/main/java/com/uber/nullaway/handlers/stream/StreamTypeRecord.java
index f1fca6c..fc1caf6 100644
--- a/nullaway/src/main/java/com/uber/nullaway/handlers/stream/StreamTypeRecord.java
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/stream/StreamTypeRecord.java
@@ -1,4 +1,5 @@
package com.uber.nullaway.handlers.stream;
+
/*
* Copyright (c) 2017 Uber Technologies, Inc.
*
@@ -20,6 +21,8 @@ package com.uber.nullaway.handlers.stream;
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
+import static com.uber.nullaway.NullabilityUtil.castToNonNull;
+
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.VisitorState;
@@ -86,7 +89,9 @@ public class StreamTypeRecord {
public MaplikeMethodRecord getMaplikeMethodRecord(Symbol.MethodSymbol methodSymbol) {
MaplikeMethodRecord record = mapMethodSigToRecord.get(methodSymbol.toString());
if (record == null) {
- record = mapMethodSimpleNameToRecord.get(methodSymbol.getQualifiedName().toString());
+ record =
+ castToNonNull(
+ mapMethodSimpleNameToRecord.get(methodSymbol.getQualifiedName().toString()));
}
return record;
}
diff --git a/nullaway/src/main/java/com/uber/nullaway/handlers/temporary/FluentFutureHandler.java b/nullaway/src/main/java/com/uber/nullaway/handlers/temporary/FluentFutureHandler.java
new file mode 100644
index 0000000..04ccd6b
--- /dev/null
+++ b/nullaway/src/main/java/com/uber/nullaway/handlers/temporary/FluentFutureHandler.java
@@ -0,0 +1,91 @@
+package com.uber.nullaway.handlers.temporary;
+
+import com.google.errorprone.VisitorState;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.source.tree.LambdaExpressionTree;
+import com.sun.source.tree.MethodInvocationTree;
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.NullabilityUtil;
+import com.uber.nullaway.Nullness;
+import com.uber.nullaway.handlers.BaseNoOpHandler;
+import java.util.Arrays;
+import javax.lang.model.element.Name;
+
+/**
+ * This handler provides a temporary workaround due to our lack of support for generics, which
+ * allows natural usage of Futures/FluentFuture Guava APIs. It can potentially introduce false
+ * negatives, however, and should be deprecated as soon as full generic support is available.
+ *
+ * <p>This works by special casing the return nullability of {@link com.google.common.base.Function}
+ * and {@link com.google.common.util.concurrent.AsyncFunction} to be e.g. {@code Function<@Nullable
+ * T>} whenever these functional interfaces are implemented as a lambda expression passed to a list
+ * of specific methods of {@link com.google.common.util.concurrent.FluentFuture} or {@link
+ * com.google.common.util.concurrent.Futures}. We cannot currently check that {@code T} for {@code
+ * FluentFuture<T>} is a {@code @Nullable} type, so this is unsound. However, we have found many
+ * cases in practice where these lambdas include {@code null} returns, which were already being
+ * ignored (due to a bug) before PR #765. This handler offers the best possible support for these
+ * cases, at least until our generics support is mature enough to handle them.
+ *
+ * <p>Note: Package {@code com.uber.nullaway.handlers.temporary} is meant for this sort of temporary
+ * workaround handler, to be removed as future NullAway features make them unnecessary. This is a
+ * hack, but the best of a bunch of bad options.
+ */
+public class FluentFutureHandler extends BaseNoOpHandler {
+
+ private static final String GUAVA_FUNCTION_CLASS_NAME = "com.google.common.base.Function";
+ private static final String GUAVA_ASYNC_FUNCTION_CLASS_NAME =
+ "com.google.common.util.concurrent.AsyncFunction";
+ private static final String FLUENT_FUTURE_CLASS_NAME =
+ "com.google.common.util.concurrent.FluentFuture";
+ private static final String FUTURES_CLASS_NAME = "com.google.common.util.concurrent.Futures";
+ private static final String FUNCTION_APPLY_METHOD_NAME = "apply";
+ private static final String[] FLUENT_FUTURE_INCLUDE_LIST_METHODS = {
+ "catching", "catchingAsync", "transform", "transformAsync"
+ };
+
+ private static boolean isGuavaFunctionDotApply(Symbol.MethodSymbol methodSymbol) {
+ Name className = methodSymbol.enclClass().flatName();
+ return (className.contentEquals(GUAVA_FUNCTION_CLASS_NAME)
+ || className.contentEquals(GUAVA_ASYNC_FUNCTION_CLASS_NAME))
+ && methodSymbol.name.contentEquals(FUNCTION_APPLY_METHOD_NAME);
+ }
+
+ private static boolean isFluentFutureIncludeListMethod(Symbol.MethodSymbol methodSymbol) {
+ Name className = methodSymbol.enclClass().flatName();
+ return (className.contentEquals(FLUENT_FUTURE_CLASS_NAME)
+ || className.contentEquals(FUTURES_CLASS_NAME))
+ && Arrays.stream(FLUENT_FUTURE_INCLUDE_LIST_METHODS)
+ .anyMatch(s -> methodSymbol.name.contentEquals(s));
+ }
+
+ @Override
+ public Nullness onOverrideMethodReturnNullability(
+ Symbol.MethodSymbol methodSymbol,
+ VisitorState state,
+ boolean isAnnotated,
+ Nullness returnNullness) {
+ // We only care about lambda's implementing Guava's Function
+ if (!isGuavaFunctionDotApply(methodSymbol)) {
+ return returnNullness;
+ }
+ // Check if we are inside a lambda passed as an argument to a method call:
+ LambdaExpressionTree enclosingLambda =
+ ASTHelpers.findEnclosingNode(state.getPath(), LambdaExpressionTree.class);
+ if (enclosingLambda == null
+ || !NullabilityUtil.getFunctionalInterfaceMethod(enclosingLambda, state.getTypes())
+ .equals(methodSymbol)) {
+ return returnNullness;
+ }
+ MethodInvocationTree methodInvocation =
+ ASTHelpers.findEnclosingNode(state.getPath(), MethodInvocationTree.class);
+ if (methodInvocation == null || !methodInvocation.getArguments().contains(enclosingLambda)) {
+ return returnNullness;
+ }
+ // Check if that method call is one of the FluentFuture APIs we care about
+ Symbol.MethodSymbol lambdaConsumerMethodSymbol = ASTHelpers.getSymbol(methodInvocation);
+ if (!isFluentFutureIncludeListMethod(lambdaConsumerMethodSymbol)) {
+ return returnNullness;
+ }
+ return Nullness.NULLABLE;
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/DummyOptionsConfigTest.java b/nullaway/src/test/java/com/uber/nullaway/DummyOptionsConfigTest.java
index 530d8ee..cefcb5c 100644
--- a/nullaway/src/test/java/com/uber/nullaway/DummyOptionsConfigTest.java
+++ b/nullaway/src/test/java/com/uber/nullaway/DummyOptionsConfigTest.java
@@ -1,8 +1,8 @@
package com.uber.nullaway;
import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.lang.reflect.InvocationTargetException;
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayAccessPathsTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayAccessPathsTests.java
index c7e85b0..f3f0c07 100644
--- a/nullaway/src/test/java/com/uber/nullaway/NullAwayAccessPathsTests.java
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayAccessPathsTests.java
@@ -115,4 +115,268 @@ public class NullAwayAccessPathsTests extends NullAwayTestsBase {
"}")
.doTest();
}
+
+ @Test
+ public void testField() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Foo {",
+ " @Nullable public Object o;",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.ArrayList;",
+ "import javax.annotation.Nullable;",
+ "public class Test {",
+ " public String testFieldCheck(Foo foo) {",
+ " if (foo.o == null) {",
+ " foo.o = new Object();",
+ " }",
+ " return foo.o.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testArrayListField() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import java.util.List;",
+ "import javax.annotation.Nullable;",
+ "public class Foo {",
+ " @Nullable public List<Object> list;",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.ArrayList;",
+ "import javax.annotation.Nullable;",
+ "public class Test {",
+ " public Object testFieldCheck(Foo foo) {",
+ " if (foo.list == null) {",
+ " foo.list = new ArrayList<Object>();",
+ " }",
+ " return foo.list.get(0);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testFieldWithoutValidAccessPath() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Foo {",
+ " @Nullable public Object o;",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.ArrayList;",
+ "import javax.annotation.Nullable;",
+ "public class Test {",
+ " public String testFieldCheck(Foo foo) {",
+ " if (foo.o == null) {",
+ " (new Foo()).o = new Object();",
+ " }",
+ " // BUG: Diagnostic contains: dereferenced expression foo.o is @Nullable",
+ " return foo.o.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testFieldWithoutValidAccessPathLongChain() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Foo {",
+ " public Foo nonNull;",
+ " public Foo() {",
+ " nonNull = this;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.ArrayList;",
+ "import javax.annotation.Nullable;",
+ "public class Test {",
+ " public String testFieldCheck(Foo foo) {",
+ " // Just checking that NullAway doesn't crash on a long but ultimately rootless access path",
+ " return (new Foo()).nonNull.nonNull.toString().toLowerCase();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testFinalFieldNullabilityPropagatesToInnerContexts() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Foo {",
+ " @Nullable public final Bar bar;",
+ " @Nullable public Bar mutableBar;",
+ " public Foo(@Nullable Bar bar) {",
+ " this.bar = bar;",
+ " this.mutableBar = bar;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Bar.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Bar {",
+ " @Nullable public final Foo foo;",
+ " @Nullable public Foo mutableFoo;",
+ " public Bar(@Nullable Foo foo) {",
+ " this.foo = foo;",
+ " this.mutableFoo = foo;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.google.common.base.Preconditions;",
+ "import java.util.ArrayList;",
+ "import java.util.function.Predicate;",
+ "import java.util.function.Function;",
+ "import javax.annotation.Nullable;",
+ "public class Test {",
+ " @Nullable private final Foo foo;",
+ " @Nullable private Foo mutableFoo;",
+ " public Test(@Nullable Foo foo) {",
+ " this.foo = foo;",
+ " this.mutableFoo = foo;",
+ " }",
+ " public Predicate<String> testReadFinalFromLambdaNoCheck() {",
+ " // BUG: Diagnostic contains: dereferenced expression foo is @Nullable",
+ " return (s -> foo.toString().equals(s));",
+ " }",
+ " public Predicate<String> testReadFinalFromLambdaAfterCheck() {",
+ " Preconditions.checkNotNull(foo);",
+ " // safe!",
+ " return (s -> foo.toString().equals(s));",
+ " }",
+ " public Predicate<String> testReadMutableFromLambdaAfterCheck() {",
+ " Preconditions.checkNotNull(mutableFoo);",
+ " // BUG: Diagnostic contains: dereferenced expression mutableFoo is @Nullable",
+ " return (s -> mutableFoo.toString().equals(s));",
+ " }",
+ " public Function<String, Predicate<String>> testReadFinalFromLambdaAfterCheckDeepContext() {",
+ " Preconditions.checkNotNull(foo);",
+ " // safe!",
+ " return (s1 -> (s2 -> foo.toString().equals(s1 + s2)));",
+ " }",
+ " public Predicate<String> testReadFinalFromLambdaAfterCheckDeepAP() {",
+ " Preconditions.checkNotNull(foo);",
+ " Preconditions.checkNotNull(foo.bar);",
+ " Preconditions.checkNotNull(foo.bar.foo);",
+ " // safe!",
+ " return (s -> foo.bar.foo.toString().equals(s));",
+ " }",
+ " public Predicate<String> testReadFinalFromLambdaAfterCheckDeepAPIncomplete() {",
+ " Preconditions.checkNotNull(foo);",
+ " Preconditions.checkNotNull(foo.bar);",
+ " // BUG: Diagnostic contains: dereferenced expression foo.bar.foo is @Nullable",
+ " return (s -> foo.bar.foo.toString().equals(s));",
+ " }",
+ " public Predicate<String> testReadMutableFromLambdaAfterCheckDeepAP1() {",
+ " Preconditions.checkNotNull(foo);",
+ " Preconditions.checkNotNull(foo.mutableBar);",
+ " Preconditions.checkNotNull(foo.mutableBar.foo);",
+ " // BUG: Diagnostic contains: dereferenced expression foo.mutableBar is @Nullable",
+ " return (s -> foo.mutableBar.foo.toString().equals(s));",
+ " }",
+ " public Predicate<String> testReadMutableFromLambdaAfterCheckDeepAP2() {",
+ " Preconditions.checkNotNull(foo);",
+ " Preconditions.checkNotNull(foo.bar);",
+ " Preconditions.checkNotNull(foo.bar.mutableFoo);",
+ " // BUG: Diagnostic contains: dereferenced expression foo.bar.mutableFoo is @Nullable",
+ " return (s -> foo.bar.mutableFoo.toString().equals(s));",
+ " }",
+ " public boolean testReadFinalFromLambdaAfterCheckLocalClass(String s) {",
+ " Preconditions.checkNotNull(foo);",
+ " // safe!",
+ " class Inner {",
+ " public Inner() { }",
+ " public boolean doTest(String s) { return foo.toString().equals(s); }",
+ " }",
+ " return (new Inner()).doTest(s);",
+ " }",
+ " public boolean testReadFinalFromLambdaCheckBeforeUseLocalClass(String s) {",
+ " class Inner {",
+ " public Inner() { }",
+ " // At the time of declaring this, foo is not known to be non-null!",
+ " public boolean doTest(String s) {",
+ " // BUG: Diagnostic contains: dereferenced expression foo is @Nullable",
+ " return foo.toString().equals(s);",
+ " }",
+ " }",
+ " // Technically safe, but hard to reason about: needs to be aware of *when* doTest() can be",
+ " // called which is generally _beyond_ NullAway.",
+ " Preconditions.checkNotNull(foo);",
+ " return (new Inner()).doTest(s);",
+ " }",
+ " public boolean testReadMutableFromLambdaAfterCheckLocalClass(String s) {",
+ " Preconditions.checkNotNull(mutableFoo);",
+ " class Inner {",
+ " public Inner() { }",
+ " public boolean doTest(String s) {",
+ " // BUG: Diagnostic contains: dereferenced expression mutableFoo is @Nullable",
+ " return mutableFoo.toString().equals(s);",
+ " }",
+ " public boolean doTestSafe(String s) {",
+ " Preconditions.checkNotNull(mutableFoo);",
+ " return mutableFoo.toString().equals(s);",
+ " }",
+ " }",
+ " if (s.length() % 2 == 0) {",
+ " return (new Inner()).doTest(s);",
+ " } else {",
+ " // safe!",
+ " return (new Inner()).doTestSafe(s);",
+ " }",
+ " }",
+ " public boolean testReadFinalFromLambdaAfterCheckLocalClassWithNameCollision(String s) {",
+ " Preconditions.checkNotNull(foo);",
+ " class Inner {",
+ " @Nullable private Foo foo;",
+ " public Inner() { this.foo = null; }",
+ " public boolean doTest(String s) {",
+ " // BUG: Diagnostic contains: dereferenced expression foo is @Nullable",
+ " return foo.toString().equals(s);",
+ " }",
+ " public boolean doTestSafe(String s) {",
+ " // TODO: Technically safe, but it would need recognizing Test.this.[...] as the same AP as",
+ " // that from the closure.",
+ " // BUG: Diagnostic contains: dereferenced expression Test.this.foo is @Nullable",
+ " return Test.this.foo.toString().equals(s);",
+ " }",
+ " }",
+ " if (s.length() % 2 == 0) {",
+ " return (new Inner()).doTest(s);",
+ " } else {",
+ " return (new Inner()).doTestSafe(s);",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayAcknowledgeRestrictiveAnnotationsTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayAcknowledgeRestrictiveAnnotationsTests.java
index 479e5ca..e84016c 100644
--- a/nullaway/src/test/java/com/uber/nullaway/NullAwayAcknowledgeRestrictiveAnnotationsTests.java
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayAcknowledgeRestrictiveAnnotationsTests.java
@@ -1,6 +1,5 @@
package com.uber.nullaway;
-import com.uber.nullaway.testlibrarymodels.TestLibraryModels;
import java.util.Arrays;
import org.junit.Test;
@@ -294,41 +293,138 @@ public class NullAwayAcknowledgeRestrictiveAnnotationsTests extends NullAwayTest
}
@Test
- public void libraryModelsOverrideRestrictiveAnnotations() {
+ public void annotatedVsUnannotatedMethodRefOverrideChecks() {
makeTestHelperWithArgs(
Arrays.asList(
- "-processorpath",
- TestLibraryModels.class
- .getProtectionDomain()
- .getCodeSource()
- .getLocation()
- .getPath(),
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber",
- "-XepOpt:NullAway:UnannotatedSubPackages=com.uber.lib.unannotated",
+ "-XepOpt:NullAway:UnannotatedSubPackages=com.uber.nullaway.[a-zA-Z0-9.]+.unannotated",
+ // Note: this is the OFF case.
+ "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=false"))
+ .addSourceLines(
+ "AnnotatedStringIDFunctions.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class AnnotatedStringIDFunctions {",
+ " public static String idRetNonNull(String s) {",
+ " return s;",
+ " }",
+ " @Nullable",
+ " public static String idRetNullable(String s) {",
+ " return s;",
+ " }",
+ "}")
+ .addSourceLines(
+ "UnannotatedStringIDFunctions.java",
+ "package com.uber.nullaway.lib.unannotated;",
+ "import javax.annotation.Nullable;",
+ "public class UnannotatedStringIDFunctions {",
+ " public static String idRetNonNull(String s) {",
+ " return s;",
+ " }",
+ " @Nullable",
+ " public static String idRetNullable(String s) {",
+ " return s;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.google.common.base.Function;", // is Function<String, String!> from model
+ "import com.google.common.collect.Maps;",
+ "import com.uber.nullaway.lib.unannotated.UnannotatedStringIDFunctions;",
+ "import java.util.List;",
+ "import java.util.Map;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " public static Map<String, String> testFunctionOverrideMethodRef1(List<String> keys) {",
+ " return Maps.toMap(keys,",
+ " /* is Function<String, String!> */ AnnotatedStringIDFunctions::idRetNonNull);",
+ " }",
+ " public static Map<String, String> testFunctionOverrideMethodRef2(List<String> keys) {",
+ " return Maps.toMap(keys,",
+ " // BUG: Diagnostic contains: method returns @Nullable, but functional interface",
+ " /* is Function<String, String?> */ AnnotatedStringIDFunctions::idRetNullable);",
+ " }",
+ " public static Map<String, String> testFunctionOverrideMethodRef3(List<String> keys) {",
+ " return Maps.toMap(keys,",
+ " /* is Function<String, String!> */ UnannotatedStringIDFunctions::idRetNonNull);",
+ " }",
+ " public static Map<String, String> testFunctionOverrideMethodRef4(List<String> keys) {",
+ " // No report, since idRetNullable() is unannotated and restrictive annotations are off",
+ " return Maps.toMap(keys,",
+ " /* is Function<String, String?> */ UnannotatedStringIDFunctions::idRetNullable);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void annotatedVsUnannotatedMethodRefOverrideWithRestrictiveAnnotations() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:UnannotatedSubPackages=com.uber.nullaway.[a-zA-Z0-9.]+.unannotated",
"-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true"))
.addSourceLines(
+ "AnnotatedStringIDFunctions.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class AnnotatedStringIDFunctions {",
+ " public static String idRetNonNull(String s) {",
+ " return s;",
+ " }",
+ " @Nullable",
+ " public static String idRetNullable(String s) {",
+ " return s;",
+ " }",
+ "}")
+ .addSourceLines(
+ "UnannotatedStringIDFunctions.java",
+ "package com.uber.nullaway.lib.unannotated;",
+ "import javax.annotation.Nullable;",
+ "public class UnannotatedStringIDFunctions {",
+ " public static String idRetNonNull(String s) {",
+ " return s;",
+ " }",
+ " @Nullable",
+ " public static String idRetNullable(String s) {",
+ " return s;",
+ " }",
+ "}")
+ .addSourceLines(
"Test.java",
"package com.uber;",
- "import com.uber.lib.unannotated.RestrictivelyAnnotatedFIWithModelOverride;",
+ "import com.google.common.base.Function;", // is Function<String, String!> from model
+ "import com.google.common.collect.Maps;",
+ "import com.uber.nullaway.lib.unannotated.UnannotatedStringIDFunctions;",
+ "import java.util.List;",
+ "import java.util.Map;",
"import javax.annotation.Nullable;",
- "public class Test {",
- " void bar(RestrictivelyAnnotatedFIWithModelOverride f) {",
- " // Param is @NullableDecl in bytecode, overridden by library model",
- " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull",
- " f.apply(null);",
- " }",
- " void foo() {",
- " RestrictivelyAnnotatedFIWithModelOverride func = (x) -> {",
- " // Param is @NullableDecl in bytecode, overridden by library model, thus safe",
- " return x.toString();",
- " };",
- " }",
- " void baz() {",
- " // Safe to pass, since Function can't have a null instance parameter",
- " bar(Object::toString);",
- " }",
+ "class Test {",
+ " public static Map<String, String> testFunctionOverrideMethodRef1(List<String> keys) {",
+ " return Maps.toMap(keys,",
+ " /* is Function<String, String!> */ AnnotatedStringIDFunctions::idRetNonNull);",
+ " }",
+ " public static Map<String, String> testFunctionOverrideMethodRef2(List<String> keys) {",
+ " return Maps.toMap(keys,",
+ " // BUG: Diagnostic contains: method returns @Nullable, but functional interface",
+ " /* is Function<String, String?> */ AnnotatedStringIDFunctions::idRetNullable);",
+ " }",
+ " public static Map<String, String> testFunctionOverrideMethodRef3(List<String> keys) {",
+ " return Maps.toMap(keys,",
+ " /* is Function<String, String!> */ UnannotatedStringIDFunctions::idRetNonNull);",
+ " }",
+ " public static Map<String, String> testFunctionOverrideMethodRef4(List<String> keys) {",
+ " // Note: doesn't matter that the method ref is unannotated, since restrictive annotations",
+ " // are on.",
+ " return Maps.toMap(keys,",
+ " // BUG: Diagnostic contains: method returns @Nullable, but functional interface",
+ " /* is Function<String, String?> */ UnannotatedStringIDFunctions::idRetNullable);",
+ " }",
"}")
.doTest();
}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayAssertionLibsTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayAssertionLibsTests.java
index f30afc9..3a6838c 100644
--- a/nullaway/src/test/java/com/uber/nullaway/NullAwayAssertionLibsTests.java
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayAssertionLibsTests.java
@@ -53,6 +53,54 @@ public class NullAwayAssertionLibsTests extends NullAwayTestsBase {
}
@Test
+ public void supportTruthAssertThatIsNotNull_MapKey() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Map;",
+ "import javax.annotation.Nullable;",
+ "import static com.google.common.truth.Truth.assertThat;",
+ "class Test {",
+ " private void foo(Map<String,Object> m) {",
+ " assertThat(m.get(\"foo\")).isNotNull();",
+ " m.get(\"foo\").toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void supportTruthAssertThatIsInstanceOf() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.lang.Object;",
+ "import java.util.Objects;",
+ "import javax.annotation.Nullable;",
+ "import static com.google.common.truth.Truth.assertThat;",
+ "class Test {",
+ " private void foo(@Nullable Object o) {",
+ " // inInstanceOf => isNotNull!",
+ " assertThat(o).isInstanceOf(Object.class);",
+ " o.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
public void doNotSupportTruthAssertThatWhenDisabled() {
makeTestHelperWithArgs(
Arrays.asList(
@@ -186,6 +234,34 @@ public class NullAwayAssertionLibsTests extends NullAwayTestsBase {
}
@Test
+ public void supportHamcrestAssertThatIsInstanceOf() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.lang.Object;",
+ "import java.util.Objects;",
+ "import javax.annotation.Nullable;",
+ "import static org.hamcrest.MatcherAssert.assertThat;",
+ "import static org.hamcrest.CoreMatchers.*;",
+ "import org.hamcrest.core.IsNull;",
+ "class Test {",
+ " private void foo(@Nullable Object a, @Nullable Object b) {",
+ " assertThat(a, is(instanceOf(Object.class)));",
+ " a.toString();",
+ " assertThat(b, isA(Object.class));",
+ " b.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
public void supportJunitAssertThatIsNotNull_Object() {
makeTestHelperWithArgs(
Arrays.asList(
@@ -213,6 +289,33 @@ public class NullAwayAssertionLibsTests extends NullAwayTestsBase {
}
@Test
+ public void supportJunitAssertThatIsInstanceOf() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.lang.Object;",
+ "import java.util.Objects;",
+ "import javax.annotation.Nullable;",
+ "import static org.junit.Assert.assertThat;",
+ "import static org.hamcrest.Matchers.*;",
+ "class Test {",
+ " private void foo(@Nullable Object a, @Nullable Object b) {",
+ " assertThat(a, is(instanceOf(Object.class)));",
+ " a.toString();",
+ " assertThat(b, isA(Object.class));",
+ " b.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
public void doNotSupportJunitAssertThatWhenDisabled() {
makeTestHelperWithArgs(
Arrays.asList(
@@ -237,4 +340,125 @@ public class NullAwayAssertionLibsTests extends NullAwayTestsBase {
"}")
.doTest();
}
+
+ @Test
+ public void supportAssertJAssertThatIsNotNull_Object() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.lang.Object;",
+ "import java.util.Objects;",
+ "import javax.annotation.Nullable;",
+ "import static org.assertj.core.api.Assertions.assertThat;",
+ "class Test {",
+ " private void foo(@Nullable Object o) {",
+ " assertThat(o).isNotNull();",
+ " o.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void supportAssertJAssertThatIsNotNull_String() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Objects;",
+ "import javax.annotation.Nullable;",
+ "import static org.assertj.core.api.Assertions.assertThat;",
+ "class Test {",
+ " private void foo(@Nullable String s) {",
+ " assertThat(s).isNotNull();",
+ " s.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void supportAssertJAssertThatIsNotNull_MapKey() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Map;",
+ "import javax.annotation.Nullable;",
+ "import static org.assertj.core.api.Assertions.assertThat;",
+ "class Test {",
+ " private void foo(Map<String,Object> m) {",
+ " assertThat(m.get(\"foo\")).isNotNull();",
+ " m.get(\"foo\").toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void supportAssertJAssertThatIsInstanceOf() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.lang.Object;",
+ "import java.util.Objects;",
+ "import javax.annotation.Nullable;",
+ "import static org.assertj.core.api.Assertions.assertThat;",
+ "class Test {",
+ " private void foo(@Nullable Object a, @Nullable Object b) {",
+ " assertThat(a).isInstanceOf(Object.class);",
+ " a.toString();",
+ " assertThat(b).isInstanceOfAny(String.class, Exception.class);",
+ " b.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void doNotSupportAssertJAssertThatWhenDisabled() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=false"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.lang.Object;",
+ "import java.util.Objects;",
+ "import javax.annotation.Nullable;",
+ "import static org.assertj.core.api.Assertions.assertThat;",
+ "class Test {",
+ " private void foo(@Nullable Object a) {",
+ " assertThat(a).isNotNull();",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " a.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayAutoSuggestNoCastTest.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayAutoSuggestNoCastTest.java
index 4ed8792..1817d25 100644
--- a/nullaway/src/test/java/com/uber/nullaway/NullAwayAutoSuggestNoCastTest.java
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayAutoSuggestNoCastTest.java
@@ -23,8 +23,6 @@
package com.uber.nullaway;
import com.google.errorprone.BugCheckerRefactoringTestHelper;
-import com.google.errorprone.ErrorProneFlags;
-import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -36,54 +34,21 @@ public class NullAwayAutoSuggestNoCastTest {
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
- private ErrorProneFlags flagsWithAutoFixSuppressionComment;
-
- private ErrorProneFlags flagsNoAutoFixSuppressionComment;
-
- @Before
- public void setup() {
- // With AutoFixSuppressionComment
- ErrorProneFlags.Builder b = ErrorProneFlags.builder();
- b.putFlag("NullAway:AnnotatedPackages", "com.uber,com.ubercab,io.reactivex");
- b.putFlag("NullAway:SuggestSuppressions", "true");
- b.putFlag("NullAway:AutoFixSuppressionComment", "PR #000000");
- flagsWithAutoFixSuppressionComment = b.build();
- // Without AutoFixSuppressionComment
- b = ErrorProneFlags.builder();
- b.putFlag("NullAway:AnnotatedPackages", "com.uber,com.ubercab,io.reactivex");
- b.putFlag("NullAway:SuggestSuppressions", "true");
- flagsNoAutoFixSuppressionComment = b.build();
- }
-
- // In EP 2.6.0 the newInstance() method we use below is deprecated. We cannot currently address
- // the warning since the replacement method was only added in EP 2.5.1, and we still want to
- // support EP 2.4.0. So, we suppress the warning for now
- @SuppressWarnings("deprecation")
private BugCheckerRefactoringTestHelper makeTestHelperWithSuppressionComment() {
- return BugCheckerRefactoringTestHelper.newInstance(
- new NullAway(flagsWithAutoFixSuppressionComment), getClass())
+ return BugCheckerRefactoringTestHelper.newInstance(NullAway.class, getClass())
.setArgs(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
- // the remaining args are not needed right now, but they will be necessary when we
- // switch to the more modern newInstance() API
"-XepOpt:NullAway:AnnotatedPackages=com.uber,com.ubercab,io.reactivex",
"-XepOpt:NullAway:SuggestSuppressions=true",
"-XepOpt:NullAway:AutoFixSuppressionComment=PR #000000");
}
- // In EP 2.6.0 the newInstance() method we use below is deprecated. We cannot currently address
- // the warning since the replacement method was only added in EP 2.5.1, and we still want to
- // support EP 2.4.0. So, we suppress the warning for now
- @SuppressWarnings("deprecation")
private BugCheckerRefactoringTestHelper makeTestHelper() {
- return BugCheckerRefactoringTestHelper.newInstance(
- new NullAway(flagsNoAutoFixSuppressionComment), getClass())
+ return BugCheckerRefactoringTestHelper.newInstance(NullAway.class, getClass())
.setArgs(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
- // the remaining args are not needed right now, but they will be necessary when we
- // switch to the more modern newInstance() API
"-XepOpt:NullAway:AnnotatedPackages=com.uber,com.ubercab,io.reactivex",
"-XepOpt:NullAway:SuggestSuppressions=true");
}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayAutoSuggestTest.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayAutoSuggestTest.java
index 5aec837..ba5eabe 100644
--- a/nullaway/src/test/java/com/uber/nullaway/NullAwayAutoSuggestTest.java
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayAutoSuggestTest.java
@@ -22,10 +22,12 @@
package com.uber.nullaway;
+import static com.google.errorprone.BugCheckerRefactoringTestHelper.TestMode.TEXT_MATCH;
+
import com.google.errorprone.BugCheckerRefactoringTestHelper;
-import com.google.errorprone.ErrorProneFlags;
+import com.sun.source.tree.Tree;
+import com.uber.nullaway.testlibrarymodels.TestLibraryModels;
import java.io.IOException;
-import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -37,28 +39,13 @@ public class NullAwayAutoSuggestTest {
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
- private ErrorProneFlags flags;
-
- @Before
- public void setup() {
- ErrorProneFlags.Builder b = ErrorProneFlags.builder();
- b.putFlag("NullAway:AnnotatedPackages", "com.uber,com.ubercab,io.reactivex");
- b.putFlag("NullAway:CastToNonNullMethod", "com.uber.nullaway.testdata.Util.castToNonNull");
- b.putFlag("NullAway:SuggestSuppressions", "true");
- flags = b.build();
- }
-
- // In EP 2.6.0 the newInstance() method we use below is deprecated. We cannot currently address
- // the warning since the replacement method was only added in EP 2.5.1, and we still want to
- // support EP 2.4.0. So, we suppress the warning for now
- @SuppressWarnings("deprecation")
private BugCheckerRefactoringTestHelper makeTestHelper() {
- return BugCheckerRefactoringTestHelper.newInstance(new NullAway(flags), getClass())
+ return BugCheckerRefactoringTestHelper.newInstance(NullAway.class, getClass())
.setArgs(
"-d",
temporaryFolder.getRoot().getAbsolutePath(),
- // the remaining args are not needed right now, but they will be necessary when we
- // switch to the more modern newInstance() API
+ "-processorpath",
+ TestLibraryModels.class.getProtectionDomain().getCodeSource().getLocation().getPath(),
"-XepOpt:NullAway:AnnotatedPackages=com.uber,com.ubercab,io.reactivex",
"-XepOpt:NullAway:CastToNonNullMethod=com.uber.nullaway.testdata.Util.castToNonNull",
"-XepOpt:NullAway:SuggestSuppressions=true");
@@ -89,7 +76,8 @@ public class NullAwayAutoSuggestTest {
"package com.uber;",
"import javax.annotation.Nullable;",
"class Test {",
- " Object test1(@Nullable Object o) {",
+ " @Nullable Object o;",
+ " Object test1() {",
" return o;",
" }",
"}")
@@ -99,13 +87,65 @@ public class NullAwayAutoSuggestTest {
"import static com.uber.nullaway.testdata.Util.castToNonNull;",
"import javax.annotation.Nullable;",
"class Test {",
- " Object test1(@Nullable Object o) {",
+ " @Nullable Object o;",
+ " Object test1() {",
" return castToNonNull(o);",
" }",
"}")
.doTest();
}
+ /**
+ * Test for cases where we heuristically decide not to wrap an expression in castToNonNull; see
+ * {@link ErrorBuilder#canBeCastToNonNull(Tree)}
+ */
+ @Test
+ public void suppressInsteadOfCastToNonNull() throws IOException {
+ makeTestHelper()
+ .addInputLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " Object f = new Object();",
+ " Object test1(@Nullable Object o) {",
+ " return o;",
+ " }",
+ " Object test2() {",
+ " return null;",
+ " }",
+ " void test3() {",
+ " f = null;",
+ " }",
+ " @Nullable Object m() { return null; }",
+ " Object shouldAddCast() {",
+ " return m();",
+ " }",
+ "}")
+ .addOutputLines(
+ "out/Test.java",
+ "package com.uber;",
+ "import static com.uber.nullaway.testdata.Util.castToNonNull;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " Object f = new Object();",
+ " @SuppressWarnings(\"NullAway\") Object test1(@Nullable Object o) {",
+ " return o;",
+ " }",
+ " @SuppressWarnings(\"NullAway\") Object test2() {",
+ " return null;",
+ " }",
+ " @SuppressWarnings(\"NullAway\") void test3() {",
+ " f = null;",
+ " }",
+ " @Nullable Object m() { return null; }",
+ " Object shouldAddCast() {",
+ " return castToNonNull(m());",
+ " }",
+ "}")
+ .doTest();
+ }
+
@Test
public void removeUnnecessaryCastToNonNull() throws IOException {
makeTestHelper()
@@ -133,6 +173,64 @@ public class NullAwayAutoSuggestTest {
}
@Test
+ public void removeUnnecessaryCastToNonNullFromLibraryModel() throws IOException {
+ makeTestHelper()
+ .addInputLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import static com.uber.nullaway.testdata.Util.castToNonNull;",
+ "class Test {",
+ " Object test1(Object o) {",
+ " return castToNonNull(\"CAST_REASON\",o,42);",
+ " }",
+ "}")
+ .addOutputLines(
+ "out/Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import static com.uber.nullaway.testdata.Util.castToNonNull;",
+ "class Test {",
+ " Object test1(Object o) {",
+ " return o;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void removeUnnecessaryCastToNonNullMultiLine() throws IOException {
+ makeTestHelper()
+ .addInputLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import static com.uber.nullaway.testdata.Util.castToNonNull;",
+ "class Test {",
+ " static class Foo { Object getObj() { return new Object(); } }",
+ " Object test1(Foo f) {",
+ " return castToNonNull(f",
+ " // comment that should not be deleted",
+ " .getObj());",
+ " }",
+ "}")
+ .addOutputLines(
+ "out/Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import static com.uber.nullaway.testdata.Util.castToNonNull;",
+ "class Test {",
+ " static class Foo { Object getObj() { return new Object(); } }",
+ " Object test1(Foo f) {",
+ " return f",
+ " // comment that should not be deleted",
+ " .getObj();",
+ " }",
+ "}")
+ .doTest(TEXT_MATCH);
+ }
+
+ @Test
public void suggestSuppressionOnMethodRef() throws IOException {
makeTestHelper()
.addInputLines(
@@ -173,4 +271,113 @@ public class NullAwayAutoSuggestTest {
"}")
.doTest();
}
+
+ @Test
+ public void suggestCastToNonNullPreserveComments() throws IOException {
+ makeTestHelper()
+ .addInputLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " Object x = new Object();",
+ " static class Foo { @Nullable Object getObj() { return null; } }",
+ " Object test1(Foo f) {",
+ " return f",
+ " // comment that should not be deleted",
+ " .getObj();",
+ " }",
+ " void test2(Foo f) {",
+ " x = f.getObj(); // comment that should not be deleted",
+ " }",
+ " Object test3(Foo f) {",
+ " return f./* keep this comment */getObj();",
+ " }",
+ "}")
+ .addOutputLines(
+ "out/Test.java",
+ "package com.uber;",
+ "import static com.uber.nullaway.testdata.Util.castToNonNull;",
+ "",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " Object x = new Object();",
+ " static class Foo { @Nullable Object getObj() { return null; } }",
+ " Object test1(Foo f) {",
+ " return castToNonNull(f",
+ " // comment that should not be deleted",
+ " .getObj());",
+ " }",
+ " void test2(Foo f) {",
+ " x = castToNonNull(f.getObj()); // comment that should not be deleted",
+ " }",
+ " Object test3(Foo f) {",
+ " return castToNonNull(f./* keep this comment */getObj());",
+ " }",
+ "}")
+ .doTest(TEXT_MATCH);
+ }
+
+ public void suggestInitSuppressionOnConstructor() throws IOException {
+ makeTestHelper()
+ .addInputLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " Object f;",
+ " Object g;",
+ " Test() {}",
+ "}")
+ .addOutputLines(
+ "out/Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " Object f;",
+ " Object g;",
+ " @SuppressWarnings(\"NullAway.Init\") Test() {}",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void suggestInitSuppressionOnField() throws IOException {
+ makeTestHelper()
+ .addInputLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " Object f;",
+ "}")
+ .addOutputLines(
+ "out/Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " @SuppressWarnings(\"NullAway.Init\") Object f;",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void updateExtantSuppressWarnings() throws IOException {
+ makeTestHelper()
+ .addInputLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " @SuppressWarnings(\"unused\") Object f;",
+ "}")
+ .addOutputLines(
+ "out/Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " @SuppressWarnings({\"unused\",\"NullAway.Init\"}) Object f;",
+ "}")
+ .doTest();
+ }
}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayContractsBooleanTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayContractsBooleanTests.java
new file mode 100644
index 0000000..b04189a
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayContractsBooleanTests.java
@@ -0,0 +1,570 @@
+package com.uber.nullaway;
+
+import com.google.errorprone.CompilationTestHelper;
+import java.util.Arrays;
+import org.junit.Test;
+
+public class NullAwayContractsBooleanTests extends NullAwayTestsBase {
+
+ @Test
+ public void nonNullCheckIsTrueIsNotNullable() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import java.util.Map;",
+ "class Test {",
+ " String test1(@Nullable Object o1) {",
+ " Validation.checkTrue(o1 != null);",
+ " return o1.toString();",
+ " }",
+ " String test2(Map<String, String> map) {",
+ " Validation.checkTrue(map.get(\"key\") != null);",
+ " return map.get(\"key\").toString();",
+ " }",
+ " interface HasNullableGetter {",
+ " @Nullable Object get();",
+ " }",
+ " String test3(HasNullableGetter obj) {",
+ " Validation.checkTrue(obj.get() != null);",
+ " return obj.get().toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nonNullCheckIsTrueIsNotNullableReversed() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import java.util.Map;",
+ "class Test {",
+ " String test(@Nullable Object o1) {",
+ " Validation.checkTrue(null != o1);",
+ " return o1.toString();",
+ " }",
+ " String test2(Map<String, String> map) {",
+ " Validation.checkTrue(null != map.get(\"key\"));",
+ " return map.get(\"key\").toString();",
+ " }",
+ " interface HasNullableGetter {",
+ " @Nullable Object get();",
+ " }",
+ " String test3(HasNullableGetter obj) {",
+ " Validation.checkTrue(null != obj.get());",
+ " return obj.get().toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullCheckIsFalseIsNotNullable() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o1) {",
+ " Validation.checkFalse(o1 == null);",
+ " return o1.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullCheckIsFalseIsNotNullableReversed() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o1) {",
+ " Validation.checkFalse(null == o1);",
+ " return o1.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullCheckIsTrueIsNull() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o1) {",
+ " Validation.checkTrue(o1 == null);",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " return o1.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullCheckIsTrueIsNullReversed() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o1) {",
+ " Validation.checkTrue(null == o1);",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " return o1.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nonNullCheckIsFalseIsNull() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o1) {",
+ " Validation.checkFalse(o1 != null);",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " return o1.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nonNullCheckIsFalseIsNullReversed() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o1) {",
+ " Validation.checkFalse(null != o1);",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " return o1.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void compositeNullCheckAndStringEquality() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import java.util.Map;",
+ "class Test {",
+ " String test1(@Nullable Object o1) {",
+ " Validation.checkTrue(o1 != null && o1.toString().equals(\"\"));",
+ " return o1.toString();",
+ " }",
+ " String test2(@Nullable Object o1) {",
+ " Validation.checkTrue(o1 != null ||",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " o1.toString().equals(\"\"));",
+ " return o1.toString();",
+ " }",
+ " String test3(Map<String, String> map) {",
+ " Validation.checkTrue(map.get(\"key\") != null && map.get(\"key\").toString().equals(\"\"));",
+ " return map.get(\"key\").toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void compositeNullCheckMultipleNonNull() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o1, @Nullable Object o2) {",
+ " Validation.checkTrue(o1 != null && o2 != null);",
+ " return o1.toString() + o2.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void compositeNullCheckFalseAndStringEquality() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o1) {",
+ " Validation.checkFalse(o1 == null || o1.toString().equals(\"\"));",
+ " return o1.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void compositeNullCheckFalseMultipleNonNull() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test1(@Nullable Object o1, @Nullable Object o2) {",
+ " Validation.checkFalse(o1 == null || o2 == null);",
+ " return o1.toString() + o2.toString();",
+ " }",
+ " String test2(@Nullable Object o1, @Nullable Object o2) {",
+ " Validation.checkFalse(o1 == null && o2 == null);",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " return o1.toString()",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " + o2.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void identityNotNull() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o1) {",
+ " if (Validation.identity(null != o1)) {",
+ " return o1.toString();",
+ " } else {",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " return o1.toString();",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void complexIdentityNotNull() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o1, Object o2) {",
+ " if (Validation.identity(null != o1, o2)) {",
+ " return o1.toString();",
+ " } else {",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " return o1.toString();",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void identityIsNull() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o1) {",
+ " if (Validation.identity(null == o1)) {",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " return o1.toString();",
+ " } else {",
+ " return o1.toString();",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void complexContractIdentityIsNull() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o1, Object o2) {",
+ " if (Validation.identity(null == o1, o2)) {",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " return o1.toString();",
+ " } else {",
+ " return o1.toString();",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void checkAndReturn() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import org.jetbrains.annotations.Contract;",
+ "class Test {",
+ " @Contract(\"false -> fail\")",
+ " static boolean checkAndReturn(boolean value) {",
+ " if (!value) {",
+ " throw new RuntimeException();",
+ " }",
+ " return true;",
+ " }",
+ " String test1(@Nullable Object o1, @Nullable Object o2) {",
+ " if (checkAndReturn(o1 != null) && o2 != null) {",
+ " return o1.toString() + o2.toString();",
+ " } else {",
+ " return o1.toString() + ",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " o2.toString();",
+ " }",
+ " }",
+ " boolean test2(@Nullable Object o1, @Nullable Object o2) {",
+ " return checkAndReturn(o1 != null) && o1.toString().isEmpty();",
+ " }",
+ " boolean test3(@Nullable Object o1, @Nullable Object o2) {",
+ " return checkAndReturn(o1 == null) ",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " && o1.toString().isEmpty();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void complexCheckAndReturn() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import org.jetbrains.annotations.Contract;",
+ "class Test {",
+ " @Contract(\"false, _ -> fail\")",
+ " static boolean checkAndReturn(boolean value, Object other) {",
+ " if (!value) {",
+ " throw new RuntimeException();",
+ " }",
+ " return true;",
+ " }",
+ " String test1(@Nullable Object o1, @Nullable Object o2, Object other) {",
+ " if (checkAndReturn(o1 != null, other) && o2 != null) {",
+ " return o1.toString() + o2.toString();",
+ " } else {",
+ " return o1.toString() + ",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " o2.toString();",
+ " }",
+ " }",
+ " boolean test2(@Nullable Object o1, @Nullable Object o2, Object other) {",
+ " return checkAndReturn(o1 != null, other) && o1.toString().isEmpty();",
+ " }",
+ " boolean test3(@Nullable Object o1, @Nullable Object o2, Object other) {",
+ " return checkAndReturn(o1 == null, other) ",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " && o1.toString().isEmpty();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void contractUnreachablePath() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "class Test {",
+ " String test(Object required) {",
+ " return Validation.identity(required == null)",
+ " ? required.toString()",
+ " : required.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void complexContractUnreachablePath() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "class Test {",
+ " String test(Object required, Object other) {",
+ " return Validation.identity(required == null, other)",
+ " ? required.toString()",
+ " : required.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void contractUnreachablePathAfterFailure() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o) {",
+ " Validation.checkTrue(o == null);",
+ " return Validation.identity(o == null)",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " ? o.toString()",
+ // This path is unreachable because o is guaranteed to be null
+ // after checkTrue(o == null). No failures should be reported.
+ // Note that we're not doing general reachability analysis,
+ // rather ensuring that we don't incorrectly produce errors.
+ " : o.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void contractNestedBooleanNullness() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o) {",
+ " return Validation.identity(o == null)",
+ " ? (Validation.identity(o != null)",
+ " ? o.toString()",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " : o.toString())",
+ " : (Validation.identity(o != null)",
+ " ? o.toString()",
+ " : o.toString());",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void complexContractNestedBooleanNullness() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test(@Nullable Object o, Object other) {",
+ " return Validation.identity(o == null, other)",
+ " ? (Validation.identity(o != null, other)",
+ " ? o.toString()",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " : o.toString())",
+ " : (Validation.identity(o != null, other)",
+ " ? o.toString()",
+ " : o.toString());",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nonNullBooleanDoubleContractTest() {
+ helper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import org.jetbrains.annotations.Contract;",
+ "class Test {",
+ " @Contract(\"false, false -> fail\")",
+ " static void falseFalseFail(boolean b1, boolean b2) {",
+ " if (!b1 && !b2) {",
+ " throw new RuntimeException();",
+ " }",
+ " }",
+ " String test1(@Nullable Object maybe, Object required) {",
+ // 'required == null' is known to be false, so if we go past this line,
+ // we know 'maybe != null' evaluates to true, hence both args are @NonNull.
+ " falseFalseFail(maybe != null, required == null);",
+ " return maybe.toString() + required.toString();",
+ " }",
+ " String test2(@Nullable Object maybe) {",
+ " String ref = null;",
+ // 'ref != null' is known to be false, so if we go past this line,
+ // we know 'maybe != null' evaluates to true.
+ " falseFalseFail(maybe != null, ref != null);",
+ " return maybe.toString();",
+ " }",
+ " String test3(@Nullable Object maybe) {",
+ " String ref = \"\";",
+ " ref = null;",
+ // 'ref != null' is known to be false, so if we go past this line,
+ // we know 'maybe != null' evaluates to true.
+ " falseFalseFail(maybe != null, ref != null);",
+ " return maybe.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ private CompilationTestHelper helper() {
+ return makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Validation.java",
+ "package com.uber;",
+ "import org.jetbrains.annotations.Contract;",
+ "import javax.annotation.Nullable;",
+ "public final class Validation {",
+ " @Contract(\"false -> fail\")",
+ " static void checkTrue(boolean value) {",
+ " if (!value) throw new RuntimeException();",
+ " }",
+ " @Contract(\"true -> fail\")",
+ " static void checkFalse(boolean value) {",
+ " if (value) throw new RuntimeException();",
+ " }",
+ " @Contract(\"true -> true; false -> false\")",
+ " static boolean identity(boolean value) {",
+ " return value;",
+ " }",
+ " @Contract(\"true, _ -> true; false, _ -> false\")",
+ " static boolean identity(boolean value, @Nullable Object other) {",
+ " return value;",
+ " }",
+ "}");
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayContractsTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayContractsTests.java
index 452676a..d3849b0 100644
--- a/nullaway/src/test/java/com/uber/nullaway/NullAwayContractsTests.java
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayContractsTests.java
@@ -64,6 +64,10 @@ public class NullAwayContractsTests extends NullAwayTestsBase {
" NullnessChecker.assertNonNull(o);",
" return o.toString();",
" }",
+ " String test4(java.util.Map<String,Object> m) {",
+ " NullnessChecker.assertNonNull(m.get(\"foo\"));",
+ " return m.get(\"foo\").toString();",
+ " }",
"}")
.doTest();
}
@@ -267,4 +271,173 @@ public class NullAwayContractsTests extends NullAwayTestsBase {
"}")
.doTest();
}
+
+ @Test
+ public void contractDeclaringBothNotNull() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "NullnessChecker.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import org.jetbrains.annotations.Contract;",
+ "public class NullnessChecker {",
+ " @Contract(\"null, _ -> false; _, null -> false\")",
+ " static boolean bothNotNull(@Nullable Object o1, @Nullable Object o2) {",
+ " // null, _ -> false",
+ " if (o1 == null) {",
+ " return false;",
+ " }",
+ " // _, null -> false",
+ " if (o2 == null) {",
+ " return false;",
+ " }",
+ " // Function cannot declare a contract for true",
+ " return System.currentTimeMillis() % 100 == 0;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test1(@Nullable Object o) {",
+ " return NullnessChecker.bothNotNull(\"\", o)",
+ " ? o.toString()",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " : o.toString();",
+ " }",
+ " String test2(@Nullable Object o) {",
+ " return NullnessChecker.bothNotNull(o, \"\")",
+ " ? o.toString()",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " : o.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void contractDeclaringEitherNull() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "NullnessChecker.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import org.jetbrains.annotations.Contract;",
+ "public class NullnessChecker {",
+ " @Contract(\"null, _ -> true; _, null -> true\")",
+ " static boolean eitherIsNullOrRandom(@Nullable Object o1, @Nullable Object o2) {",
+ " // null, _ -> true",
+ " if (o1 == null) {",
+ " return true;",
+ " }",
+ " // _, null -> true",
+ " if (o2 == null) {",
+ " return true;",
+ " }",
+ " // Function cannot declare a contract for false",
+ " return System.currentTimeMillis() % 100 == 0;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test1(@Nullable Object o) {",
+ " return NullnessChecker.eitherIsNullOrRandom(\"\", o)",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " ? o.toString()",
+ " : o.toString();",
+ " }",
+ " String test2(@Nullable Object o) {",
+ " return NullnessChecker.eitherIsNullOrRandom(o, \"\")",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " ? o.toString()",
+ " : o.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void contractDeclaringNullOrRandomFalse() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "NullnessChecker.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import org.jetbrains.annotations.Contract;",
+ "public class NullnessChecker {",
+ " @Contract(\"null -> false\")",
+ " static boolean isHashCodeZero(@Nullable Object o) {",
+ " // null -> false",
+ " if (o == null) {",
+ " return false;",
+ " }",
+ " // Function cannot declare a contract for true",
+ " return o.hashCode() == 0;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " String test1(@Nullable Object o) {",
+ " return NullnessChecker.isHashCodeZero(o)",
+ " ? o.toString()",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " : o.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void contractUnreachablePath() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "NullnessChecker.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import org.jetbrains.annotations.Contract;",
+ "public class NullnessChecker {",
+ " @Contract(\"!null -> false\")",
+ " static boolean isNull(@Nullable Object o) {",
+ " // !null -> false",
+ " if (o != null) {",
+ " return false;",
+ " }",
+ " return true;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "class Test {",
+ " String test(Object required) {",
+ " return NullnessChecker.isNull(required)",
+ " ? required.toString()",
+ " : required.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayCoreTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayCoreTests.java
index f1bdd49..f8f3ed6 100644
--- a/nullaway/src/test/java/com/uber/nullaway/NullAwayCoreTests.java
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayCoreTests.java
@@ -254,6 +254,36 @@ public class NullAwayCoreTests extends NullAwayTestsBase {
}
@Test
+ public void testCastToNonNullExtraArgsWarning() {
+ defaultCompilationHelper
+ .addSourceFile("Util.java")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import static com.uber.nullaway.testdata.Util.castToNonNull;",
+ "class Test {",
+ " Object test1(Object o) {",
+ " // BUG: Diagnostic contains: passing known @NonNull parameter 'o' to CastToNonNullMethod",
+ " return castToNonNull(o, \"o should be @Nullable but never actually null\");",
+ " }",
+ " Object test2(Object o) {",
+ " // BUG: Diagnostic contains: passing known @NonNull parameter 'o' to CastToNonNullMethod",
+ " return castToNonNull(\"o should be @Nullable but never actually null\", o, 0);",
+ " }",
+ " Object test3(@Nullable Object o) {",
+ " // Expected use of cast",
+ " return castToNonNull(o, \"o should be @Nullable but never actually null\");",
+ " }",
+ " Object test4(@Nullable Object o) {",
+ " // Expected use of cast",
+ " return castToNonNull(o, \"o should be @Nullable but never actually null\");",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
public void testReadStaticInConstructor() {
defaultCompilationHelper
.addSourceLines(
@@ -507,6 +537,80 @@ public class NullAwayCoreTests extends NullAwayTestsBase {
}
@Test
+ public void testMapPutAndPutIfAbsent() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Map;",
+ "class Test {",
+ " Object testPut(String key, Object o, Map<String, Object> m){",
+ " m.put(key, o);",
+ " return m.get(key);",
+ " }",
+ " Object testPutIfAbsent(String key, Object o, Map<String, Object> m){",
+ " m.putIfAbsent(key, o);",
+ " return m.get(key);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testMapComputeIfAbsent() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Map;",
+ "import java.util.function.Function;",
+ // Need JSpecify (vs javax) for annotating generics
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " Object testComputeIfAbsent(String key, Function<String, Object> f, Map<String, Object> m){",
+ " m.computeIfAbsent(key, f);",
+ " return m.get(key);",
+ " }",
+ " Object testComputeIfAbsentLambda(String key, Map<String, Object> m){",
+ " m.computeIfAbsent(key, k -> k);",
+ " return m.get(key);",
+ " }",
+ " Object testComputeIfAbsentNull(String key, Function<String, @Nullable Object> f, Map<String, Object> m){",
+ " m.computeIfAbsent(key, f);",
+ " // BUG: Diagnostic contains: returning @Nullable expression from method with @NonNull return type",
+ " return m.get(key);",
+ " }",
+ " // ToDo: should error somewhere, but doesn't, due to limited checking of generics",
+ " Object testComputeIfAbsentNullLambda(String key, Map<String, Object> m){",
+ " m.computeIfAbsent(key, k -> null);",
+ " return m.get(key);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testMapWithMapGetKey() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Map;",
+ "import java.util.function.Function;",
+ "class Test {",
+ " String testMapWithMapGetKey(Map<String,String> m1, Map<String,String> m2) {",
+ " if (m1.containsKey(\"s1\")) {",
+ " if (m2.containsKey(m1.get(\"s1\"))) {",
+ " return m2.get(m1.get(\"s1\")).toString();",
+ " }",
+ " }",
+ " return \"no\";",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
public void tryFinallySupport() {
defaultCompilationHelper.addSourceFile("NullAwayTryFinallyCases.java").doTest();
}
@@ -583,4 +687,277 @@ public class NullAwayCoreTests extends NullAwayTestsBase {
"}")
.doTest();
}
+
+ @Test
+ public void nullableOnJavaLangVoid() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " Void foo1() {",
+ " // temporarily, we treat a Void return type as if it was @Nullable Void",
+ " return null;",
+ " }",
+ " @Nullable Void foo2() {",
+ " return null;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullableOnJavaLangVoidWithCast() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " // Currently unhandled. In fact, it *should* produce an error. This entire test case",
+ " // needs to be rethought once we properly support generics, such that it works on T v",
+ " // when T == @Nullable Void, but not when T == Void. Without generics, though, this is the",
+ " // best we can do.",
+ " @SuppressWarnings(\"NullAway\")",
+ " private Void v = (Void)null;",
+ " Void foo1() {",
+ " // temporarily, we treat a Void return type as if it was @Nullable Void",
+ " return (Void)null;",
+ " }",
+ " // Temporarily, we treat any Void formal as if it were @Nullable Void",
+ " void consumeVoid(Void v) {",
+ " }",
+ " @Nullable Void foo2() {",
+ " consumeVoid(null); // See comment on consumeVoid for why this is allowed",
+ " consumeVoid((Void)null);",
+ " return (Void)null;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void staticCallZeroArgsNullCheck() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " @Nullable static Object nullableReturn() { return new Object(); }",
+ " void foo() {",
+ " if (nullableReturn() != null) {",
+ " nullableReturn().toString();",
+ " }",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " nullableReturn().toString();",
+ " }",
+ " void foo2() {",
+ " if (Test.nullableReturn() != null) {",
+ " nullableReturn().toString();",
+ " }",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " Test.nullableReturn().toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void primitiveCasts() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "class Test {",
+ " static void foo(int i) { }",
+ " static void m() {",
+ " Integer i = null;",
+ " // BUG: Diagnostic contains: unboxing",
+ " int i2 = (int) i;",
+ " // this is fine",
+ " int i3 = (int) Integer.valueOf(3);",
+ " // BUG: Diagnostic contains: unboxing",
+ " int i4 = ((int) i) + 1;",
+ " // BUG: Diagnostic contains: unboxing",
+ " foo((int) i);",
+ " // try another type",
+ " Double d = null;",
+ " // BUG: Diagnostic contains: unboxing",
+ " double d2 = (double) d;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void unboxingInBinaryTrees() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "class Test {",
+ " static void m1() {",
+ " Integer i = null;",
+ " Integer j = null;",
+ " // BUG: Diagnostic contains: unboxing",
+ " int i2 = i + j;",
+ " }",
+ " static void m2() {",
+ " Integer i = null;",
+ " // this is fine",
+ " String s = i + \"hi\";",
+ " }",
+ " static void m3() {",
+ " Integer i = null;",
+ " Integer j = null;",
+ " // BUG: Diagnostic contains: unboxing",
+ " int i3 = i - j;",
+ " }",
+ " static void m4() {",
+ " Integer i = null;",
+ " Integer j = null;",
+ " // BUG: Diagnostic contains: unboxing",
+ " int i4 = i * j;",
+ " }",
+ " static void m5() {",
+ " Integer i = null;",
+ " // BUG: Diagnostic contains: unboxing",
+ " int i5 = i << 2;",
+ " }",
+ " static void m6() {",
+ " Integer i = null;",
+ " Integer j = null;",
+ " // BUG: Diagnostic contains: unboxing",
+ " boolean b1 = i <= j;",
+ " }",
+ " static void m7() {",
+ " Boolean x = null;",
+ " Boolean y = null;",
+ " // BUG: Diagnostic contains: unboxing",
+ " boolean b2 = x && y;",
+ " }",
+ " static void m8() {",
+ " Integer i = null;",
+ " Integer j = null;",
+ " // this is fine",
+ " boolean b = i == j;",
+ " }",
+ " static void m9() {",
+ " Integer i = null;",
+ " // BUG: Diagnostic contains: unboxing",
+ " boolean b = i != 0;",
+ " }",
+ " static void m10() {",
+ " Integer i = null;",
+ " // BUG: Diagnostic contains: unboxing",
+ " int j = 3 - i;",
+ " }",
+ " static void m11() {",
+ " Integer i = null;",
+ " Integer j = null;",
+ " // BUG: Diagnostic contains: unboxing",
+ " int i2 = i",
+ " +",
+ " // BUG: Diagnostic contains: unboxing",
+ " j;",
+ " }",
+ " static void m12() {",
+ " Integer i = null;",
+ " // BUG: Diagnostic contains: unboxing",
+ " int i2 = i",
+ " +",
+ " // no error here, due to previous unbox of i",
+ " i;",
+ " }",
+ " static void m13() {",
+ " int[] arr = null;",
+ " Integer i = null;",
+ " // BUG: Diagnostic contains: dereferenced",
+ " int i2 = arr[",
+ " // BUG: Diagnostic contains: unboxing",
+ " i];",
+ " }",
+ " static void primitiveArgs(int x, int y) {}",
+ " static void m14() {",
+ " Integer i = null;",
+ " Integer j = null;",
+ " primitiveArgs(",
+ " // BUG: Diagnostic contains: unboxing",
+ " i,",
+ " // BUG: Diagnostic contains: unboxing",
+ " j);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void primitiveCastsRememberNullChecks() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Map;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "class Test {",
+ " static void foo(int i) { }",
+ " static void m1(@Nullable Integer i) {",
+ " // BUG: Diagnostic contains: unboxing",
+ " int i1 = (int) i;",
+ " }",
+ " static void m2(@Nullable Integer i) {",
+ " if (i != null) {",
+ " // this is fine",
+ " int i2 = (int) i;",
+ " }",
+ " }",
+ " static void m3(@Nullable Integer i) {",
+ " // BUG: Diagnostic contains: unboxing",
+ " int i3 = (int) i;",
+ " }",
+ " static void m4(@Nullable Integer i) {",
+ " Preconditions.checkNotNull(i);",
+ " // this is fine",
+ " int i4 = (int) i;",
+ " }",
+ " static private void consumeInt(int i) { }",
+ " static void m5(@Nullable Integer i) {",
+ " // BUG: Diagnostic contains: unboxing",
+ " consumeInt((int) i);",
+ " }",
+ " static void m6(@Nullable Integer i) {",
+ " Preconditions.checkNotNull(i);",
+ " // this is fine",
+ " consumeInt((int) i);",
+ " }",
+ " static void m7(@Nullable Object o) {",
+ " // BUG: Diagnostic contains: unboxing",
+ " consumeInt((int) o);",
+ " }",
+ " static void m8(@Nullable Object o) {",
+ " Preconditions.checkNotNull(o);",
+ " // this is fine",
+ " consumeInt((int) o);",
+ " }",
+ " static void m9(Map<String,Object> m) {",
+ " // BUG: Diagnostic contains: unboxing",
+ " consumeInt((int) m.get(\"foo\"));",
+ " }",
+ " static void m10(Map<String,Object> m) {",
+ " if(m.get(\"bar\") != null) {",
+ " // this is fine",
+ " consumeInt((int) m.get(\"bar\"));",
+ " }",
+ " }",
+ " static void m11(Map<String,Object> m) {",
+ " Preconditions.checkNotNull(m.get(\"bar\"));",
+ " // this is fine",
+ " consumeInt((int) m.get(\"bar\"));",
+ " }",
+ "}")
+ .doTest();
+ }
}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayCustomLibraryModelsTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayCustomLibraryModelsTests.java
new file mode 100644
index 0000000..937752c
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayCustomLibraryModelsTests.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway;
+
+import com.google.errorprone.CompilationTestHelper;
+import com.uber.nullaway.testlibrarymodels.TestLibraryModels;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+
+public class NullAwayCustomLibraryModelsTests extends NullAwayTestsBase {
+
+ private CompilationTestHelper makeLibraryModelsTestHelperWithArgs(List<String> args) {
+ // Adding directly to args will throw an UnsupportedOperationException, since that list is
+ // created by calling Arrays.asList (for consistency with the rest of NullAway's test cases),
+ // which produces a list which doesn't support add/addAll. Because of this, before we add our
+ // additional arguments, we must first copy the list into a mutable ArrayList.
+ List<String> extendedArguments = new ArrayList<>(args);
+ extendedArguments.addAll(
+ 0,
+ Arrays.asList(
+ "-processorpath",
+ TestLibraryModels.class.getProtectionDomain().getCodeSource().getLocation().getPath()));
+ return makeTestHelperWithArgs(extendedArguments);
+ }
+
+ @Test
+ public void allowLibraryModelsOverrideAnnotations() {
+ makeLibraryModelsTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "AnnotatedWithModels.java",
+ "package com.uber;",
+ "public class AnnotatedWithModels {",
+ " Object field = new Object();",
+ " // implicitly @Nullable due to library model",
+ " Object returnsNullFromModel() {",
+ " // null is valid here only because of the library model",
+ " return null;",
+ " }",
+ " Object nullableReturn() {",
+ " // BUG: Diagnostic contains: returning @Nullable",
+ " return returnsNullFromModel();",
+ " }",
+ " void run() {",
+ " // just to make sure, flow analysis is also impacted by library models information",
+ " Object temp = returnsNullFromModel();",
+ " // BUG: Diagnostic contains: assigning @Nullable",
+ " this.field = temp;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void libraryModelsOverrideRestrictiveAnnotations() {
+ makeLibraryModelsTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:UnannotatedSubPackages=com.uber.lib.unannotated",
+ "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.uber.lib.unannotated.RestrictivelyAnnotatedFIWithModelOverride;",
+ "import javax.annotation.Nullable;",
+ "public class Test {",
+ " void bar(RestrictivelyAnnotatedFIWithModelOverride f) {",
+ " // Param is @NullableDecl in bytecode, overridden by library model",
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull",
+ " f.apply(null);",
+ " }",
+ " void foo() {",
+ " RestrictivelyAnnotatedFIWithModelOverride func = (x) -> {",
+ " // Param is @NullableDecl in bytecode, overridden by library model, thus safe",
+ " return x.toString();",
+ " };",
+ " }",
+ " void baz() {",
+ " // Safe to pass, since Function can't have a null instance parameter",
+ " bar(Object::toString);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void libraryModelsAndSelectiveSkippingViaCommandLineOptions() {
+ // First test with the library models in effect
+ makeLibraryModelsTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:UnannotatedSubPackages=com.uber.lib.unannotated"))
+ .addSourceLines(
+ "CallMethods.java",
+ "package com.uber;",
+ "import com.uber.lib.unannotated.UnannotatedWithModels;",
+ "import javax.annotation.Nullable;",
+ "public class CallMethods {",
+ " Object testWithoutCheck(UnannotatedWithModels u) {",
+ " // BUG: Diagnostic contains: returning @Nullable expression from method with @NonNull return type",
+ " return u.returnsNullUnannotated();",
+ " }",
+ " Object testWithCheck(@Nullable Object o) {",
+ " if (UnannotatedWithModels.isNonNull(o)) {",
+ " return o;",
+ " }",
+ " return new Object();",
+ " }",
+ "}")
+ .addSourceLines(
+ "OverrideCheck.java",
+ "package com.uber;",
+ "import com.uber.lib.unannotated.UnannotatedWithModels;",
+ "import javax.annotation.Nullable;",
+ "public class OverrideCheck extends UnannotatedWithModels {",
+ " @Nullable",
+ " public Object returnsNullUnannotated() {",
+ " return null;",
+ " }",
+ "}")
+ .doTest();
+ // Now test disabling the library model
+ makeLibraryModelsTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:UnannotatedSubPackages=com.uber.lib.unannotated",
+ "-XepOpt:NullAway:IgnoreLibraryModelsFor=com.uber.lib.unannotated.UnannotatedWithModels.returnsNullUnannotated,com.uber.lib.unannotated.UnannotatedWithModels.isNonNull"))
+ .addSourceLines(
+ "CallMethods.java",
+ "package com.uber;",
+ "import com.uber.lib.unannotated.UnannotatedWithModels;",
+ "import javax.annotation.Nullable;",
+ "public class CallMethods {",
+ " Object testWithoutCheck(UnannotatedWithModels u) {",
+ " // Ok. Library model ignored",
+ " return u.returnsNullUnannotated();",
+ " }",
+ " Object testWithoutCheckNonIgnoredMethod(UnannotatedWithModels u) {",
+ " // BUG: Diagnostic contains: returning @Nullable expression from method with @NonNull return type",
+ " return u.returnsNullUnannotated2();",
+ " }",
+ " Object testWithCheck(@Nullable Object o) {",
+ " if (UnannotatedWithModels.isNonNull(o)) {",
+ " // BUG: Diagnostic contains: returning @Nullable expression from method with @NonNull return type",
+ " return o;",
+ " }",
+ " return new Object();",
+ " }",
+ "}")
+ .addSourceLines(
+ "OverrideCheck.java",
+ "package com.uber;",
+ "import com.uber.lib.unannotated.UnannotatedWithModels;",
+ "import javax.annotation.Nullable;",
+ "public class OverrideCheck extends UnannotatedWithModels {",
+ " @Nullable", // Still safe, because the method is not @NonNull, it's unannotated
+ // without model!
+ " public Object returnsNullUnannotated() {",
+ " return null;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void libraryModelsAndSelectiveSkippingViaCommandLineOptions2() {
+ makeLibraryModelsTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:UnannotatedSubPackages=com.uber.lib.unannotated",
+ "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true",
+ "-XepOpt:NullAway:IgnoreLibraryModelsFor=com.uber.lib.unannotated.RestrictivelyAnnotatedFIWithModelOverride.apply"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.uber.lib.unannotated.RestrictivelyAnnotatedFIWithModelOverride;",
+ "import javax.annotation.Nullable;",
+ "public class Test {",
+ " void bar(RestrictivelyAnnotatedFIWithModelOverride f) {",
+ " // Param is @NullableDecl in bytecode, and library model making it non-null is skipped",
+ " f.apply(null);",
+ " }",
+ " void foo() {",
+ " RestrictivelyAnnotatedFIWithModelOverride func = (x) -> {",
+ " // Param is @NullableDecl in bytecode, and overriding library model is ignored, thus unsafe",
+ " // BUG: Diagnostic contains: dereferenced expression x is @Nullable",
+ " return x.toString();",
+ " };",
+ " }",
+ " void baz() {",
+ " // BUG: Diagnostic contains: unbound instance method reference cannot be used",
+ " bar(Object::toString);",
+ " }",
+ "}")
+ .doTest();
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayFrameworkTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayFrameworkTests.java
index 7f05e11..ed62ecc 100644
--- a/nullaway/src/test/java/com/uber/nullaway/NullAwayFrameworkTests.java
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayFrameworkTests.java
@@ -1,5 +1,6 @@
package com.uber.nullaway;
+import java.util.Arrays;
import org.junit.Test;
public class NullAwayFrameworkTests extends NullAwayTestsBase {
@@ -97,6 +98,27 @@ public class NullAwayFrameworkTests extends NullAwayTestsBase {
}
@Test
+ public void defaultLibraryModelsClassIsInstance() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Objects;",
+ "import javax.annotation.Nullable;",
+ "public class Test {",
+ " int classIsInstance(@Nullable String s) {",
+ " if (CharSequence.class.isInstance(s)) {",
+ " return s.hashCode();",
+ " } else {",
+ " // BUG: Diagnostic contains: dereferenced",
+ " return s.hashCode();",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
public void checkForNullSupport() {
defaultCompilationHelper
// This is just to check the behavior is the same between @Nullable and @CheckForNull
@@ -241,6 +263,43 @@ public class NullAwayFrameworkTests extends NullAwayTestsBase {
}
@Test
+ public void springTestAutowiredFieldTest() {
+ defaultCompilationHelper
+ .addSourceFile("springboot-annotations/MockBean.java")
+ .addSourceFile("springboot-annotations/SpyBean.java")
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import org.springframework.stereotype.Component;",
+ "@Component",
+ "public class Foo {",
+ " @Nullable String bar;",
+ " public void setBar(String s) {",
+ " bar = s;",
+ " }",
+ "}")
+ .addSourceLines(
+ "TestCase.java",
+ "package com.uber;",
+ "import org.junit.jupiter.api.Test;",
+ "import org.springframework.boot.test.mock.mockito.SpyBean;",
+ "import org.springframework.boot.test.mock.mockito.MockBean;",
+ "public class TestCase {",
+ " @SpyBean",
+ " private Foo spy;", // Initialized by spring test (via Mockito).
+ " @MockBean",
+ " private Foo mock;", // Initialized by spring test (via Mockito).
+ " @Test",
+ " void springTest() {",
+ " spy.setBar(\"hello\");",
+ " mock.setBar(\"hello\");",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
public void springAutowiredConstructorTest() {
defaultCompilationHelper
.addSourceLines(
@@ -273,4 +332,571 @@ public class NullAwayFrameworkTests extends NullAwayTestsBase {
"}")
.doTest();
}
+
+ @Test
+ public void testLombokBuilderWithGeneratedAsUnannotated() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:TreatGeneratedAsUnannotated=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.uber.lombok.LombokDTO;",
+ "class Test {",
+ " void testSetters(LombokDTO ldto) {",
+ " ldto.setNullableField(null);",
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required",
+ " ldto.setField(null);",
+ " }",
+ " String testGetterSafe(LombokDTO ldto) {",
+ " return ldto.getField();",
+ " }",
+ " String testGetterNullable(LombokDTO ldto) {",
+ " // BUG: Diagnostic contains: returning @Nullable expression from method with @NonNull return type",
+ " return ldto.getNullableField();",
+ " }",
+ " LombokDTO testBuilderSafe(@Nullable String s1, String s2) {",
+ " // Safe, because s2 is non-null and nullableField can take @Nullable",
+ " return LombokDTO.builder().nullableField(s1).field(s2).build();",
+ " }",
+ " LombokDTO testBuilderUnsafe(@Nullable String s1, @Nullable String s2) {",
+ " // No error, because the code of LombokDTO.Builder is @Generated and we are",
+ " // building with TreatGeneratedAsUnannotated=true",
+ " return LombokDTO.builder().nullableField(s1).field(s2).build();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testLombokBuilderWithoutGeneratedAsUnannotated() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.uber.lombok.LombokDTO;",
+ "class Test {",
+ " void testSetters(LombokDTO ldto) {",
+ " ldto.setNullableField(null);",
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required",
+ " ldto.setField(null);",
+ " }",
+ " String testGetterSafe(LombokDTO ldto) {",
+ " return ldto.getField();",
+ " }",
+ " String testGetterNullable(LombokDTO ldto) {",
+ " // BUG: Diagnostic contains: returning @Nullable expression from method with @NonNull return type",
+ " return ldto.getNullableField();",
+ " }",
+ " LombokDTO testBuilderSafe(@Nullable String s1, String s2) {",
+ " // Safe, because s2 is non-null and nullableField can take @Nullable",
+ " return LombokDTO.builder().nullableField(s1).field(s2).build();",
+ " }",
+ " LombokDTO testBuilderUnsafe(@Nullable String s1, @Nullable String s2) {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter 's2' where @NonNull is required",
+ " return LombokDTO.builder().nullableField(s1).field(s2).build();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ /**
+ * This test is solely to check if we can run through some of the {@link
+ * com.uber.nullaway.handlers.LombokHandler} logic without crashing. It does not check that the
+ * logic is correct.
+ */
+ @Test
+ public void lombokHandlerRunsWithoutCrashing() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " @Nullable Object test;",
+ " @lombok.Generated",
+ " Object $default$test() {",
+ " return new Object();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void systemConsoleNullable() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "class Test {",
+ " void foo() {",
+ " // BUG: Diagnostic contains: dereferenced expression System.console()",
+ " System.console().toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void mapGetOrDefault() {
+ String[] sourceLines =
+ new String[] {
+ "package com.uber;",
+ "import java.util.Map;",
+ "import com.google.common.collect.ImmutableMap;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " void testGetOrDefaultMap(Map<String, String> m, String nonNullString, @Nullable String nullableString) {",
+ " m.getOrDefault(\"key\", \"value\").toString();",
+ " m.getOrDefault(\"key\", nonNullString).toString();",
+ " // BUG: Diagnostic contains: dereferenced",
+ " m.getOrDefault(\"key\", null).toString();",
+ " // BUG: Diagnostic contains: dereferenced",
+ " m.getOrDefault(\"key\", nullableString).toString();",
+ " }",
+ " void testGetOrDefaultImmutableMap(ImmutableMap<String, String> im, String nonNullString, @Nullable String nullableString) {",
+ " im.getOrDefault(\"key\", \"value\").toString();",
+ " im.getOrDefault(\"key\", nonNullString).toString();",
+ " // BUG: Diagnostic contains: dereferenced",
+ " im.getOrDefault(\"key\", null).toString();",
+ " // BUG: Diagnostic contains: dereferenced",
+ " im.getOrDefault(\"key\", nullableString).toString();",
+ " }",
+ "}"
+ };
+ // test *without* restrictive annotations enabled
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines("Test.java", sourceLines)
+ .doTest();
+ // test *with* restrictive annotations enabled
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true"))
+ .addSourceLines("Test.java", sourceLines)
+ .doTest();
+ }
+
+ @Test
+ public void defaultLibraryModelsClassCast() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " void castNullable(@Nullable String s) {",
+ " // BUG: Diagnostic contains: dereferenced",
+ " CharSequence.class.cast(s).hashCode();",
+ " }",
+ " void castNonnull(String s1, @Nullable String s2) {",
+ " CharSequence.class.cast(s1).hashCode();",
+ " if (s2 instanceof CharSequence) {",
+ " CharSequence.class.cast(s2).hashCode();",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateNotNull() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "public class Foo {",
+ " public void bar(@Nullable String s) {",
+ " Validate.notNull(s);",
+ " int l = s.length();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateNotNullWithMessage() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "public class Foo {",
+ " public void bar(@Nullable String s) {",
+ " Validate.notNull(s, \"Message\");",
+ " int l = s.length();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateArrayNotEmptyWithMessage() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "public class Foo {",
+ " public void bar(@Nullable String[] s) {",
+ " Validate.notEmpty(s, \"Message\");",
+ " int l = s.length;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateArrayNotEmpty() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "public class Foo {",
+ " public void bar(@Nullable String[] s) {",
+ " Validate.notEmpty(s);",
+ " int l = s.length;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateListNotEmptyWithMessage() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "import java.util.List;",
+ "public class Foo {",
+ " public void bar(@Nullable List<String> s) {",
+ " Validate.notEmpty(s, \"Message\");",
+ " int l = s.size();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateListNotEmpty() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "import java.util.List;",
+ "public class Foo {",
+ " public void bar(@Nullable List<String> s) {",
+ " Validate.notEmpty(s);",
+ " int l = s.size();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateMapNotEmptyWithMessage() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "import java.util.Map;",
+ "public class Foo {",
+ " public void bar(@Nullable Map<String, String> s) {",
+ " Validate.notEmpty(s, \"Message\");",
+ " int l = s.size();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateMapNotEmpty() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "import java.util.Map;",
+ "public class Foo {",
+ " public void bar(@Nullable Map<String, String> s) {",
+ " Validate.notEmpty(s);",
+ " int l = s.size();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateStringNotEmptyWithMessage() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "public class Foo {",
+ " public void bar(@Nullable String s) {",
+ " Validate.notEmpty(s, \"Message\");",
+ " int l = s.length();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateStringNotEmpty() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "public class Foo {",
+ " public void bar(@Nullable String s) {",
+ " Validate.notEmpty(s);",
+ " int l = s.length();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateStringNotBlankWithMessage() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "public class Foo {",
+ " public void bar(@Nullable String s) {",
+ " Validate.notBlank(s, \"Message\");",
+ " int l = s.length();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateStringNotBlank() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "public class Foo {",
+ " public void bar(@Nullable String s) {",
+ " Validate.notBlank(s);",
+ " int l = s.length();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateArrayNoNullElementsWithMessage() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "public class Foo {",
+ " public void bar(@Nullable String[] s) {",
+ " Validate.noNullElements(s, \"Message\");",
+ " int l = s.length;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateArrayNoNullElements() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "public class Foo {",
+ " public void bar(@Nullable String[] s) {",
+ " Validate.noNullElements(s);",
+ " int l = s.length;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateIterableNoNullElementsWithMessage() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "import java.util.Iterator;",
+ "public class Foo {",
+ " public void bar(@Nullable Iterable<String> s) {",
+ " Validate.noNullElements(s, \"Message\");",
+ " Iterator<String> l = s.iterator();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateIterableNoNullElements() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "import java.util.Iterator;",
+ "public class Foo {",
+ " public void bar(@Nullable Iterable<String> s) {",
+ " Validate.noNullElements(s);",
+ " Iterator<String> l = s.iterator();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateArrayValidIndexWithMessage() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "public class Foo {",
+ " public void bar(@Nullable String[] s) {",
+ " Validate.validIndex(s, 0, \"Message\");",
+ " int l = s.length;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateArrayValidIndex() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "public class Foo {",
+ " public void bar(@Nullable String[] s) {",
+ " Validate.validIndex(s, 0);",
+ " int l = s.length;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateCollectionValidIndexWithMessage() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "import java.util.List;",
+ "public class Foo {",
+ " public void bar(@Nullable List<String> s) {",
+ " Validate.validIndex(s, 0, \"Message\");",
+ " int l = s.size();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateCollectionValidIndex() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "import java.util.List;",
+ "public class Foo {",
+ " public void bar(@Nullable List<String> s) {",
+ " Validate.validIndex(s, 0);",
+ " int l = s.size();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateStringValidIndexWithMessage() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "public class Foo {",
+ " public void bar(@Nullable String s) {",
+ " Validate.validIndex(s, 0, \"Message\");",
+ " int l = s.length();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void apacheValidateStringValidIndex() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.apache.commons.lang3.Validate;",
+ "import org.jetbrains.annotations.Nullable;",
+ "public class Foo {",
+ " public void bar(@Nullable String s) {",
+ " Validate.validIndex(s, 0);",
+ " int l = s.length();",
+ " }",
+ "}")
+ .doTest();
+ }
}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayFunctionalInterfaceNullabilityTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayFunctionalInterfaceNullabilityTests.java
new file mode 100644
index 0000000..6406b2b
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayFunctionalInterfaceNullabilityTests.java
@@ -0,0 +1,110 @@
+package com.uber.nullaway;
+
+import org.junit.Test;
+
+public class NullAwayFunctionalInterfaceNullabilityTests extends NullAwayTestsBase {
+
+ @Test
+ public void multipleTypeParametersInstantiation() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "NullableFunction.java",
+ "package com.uber.unannotated;", // As if a third-party lib, since override is invalid
+ "import javax.annotation.Nullable;",
+ "import java.util.function.Function;",
+ "@FunctionalInterface",
+ "public interface NullableFunction<F, T> extends Function<F, T> {",
+ " @Override",
+ " @Nullable",
+ " T apply(@Nullable F input);",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import java.util.function.Function;",
+ "import com.uber.unannotated.NullableFunction;",
+ "class Test {",
+ " private static void takesNullableFunction(NullableFunction<String, String> nf) { }",
+ " private static void takesNonNullableFunction(Function<String, String> f) { }",
+ " private static void passesNullableFunction() {",
+ " takesNullableFunction(s -> { return null; });",
+ " }",
+ " private static void passesNullableFunctionToNonNull() {",
+ " takesNonNullableFunction(s -> { return null; });",
+ " }",
+ "}")
+ .addSourceLines(
+ "TestGuava.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Function;",
+ "import com.uber.unannotated.NullableFunction;",
+ "class TestGuava {",
+ " private static void takesNullableFunction(NullableFunction<String, String> nf) { }",
+ " private static void takesNonNullableFunction(Function<String, String> f) { }",
+ " private static void passesNullableFunction() {",
+ " takesNullableFunction(s -> { return null; });",
+ " }",
+ " private static void passesNullableFunctionToNonNull() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " takesNonNullableFunction(s -> { return null; });",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void futuresFunctionLambdas() {
+ // See FluentFutureHandler
+ defaultCompilationHelper
+ .addSourceLines(
+ "TestGuava.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "import com.google.common.base.Function;",
+ "import com.google.common.util.concurrent.FluentFuture;",
+ "import com.google.common.util.concurrent.Futures;",
+ "import com.google.common.util.concurrent.ListenableFuture;",
+ "import java.util.concurrent.Executor;",
+ "class TestGuava {",
+ " private static ListenableFuture<@Nullable String> fluentFutureCatching(Executor executor) {",
+ " return FluentFuture",
+ " .from(Futures.immediateFuture(\"hi\"))",
+ " .catching(Throwable.class, e -> { return null; }, executor);",
+ " }",
+ " private static ListenableFuture<@Nullable String> fluentFutureCatchingAsync(Executor executor) {",
+ " return FluentFuture",
+ " .from(Futures.immediateFuture(\"hi\"))",
+ " .catchingAsync(Throwable.class, e -> { return null; }, executor);",
+ " }",
+ " private static ListenableFuture<@Nullable String> fluentFutureTransform(Executor executor) {",
+ " return FluentFuture",
+ " .from(Futures.immediateFuture(\"hi\"))",
+ " .transform(s -> { return null; }, executor);",
+ " }",
+ " private static ListenableFuture<@Nullable String> fluentFutureTransformAsync(Executor executor) {",
+ " return FluentFuture",
+ " .from(Futures.immediateFuture(\"hi\"))",
+ " .transformAsync(s -> { return null; }, executor);",
+ " }",
+ " private static ListenableFuture<String> fluentFutureTransformNoNull(Executor executor) {",
+ " return FluentFuture",
+ " .from(Futures.immediateFuture(\"hi\"))",
+ " // Should be an error when we have full generics support, false-negative for now",
+ " .transform(s -> { return s; }, executor);",
+ " }",
+ " private static ListenableFuture<String> fluentFutureUnsafe(Executor executor) {",
+ " return FluentFuture",
+ " .from(Futures.immediateFuture(\"hi\"))",
+ " // Should be an error when we have full generics support, false-negative for now",
+ " .transform(s -> { return null; }, executor);",
+ " }",
+ " private static ListenableFuture<@Nullable String> futuresTransform(Executor executor) {",
+ " return Futures",
+ " .transform(Futures.immediateFuture(\"hi\"), s -> { return null; }, executor);",
+ " }",
+ "}")
+ .doTest();
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayGuavaAssertionsTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayGuavaAssertionsTests.java
new file mode 100644
index 0000000..83ee0c9
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayGuavaAssertionsTests.java
@@ -0,0 +1,355 @@
+package com.uber.nullaway;
+
+import java.util.Arrays;
+import org.junit.Test;
+
+public class NullAwayGuavaAssertionsTests extends NullAwayTestsBase {
+
+ @Test
+ public void checkNotNullTest() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "class Test {",
+ " private void foo(@Nullable Object a) {",
+ " Preconditions.checkNotNull(a);",
+ " a.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void checkNotNullComplexAccessPathsTest() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "TestField.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "class TestField {",
+ " @Nullable private Object f = null;",
+ " private void foo(@Nullable TestField a) {",
+ " Preconditions.checkNotNull(a);",
+ " Preconditions.checkNotNull(a.f);",
+ " a.f.toString();",
+ " }",
+ "}")
+ .addSourceLines(
+ "TestMap.java",
+ "package com.uber;",
+ "import java.util.Map;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "class TestMap {",
+ " private void foo(@Nullable Map<String,Object> m) {",
+ " Preconditions.checkNotNull(m);",
+ " Preconditions.checkNotNull(m.get(\"foo\"));",
+ " m.get(\"foo\").toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void verifyNotNullTest() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Verify;",
+ "class Test {",
+ " private void foo(@Nullable Object a) {",
+ " Verify.verifyNotNull(a);",
+ " a.toString();",
+ " }",
+ " private void bar(@Nullable Object a) {",
+ " Verify.verifyNotNull(a, \"message\", new Object(), new Object());",
+ " a.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void simpleCheckArgumentTest() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "class Test {",
+ " private void foo(@Nullable Object a) {",
+ " Preconditions.checkArgument(a != null);",
+ " a.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void simpleCheckStateTest() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "class Test {",
+ " @Nullable private Object a;",
+ " private void foo() {",
+ " Preconditions.checkState(this.a != null);",
+ " a.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void simpleVerifyTest() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Verify;",
+ "class Test {",
+ " private void foo(@Nullable Object a) {",
+ " Verify.verify(a != null);",
+ " a.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void simpleCheckArgumentWithMessageTest() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "class Test {",
+ " private void foo(@Nullable Object a) {",
+ " Preconditions.checkArgument(a != null, \"a ought to be non-null\");",
+ " a.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void compoundCheckArgumentTest() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "class Test {",
+ " private void foo(@Nullable Object a) {",
+ " Preconditions.checkArgument(a != null && !a.equals(this));",
+ " a.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void compoundCheckArgumentLastTest() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "class Test {",
+ " private void foo(@Nullable Object a) {",
+ " Preconditions.checkArgument(this.hashCode() != 5 && a != null);",
+ " a.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void compoundCheckArgumentLongTest() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "class Test {",
+ " private void foo(@Nullable Object a, @Nullable Object b, @Nullable Object c, @Nullable Object d, @Nullable Object e) {",
+ " Preconditions.checkArgument(a != null && b != null && c != null && d != null && e != null);",
+ " a.toString();",
+ " b.toString();",
+ " c.toString();",
+ " d.toString();",
+ " e.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nestedCallCheckArgumentTest() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "import com.google.common.base.Strings;",
+ "class Test {",
+ " private void foo(@Nullable String a) {",
+ " Preconditions.checkArgument(!Strings.isNullOrEmpty(a));",
+ " a.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void irrelevantCheckArgumentTest() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "class Test {",
+ " private void foo(@Nullable Object a) {",
+ " Preconditions.checkArgument(this.hashCode() != 5);",
+ " // BUG: Diagnostic contains: dereferenced expression a is @Nullable",
+ " a.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void inconclusiveCheckArgumentTest() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "class Test {",
+ " private void foo(@Nullable Object a) {",
+ " Preconditions.checkArgument(this.hashCode() != 5 || a != null);",
+ " // BUG: Diagnostic contains: dereferenced expression a is @Nullable",
+ " a.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void checkArgumentCatchException() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "class Test {",
+ " private void foo(@Nullable Object a) {",
+ " try {",
+ " Preconditions.checkArgument(a != null);",
+ " } catch (IllegalArgumentException e) {}",
+ " // BUG: Diagnostic contains: dereferenced expression a is @Nullable",
+ " a.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void checkStateCatchException() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.google.common.base.Preconditions;",
+ "class Test {",
+ " private void foo(@Nullable Object a) {",
+ " try {",
+ " Preconditions.checkState(a != null);",
+ " } catch (IllegalStateException e) {}",
+ " // BUG: Diagnostic contains: dereferenced expression a is @Nullable",
+ " a.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayInitializationTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayInitializationTests.java
index 1ad91e0..f953d8c 100644
--- a/nullaway/src/test/java/com/uber/nullaway/NullAwayInitializationTests.java
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayInitializationTests.java
@@ -1,5 +1,6 @@
package com.uber.nullaway;
+import java.util.Arrays;
import org.junit.Test;
public class NullAwayInitializationTests extends NullAwayTestsBase {
@@ -69,6 +70,59 @@ public class NullAwayInitializationTests extends NullAwayTestsBase {
}
@Test
+ public void externalInitSupportConstructors() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:ExternalInitAnnotations=com.uber.ExternalInitConstructor"))
+ .addSourceLines(
+ "ExternalInitConstructor.java",
+ "package com.uber;",
+ "import java.lang.annotation.ElementType;",
+ "import java.lang.annotation.Retention;",
+ "import java.lang.annotation.RetentionPolicy;",
+ "import java.lang.annotation.Target;",
+ "@Retention(RetentionPolicy.CLASS)",
+ "@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})",
+ "public @interface ExternalInitConstructor {}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "class Test {",
+ " Object f;",
+ // no error here due to external init
+ " @ExternalInitConstructor",
+ " public Test() {}",
+ " // BUG: Diagnostic contains: initializer method does not guarantee @NonNull field",
+ " public Test(int x) {}",
+ " public Test(Object o) { this.f = o; }",
+ "}")
+ .addSourceLines(
+ "Test2.java",
+ "package com.uber;",
+ "class Test2 {",
+ " // BUG: Diagnostic contains: @NonNull field f not initialized",
+ " Object f;",
+ // must be on a constructor!
+ " @ExternalInitConstructor",
+ " public void init() {}",
+ "}")
+ .addSourceLines(
+ "Test3.java",
+ "package com.uber;",
+ "class Test3 {",
+ " Object f;",
+ // Must be zero-args constructor!
+ " @ExternalInitConstructor",
+ " // BUG: Diagnostic contains: initializer method does not guarantee @NonNull field",
+ " public Test3(int x) {}",
+ "}")
+ .doTest();
+ }
+
+ @Test
public void externalInitSupportFields() {
defaultCompilationHelper
.addSourceLines(
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyArrayTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyArrayTests.java
new file mode 100644
index 0000000..5426785
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyArrayTests.java
@@ -0,0 +1,118 @@
+package com.uber.nullaway;
+
+import com.google.errorprone.CompilationTestHelper;
+import java.util.Arrays;
+import org.junit.Test;
+
+public class NullAwayJSpecifyArrayTests extends NullAwayTestsBase {
+
+ @Test
+ public void arrayTopLevelAnnotationDereference() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static Integer @Nullable [] fizz = {1};",
+ " static void foo() {",
+ " // BUG: Diagnostic contains: dereferenced expression fizz is @Nullable",
+ " int bar = fizz.length;",
+ " }",
+ " static void bar() {",
+ " // BUG: Diagnostic contains: dereferenced expression fizz is @Nullable",
+ " int bar = fizz[0];",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void arrayTopLevelAnnotationAssignment() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " Object foo = new Object();",
+ " void m( Integer @Nullable [] bar) {",
+ " // BUG: Diagnostic contains: assigning @Nullable expression to @NonNull field",
+ " foo = bar;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void arrayContentsAnnotationDereference() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static @Nullable String [] fizz = {\"1\"};",
+ " static Object foo = new Object();",
+ " static void foo() {",
+ " // TODO: This should report an error due to dereference of @Nullable fizz[0]",
+ " int bar = fizz[0].length();",
+ " // OK: valid dereference since only elements of the array can be null",
+ " foo = fizz.length;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void arrayContentsAnnotationAssignment() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " Object fizz = new Object();",
+ " void m( @Nullable Integer [] foo) {",
+ " // TODO: This should report an error due to assignment of @Nullable foo[0] to @NonNull field",
+ " fizz = foo[0];",
+ " // OK: valid assignment since only elements can be null",
+ " fizz = foo;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ /**
+ * Currently in JSpecify mode, JSpecify syntax only applies to type-use annotations. Declaration
+ * annotations preserve their existing behavior, with annotations being treated on the top-level
+ * type. We will very likely revisit this design in the future.
+ */
+ @Test
+ public void arrayDeclarationAnnotation() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "class Test {",
+ " static @Nullable String [] fizz = {\"1\"};",
+ " static Object o1 = new Object();",
+ " static void foo() {",
+ " // This should not report an error while using JSpecify type-use annotation",
+ " // BUG: Diagnostic contains: assigning @Nullable expression to @NonNull field",
+ " o1 = fizz;",
+ " // This should not report an error while using JSpecify type-use annotation",
+ " // BUG: Diagnostic contains: dereferenced expression fizz is @Nullable",
+ " o1 = fizz.length;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ private CompilationTestHelper makeHelper() {
+ return makeTestHelperWithArgs(
+ Arrays.asList(
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber", "-XepOpt:NullAway:JSpecifyMode=true"));
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyGenericsTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyGenericsTests.java
new file mode 100644
index 0000000..e722706
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyGenericsTests.java
@@ -0,0 +1,1506 @@
+package com.uber.nullaway;
+
+import com.google.errorprone.CompilationTestHelper;
+import java.util.Arrays;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class NullAwayJSpecifyGenericsTests extends NullAwayTestsBase {
+
+ @Test
+ public void basicTypeParamInstantiation() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class NonNullTypeParam<E> {}",
+ " static class NullableTypeParam<E extends @Nullable Object> {}",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " static void testBadNonNull(NonNullTypeParam<@Nullable String> t1) {",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " NonNullTypeParam<@Nullable String> t2 = null;",
+ " NullableTypeParam<@Nullable String> t3 = null;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void constructorTypeParamInstantiation() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class NonNullTypeParam<E> {}",
+ " static class NullableTypeParam<E extends @Nullable Object> {}",
+ " static void testOkNonNull(NonNullTypeParam<String> t) {",
+ " NonNullTypeParam<String> t2 = new NonNullTypeParam<String>();",
+ " }",
+ " static void testBadNonNull(NonNullTypeParam<String> t) {",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " NonNullTypeParam<String> t2 = new NonNullTypeParam<@Nullable String>();",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " testBadNonNull(new NonNullTypeParam<@Nullable String>());",
+ " testBadNonNull(",
+ " // BUG: Diagnostic contains: Cannot pass parameter of type NonNullTypeParam<@Nullable String>",
+ " new NonNullTypeParam<",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " @Nullable String>());",
+ " }",
+ " static void testOkNullable(NullableTypeParam<String> t1, NullableTypeParam<@Nullable String> t2) {",
+ " NullableTypeParam<String> t3 = new NullableTypeParam<String>();",
+ " NullableTypeParam<@Nullable String> t4 = new NullableTypeParam<@Nullable String>();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void multipleTypeParametersInstantiation() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class MixedTypeParam<E1, E2 extends @Nullable Object, E3 extends @Nullable Object, E4> {}",
+ " static class PartiallyInvalidSubclass",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " extends MixedTypeParam<@Nullable String, String, String, @Nullable String> {}",
+ " static class ValidSubclass1",
+ " extends MixedTypeParam<String, @Nullable String, @Nullable String, String> {}",
+ " static class PartiallyInvalidSubclass2",
+ " extends MixedTypeParam<",
+ " String,",
+ " String,",
+ " String,",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " @Nullable String> {}",
+ " static class ValidSubclass2 extends MixedTypeParam<String, String, String, String> {}",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void subClassTypeParamInstantiation() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class NonNullTypeParam<E> {}",
+ " static class NullableTypeParam<E extends @Nullable Object> {}",
+ " static class SuperClassForValidSubclass {",
+ " static class ValidSubclass extends NullableTypeParam<@Nullable String> {}",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " static class InvalidSubclass extends NonNullTypeParam<@Nullable String> {}",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void interfaceImplementationTypeParamInstantiation() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static interface NonNullTypeParamInterface<E> {}",
+ " static interface NullableTypeParamInterface<E extends @Nullable Object> {}",
+ " static class InvalidInterfaceImplementation",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " implements NonNullTypeParamInterface<@Nullable String> {}",
+ " static class ValidInterfaceImplementation implements NullableTypeParamInterface<String> {}",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nestedTypeParams() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class NonNullTypeParam<E> {}",
+ " static class NullableTypeParam<E extends @Nullable Object> {}",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " static void testBadNonNull(NullableTypeParam<NonNullTypeParam<@Nullable String>> t) {",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " NullableTypeParam<NonNullTypeParam<NonNullTypeParam<@Nullable String>>> t2 = null;",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " t2 = new NullableTypeParam<NonNullTypeParam<NonNullTypeParam<@Nullable String>>>();",
+ " // this is fine",
+ " NullableTypeParam<NonNullTypeParam<NullableTypeParam<@Nullable String>>> t3 = null;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void returnTypeParamInstantiation() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class NonNullTypeParam<E> {}",
+ " static class NullableTypeParam<E extends @Nullable Object> {}",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " static NonNullTypeParam<@Nullable String> testBadNonNull() {",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " return new NonNullTypeParam<@Nullable String>();",
+ " }",
+ " static NullableTypeParam<@Nullable String> testOKNull() {",
+ " return new NullableTypeParam<@Nullable String>();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testOKNewClassInstantiationForOtherAnnotations() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import lombok.NonNull;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class NonNullTypeParam<E> {}",
+ " static class DifferentAnnotTypeParam1<E extends @NonNull Object> {}",
+ " static class DifferentAnnotTypeParam2<@NonNull E> {}",
+ " static void testOKOtherAnnotation(NonNullTypeParam<String> t) {",
+ " // should not show error for annotation other than @Nullable",
+ " testOKOtherAnnotation(new NonNullTypeParam<@NonNull String>());",
+ " DifferentAnnotTypeParam1<String> t1 = new DifferentAnnotTypeParam1<String>();",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " DifferentAnnotTypeParam2<String> t2 = new DifferentAnnotTypeParam2<@Nullable String>();",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " DifferentAnnotTypeParam1<String> t3 = new DifferentAnnotTypeParam1<@Nullable String>();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nestedGenericTypes() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " class Wrapper<P extends @Nullable Object> {",
+ " abstract class Fn<R extends @Nullable Object> {",
+ " abstract R apply(P p);",
+ " }",
+ " }",
+ " static void param(@Nullable Wrapper<String>.Fn<String> p) {}",
+ " static void positiveParam() {",
+ " Wrapper<@Nullable String>.Fn<String> x = null;",
+ " // BUG: Diagnostic contains: Cannot pass parameter of type Test.Wrapper<@Nullable String>.Fn<String>",
+ " param(x);",
+ " }",
+ " static void positiveAssign() {",
+ " Wrapper<@Nullable String>.Fn<String> p1 = null;",
+ " // BUG: Diagnostic contains: Cannot assign from type Test.Wrapper<@Nullable String>.Fn<String> to type Test.Wrapper<String>.Fn<String>",
+ " Wrapper<String>.Fn<String> p2 = p1;",
+ " }",
+ " static @Nullable Wrapper<String>.Fn<String> positiveReturn() {",
+ " Wrapper<@Nullable String>.Fn<String> p1 = null;",
+ " // BUG: Diagnostic contains: Cannot return expression of type Test.Wrapper<@Nullable String>.Fn<String>",
+ " return p1;",
+ " }",
+ " static void negativeParam() {",
+ " Wrapper<String>.Fn<String> x = null;",
+ " param(x);",
+ " }",
+ " static void negativeAssign() {",
+ " Wrapper<@Nullable String>.Fn<String> p1 = null;",
+ " Wrapper<@Nullable String>.Fn<String> p2 = p1;",
+ " }",
+ " static @Nullable Wrapper<@Nullable String>.Fn<String> negativeReturn() {",
+ " Wrapper<@Nullable String>.Fn<String> p1 = null;",
+ " return p1;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void downcastInstantiation() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class NonNullTypeParam<E> {}",
+ " static void instOf(Object o) {",
+ " // BUG: Diagnostic contains: Generic type parameter",
+ " Object p = (NonNullTypeParam<@Nullable String>) o;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ /** check that we don't report errors on invalid instantiations in unannotated code */
+ @Test
+ public void instantiationInUnannotatedCode() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.other;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class NonNullTypeParam<E> {}",
+ " static void instOf(Object o) {",
+ " Object p = (NonNullTypeParam<@Nullable String>) o;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void genericsChecksForAssignments() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class NullableTypeParam<E extends @Nullable Object> {}",
+ " static void testPositive(NullableTypeParam<@Nullable String> t1) {",
+ " // BUG: Diagnostic contains: Cannot assign from type NullableTypeParam<@Nullable String>",
+ " NullableTypeParam<String> t2 = t1;",
+ " }",
+ " static void testNegative(NullableTypeParam<@Nullable String> t1) {",
+ " NullableTypeParam<@Nullable String> t2 = t1;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nestedChecksForAssignmentsMultipleArguments() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class SampleClass<E extends @Nullable Object> {}",
+ " static class SampleClassMultipleArguments<E1 extends @Nullable Object, E2> {}",
+ " static void testPositive() {",
+ " // BUG: Diagnostic contains: Cannot assign from type SampleClassMultipleArguments<SampleClass<SampleClass<String>>",
+ " SampleClassMultipleArguments<SampleClass<SampleClass<@Nullable String>>, String> t1 =",
+ " new SampleClassMultipleArguments<SampleClass<SampleClass<String>>, String>();",
+ " }",
+ " static void testNegative() {",
+ " SampleClassMultipleArguments<SampleClass<SampleClass<@Nullable String>>, String> t1 =",
+ " new SampleClassMultipleArguments<SampleClass<SampleClass<@Nullable String>>, String>();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void superTypeAssignmentChecksSingleInterface() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface Fn<P extends @Nullable Object, R extends @Nullable Object> {}",
+ " class FnImpl implements Fn<@Nullable String, @Nullable String> {}",
+ " void testPositive() {",
+ " // BUG: Diagnostic contains: Cannot assign from type Test.FnImpl",
+ " Fn<@Nullable String, String> f = new FnImpl();",
+ " }",
+ " void testNegative() {",
+ " Fn<@Nullable String, @Nullable String> f = new FnImpl();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void superTypeAssignmentChecksMultipleInterface() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface Fn1<P1 extends @Nullable Object, P2 extends @Nullable Object> {}",
+ " interface Fn2<P extends @Nullable Object> {}",
+ " class FnImpl implements Fn1<@Nullable String, @Nullable String>, Fn2<String> {}",
+ " void testPositive() {",
+ " // BUG: Diagnostic contains: Cannot assign from type",
+ " Fn2<@Nullable String> f = new FnImpl();",
+ " }",
+ " void testNegative() {",
+ " Fn2<String> f = new FnImpl();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void superTypeAssignmentChecksMultipleLevelInheritance() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " class SuperClassC<P1 extends @Nullable Object> {}",
+ " class SuperClassB<P extends @Nullable Object> extends SuperClassC<P> {}",
+ " class SubClassA<P extends @Nullable Object> extends SuperClassB<P> {}",
+ " class FnImpl1 extends SubClassA<String> {}",
+ " class FnImpl2 extends SubClassA<@Nullable String> {}",
+ " void testPositive() {",
+ " SuperClassC<@Nullable String> f;",
+ " // BUG: Diagnostic contains: Cannot assign from type",
+ " f = new FnImpl1();",
+ " }",
+ " void testNegative() {",
+ " SuperClassC<@Nullable String> f;",
+ " // No error",
+ " f = new FnImpl2();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void subtypeWithParameters() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " class D<P extends @Nullable Object> {}",
+ " class B<P extends @Nullable Object> extends D<P> {}",
+ " void testPositive(B<@Nullable String> b) {",
+ " // BUG: Diagnostic contains: Cannot assign from type",
+ " D<String> f1 = new B<@Nullable String>();",
+ " // BUG: Diagnostic contains: Cannot assign from type",
+ " D<String> f2 = b;",
+ " }",
+ " void testNegative(B<String> b) {",
+ " D<String> f1 = new B<String>();",
+ " D<String> f2 = b;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void fancierSubtypeWithParameters() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " class Super<A extends @Nullable Object, B> {}",
+ " class Sub<C, D extends @Nullable Object> extends Super<D, C> {}",
+ " void testNegative() {",
+ " // valid assignment",
+ " Super<@Nullable String, String> s = new Sub<String, @Nullable String>();",
+ " }",
+ " void testPositive() {",
+ " // BUG: Diagnostic contains: Cannot assign from type",
+ " Super<@Nullable String, String> s2 = new Sub<@Nullable String, String>();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nestedVariableDeclarationChecks() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " class D<P extends @Nullable Object> {}",
+ " class B<P extends @Nullable Object> extends D<P> {}",
+ " class C<P extends @Nullable Object> {}",
+ " class A<T extends C<P>, P extends @Nullable Object> {}",
+ " void testPositive() {",
+ " // BUG: Diagnostic contains: Cannot assign from type",
+ " D<C<String>> f1 = new B<C<@Nullable String>>();",
+ " // BUG: Diagnostic contains: Cannot assign from type",
+ " A<C<String>, String> f2 = new A<C<String>, @Nullable String>();",
+ " // BUG: Diagnostic contains: Cannot assign from type",
+ " D<C<String>> f3 = new B<@Nullable C<String>>();",
+ " }",
+ " void testNegative() {",
+ " D<C<@Nullable String>> f1 = new B<C<@Nullable String>>();",
+ " A<C<String>, String> f2 = new A<C<String>, String>();",
+ " D<@Nullable C<String>> f3 = new B<@Nullable C<String>>();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testForMethodReferenceInAnAssignment() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface A<T1 extends @Nullable Object> {",
+ " String function(T1 o);",
+ " }",
+ " static String foo(Object o) {",
+ " return o.toString();",
+ " }",
+ " static void testPositive() {",
+ " // BUG: Diagnostic contains: parameter o of referenced method is @NonNull",
+ " A<@Nullable Object> p = Test::foo;",
+ " }",
+ " static void testNegative() {",
+ " A<Object> p = Test::foo;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testForMethodReferenceForClassFieldAssignment() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface A<T1 extends @Nullable Object> {",
+ " T1 function(Object o);",
+ " }",
+ " static @Nullable String foo(Object o) {",
+ " return o.toString();",
+ " }",
+ " // BUG: Diagnostic contains: referenced method returns @Nullable",
+ " A<String> positiveField = Test::foo;",
+ " A<@Nullable String> negativeField = Test::foo;",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testForMethodReferenceReturnTypeInAnAssignment() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface A<T1 extends @Nullable Object> {",
+ " T1 function(Object o);",
+ " }",
+ " static @Nullable String foo(Object o) {",
+ " return o.toString();",
+ " }",
+ " static void testPositive() {",
+ " // BUG: Diagnostic contains: referenced method returns @Nullable",
+ " A<String> p = Test::foo;",
+ " }",
+ " static void testNegative() {",
+ " A<@Nullable String> p = Test::foo;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testForMethodReferenceWhenReturned() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface A<T1 extends @Nullable Object> {",
+ " T1 function(Object o);",
+ " }",
+ " static @Nullable String foo(Object o) {",
+ " return o.toString();",
+ " }",
+ " static A<String> testPositive() {",
+ " // BUG: Diagnostic contains: referenced method returns @Nullable",
+ " return Test::foo;",
+ " }",
+ " static A<@Nullable String> testNegative() {",
+ " return Test::foo;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testForMethodReferenceAsMethodParameter() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface A<T1 extends @Nullable Object> {",
+ " T1 function(Object o);",
+ " }",
+ " static @Nullable String foo(Object o) {",
+ " return o.toString();",
+ " }",
+ " static void fooPositive(A<String> a) {",
+ " }",
+ " static void fooNegative(A<@Nullable String> a) {",
+ " }",
+ " static void testPositive() {",
+ " // BUG: Diagnostic contains: referenced method returns @Nullable",
+ " fooPositive(Test::foo);",
+ " }",
+ " static void testNegative() {",
+ " fooNegative(Test::foo);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testForLambdasInAnAssignmentWithSingleParam() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface A<T1 extends @Nullable Object> {",
+ " String function(T1 o);",
+ " }",
+ " static void testPositive() {",
+ " // BUG: Diagnostic contains: dereferenced expression o is @Nullable",
+ " A<@Nullable Object> p = o -> o.toString();",
+ " }",
+ " static void testNegative() {",
+ " A<Object> p = o -> o.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testForLambdasInAnAssignmentWithMultipleParams() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface A<T1 extends @Nullable Object,T2 extends @Nullable Object> {",
+ " String function(T1 o1,T2 o2);",
+ " }",
+ " static void testPositive() {",
+ " // BUG: Diagnostic contains: dereferenced expression o1 is @Nullable",
+ " A<@Nullable Object,Object> p = (o1,o2) -> o1.toString();",
+ " }",
+ " static void testNegative() {",
+ " A<@Nullable Object,Object> p = (o1,o2) -> o2.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testForLambdasInAnAssignmentWithoutJSpecifyMode() {
+ makeHelperWithoutJSpecifyMode()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface A<T1 extends @Nullable Object> {",
+ " String function(T1 o);",
+ " }",
+ " static void testPositive() {",
+ " // Using outside JSpecify Mode So we don't get a bug here",
+ " A<@Nullable Object> p = o -> o.toString();",
+ " }",
+ " static void testNegative() {",
+ " A<Object> p = o -> o.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testForLambdaReturnTypeInAnAssignment() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface A<T1 extends @Nullable Object> {",
+ " T1 function(Object o);",
+ " }",
+ " static void testPositive1() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression from method with @NonNull return type",
+ " A<String> p = x -> null;",
+ " }",
+ " static void testPositive2() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression from method with @NonNull return type",
+ " A<String> p = x -> { return null; };",
+ " }",
+ " static void testNegative1() {",
+ " A<@Nullable String> p = x -> null;",
+ " }",
+ " static void testNegative2() {",
+ " A<@Nullable String> p = x -> { return null; };",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void testForDiamondInAnAssignment() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface A<T1 extends @Nullable Object> {",
+ " String function(T1 o);",
+ " }",
+ " static class B<T1> implements A<T1> {",
+ " public String function(T1 o) {",
+ " return o.toString();",
+ " }",
+ " }",
+ " static void testPositive() {",
+ " // TODO: we should report an error here, since B's type parameter",
+ " // cannot be @Nullable; we do not catch this yet",
+ " A<@Nullable Object> p = new B<>();",
+ " }",
+ " static void testNegative() {",
+ " A<Object> p = new B<>();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void genericFunctionReturnTypeNewClassTree() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class A<T extends @Nullable Object> { }",
+ " static A<String> testPositive1() {",
+ " // BUG: Diagnostic contains: Cannot return expression of type A<@Nullable String>",
+ " return new A<@Nullable String>();",
+ " }",
+ " static A<@Nullable String> testPositive2() {",
+ " // BUG: Diagnostic contains: mismatched nullability of type parameters",
+ " return new A<String>();",
+ " }",
+ " static A<@Nullable String> testNegative() {",
+ " return new A<@Nullable String>();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void genericFunctionReturnTypeNormalTree() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class A<T extends @Nullable Object> { }",
+ " static A<String> testPositive(A<@Nullable String> a) {",
+ " // BUG: Diagnostic contains: mismatched nullability of type parameters",
+ " return a;",
+ " }",
+ " static A<@Nullable String> testNegative(A<@Nullable String> a) {",
+ " return a;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void genericFunctionReturnTypeMultipleReturnStatementsIfElseBlock() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class A<T extends @Nullable Object> { }",
+ " static A<String> testPositive(A<@Nullable String> a, int num) {",
+ " if (num % 2 == 0) {",
+ " // BUG: Diagnostic contains: mismatched nullability of type parameters",
+ " return a;",
+ " } else {",
+ " return new A<String>();",
+ " }",
+ " }",
+ " static A<String> testNegative(A<String> a, int num) {",
+ " return a;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void genericsChecksForTernaryOperator() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ "static class A<T extends @Nullable Object> { }",
+ " static A<String> testPositive(A<String> a, boolean t) {",
+ " // BUG: Diagnostic contains: Conditional expression must have type A<@Nullable String>",
+ " A<@Nullable String> t1 = t ? new A<String>() : new A<@Nullable String>();",
+ " // BUG: Diagnostic contains: Conditional expression must have type",
+ " return t ? new A<@Nullable String>() : new A<@Nullable String>();",
+ " }",
+ " static void testPositiveTernaryMethodArgument(boolean t) {",
+ " // BUG: Diagnostic contains: Conditional expression must have type",
+ " A<String> a = testPositive(t ? new A<String>() : new A<@Nullable String>(), t);",
+ " }",
+ " static A<@Nullable String> testNegative(boolean t) {",
+ " A<@Nullable String> t1 = t ? new A<@Nullable String>() : new A<@Nullable String>();",
+ " return t ? new A<@Nullable String>() : new A<@Nullable String>();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void ternaryOperatorComplexSubtyping() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class A<T extends @Nullable Object> {}",
+ " static class B<T extends @Nullable Object> extends A<T> {}",
+ " static class C<T extends @Nullable Object> extends A<T> {}",
+ " static void testPositive(boolean t) {",
+ " // BUG: Diagnostic contains: Conditional expression must have type",
+ " A<@Nullable String> t1 = t ? new B<@Nullable String>() : new C<String>();",
+ " // BUG: Diagnostic contains: Conditional expression must have type",
+ " A<@Nullable String> t2 = t ? new C<String>() : new B<@Nullable String>();",
+ " // BUG: Diagnostic contains:Conditional expression must have type",
+ " A<@Nullable String> t3 = t ? new B<String>() : new C<@Nullable String>();",
+ " // BUG: Diagnostic contains: Conditional expression must have type",
+ " A<String> t4 = t ? new B<@Nullable String>() : new C<@Nullable String>();",
+ " }",
+ " static void testNegative(boolean t) {",
+ " A<@Nullable String> t1 = t ? new B<@Nullable String>() : new C<@Nullable String>();",
+ " A<@Nullable String> t2 = t ? new C<@Nullable String>() : new B<@Nullable String>();",
+ " A<String> t3 = t ? new C<String>() : new B<String>();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nestedTernary() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class A<T extends @Nullable Object> {}",
+ " static class B<T extends @Nullable Object> extends A<T> {}",
+ " static class C<T extends @Nullable Object> extends A<T> {}",
+ " static void testPositive(boolean t) {",
+ " A<@Nullable String> t1 = t ? new C<@Nullable String>() :",
+ " // BUG: Diagnostic contains: Conditional expression must have type",
+ " (t ? new B<@Nullable String>() : new A<String>());",
+ " }",
+ " static void testNegative(boolean t) {",
+ " A<@Nullable String> t1 = t ? new C<@Nullable String>() :",
+ " (t ? new B<@Nullable String>() : new A<@Nullable String>());",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void ternaryMismatchedAssignmentContext() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ "static class A<T extends @Nullable Object> { }",
+ " static void testPositive(boolean t) {",
+ " // we get two errors here, one for each sub-expression; perhaps ideally we would report",
+ " // just one error (that the ternary operator has type A<String> but the assignment LHS",
+ " // has type A<@Nullable String>), but implementing that check in general is",
+ " // a bit tricky",
+ " A<@Nullable String> t1 = t",
+ " // BUG: Diagnostic contains: Conditional expression must have type",
+ " ? new A<String>()",
+ " // BUG: Diagnostic contains: Conditional expression must have type",
+ " : new A<String>();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void parameterPassing() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ "static class A<T extends @Nullable Object> { }",
+ " static A<String> sampleMethod1(A<A<String>> a1, A<String> a2) {",
+ " return a2;",
+ " }",
+ " static A<String> sampleMethod2(A<A<@Nullable String>> a1, A<String> a2) {",
+ " return a2;",
+ " }",
+ " static void sampleMethod3(A<@Nullable String> a1) {",
+ " }",
+ " static void testPositive1(A<A<@Nullable String>> a1, A<String> a2) {",
+ " // BUG: Diagnostic contains: Cannot pass parameter of type A<A<@Nullable String>>",
+ " A<String> a = sampleMethod1(a1, a2);",
+ " }",
+ " static void testPositive2(A<A<String>> a1, A<String> a2) {",
+ " // BUG: Diagnostic contains: Cannot pass parameter of type",
+ " A<String> a = sampleMethod2(a1, a2);",
+ " }",
+ " static void testPositive3(A<String> a1) {",
+ " // BUG: Diagnostic contains: Cannot pass parameter of type",
+ " sampleMethod3(a1);",
+ " }",
+ " static void testNegative(A<A<String>> a1, A<String> a2) {",
+ " A<String> a = sampleMethod1(a1, a2);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void varargsParameter() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class A<T extends @Nullable Object> { }",
+ " static A<@Nullable String> sampleMethodWithVarArgs(A<String>... args) {",
+ " return new A<@Nullable String>();",
+ " }",
+ " static void testPositive(A<@Nullable String> a1, A<String> a2) {",
+ " // BUG: Diagnostic contains: Cannot pass parameter of type",
+ " A<@Nullable String> b = sampleMethodWithVarArgs(a1);",
+ " // BUG: Diagnostic contains: Cannot pass parameter of type",
+ " A<@Nullable String> b2 = sampleMethodWithVarArgs(a2, a1);",
+ " }",
+ " static void testNegative(A<String> a1, A<String> a2) {",
+ " A<@Nullable String> b = sampleMethodWithVarArgs(a1);",
+ " A<@Nullable String> b2 = sampleMethodWithVarArgs(a2, a1);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ /**
+ * Currently this test is solely to ensure NullAway does not crash in the presence of raw types.
+ * Further study of the JSpecify documents is needed to determine whether any errors should be
+ * reported for these cases.
+ */
+ @Test
+ public void rawTypes() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class NonNullTypeParam<E> {}",
+ " static class NullableTypeParam<E extends @Nullable Object> {}",
+ " static void rawLocals() {",
+ " NonNullTypeParam<String> t1 = new NonNullTypeParam();",
+ " NullableTypeParam<@Nullable String> t2 = new NullableTypeParam();",
+ " NonNullTypeParam t3 = new NonNullTypeParam<String>();",
+ " NullableTypeParam t4 = new NullableTypeParam<@Nullable String>();",
+ " NonNullTypeParam t5 = new NonNullTypeParam();",
+ " NullableTypeParam t6 = new NullableTypeParam();",
+ " }",
+ " static void rawConditionalExpression(boolean b, NullableTypeParam<@Nullable String> p) {",
+ " NullableTypeParam<@Nullable String> t = b ? new NullableTypeParam() : p;",
+ " }",
+ " static void doNothing(NullableTypeParam<@Nullable String> p) { }",
+ " static void rawParameterPassing() { doNothing(new NullableTypeParam()); }",
+ " static NullableTypeParam<@Nullable String> rawReturn() {",
+ " return new NullableTypeParam();",
+ "}",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nestedGenericTypeAssignment() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class A<T extends @Nullable Object> { }",
+ " static void testPositive() {",
+ " // BUG: Diagnostic contains: Cannot assign from type",
+ " A<A<@Nullable String>[]> var1 = new A<A<String>[]>();",
+ " // BUG: Diagnostic contains: Cannot assign from type",
+ " A<A<String>[]> var2 = new A<A<@Nullable String>[]>();",
+ " }",
+ " static void testNegative() {",
+ " A<A<@Nullable String>[]> var1 = new A<A<@Nullable String>[]>();",
+ " A<A<String>[]> var2 = new A<A<String>[]>();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void genericPrimitiveArrayTypeAssignment() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class A<T extends @Nullable Object> { }",
+ " static void testPositive() {",
+ " // BUG: Diagnostic contains: Cannot assign from type A<int[]>",
+ " A<int @Nullable[]> x = new A<int[]>();",
+ " }",
+ " static void testNegative() {",
+ " A<int @Nullable[]> x = new A<int @Nullable[]>();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nestedGenericTypeVariables() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class A<T extends @Nullable Object> { }",
+ " static class B<T> {",
+ " void foo() {",
+ " A<A<T>[]> x = new A<A<T>[]>();",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nestedGenericWildcardTypeVariables() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " static class A<T extends @Nullable Object> { }",
+ " static class B<T> {",
+ " void foo() {",
+ " A<A<? extends String>[]> x = new A<A<? extends String>[]>();",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void overrideReturnTypes() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface Fn<P extends @Nullable Object, R extends @Nullable Object> {",
+ " R apply(P p);",
+ " }",
+ " static class TestFunc1 implements Fn<String, @Nullable String> {",
+ " @Override",
+ " public @Nullable String apply(String s) {",
+ " return s;",
+ " }",
+ " }",
+ " static class TestFunc2 implements Fn<String, @Nullable String> {",
+ " @Override",
+ " public String apply(String s) {",
+ " return s;",
+ " }",
+ " }",
+ " static class TestFunc3 implements Fn<String, String> {",
+ " @Override",
+ " // BUG: Diagnostic contains: method returns @Nullable, but superclass",
+ " public @Nullable String apply(String s) {",
+ " return s;",
+ " }",
+ " }",
+ " static class TestFunc4 implements Fn<@Nullable String, String> {",
+ " @Override",
+ " // BUG: Diagnostic contains: method returns @Nullable, but superclass",
+ " public @Nullable String apply(String s) {",
+ " return s;",
+ " }",
+ " }",
+ " static void useTestFunc(String s) {",
+ " Fn<String, @Nullable String> f1 = new TestFunc1();",
+ " String t1 = f1.apply(s);",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " t1.hashCode();",
+ " TestFunc2 f2 = new TestFunc2();",
+ " String t2 = f2.apply(s);",
+ " // There should not be an error here",
+ " t2.hashCode();",
+ " Fn<String, @Nullable String> f3 = new TestFunc2();",
+ " String t3 = f3.apply(s);",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " t3.hashCode();",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " f3.apply(s).hashCode();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void overrideWithNullCheck() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface Fn<P extends @Nullable Object, R extends @Nullable Object> {",
+ " R apply(P p);",
+ " }",
+ " static class TestFunc1 implements Fn<String, @Nullable String> {",
+ " @Override",
+ " public @Nullable String apply(String s) {",
+ " return s;",
+ " }",
+ " }",
+ " static void useTestFuncWithCast() {",
+ " Fn<String, @Nullable String> f1 = new TestFunc1();",
+ " if (f1.apply(\"hello\") != null) {",
+ " String t1 = f1.apply(\"hello\");",
+ " // no error here due to null check",
+ " t1.hashCode();",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void overrideParameterType() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface Fn<P extends @Nullable Object, R extends @Nullable Object> {",
+ " R apply(P p);",
+ " }",
+ " static class TestFunc1 implements Fn<@Nullable String, String> {",
+ " @Override",
+ " // BUG: Diagnostic contains: parameter s is",
+ " public String apply(String s) {",
+ " return s;",
+ " }",
+ " }",
+ " static class TestFunc2 implements Fn<@Nullable String, String> {",
+ " @Override",
+ " public String apply(@Nullable String s) {",
+ " return \"hi\";",
+ " }",
+ " }",
+ " static class TestFunc3 implements Fn<String, String> {",
+ " @Override",
+ " public String apply(String s) {",
+ " return \"hi\";",
+ " }",
+ " }",
+ " static class TestFunc4 implements Fn<String, String> {",
+ " // this override is legal, we should get no error",
+ " @Override",
+ " public String apply(@Nullable String s) {",
+ " return \"hi\";",
+ " }",
+ " }",
+ " static void useTestFunc(String s) {",
+ " Fn<@Nullable String, String> f1 = new TestFunc2();",
+ " // should get no error here",
+ " f1.apply(null);",
+ " Fn<String, String> f2 = new TestFunc3();",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " f2.apply(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void overrideExplicitlyTypedAnonymousClass() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface Fn<P extends @Nullable Object, R extends @Nullable Object> {",
+ " R apply(P p);",
+ " }",
+ " static abstract class FnClass<P extends @Nullable Object, R extends @Nullable Object> {",
+ " abstract R apply(P p);",
+ " }",
+ " static void anonymousClasses() {",
+ " Fn<@Nullable String, String> fn1 = new Fn<@Nullable String, String>() {",
+ " // BUG: Diagnostic contains: parameter s is @NonNull, but parameter in superclass method",
+ " public String apply(String s) { return s; }",
+ " };",
+ " FnClass<String, String> fn2 = new FnClass<String, String>() {",
+ " // BUG: Diagnostic contains: method returns @Nullable, but superclass method",
+ " public @Nullable String apply(String s) { return null; }",
+ " };",
+ " Fn<String, @Nullable String> fn3 = new Fn<String, @Nullable String>() {",
+ " public @Nullable String apply(String s) { return null; }",
+ " };",
+ " FnClass<@Nullable String, String> fn4 = new FnClass<@Nullable String, String>() {",
+ " public String apply(@Nullable String s) { return \"hello\"; }",
+ " };",
+ " }",
+ " static void anonymousClassesFullName() {",
+ " Test.Fn<@Nullable String, String> fn1 = new Test.Fn<@Nullable String, String>() {",
+ " // BUG: Diagnostic contains: parameter s is @NonNull, but parameter in superclass method",
+ " public String apply(String s) { return s; }",
+ " };",
+ " Test.FnClass<String, String> fn2 = new Test.FnClass<String, String>() {",
+ " // BUG: Diagnostic contains: method returns @Nullable, but superclass method",
+ " public @Nullable String apply(String s) { return null; }",
+ " };",
+ " Test.Fn<String, @Nullable String> fn3 = new Test.Fn<String, @Nullable String>() {",
+ " public @Nullable String apply(String s) { return null; }",
+ " };",
+ " Test.FnClass<@Nullable String, String> fn4 = new Test.FnClass<@Nullable String, String>() {",
+ " public String apply(@Nullable String s) { return \"hello\"; }",
+ " };",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Ignore("https://github.com/uber/NullAway/issues/836")
+ @Test
+ public void overrideAnonymousNestedClass() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " class Wrapper<P extends @Nullable Object> {",
+ " abstract class Fn<R extends @Nullable Object> {",
+ " abstract R apply(P p);",
+ " }",
+ " }",
+ " void anonymousNestedClasses() {",
+ " Wrapper<@Nullable String>.Fn<String> fn1 = (this.new Wrapper<@Nullable String>()).new Fn<String>() {",
+ " // BUG: Diagnostic contains: parameter s is @NonNull, but parameter in superclass method",
+ " public String apply(String s) { return s; }",
+ " };",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void explicitlyTypedAnonymousClassAsReceiver() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface Fn<P extends @Nullable Object, R extends @Nullable Object> {",
+ " R apply(P p);",
+ " }",
+ " static abstract class FnClass<P extends @Nullable Object, R extends @Nullable Object> {",
+ " abstract R apply(P p);",
+ " }",
+ " static void anonymousClasses() {",
+ " String s1 = (new Fn<String, @Nullable String>() {",
+ " public @Nullable String apply(String s) { return null; }",
+ " }).apply(\"hi\");",
+ " // BUG: Diagnostic contains: dereferenced expression s1",
+ " s1.hashCode();",
+ " String s2 = (new FnClass<String, @Nullable String>() {",
+ " public @Nullable String apply(String s) { return null; }",
+ " }).apply(\"hi\");",
+ " // BUG: Diagnostic contains: dereferenced expression s2",
+ " s2.hashCode();",
+ " (new Fn<String, String>() {",
+ " public String apply(String s) { return \"hi\"; }",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " }).apply(null);",
+ " (new FnClass<String, String>() {",
+ " public String apply(String s) { return \"hi\"; }",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " }).apply(null);",
+ " (new Fn<@Nullable String, String>() {",
+ " public String apply(@Nullable String s) { return \"hi\"; }",
+ " }).apply(null);",
+ " (new FnClass<@Nullable String, String>() {",
+ " public String apply(@Nullable String s) { return \"hi\"; }",
+ " }).apply(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ /** Diamond anonymous classes are not supported yet; tests are for future reference */
+ @Test
+ public void overrideDiamondAnonymousClass() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface Fn<P extends @Nullable Object, R extends @Nullable Object> {",
+ " R apply(P p);",
+ " }",
+ " static abstract class FnClass<P extends @Nullable Object, R extends @Nullable Object> {",
+ " abstract R apply(P p);",
+ " }",
+ " static void anonymousClasses() {",
+ " Fn<@Nullable String, String> fn1 = new Fn<>() {",
+ " // TODO: should report a bug here",
+ " public String apply(String s) { return s; }",
+ " };",
+ " FnClass<@Nullable String, String> fn2 = new FnClass<>() {",
+ " // TODO: should report a bug here",
+ " public String apply(String s) { return s; }",
+ " };",
+ " Fn<String, @Nullable String> fn3 = new Fn<>() {",
+ " // TODO: this is a false positive",
+ " // BUG: Diagnostic contains: method returns @Nullable, but superclass method",
+ " public @Nullable String apply(String s) { return null; }",
+ " };",
+ " FnClass<String, @Nullable String> fn4 = new FnClass<>() {",
+ " // TODO: this is a false positive",
+ " // BUG: Diagnostic contains: method returns @Nullable, but superclass method",
+ " public @Nullable String apply(String s) { return null; }",
+ " };",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullableGenericTypeVariableReturnType() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " interface Fn<P extends @Nullable Object, R> {",
+ " @Nullable R apply(P p);",
+ " }",
+ " static class TestFunc implements Fn<String, String> {",
+ " @Override",
+ " //This override is fine and is handled by the current code",
+ " public @Nullable String apply(String s) {",
+ " return s;",
+ " }",
+ " }",
+ " static void useTestFunc(String s) {",
+ " Fn<String, String> f = new TestFunc();",
+ " String t = f.apply(s);",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " t.hashCode();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void overrideWithNestedTypeParametersInReturnType() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " class P<T1 extends @Nullable Object, T2 extends @Nullable Object>{}",
+ " interface Fn<T3 extends P<T4, T4>, T4 extends @Nullable Object> {",
+ " T3 apply();",
+ " }",
+ " class TestFunc1 implements Fn<P<@Nullable String, String>, @Nullable String> {",
+ " @Override",
+ " // BUG: Diagnostic contains: Method returns Test.P<@Nullable String, @Nullable String>, but overridden method",
+ " public P<@Nullable String, @Nullable String> apply() {",
+ " return new P<@Nullable String, @Nullable String>();",
+ " }",
+ " }",
+ " class TestFunc2 implements Fn<P<@Nullable String, @Nullable String>, @Nullable String> {",
+ " @Override",
+ " // BUG: Diagnostic contains: Method returns Test.P<@Nullable String, String>, but overridden method returns",
+ " public P<@Nullable String, String> apply() {",
+ " return new P<@Nullable String, String>();",
+ " }",
+ " }",
+ " class TestFunc3 implements Fn<P<@Nullable String, @Nullable String>, @Nullable String> {",
+ " @Override",
+ " public P<@Nullable String, @Nullable String> apply() {",
+ " return new P<@Nullable String, @Nullable String>();",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void overrideWithNestedTypeParametersInParameterType() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "class Test {",
+ " class P<T1 extends @Nullable Object, T2 extends @Nullable Object>{}",
+ " interface Fn<T extends P<R, R>, R extends @Nullable Object> {",
+ " String apply(T t, String s);",
+ " }",
+ " class TestFunc implements Fn<P<String, String>, String> {",
+ " @Override",
+ " // BUG: Diagnostic contains: Parameter has type Test.P<@Nullable String, String>, but overridden method has parameter type Test.P<String, String>",
+ " public String apply(P<@Nullable String, String> p, String s) {",
+ " return s;",
+ " }",
+ " }",
+ " class TestFunc2 implements Fn<P<String, String>, String> {",
+ " @Override",
+ " public String apply(P<String, String> p, String s) {",
+ " return s;",
+ " }",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void interactionWithContracts() {
+ makeHelper()
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "import org.jetbrains.annotations.Contract;",
+ "class Test {",
+ " interface Fn1<P extends @Nullable Object, R extends @Nullable Object> {",
+ " R apply(P p);",
+ " }",
+ " static class TestFunc1 implements Fn1<@Nullable String, @Nullable String> {",
+ " @Override",
+ " @Contract(\"!null -> !null\")",
+ " public @Nullable String apply(@Nullable String s) {",
+ " return s;",
+ " }",
+ " }",
+ " interface Fn2<P extends @Nullable Object, R extends @Nullable Object> {",
+ " @Contract(\"!null -> !null\")",
+ " R apply(P p);",
+ " }",
+ " static class TestFunc2 implements Fn2<@Nullable String, @Nullable String> {",
+ " @Override",
+ " public @Nullable String apply(@Nullable String s) {",
+ " return s;",
+ " }",
+ " }",
+ " static class TestFunc2_Bad implements Fn2<@Nullable String, @Nullable String> {",
+ " @Override",
+ " public @Nullable String apply(@Nullable String s) {",
+ " // False negative: with contract checking enabled, this should be rejected",
+ " // See https://github.com/uber/NullAway/issues/803",
+ " return null;",
+ " }",
+ " }",
+ " static void testMethod() {",
+ " // No error due to @Contract",
+ " (new TestFunc1()).apply(\"hello\").hashCode();",
+ " Fn1<@Nullable String, @Nullable String> fn1 = new TestFunc1();",
+ " // BUG: Diagnostic contains: dereferenced expression fn1.apply(\"hello\")",
+ " fn1.apply(\"hello\").hashCode();",
+ " // BUG: Diagnostic contains: dereferenced expression (new TestFunc2())",
+ " (new TestFunc2()).apply(\"hello\").hashCode();",
+ " Fn2<@Nullable String, @Nullable String> fn2 = new TestFunc2();",
+ " // No error due to @Contract",
+ " fn2.apply(\"hello\").hashCode();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ private CompilationTestHelper makeHelper() {
+ return makeTestHelperWithArgs(
+ Arrays.asList(
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber", "-XepOpt:NullAway:JSpecifyMode=true"));
+ }
+
+ private CompilationTestHelper makeHelperWithoutJSpecifyMode() {
+ return makeTestHelperWithArgs(Arrays.asList("-XepOpt:NullAway:AnnotatedPackages=com.uber"));
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyTests.java
new file mode 100644
index 0000000..ed9acb1
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyTests.java
@@ -0,0 +1,1133 @@
+package com.uber.nullaway;
+
+import java.util.Arrays;
+import org.junit.Test;
+
+public class NullAwayJSpecifyTests extends NullAwayTestsBase {
+
+ @Test
+ public void nullMarkedPackageLevel() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "package-info.java",
+ "@NullMarked package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;")
+ .addSourceLines(
+ "ThirdPartyAnnotatedUtils.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.Nullable;",
+ "public class ThirdPartyAnnotatedUtils {",
+ " public static String toStringOrDefault(@Nullable Object o1, String s) {",
+ " if (o1 != null) {",
+ " return o1.toString();",
+ " }",
+ " return s;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.example.thirdparty.ThirdPartyAnnotatedUtils;",
+ "public class Test {",
+ " public static void test(Object o) {",
+ " // Safe: passing @NonNull on both args",
+ " ThirdPartyAnnotatedUtils.toStringOrDefault(o, \"default\");",
+ " // Safe: first arg is @Nullable",
+ " ThirdPartyAnnotatedUtils.toStringOrDefault(null, \"default\");",
+ " // Unsafe: @NullMarked means the second arg is @NonNull by default, not @Nullable",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " ThirdPartyAnnotatedUtils.toStringOrDefault(o, null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullMarkedPackageEnablesChecking() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "package-info.java",
+ "@NullMarked package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;")
+ .addSourceLines(
+ "Foo.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.Nullable;",
+ "public class Foo {",
+ " public static String foo(String s) {",
+ " return s;",
+ " }",
+ " public static void test() {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " foo(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullMarkedClassLevel() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;",
+ "@NullMarked",
+ "public class Foo {",
+ " public static String foo(String s) {",
+ " return s;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.example.thirdparty.Foo;",
+ "public class Test {",
+ " public static void test(Object o) {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Foo.foo(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullMarkedClassLevelEnablesChecking() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;",
+ "@NullMarked",
+ "public class Foo {",
+ " public static String foo(String s) {",
+ " return s;",
+ " }",
+ " public static void test(Object o) {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " foo(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullMarkedClassLevelOuter() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Bar.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.Nullable;",
+ "import org.jspecify.annotations.NullMarked;",
+ "@NullMarked",
+ "public class Bar {",
+ " public static class Foo {",
+ " public static String foo(String s) {",
+ " return s;",
+ " }",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.example.thirdparty.Bar;",
+ "public class Test {",
+ " public static void test(Object o) {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Bar.Foo.foo(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullMarkedClassLevelInner() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Bar.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.Nullable;",
+ "import org.jspecify.annotations.NullMarked;",
+ "public class Bar {",
+ " @NullMarked",
+ " public static class Foo {",
+ " public static String foo(String s) {",
+ " return s;",
+ " }",
+ " }",
+ " public static void unchecked(Object o) {}",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.example.thirdparty.Bar;",
+ "public class Test {",
+ " public static void test(Object o) {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Bar.Foo.foo(null);",
+ " Bar.unchecked(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullMarkedClassLevelInnerControlsChecking() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Bar.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.Nullable;",
+ "import org.jspecify.annotations.NullMarked;",
+ "public class Bar {",
+ " @NullMarked",
+ " public static class Foo {",
+ " public static String foo(String s) {",
+ " return s;",
+ " }",
+ " // @NullMarked should also control checking of source",
+ " public static void test(Object o) {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " foo(null);",
+ " }",
+ " }",
+ " public static void unchecked() {",
+ " Object x = null;",
+ " // fine since this code is still unchecked",
+ " x.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullMarkedClassLevelLocalAndEnvironment() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.Nullable;",
+ "import org.jspecify.annotations.NullMarked;",
+ "public class Test {",
+ " public static Object test() {",
+ " Object x = null;",
+ " final Object y = new Object();",
+ " @NullMarked",
+ " class Local {",
+ " public Object returnsNonNull() {",
+ " return y;",
+ " }",
+ " @Nullable",
+ " public Object returnsNullable() {",
+ " return x;",
+ " }",
+ " public Object returnsNonNullWithError() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return x;",
+ " }",
+ " }",
+ " Local local = new Local();",
+ " // Allowed, since unmarked",
+ " return local.returnsNullable();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullMarkedMethodLevel() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;",
+ "public class Foo {",
+ " @NullMarked",
+ " public static String foo(String s) {",
+ " return s;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.example.thirdparty.Foo;",
+ "public class Test {",
+ " public static void test(Object o) {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Foo.foo(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullMarkedMethodLevelScan() {
+ // Test that we turn on analysis/scanning within @NullMarked methods in a non-annotated
+ // package/class
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;",
+ "public class Foo {",
+ " @NullMarked",
+ " public static String foo(String s) {",
+ " return s;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Bar.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;",
+ "public class Bar {",
+ " public static void bar1() {",
+ " // No report, unannotated caller!",
+ " Foo.foo(null);",
+ " }",
+ " @NullMarked",
+ " public static void bar2() {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Foo.foo(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullMarkedOuterMethodLevelWithAnonymousClass() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;",
+ "public class Foo {",
+ " @NullMarked",
+ " public static String foo(String s) {",
+ " return s;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Bar.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;",
+ "public class Bar {",
+ " @NullMarked",
+ " public static Runnable runFoo() {",
+ " return new Runnable() {",
+ " public void run() {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Foo.foo(null);",
+ " }",
+ " };",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullMarkedOuterMethodLevelUsage() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "IConsumer.java",
+ "package com.example.thirdparty;",
+ "public interface IConsumer {",
+ " void consume(Object s);",
+ "}")
+ .addSourceLines(
+ "Foo.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;",
+ "public class Foo {",
+ " @NullMarked",
+ " public static IConsumer getConsumer() {",
+ " return new IConsumer() {",
+ " // Transitively null marked! Explicitly non-null arg, which is a safe override of unknown-nullness.",
+ " public void consume(Object s) {",
+ " System.out.println(s);",
+ " }",
+ " };",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.example.thirdparty.Foo;",
+ "public class Test {",
+ " public static void test(Object o) {",
+ " // Safe because IConsumer::consume is unmarked? And no static knowledge of Foo$1",
+ " Foo.getConsumer().consume(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullMarkedOuterMethodLevelWithLocalClass() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;",
+ "public class Foo {",
+ " @NullMarked",
+ " public static String foo(String s) {",
+ " return s;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Bar.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;",
+ "public class Bar {",
+ " @NullMarked",
+ " public static Object bar() {",
+ " class Baz {",
+ " public void baz() {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Foo.foo(null);",
+ " }",
+ " }",
+ " Baz b = new Baz();",
+ " b.baz();",
+ " return b;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullMarkedOuterMethodLevelWithLocalClassInit() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;",
+ "public class Test {",
+ " @NullMarked",
+ " public static Object test() {",
+ " class Foo {",
+ " private Object o;",
+ " // BUG: Diagnostic contains: initializer method does not guarantee @NonNull field o",
+ " public Foo() { }",
+ " public String foo(String s) {",
+ " return s;",
+ " }",
+ " }",
+ " return new Foo();",
+ " }",
+ " public static Object test2() {",
+ " class Foo {",
+ " private Object o;", // No init checking, since Test$2Foo is unmarked.
+ " public Foo() { }",
+ " @NullMarked",
+ " public String foo(String s) {",
+ " return s;",
+ " }",
+ " }",
+ " return new Foo();",
+ " }",
+ " public static Object test3() {",
+ " class Foo {",
+ " private Object o;", // No init checking, since Test$2Foo is unmarked.
+ " public Foo() { }",
+ " @NullMarked",
+ " public String foo() {",
+ " return o.toString();",
+ " }",
+ " }",
+ " return new Foo();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void configUnannotatedOverridesNullMarked() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:UnannotatedSubPackages=com.example"))
+ .addSourceLines(
+ "package-info.java",
+ "@NullMarked package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;")
+ .addSourceLines(
+ "Foo.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.Nullable;",
+ "public class Foo {",
+ " public static String foo(String s) {",
+ " return s;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.example.thirdparty.Foo;",
+ "public class Test {",
+ " public static void test(Object o) {",
+ " // Safe: Foo is treated as unannotated",
+ " Foo.foo(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void bytecodeNullMarkedPackageLevel() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.example.jspecify.annotatedpackage.Utils;",
+ "public class Test {",
+ " public static void test(Object o) {",
+ " // Safe: passing @NonNull on both args",
+ " Utils.toStringOrDefault(o, \"default\");",
+ " // Safe: first arg is @Nullable",
+ " Utils.toStringOrDefault(null, \"default\");",
+ " // Unsafe: @NullMarked means the second arg is @NonNull by default, not @Nullable",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Utils.toStringOrDefault(o, null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void bytecodeNullMarkedClassLevel() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.example.jspecify.unannotatedpackage.TopLevel;",
+ "public class Test {",
+ " public static void test(Object o) {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " TopLevel.foo(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void bytecodeNullMarkedClassLevelInner() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.example.jspecify.unannotatedpackage.Outer;",
+ "public class Test {",
+ " public static void test(Object o) {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Outer.Inner.foo(null);",
+ " Outer.unchecked(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void bytecodeNullMarkedMethodLevel() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.example.jspecify.unannotatedpackage.Methods;",
+ "public class Test {",
+ " public static void test(Object o) {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Methods.foo(null);",
+ " Methods.unchecked(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void bytecodeNullMarkedMethodLevelOverriding() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.example.jspecify.unannotatedpackage.Methods;",
+ "import org.jspecify.annotations.Nullable;",
+ "public class Test extends Methods.ExtendMe {",
+ " @Nullable",
+ " // BUG: Diagnostic contains: method returns @Nullable, but superclass method",
+ " public Object foo(@Nullable Object o) { return o; }",
+ " @Nullable",
+ " public Object unchecked(@Nullable Object o) { return o; }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullUnmarkedPackageLevel() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "package-info.java",
+ "@NullUnmarked package com.uber.unmarked;",
+ "import org.jspecify.annotations.NullUnmarked;")
+ .addSourceLines(
+ "MarkedBecauseAnnotatedFlag.java",
+ "package com.uber.marked;",
+ "import org.jspecify.annotations.Nullable;",
+ "public class MarkedBecauseAnnotatedFlag {",
+ " public static String nullSafeStringOrDefault(@Nullable Object o1, String s) {",
+ " if (o1 != null) {",
+ " return o1.toString();",
+ " }",
+ " return s;",
+ " }",
+ " @Nullable",
+ " public static String nullRet() {",
+ " return null;",
+ " }",
+ " public static String unsafeStringOrDefault(@Nullable Object o1, String s) {",
+ " // BUG: Diagnostic contains: dereferenced expression o1 is @Nullable",
+ " return o1.toString();",
+ " }",
+ "}")
+ .addSourceLines(
+ "UnmarkedBecausePackageDirectAnnotation.java",
+ "package com.uber.unmarked;",
+ "import com.uber.marked.MarkedBecauseAnnotatedFlag;",
+ "import org.jspecify.annotations.Nullable;",
+ "public class UnmarkedBecausePackageDirectAnnotation {",
+ " public static String directlyUnsafeStringOrDefault(@Nullable Object o1, String s) {",
+ " // No error: unannotated",
+ " return o1.toString();",
+ " }",
+ " @Nullable",
+ " public static String nullRet() {",
+ " return null;",
+ " }",
+ " public static String indirectlyUnsafeStringOrDefault(@Nullable Object o1, String s) {",
+ " // No error: unannotated",
+ " return (o1 == null ? MarkedBecauseAnnotatedFlag.nullRet() : o1.toString());",
+ " }",
+ "}")
+ .addSourceLines(
+ "MarkedImplicitly.java",
+ "package com.uber.unmarked.bar;",
+ "// Note: this package is annotated, because packages do not enclose each other for the purposes",
+ "// of @NullMarked/@NullUnmarked, see https://jspecify.dev/docs/spec#null-marked-scope",
+ "import com.uber.marked.MarkedBecauseAnnotatedFlag;",
+ "import com.uber.unmarked.UnmarkedBecausePackageDirectAnnotation;",
+ "import org.jspecify.annotations.Nullable;",
+ "public class MarkedImplicitly {",
+ " public static String directlyUnsafeStringOrDefault(@Nullable Object o1, String s) {",
+ " // BUG: Diagnostic contains: dereferenced expression o1 is @Nullable",
+ " return o1.toString();",
+ " }",
+ " public static String indirectlyUnsafeStringOrDefault(@Nullable Object o1, String s) {",
+ " // BUG: Diagnostic contains: returning @Nullable expression from method",
+ " return (o1 == null ? MarkedBecauseAnnotatedFlag.nullRet() : o1.toString());",
+ " }",
+ " public static String indirectlyUnsafeStringOrDefaultCallingUnmarked(@Nullable Object o1, String s) {",
+ " // No error: nullRet() is unannotated",
+ " return (o1 == null ? UnmarkedBecausePackageDirectAnnotation.nullRet() : o1.toString());",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullUnmarkedClassLevel() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "package-info.java",
+ "@NullMarked package com.example.thirdparty.marked;",
+ "import org.jspecify.annotations.NullMarked;")
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber.foo;",
+ "import org.jspecify.annotations.NullUnmarked;",
+ "import org.jspecify.annotations.Nullable;",
+ "@NullUnmarked",
+ "public class Foo {",
+ " @Nullable",
+ " public static String nullRet() {",
+ " return null;",
+ " }",
+ " public static String takeNonNull(Object o) {",
+ " return o.toString();",
+ " }",
+ " public static String takeNullable(@Nullable Object o) {",
+ " return o.toString();",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.uber.foo.Foo;",
+ "public class Test {",
+ " public static Object test(Object o) {",
+ " // No errors, because Foo is @NullUnmarked",
+ " Foo.takeNonNull(null);",
+ " return Foo.nullRet();",
+ " }",
+ " public static String sanity() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ "}")
+ // Note: Safe to have same-name files in recent Error Prone, but breaks EP 2.4.0
+ .addSourceLines(
+ "Test2.java",
+ "package com.example.thirdparty.marked;",
+ "import com.uber.foo.Foo;",
+ "public class Test2 {",
+ " public static Object test(Object o) {",
+ " // No errors, because Foo is @NullUnmarked",
+ " Foo.takeNonNull(null);",
+ " return Foo.nullRet();",
+ " }",
+ " public static String sanity() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullUnmarkedClassLevelOuter() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Bar.java",
+ "package com.uber.foo;",
+ "import org.jspecify.annotations.NullUnmarked;",
+ "import org.jspecify.annotations.Nullable;",
+ "@NullUnmarked",
+ "public class Bar {",
+ " public static class Foo {",
+ " @Nullable",
+ " public static String nullRet() {",
+ " return null;",
+ " }",
+ " public static String takeNonNull(Object o) {",
+ " return o.toString();",
+ " }",
+ " public static String takeNullable(@Nullable Object o) {",
+ " // No errors, because Foo is @NullUnmarked",
+ " return o.toString();",
+ " }",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.uber.foo.Bar;",
+ "public class Test {",
+ " public static Object test(Object o) {",
+ " // No errors, because Foo is @NullUnmarked",
+ " Bar.Foo.takeNonNull(null);",
+ " return Bar.Foo.nullRet();",
+ " }",
+ " public static String sanity() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullUnmarkedMarkedClassLevelInner() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Bar.java",
+ "package com.uber.foo;",
+ "import org.jspecify.annotations.NullUnmarked;",
+ "import org.jspecify.annotations.Nullable;",
+ "public class Bar {",
+ " @NullUnmarked",
+ " public static class Foo {",
+ " @Nullable",
+ " public static String nullRet() {",
+ " return null;",
+ " }",
+ " public static String takeNonNull(Object o) {",
+ " return o.toString();",
+ " }",
+ " public static String takeNullable(@Nullable Object o) {",
+ " // No errors, because Foo is @NullUnmarked",
+ " return o.toString();",
+ " }",
+ " }",
+ " // In marked outer class Bar",
+ " public static String sanity() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.uber.foo.Bar;",
+ "public class Test {",
+ " public static Object test(Object o) {",
+ " // No errors, because Foo is @NullUnmarked",
+ " Bar.Foo.takeNonNull(null);",
+ " return Bar.Foo.nullRet();",
+ " }",
+ " public static String sanity() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullUnmarkedClassLevelDeep() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Bar.java",
+ "package com.uber.foo;",
+ "import org.jspecify.annotations.NullMarked;",
+ "import org.jspecify.annotations.NullUnmarked;",
+ "import org.jspecify.annotations.Nullable;",
+ "public class Bar {",
+ " @NullUnmarked",
+ " public static class Foo {",
+ " @NullMarked",
+ " public static class Deep {",
+ " @Nullable",
+ " public static String nullRet() {",
+ " return null;",
+ " }",
+ " public static String takeNonNull(Object o) {",
+ " return o.toString();",
+ " }",
+ " public static String takeNullable(@Nullable Object o) {",
+ " // BUG: Diagnostic contains: dereferenced expression o is @Nullable",
+ " return o.toString();",
+ " }",
+ " }",
+ " // In unmarked inner class Foo",
+ " public static String sanity() {",
+ " // No errors, because Foo is @NullUnmarked,",
+ " return null;",
+ " }",
+ " }",
+ " // In marked outer class Bar",
+ " public static String sanity() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.uber.foo.Bar;",
+ "public class Test {",
+ " public static void test(Object o) {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'Bar.Foo.Deep.nullRet()'",
+ " Bar.Foo.Deep.takeNonNull(Bar.Foo.Deep.nullRet());",
+ " }",
+ " public static String sanity() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullUnmarkedMethodLevel() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.NullUnmarked;",
+ "import org.jspecify.annotations.Nullable;",
+ "public class Foo {",
+ " @NullUnmarked",
+ " @Nullable",
+ " public static String callee(@Nullable Object o) {",
+ " // No error, since this code is unannotated",
+ " return o.toString();",
+ " }",
+ " public static String caller() {",
+ " // No error, since callee is unannotated",
+ " return callee(null);",
+ " }",
+ " public static String sanity() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullUnmarkedOuterMethodLevelWithLocalClass() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Bar.java",
+ "package com.example.thirdparty;",
+ "import org.jspecify.annotations.NullMarked;",
+ "import org.jspecify.annotations.NullUnmarked;",
+ "@NullMarked",
+ "public class Bar {",
+ " public static String takeNonNull(Object o) {",
+ " return o.toString();",
+ " }",
+ " @NullUnmarked",
+ " public static Object bar1() {",
+ " class Baz {",
+ " public void baz() {",
+ " // No error, unmarked code",
+ " Bar.takeNonNull(null);",
+ " }",
+ " }",
+ " Baz b = new Baz();",
+ " b.baz();",
+ " return b;",
+ " }",
+ " @NullUnmarked",
+ " public static Object bar2() {",
+ " @NullMarked",
+ " class Baz {",
+ " public void baz() {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Bar.takeNonNull(null);",
+ " }",
+ " }",
+ " Baz b = new Baz();",
+ " b.baz();",
+ " return b;",
+ " }",
+ " @NullUnmarked",
+ " public static Object bar3() {",
+ " class Baz {",
+ " @NullMarked",
+ " public void baz() {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Bar.takeNonNull(null);",
+ " }",
+ " }",
+ " Baz b = new Baz();",
+ " b.baz();",
+ " return b;",
+ " }",
+ " public static String sanity() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void bytecodeNullUnmarkedMethodLevel() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import com.example.jspecify.unannotatedpackage.Methods;",
+ "public class Test {",
+ " public static void test(Object o) {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Methods.Marked.foo(null);",
+ " Methods.Marked.unchecked(null);",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullUnmarkedAndAcknowledgeRestrictiveAnnotations() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ // Flag is required for now, but might no longer be need with @NullMarked!
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber.dontcare",
+ "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true"))
+ .addSourceLines(
+ "Foo.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.NullMarked;",
+ "import org.jspecify.annotations.NullUnmarked;",
+ "import org.jspecify.annotations.Nullable;",
+ "@NullUnmarked",
+ "public class Foo {",
+ " // No initialization warning, Foo is unmarked",
+ " @Nullable public Object f;",
+ " @Nullable",
+ " public String callee(@Nullable Object o) {",
+ " // No error, since this code is unannotated",
+ " return o.toString() + f.toString();",
+ " }",
+ " @NullMarked",
+ " public String caller() {",
+ " // Error, since callee still has restrictive annotations!",
+ " // BUG: Diagnostic contains: returning @Nullable expression from method",
+ " return callee(null);",
+ " }",
+ " @NullMarked",
+ " public Object getF() {",
+ " // Error, since callee still has restrictive annotations!",
+ " // BUG: Diagnostic contains: returning @Nullable expression from method",
+ " return f;",
+ " }",
+ " @NullMarked",
+ " public String derefUnmarkedField() {",
+ " // Error, since callee still has restrictive annotations!",
+ " // BUG: Diagnostic contains: dereferenced expression f is @Nullable",
+ " return f.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void nullMarkedStaticImports() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ // Flag is required for now, but might no longer be need with @NullMarked!
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber.dontcare",
+ "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true"))
+ .addSourceLines(
+ "StaticMethods.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.NullMarked;",
+ "import org.jspecify.annotations.Nullable;",
+ "public final class StaticMethods {",
+ " private StaticMethods() {}",
+ " @NullMarked",
+ " public static Object nonNullCallee(Object o) {",
+ " return o;",
+ " }",
+ " @NullMarked",
+ " @Nullable",
+ " public static Object nullableCallee(@Nullable Object o) {",
+ " return o;",
+ " }",
+ " public static Object unmarkedCallee(@Nullable Object o) {",
+ " // no error, because unmarked",
+ " return o;",
+ " }",
+ " @Nullable",
+ " public static Object unmarkedNullableCallee(@Nullable Object o) {",
+ " return o;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import static com.uber.StaticMethods.nonNullCallee;",
+ "import static com.uber.StaticMethods.nullableCallee;",
+ "import static com.uber.StaticMethods.unmarkedCallee;",
+ "import static com.uber.StaticMethods.unmarkedNullableCallee;",
+ "import org.jspecify.annotations.NullMarked;",
+ "import org.jspecify.annotations.Nullable;",
+ "@NullMarked",
+ "public class Test {",
+ " public Object getNewObject() {",
+ " return new Object();",
+ " }",
+ " public void test() {",
+ " Object o = getNewObject();",
+ " nonNullCallee(o).toString();",
+ " // BUG: Diagnostic contains: dereferenced expression nullableCallee(o) is @Nullable",
+ " nullableCallee(o).toString();",
+ " unmarkedCallee(o).toString();",
+ " // BUG: Diagnostic contains: dereferenced expression unmarkedNullableCallee(o) is @Nullable",
+ " unmarkedNullableCallee(o).toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void dotClassSanityTest1() {
+ // Check that we do not crash while determining the nullmarked-ness of primitive.class (e.g.
+ // int.class)
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ // Flag is required for now, but might no longer be need with @NullMarked!
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber.dontcare",
+ "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.NullMarked;",
+ "import org.jspecify.annotations.Nullable;",
+ "import java.lang.reflect.Field;",
+ "@NullMarked",
+ "public class Test {",
+ " public void takesClass(Class c) {",
+ " }",
+ " public Object test(boolean flag) {",
+ " takesClass(Test.class);",
+ " takesClass(String.class);",
+ " takesClass(int.class);",
+ " takesClass(boolean.class);",
+ " takesClass(float.class);",
+ " takesClass(void.class);",
+ " // NEEDED TO TRIGGER DATAFLOW:",
+ " return flag ? Test.class : new Object();",
+ " }",
+ " public boolean test2(Field field) {",
+ " if (field.getType() == int.class || field.getType() == Integer.class) {",
+ " return true;",
+ " }",
+ " return false;",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void dotClassSanityTest2() {
+ // Check that we do not crash while determining the nullmarked-ness of primitive.class (e.g.
+ // int.class)
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ // Flag is required for now, but might no longer be need with @NullMarked!
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber.dontcare",
+ "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true",
+ "-XepOpt:NullAway:TreatGeneratedAsUnannotated=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.NullMarked;",
+ "import org.jspecify.annotations.Nullable;",
+ "import java.lang.reflect.Field;",
+ "@NullMarked",
+ "public class Test {",
+ " public void takesClass(Class c) {",
+ " }",
+ " public Object test(boolean flag) {",
+ " takesClass(Test.class);",
+ " takesClass(String.class);",
+ " takesClass(int.class);",
+ " takesClass(boolean.class);",
+ " takesClass(float.class);",
+ " takesClass(void.class);",
+ " // NEEDED TO TRIGGER DATAFLOW:",
+ " return flag ? Test.class : new Object();",
+ " }",
+ " public boolean test2(Field field) {",
+ " if (field.getType() == int.class || field.getType() == Integer.class) {",
+ " return true;",
+ " }",
+ " return false;",
+ " }",
+ "}")
+ .doTest();
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayOptionalEmptinessTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayOptionalEmptinessTests.java
index 9dd928d..32199f4 100644
--- a/nullaway/src/test/java/com/uber/nullaway/NullAwayOptionalEmptinessTests.java
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayOptionalEmptinessTests.java
@@ -372,6 +372,155 @@ public class NullAwayOptionalEmptinessTests extends NullAwayTestsBase {
}
@Test
+ public void optionalEmptinessHandleAssertionLibraryTruthAssertThat() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:CheckOptionalEmptiness=true",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Optional;",
+ "import com.google.common.truth.Truth;",
+ "",
+ "public class Test {",
+ " void truthAssertThatIsPresentIsTrue() {",
+ " Optional<Object> a = Optional.empty();",
+ " Truth.assertThat(a.isPresent()).isFalse(); // no impact",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional a",
+ " a.get().toString();",
+ " Truth.assertThat(a.isPresent()).isTrue();",
+ " a.get().toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void optionalEmptinessHandleAssertionLibraryAssertJAssertThat() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:CheckOptionalEmptiness=true",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.HashMap;",
+ "import java.util.Map;",
+ "import java.util.Optional;",
+ "import org.assertj.core.api.Assertions;",
+ "",
+ "public class Test {",
+ " void assertJAssertThatIsPresentIsTrue() {",
+ " Optional<Object> a = Optional.empty();",
+ " Assertions.assertThat(a.isPresent()).isFalse(); // no impact",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional a",
+ " a.get().toString();",
+ " Assertions.assertThat(a.isPresent()).isTrue();",
+ " a.get().toString();",
+ " }",
+ "",
+ " void assertJAssertThatOptionalIsPresent() {",
+ " Optional<Object> c = Optional.empty();",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional c",
+ " c.get().toString();",
+ " Assertions.assertThat(c).isPresent();",
+ " c.get().toString();",
+ " }",
+ "",
+ " void assertJAssertThatOptionalIsNotEmpty() {",
+ " Optional<Object> d = Optional.empty();",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional d",
+ " d.get().toString();",
+ " Assertions.assertThat(d).isNotEmpty();",
+ " d.get().toString();",
+ " }",
+ "",
+ " static Optional<String> aStaticField = Optional.empty();",
+ " Optional<String> aField = Optional.empty();",
+ "",
+ " void assertJAssertThatOptionalFields() {",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional aField",
+ " aField.get().toString();",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional aStaticField",
+ " aStaticField.get().toString();",
+ " Assertions.assertThat(aStaticField).isPresent();",
+ " aStaticField.get().toString();",
+ " Assertions.assertThat(aField).isNotEmpty();",
+ " aField.get().toString();",
+ " }",
+ "",
+ " Optional<Long> getOptional() {",
+ " return Optional.of(2L);",
+ " }",
+ "",
+ " void assertJAssertThatOptionalGetter() {",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional getOptional()",
+ " long x = 2L + getOptional().get();",
+ " Assertions.assertThat(getOptional()).isPresent();",
+ " long y = 2L + getOptional().get();",
+ " }",
+ "",
+ " Map<String, Optional<Integer>> getMapWithOptional() {",
+ " return new HashMap<>();",
+ " }",
+ "",
+ " void assertJAssertThatMapWithOptional() {",
+ " Assertions.assertThat(getMapWithOptional().get(\"thekey\")).isNotNull();",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional getMapWithOptional().get(\"thekey\")",
+ " int x = 1 + getMapWithOptional().get(\"thekey\").get();",
+ " Assertions.assertThat(getMapWithOptional().get(\"thekey\")).isPresent();",
+ " int y = 1 + getMapWithOptional().get(\"thekey\").get();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void optionalEmptinessHandleAssertionLibraryJUnitAssertions() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:CheckOptionalEmptiness=true",
+ "-XepOpt:NullAway:HandleTestAssertionLibraries=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Optional;",
+ "import org.junit.Assert;",
+ "import org.junit.jupiter.api.Assertions;",
+ "",
+ "public class Test {",
+ " void junit4AssertTrueIsPresent() {",
+ " Optional<Object> a = Optional.empty();",
+ " Assert.assertFalse(a.isPresent()); // no impact",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional a",
+ " a.get().toString();",
+ " Assert.assertTrue(\"not present\", a.isPresent());",
+ " a.get().toString();",
+ " }",
+ "",
+ " void junit5AssertTrueIsPresent() {",
+ " Optional<Object> c = Optional.empty();",
+ " Assert.assertFalse(c.isPresent()); // no impact",
+ " // BUG: Diagnostic contains: Invoking get() on possibly empty Optional c",
+ " c.get().toString();",
+ " Assertions.assertTrue(c.isPresent(), \"not present\");",
+ " c.get().toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
public void optionalEmptinessAssignmentCheckNegativeTest() {
makeTestHelperWithArgs(
Arrays.asList(
@@ -505,4 +654,32 @@ public class NullAwayOptionalEmptinessTests extends NullAwayTestsBase {
"}")
.doTest();
}
+
+ @Test
+ public void optionalOfMapResultTest() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:CheckOptionalEmptiness=true"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Map;",
+ "import java.util.Optional;",
+ "class Test {",
+ " private Optional<String> f(Map<String, String> map1, Map<String, String> map2) {",
+ " Optional<String> opt = Optional.ofNullable(map1.get(\"key\"));",
+ " if (!opt.isPresent()) {",
+ " return Optional.empty();",
+ " }",
+ " return map2.entrySet().stream()",
+ " .filter(entry -> entry.getValue().equals(opt.get()))",
+ " .map(Map.Entry::getKey)",
+ " .findFirst();",
+ " }",
+ "}")
+ .doTest();
+ }
}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwaySerializationTest.java b/nullaway/src/test/java/com/uber/nullaway/NullAwaySerializationTest.java
new file mode 100644
index 0000000..86eff7c
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwaySerializationTest.java
@@ -0,0 +1,2180 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+
+import com.google.common.base.Preconditions;
+import com.google.errorprone.util.ASTHelpers;
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.fixserialization.FixSerializationConfig;
+import com.uber.nullaway.fixserialization.adapters.SerializationV1Adapter;
+import com.uber.nullaway.fixserialization.adapters.SerializationV3Adapter;
+import com.uber.nullaway.fixserialization.out.FieldInitializationInfo;
+import com.uber.nullaway.fixserialization.out.SuggestedNullableFixInfo;
+import com.uber.nullaway.tools.DisplayFactory;
+import com.uber.nullaway.tools.ErrorDisplay;
+import com.uber.nullaway.tools.FieldInitDisplay;
+import com.uber.nullaway.tools.FixDisplay;
+import com.uber.nullaway.tools.SerializationTestHelper;
+import com.uber.nullaway.tools.version1.ErrorDisplayV1;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+/** Unit tests for {@link com.uber.nullaway.NullAway}. */
+@RunWith(JUnit4.class)
+public class NullAwaySerializationTest extends NullAwayTestsBase {
+ private String configPath;
+ private Path root;
+ private final DisplayFactory<FixDisplay> fixDisplayFactory;
+ private final DisplayFactory<ErrorDisplay> errorDisplayFactory;
+ private final DisplayFactory<FieldInitDisplay> fieldInitDisplayFactory;
+
+ private static final String SUGGEST_FIX_FILE_NAME = "fixes.tsv";
+ private static final String SUGGEST_FIX_FILE_HEADER = SuggestedNullableFixInfo.header();
+ private static final String ERROR_FILE_NAME = "errors.tsv";
+ private static final String ERROR_FILE_HEADER =
+ new SerializationV3Adapter().getErrorsOutputFileHeader();
+ private static final String FIELD_INIT_FILE_NAME = "field_init.tsv";
+ private static final String FIELD_INIT_HEADER = FieldInitializationInfo.header();
+
+ public NullAwaySerializationTest() {
+ this.fixDisplayFactory =
+ values -> {
+ Preconditions.checkArgument(
+ values.length == 10,
+ "Needs exactly 10 values to create FixDisplay object but found: " + values.length);
+ // Fixes are written in Temp Directory and is not known at compile time, therefore,
+ // relative paths are getting compared.
+ return new FixDisplay(
+ values[7],
+ values[2],
+ values[3],
+ values[0],
+ values[1],
+ SerializationTestHelper.getRelativePathFromUnitTestTempDirectory(values[5]));
+ };
+ this.errorDisplayFactory =
+ values -> {
+ Preconditions.checkArgument(
+ values.length == 12,
+ "Needs exactly 12 values to create ErrorDisplay object but found: " + values.length);
+ return new ErrorDisplay(
+ values[0],
+ values[1],
+ values[2],
+ values[3],
+ Integer.parseInt(values[4]),
+ SerializationTestHelper.getRelativePathFromUnitTestTempDirectory(values[5]),
+ values[6],
+ values[7],
+ values[8],
+ values[9],
+ values[10],
+ SerializationTestHelper.getRelativePathFromUnitTestTempDirectory(values[11]));
+ };
+ this.fieldInitDisplayFactory =
+ values -> {
+ Preconditions.checkArgument(
+ values.length == 7,
+ "Needs exactly 7 values to create FieldInitDisplay object but found: "
+ + values.length);
+ return new FieldInitDisplay(
+ values[6],
+ values[2],
+ values[3],
+ values[0],
+ values[1],
+ SerializationTestHelper.getRelativePathFromUnitTestTempDirectory(values[5]));
+ };
+ }
+
+ @Before
+ @Override
+ public void setup() {
+ root = Paths.get(temporaryFolder.getRoot().getAbsolutePath());
+ String output = root.toString();
+ try {
+ Files.createDirectories(root);
+ FixSerializationConfig.Builder builder =
+ new FixSerializationConfig.Builder().setSuggest(true, false).setOutputDirectory(output);
+ Path config = root.resolve("serializer.xml");
+ Files.createFile(config);
+ configPath = config.toString();
+ builder.writeAsXML(configPath);
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ }
+
+ @Test
+ public void suggestNullableReturnSimpleTest() {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/SubClass.java",
+ "package com.uber;",
+ "public class SubClass {",
+ " Object test(boolean flag) {",
+ " if(flag) {",
+ " return new Object();",
+ " } ",
+ " // BUG: Diagnostic contains: returning @Nullable",
+ " else return null;",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new FixDisplay(
+ "nullable",
+ "test(boolean)",
+ "null",
+ "METHOD",
+ "com.uber.SubClass",
+ "com/uber/SubClass.java"))
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void suggestNullableReturnSuperClassTest() {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/android/Super.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import javax.annotation.Nonnull;",
+ "public class Super {",
+ " Object test(boolean flag) {",
+ " return new Object();",
+ " }",
+ "}")
+ .addSourceLines(
+ "com/uber/test/SubClass.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import javax.annotation.Nonnull;",
+ "public class SubClass extends Super {",
+ " // BUG: Diagnostic contains: returns @Nullable",
+ " @Nullable Object test(boolean flag) {",
+ " if(flag) {",
+ " return new Object();",
+ " } ",
+ " else return null;",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new FixDisplay(
+ "nullable",
+ "test(boolean)",
+ "null",
+ "METHOD",
+ "com.uber.Super",
+ "com/uber/android/Super.java"))
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void suggestNullableParamSimpleTest() {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/android/Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import javax.annotation.Nonnull;",
+ "public class Test {",
+ " Object run(int i, Object h) {",
+ " return h;",
+ " }",
+ " Object test_param(@Nullable String o) {",
+ " // BUG: Diagnostic contains: passing @Nullable",
+ " return run(0, o);",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new FixDisplay(
+ "nullable",
+ "run(int,java.lang.Object)",
+ "h",
+ "PARAMETER",
+ "com.uber.Test",
+ "com/uber/android/Test.java"))
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void suggestNullableParamSubclassTest() {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/android/Super.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import javax.annotation.Nonnull;",
+ "public class Super {",
+ " @Nullable String test(@Nullable Object o) {",
+ " if(o != null) {",
+ " return o.toString();",
+ " }",
+ " return null;",
+ " }",
+ "}")
+ .addSourceLines(
+ "com/uber/test/SubClass.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import javax.annotation.Nonnull;",
+ "public class SubClass extends Super {",
+ " // BUG: Diagnostic contains: parameter o is @NonNull",
+ " @Nullable String test(Object o) {",
+ " return o.toString();",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new FixDisplay(
+ "nullable",
+ "test(java.lang.Object)",
+ "o",
+ "PARAMETER",
+ "com.uber.SubClass",
+ "com/uber/test/SubClass.java"))
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void suggestNullableParamThisConstructorTest() {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/test/Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import javax.annotation.Nonnull;",
+ "public class Test {",
+ " Test () {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'null'",
+ " this(null);",
+ " }",
+ " Test (Object o) {",
+ " System.out.println(o.toString());",
+ " }",
+ "",
+ "}")
+ .setExpectedOutputs(
+ new FixDisplay(
+ "nullable",
+ "Test(java.lang.Object)",
+ "o",
+ "PARAMETER",
+ "com.uber.Test",
+ "com/uber/test/Test.java"))
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void suggestNullableParamGenericsTest() {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/Super.java",
+ "package com.uber;",
+ "import java.util.ArrayList;",
+ "class Super<T extends Object> {",
+ " public boolean newStatement(",
+ " T lhs, ArrayList<T> operator, boolean toWorkList, boolean eager) {",
+ " return false;",
+ " }",
+ "}")
+ .addSourceLines(
+ "com/uber/Child.java",
+ "package com.uber;",
+ "import java.util.ArrayList;",
+ "public class Child extends Super<String>{",
+ " public void newSideEffect(ArrayList<String> op) {",
+ " // BUG: Diagnostic contains: passing @Nullable",
+ " newStatement(null, op, true, true);",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new FixDisplay(
+ "nullable",
+ "newStatement(T,java.util.ArrayList,boolean,boolean)",
+ "lhs",
+ "PARAMETER",
+ "com.uber.Super",
+ "com/uber/Super.java"))
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void suggestNullableFieldSimpleTest() {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/android/Super.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import javax.annotation.Nonnull;",
+ "public class Super {",
+ " Object h = new Object();",
+ " public void test(@Nullable Object f) {",
+ " // BUG: Diagnostic contains: assigning @Nullable",
+ " h = f;",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new FixDisplay(
+ "nullable", "null", "h", "FIELD", "com.uber.Super", "com/uber/android/Super.java"))
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void suggestNullableFieldInitializationTest() {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/android/Super.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Super {",
+ " // BUG: Diagnostic contains: assigning @Nullable",
+ " Object f = foo();",
+ " void test() {",
+ " System.out.println(f.toString());",
+ " }",
+ " @Nullable Object foo() {",
+ " return null;",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new FixDisplay(
+ "nullable", "null", "f", "FIELD", "com.uber.Super", "com/uber/android/Super.java"))
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void suggestNullableFieldControlFlowTest() {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/android/Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import javax.annotation.Nonnull;",
+ "public class Test {",
+ " Object h, f, g, i, k;",
+ " // BUG: Diagnostic contains: initializer method",
+ " public Test(boolean b) {",
+ " g = new Object();",
+ " k = new Object();",
+ " i = g;",
+ " if(b) {",
+ " h = new Object();",
+ " }",
+ " else{",
+ " f = new Object();",
+ " }",
+ " }",
+ " // BUG: Diagnostic contains: initializer method",
+ " public Test(boolean b, boolean a) {",
+ " f = new Object();",
+ " k = new Object();",
+ " h = f;",
+ " if(a) {",
+ " g = new Object();",
+ " }",
+ " else{",
+ " i = new Object();",
+ " }",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new FixDisplay(
+ "nullable", "null", "h", "FIELD", "com.uber.Test", "com/uber/android/Test.java"),
+ new FixDisplay(
+ "nullable", "null", "f", "FIELD", "com.uber.Test", "com/uber/android/Test.java"),
+ new FixDisplay(
+ "nullable", "null", "g", "FIELD", "com.uber.Test", "com/uber/android/Test.java"),
+ new FixDisplay(
+ "nullable", "null", "i", "FIELD", "com.uber.Test", "com/uber/android/Test.java"))
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void suggestNullableNoInitializationFieldTest() {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/android/Test.java",
+ "package com.uber;",
+ "public class Test {",
+ " // BUG: Diagnostic contains: field f not initialized",
+ " Object f;",
+ "}")
+ .setExpectedOutputs(
+ new FixDisplay(
+ "nullable", "null", "f", "FIELD", "com.uber.Test", "com/uber/android/Test.java"))
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void skipSuggestPassNullableParamExplicitNonnullTest() {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/android/Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import javax.annotation.Nonnull;",
+ "public class Test {",
+ " Object test(int i, @Nonnull Object h) {",
+ " return h;",
+ " }",
+ " Object test_param(@Nullable String o) {",
+ " // BUG: Diagnostic contains: passing @Nullable",
+ " return test(0, o);",
+ " }",
+ "}")
+ .expectNoOutput()
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void skipSuggestReturnNullableExplicitNonnullTest() {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/Base.java",
+ "package com.uber;",
+ "import javax.annotation.Nonnull;",
+ "public class Base {",
+ " @Nonnull Object test() {",
+ " // BUG: Diagnostic contains: returning @Nullable",
+ " return null;",
+ " }",
+ "}")
+ .expectNoOutput()
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void skipSuggestFieldNullableExplicitNonnullTest() {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/Base.java",
+ "package com.uber;",
+ "import javax.annotation.Nonnull;",
+ "public class Base {",
+ " // BUG: Diagnostic contains: field f not initialized",
+ " @Nonnull Object f;",
+ "}")
+ .expectNoOutput()
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void errorSerializationTest() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/Super.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Super {",
+ " Object foo;",
+ " // BUG: Diagnostic contains: initializer method does not guarantee @NonNull field foo",
+ " Super(boolean b) {",
+ " }",
+ " String test(@Nullable Object o) {",
+ " // BUG: Diagnostic contains: assigning @Nullable expression to @NonNull",
+ " foo = null;",
+ " if(o == null) {",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " return o.toString();",
+ " }",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ " protected void expectNonNull(Object o) {",
+ " System.out.println(o);",
+ " }",
+ "}")
+ .addSourceLines(
+ "com/uber/SubClass.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class SubClass extends Super{",
+ " SubClass(boolean b) {",
+ " super(b);",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " test(null);",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " expectNonNull(null);",
+ " }",
+ " // BUG: Diagnostic contains: method returns @Nullable, but superclass",
+ " @Nullable String test(Object o) {",
+ " return null;",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "METHOD_NO_INIT",
+ "initializer method does not guarantee @NonNull field foo",
+ "com.uber.Super",
+ "Super(boolean)",
+ 180,
+ "com/uber/Super.java"),
+ new ErrorDisplay(
+ "ASSIGN_FIELD_NULLABLE",
+ "assigning @Nullable expression to @NonNull field",
+ "com.uber.Super",
+ "test(java.lang.Object)",
+ 323,
+ "com/uber/Super.java",
+ "FIELD",
+ "com.uber.Super",
+ "null",
+ "foo",
+ "null",
+ "com/uber/Super.java"),
+ new ErrorDisplay(
+ "DEREFERENCE_NULLABLE",
+ "dereferenced expression o is @Nullable",
+ "com.uber.Super",
+ "test(java.lang.Object)",
+ 430,
+ "com/uber/Super.java"),
+ new ErrorDisplay(
+ "RETURN_NULLABLE",
+ "returning @Nullable expression from method",
+ "com.uber.Super",
+ "test(java.lang.Object)",
+ 521,
+ "com/uber/Super.java",
+ "METHOD",
+ "com.uber.Super",
+ "test(java.lang.Object)",
+ "null",
+ "null",
+ "com/uber/Super.java"),
+ new ErrorDisplay(
+ "PASS_NULLABLE",
+ "passing @Nullable parameter",
+ "com.uber.SubClass",
+ "SubClass(boolean)",
+ 199,
+ "com/uber/SubClass.java",
+ "PARAMETER",
+ "com.uber.SubClass",
+ "test(java.lang.Object)",
+ "o",
+ "0",
+ "com/uber/SubClass.java"),
+ new ErrorDisplay(
+ "PASS_NULLABLE",
+ "passing @Nullable parameter",
+ "com.uber.SubClass",
+ "SubClass(boolean)",
+ 280,
+ "com/uber/SubClass.java",
+ "PARAMETER",
+ "com.uber.Super",
+ "expectNonNull(java.lang.Object)",
+ "o",
+ "0",
+ "com/uber/Super.java"),
+ new ErrorDisplay(
+ "WRONG_OVERRIDE_RETURN",
+ "method returns @Nullable, but superclass",
+ "com.uber.SubClass",
+ "test(java.lang.Object)",
+ 382,
+ "com/uber/SubClass.java",
+ "METHOD",
+ "com.uber.Super",
+ "test(java.lang.Object)",
+ "null",
+ "null",
+ "com/uber/Super.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void errorSerializationEscapeSpecialCharactersTest() {
+ // Input source lines for this test are not correctly formatted intentionally to make sure error
+ // serialization will not be affected by any existing white spaces in the source code.
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/Test.java",
+ "package com.uber;",
+ "public class Test {",
+ " Object m = new Object();",
+ " public void run() {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'm.hashCode()",
+ " foo(m.hashCode() == 2 || m.toString().equals('\\t') ? \t",
+ "\t",
+ " new Object() : null);",
+ " }",
+ " public void foo(Object o) { }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "PASS_NULLABLE",
+ "passing @Nullable parameter 'm.hashCode() == 2 || m.toString().equals('\\\\t') ? \\t\\n\\t\\n new Object() : null'",
+ "com.uber.Test",
+ "run()",
+ 170,
+ "com/uber/Test.java",
+ "PARAMETER",
+ "com.uber.Test",
+ "foo(java.lang.Object)",
+ "o",
+ "0",
+ "com/uber/Test.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void fieldInitializationSerializationTest() {
+ Path tempRoot = Paths.get(temporaryFolder.getRoot().getAbsolutePath(), "test_field_init");
+ String output = tempRoot.toString();
+ try {
+ Files.createDirectories(tempRoot);
+ FixSerializationConfig.Builder builder =
+ new FixSerializationConfig.Builder()
+ .setSuggest(true, false)
+ .setFieldInitInfo(true)
+ .setOutputDirectory(output);
+ Path config = tempRoot.resolve("serializer.xml");
+ Files.createFile(config);
+ configPath = config.toString();
+ builder.writeAsXML(configPath);
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ SerializationTestHelper<FieldInitDisplay> tester = new SerializationTestHelper<>(tempRoot);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Test {",
+ " Object foo;",
+ " Object bar;",
+ " @Nullable Object nullableFoo;",
+ " // BUG: Diagnostic contains: initializer method does not guarantee @NonNull field foo",
+ " Test() {",
+ " // We are not tracing initializations in constructors.",
+ " bar = new Object();",
+ " }",
+ " void notInit() {",
+ " if(foo == null){",
+ " throw new RuntimeException();",
+ " }",
+ " }",
+ " void actualInit() {",
+ " foo = new Object();",
+ " // We are not tracing initialization of @Nullable fields.",
+ " nullableFoo = new Object();",
+ " }",
+ " void notInit2(@Nullable Object bar) {",
+ " foo = new Object();",
+ " // BUG: Diagnostic contains: assigning @Nullable expression to @NonNull field",
+ " foo = bar;",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new FieldInitDisplay(
+ "foo", "actualInit()", "null", "METHOD", "com.uber.Test", "com/uber/Test.java"))
+ .setOutputFileNameAndHeader(FIELD_INIT_FILE_NAME, FIELD_INIT_HEADER)
+ .setFactory(fieldInitDisplayFactory)
+ .doTest();
+ }
+
+ @Test
+ public void errorSerializationTestAnonymousInnerClass() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/TestWithAnonymousRunnable.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class TestWithAnonymousRunnable {",
+ " void takesNonNull(String s) { }",
+ " void test(Object o) {",
+ " Runnable r = new Runnable() {",
+ " public String returnsNullable() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ " @Override",
+ " public void run() {",
+ " takesNonNull(this.returnsNullable());",
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'null'",
+ " takesNonNull(null);",
+ " }",
+ " };",
+ " r.run();",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "RETURN_NULLABLE",
+ "returning @Nullable expression from method with @NonNull return type",
+ "com.uber.TestWithAnonymousRunnable$1",
+ "returnsNullable()",
+ 317,
+ "com/uber/TestWithAnonymousRunnable.java",
+ "METHOD",
+ "com.uber.TestWithAnonymousRunnable$1",
+ "returnsNullable()",
+ "null",
+ "null",
+ "com/uber/TestWithAnonymousRunnable.java"),
+ new ErrorDisplay(
+ "PASS_NULLABLE",
+ "passing @Nullable parameter 'null' where @NonNull is required",
+ "com.uber.TestWithAnonymousRunnable$1",
+ "run()",
+ 530,
+ "com/uber/TestWithAnonymousRunnable.java",
+ "PARAMETER",
+ "com.uber.TestWithAnonymousRunnable",
+ "takesNonNull(java.lang.String)",
+ "s",
+ "0",
+ "com/uber/TestWithAnonymousRunnable.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void errorSerializationTestLocalTypes() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/TestWithLocalType.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class TestWithLocalType {",
+ " @Nullable String test(Object o) {",
+ " class LocalType {",
+ " public String returnsNullable() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ " }",
+ " LocalType local = new LocalType();",
+ " return local.returnsNullable();",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "RETURN_NULLABLE",
+ "returning @Nullable expression from method with @NonNull return type",
+ "com.uber.TestWithLocalType$1LocalType",
+ "returnsNullable()",
+ 274,
+ "com/uber/TestWithLocalType.java",
+ "METHOD",
+ "com.uber.TestWithLocalType$1LocalType",
+ "returnsNullable()",
+ "null",
+ "null",
+ "com/uber/TestWithLocalType.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void errorSerializationTestIdenticalLocalTypes() {
+ String[] sourceLines = {
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class TestWithLocalTypes {",
+ " @Nullable String test(Object o) {",
+ " class LocalType {",
+ " public String returnsNullable() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ " }",
+ " LocalType local = new LocalType();",
+ " return local.returnsNullable();",
+ " }",
+ " @Nullable String test2(Object o) {",
+ " class LocalType {",
+ " public String returnsNullable2() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ " }",
+ " LocalType local = new LocalType();",
+ " return local.returnsNullable2();",
+ " }",
+ " @Nullable String test2() {",
+ " class LocalType {",
+ " public String returnsNullable2() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ " }",
+ " LocalType local = new LocalType();",
+ " return local.returnsNullable2();",
+ " }",
+ "}"
+ };
+ SerializationTestHelper<ErrorDisplay> errorTester = new SerializationTestHelper<>(root);
+ errorTester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines("com/uber/TestWithLocalTypes.java", sourceLines)
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "RETURN_NULLABLE",
+ "returning @Nullable expression from method with @NonNull return type",
+ "com.uber.TestWithLocalTypes$1LocalType",
+ "returnsNullable()",
+ 275,
+ "com/uber/TestWithLocalTypes.java",
+ "METHOD",
+ "com.uber.TestWithLocalTypes$1LocalType",
+ "returnsNullable()",
+ "null",
+ "null",
+ "com/uber/TestWithLocalTypes.java"),
+ new ErrorDisplay(
+ "RETURN_NULLABLE",
+ "returning @Nullable expression from method with @NonNull return type",
+ "com.uber.TestWithLocalTypes$2LocalType",
+ "returnsNullable2()",
+ 579,
+ "com/uber/TestWithLocalTypes.java",
+ "METHOD",
+ "com.uber.TestWithLocalTypes$2LocalType",
+ "returnsNullable2()",
+ "null",
+ "null",
+ "com/uber/TestWithLocalTypes.java"),
+ new ErrorDisplay(
+ "RETURN_NULLABLE",
+ "returning @Nullable expression from method with @NonNull return type",
+ "com.uber.TestWithLocalTypes$3LocalType",
+ "returnsNullable2()",
+ 876,
+ "com/uber/TestWithLocalTypes.java",
+ "METHOD",
+ "com.uber.TestWithLocalTypes$3LocalType",
+ "returnsNullable2()",
+ "null",
+ "null",
+ "com/uber/TestWithLocalTypes.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ SerializationTestHelper<FixDisplay> fixTester = new SerializationTestHelper<>(root);
+ fixTester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines("com/uber/TestWithLocalTypes.java", sourceLines)
+ .setExpectedOutputs(
+ new FixDisplay(
+ "nullable",
+ "returnsNullable()",
+ "null",
+ "METHOD",
+ "com.uber.TestWithLocalTypes$1LocalType",
+ "com/uber/TestWithLocalTypes.java"),
+ new FixDisplay(
+ "nullable",
+ "returnsNullable2()",
+ "null",
+ "METHOD",
+ "com.uber.TestWithLocalTypes$2LocalType",
+ "com/uber/TestWithLocalTypes.java"),
+ new FixDisplay(
+ "nullable",
+ "returnsNullable2()",
+ "null",
+ "METHOD",
+ "com.uber.TestWithLocalTypes$3LocalType",
+ "com/uber/TestWithLocalTypes.java"))
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void errorSerializationTestLocalTypesNested() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/TestWithLocalType.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class TestWithLocalType {",
+ " @Nullable String test(Object o) {",
+ " class LocalTypeA {",
+ " @Nullable",
+ " public String returnsNullable() {",
+ " class LocalTypeB {",
+ " public String returnsNullable() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ " }",
+ " LocalTypeB local = new LocalTypeB();",
+ " return local.returnsNullable();",
+ " }",
+ " }",
+ " LocalTypeA local = new LocalTypeA();",
+ " return local.returnsNullable();",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "RETURN_NULLABLE",
+ "returning @Nullable expression from method with @NonNull return type",
+ "com.uber.TestWithLocalType$1LocalTypeA$1LocalTypeB",
+ "returnsNullable()",
+ 393,
+ "com/uber/TestWithLocalType.java",
+ "METHOD",
+ "com.uber.TestWithLocalType$1LocalTypeA$1LocalTypeB",
+ "returnsNullable()",
+ "null",
+ "null",
+ "com/uber/TestWithLocalType.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void errorSerializationTestLocalTypesInitializers() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/TestWithLocalTypes.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class TestWithLocalTypes {",
+ " private Object o1;",
+ " private Object o2;",
+ " private Object o3;",
+ " {",
+ " class LocalType {",
+ " public String returnsNullable() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ " }",
+ " o1 = new LocalType();",
+ " }",
+ " {",
+ " class LocalType {",
+ " public String returnsNullable() {",
+ " return \"\";",
+ " }",
+ " }",
+ " o2 = new LocalType();",
+ " }",
+ " {",
+ " class LocalType {",
+ " public String returnsNullable() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ " }",
+ " o3 = new LocalType();",
+ " }",
+ " void test(Object o) {",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "RETURN_NULLABLE",
+ "returning @Nullable expression from method with @NonNull return type",
+ "com.uber.TestWithLocalTypes$1LocalType",
+ "returnsNullable()",
+ 309,
+ "com/uber/TestWithLocalTypes.java",
+ "METHOD",
+ "com.uber.TestWithLocalTypes$1LocalType",
+ "returnsNullable()",
+ "null",
+ "null",
+ "com/uber/TestWithLocalTypes.java"),
+ new ErrorDisplay(
+ "RETURN_NULLABLE",
+ "returning @Nullable expression from method with @NonNull return type",
+ "com.uber.TestWithLocalTypes$3LocalType",
+ "returnsNullable()",
+ 674,
+ "com/uber/TestWithLocalTypes.java",
+ "METHOD",
+ "com.uber.TestWithLocalTypes$3LocalType",
+ "returnsNullable()",
+ "null",
+ "null",
+ "com/uber/TestWithLocalTypes.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void errorSerializationTestInheritanceViolationForParameter() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/Foo.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public interface Foo {",
+ " void bar(@Nullable Object o);",
+ "}")
+ .addSourceLines(
+ "com/uber/Main.java",
+ "package com.uber;",
+ "public class Main {",
+ " public void run(){",
+ " Foo foo = new Foo() {",
+ " @Override",
+ " // BUG: Diagnostic contains: parameter o is @NonNull",
+ " public void bar(Object o) {",
+ " }",
+ " };",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "WRONG_OVERRIDE_PARAM",
+ "parameter o is @NonNull, but parameter in superclass method com.uber.Foo.bar(java.lang.Object) is @Nullable",
+ "com.uber.Main$1",
+ "bar(java.lang.Object)",
+ 94,
+ "com/uber/Main.java",
+ "PARAMETER",
+ "com.uber.Main$1",
+ "bar(java.lang.Object)",
+ "o",
+ "0",
+ "com/uber/Main.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void errorSerializationTestInheritanceViolationForMethod() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/Foo.java",
+ "package com.uber;",
+ "public interface Foo {",
+ " Object bar();",
+ "}")
+ .addSourceLines(
+ "com/uber/Main.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Main {",
+ " public void run(){",
+ " Foo foo = new Foo() {",
+ " @Override",
+ " @Nullable",
+ " // BUG: Diagnostic contains: method returns @Nullable, but superclass",
+ " public Object bar() {",
+ " return null;",
+ " }",
+ " };",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "WRONG_OVERRIDE_RETURN",
+ "method returns @Nullable, but superclass method com.uber.Foo.bar() returns @NonNull",
+ "com.uber.Main$1",
+ "bar()",
+ 128,
+ "com/uber/Main.java",
+ "METHOD",
+ "com.uber.Foo",
+ "bar()",
+ "null",
+ "null",
+ "com/uber/Foo.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void errorSerializationTestAnonymousClassField() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines("com/uber/Foo.java", "package com.uber;", "public interface Foo { }")
+ .addSourceLines(
+ "com/uber/Main.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Main {",
+ " public void run(){",
+ " Foo foo = new Foo() {",
+ " // BUG: Diagnostic contains: @NonNull field Main$1.bar not initialized",
+ " Object bar;",
+ " };",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "FIELD_NO_INIT",
+ "@NonNull field Main$1.bar not initialized",
+ "com.uber.Main$1",
+ "bar",
+ 206,
+ "com/uber/Main.java",
+ "FIELD",
+ "com.uber.Main$1",
+ "null",
+ "bar",
+ "null",
+ "com/uber/Main.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void errorSerializationTestLocalClassField() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/Main.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Main {",
+ " public void run(){",
+ " class Foo {",
+ " // BUG: Diagnostic contains: @NonNull field Main$1Foo.bar not initialized",
+ " Object bar;",
+ " };",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "FIELD_NO_INIT",
+ "@NonNull field Main$1Foo.bar not initialized",
+ "com.uber.Main$1Foo",
+ "bar",
+ 199,
+ "com/uber/Main.java",
+ "FIELD",
+ "com.uber.Main$1Foo",
+ "null",
+ "bar",
+ "null",
+ "com/uber/Main.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void verifySerializationVersionIsSerialized() {
+ // Check for serialization version 1.
+ checkVersionSerialization(1);
+ // Check for serialization version 3 (recall: 2 is skipped and was only used for an alpha
+ // release of auto-annotator).
+ checkVersionSerialization(3);
+ }
+
+ @Test
+ public void errorSerializationTestEnclosedByFieldDeclaration() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/Main.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Main {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Foo f = new Foo(null);", // Member should be "f"
+ " // BUG: Diagnostic contains: assigning @Nullable expression",
+ " Foo f1 = null;", // Member should be "f1"
+ " // BUG: Diagnostic contains: assigning @Nullable expression",
+ " static Foo f2 = null;", // Member should be "f2"
+ " static {",
+ " // BUG: Diagnostic contains: assigning @Nullable expression",
+ " f2 = null;", // Member should be "null" (not field or method)
+ " }",
+ " class Inner {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Foo f = new Foo(null);", // Member should be "f" but class is Main$Inner
+ " }",
+ "}")
+ .addSourceLines(
+ "com/uber/Foo.java",
+ "package com.uber;",
+ "public class Foo {",
+ " public Foo(Object foo){",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "PASS_NULLABLE",
+ "passing @Nullable parameter",
+ "com.uber.Main",
+ "f",
+ 143,
+ "com/uber/Main.java",
+ "PARAMETER",
+ "com.uber.Foo",
+ "Foo(java.lang.Object)",
+ "foo",
+ "0",
+ "com/uber/Foo.java"),
+ new ErrorDisplay(
+ "ASSIGN_FIELD_NULLABLE",
+ "assigning @Nullable expression to @NonNull field",
+ "com.uber.Main",
+ "f1",
+ 224,
+ "com/uber/Main.java",
+ "FIELD",
+ "com.uber.Main",
+ "null",
+ "f1",
+ "null",
+ "com/uber/Main.java"),
+ new ErrorDisplay(
+ "ASSIGN_FIELD_NULLABLE",
+ "assigning @Nullable expression to @NonNull field",
+ "com.uber.Main",
+ "f2",
+ 305,
+ "com/uber/Main.java",
+ "FIELD",
+ "com.uber.Main",
+ "null",
+ "f2",
+ "null",
+ "com/uber/Main.java"),
+ new ErrorDisplay(
+ "ASSIGN_FIELD_NULLABLE",
+ "assigning @Nullable expression to @NonNull field",
+ "com.uber.Main",
+ "null",
+ 413,
+ "com/uber/Main.java",
+ "FIELD",
+ "com.uber.Main",
+ "null",
+ "f2",
+ "null",
+ "com/uber/Main.java"),
+ new ErrorDisplay(
+ "PASS_NULLABLE",
+ "passing @Nullable parameter",
+ "com.uber.Main$Inner",
+ "f",
+ 525,
+ "com/uber/Main.java",
+ "PARAMETER",
+ "com.uber.Foo",
+ "Foo(java.lang.Object)",
+ "foo",
+ "0",
+ "com/uber/Foo.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void suggestNullableArgumentOnBytecode() {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ // Explicitly avoid excluding com.uber.nullaway.testdata.unannotated,
+ // so we can suggest fixes there
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/UsesUnannotated.java",
+ "package com.uber;",
+ "import com.uber.nullaway.testdata.unannotated.MinimalUnannotatedClass;",
+ "public class UsesUnannotated {",
+ " Object test(boolean flag) {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required",
+ " return MinimalUnannotatedClass.foo(null);",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new FixDisplay(
+ "nullable",
+ "foo(java.lang.Object)",
+ "x",
+ "PARAMETER",
+ "com.uber.nullaway.testdata.unannotated.MinimalUnannotatedClass",
+ "com/uber/nullaway/testdata/unannotated/MinimalUnannotatedClass.java"))
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void suggestNullableArgumentOnBytecodeNoFileInfo() {
+ // Simulate a build system which elides sourcefile/classfile info
+ try (MockedStatic<ASTHelpers> astHelpersMockedStatic =
+ Mockito.mockStatic(ASTHelpers.class, Mockito.CALLS_REAL_METHODS)) {
+ astHelpersMockedStatic
+ .when(() -> ASTHelpers.enclosingClass(any(Symbol.class)))
+ .thenAnswer(
+ (Answer<Symbol.ClassSymbol>)
+ invocation -> {
+ Symbol.ClassSymbol answer = (Symbol.ClassSymbol) invocation.callRealMethod();
+ if (answer.sourcefile != null
+ && answer
+ .sourcefile
+ .toUri()
+ .toASCIIString()
+ .contains("com/uber/nullaway/testdata/unannotated")) {
+ answer.sourcefile = null;
+ answer.classfile = null;
+ }
+ return answer;
+ });
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ // Explicitly avoid excluding com.uber.nullaway.testdata.unannotated,
+ // so we can suggest fixes there
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/UsesUnannotated.java",
+ "package com.uber;",
+ "import com.uber.nullaway.testdata.unannotated.MinimalUnannotatedClass;",
+ "public class UsesUnannotated {",
+ " Object test(boolean flag) {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'null' where @NonNull is required",
+ " return MinimalUnannotatedClass.foo(null);",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new FixDisplay(
+ "nullable",
+ "foo(java.lang.Object)",
+ "x",
+ "PARAMETER",
+ "com.uber.nullaway.testdata.unannotated.MinimalUnannotatedClass",
+ "null")) // <- ! the important bit
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+ }
+ }
+
+ @Test
+ public void fieldRegionComputationWithMemberSelectTest() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/A.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class A {",
+ " B b1 = new B();",
+ " @Nullable B b2 = null;",
+ " // BUG: Diagnostic contains: assigning @Nullable expression to @NonNull field",
+ " Object baz1 = b1.foo;",
+ " // BUG: Diagnostic contains: dereferenced expression b2 is @Nullable",
+ " Object baz2 = b2.foo;",
+ " // BUG: Diagnostic contains: assigning @Nullable expression to @NonNull field",
+ " Object baz3 = b1.nonnullC.val;",
+ " // BUG: Diagnostic contains: dereferenced expression b1.nullableC is @Nullable",
+ " @Nullable Object baz4 = b1.nullableC.val;",
+ "}",
+ "class B {",
+ " @Nullable Object foo;",
+ " C nonnullC = new C();",
+ " @Nullable C nullableC = new C();",
+ "}",
+ "class C {",
+ " @Nullable Object val;",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "ASSIGN_FIELD_NULLABLE",
+ "assigning @Nullable expression to @NonNull field",
+ "com.uber.A",
+ "baz1",
+ 198,
+ "com/uber/A.java",
+ "FIELD",
+ "com.uber.A",
+ "null",
+ "baz1",
+ "null",
+ "com/uber/A.java"),
+ new ErrorDisplay(
+ "ASSIGN_FIELD_NULLABLE",
+ "assigning @Nullable expression to @NonNull field",
+ "com.uber.A",
+ "baz2",
+ 295,
+ "com/uber/A.java",
+ "FIELD",
+ "com.uber.A",
+ "null",
+ "baz2",
+ "null",
+ "com/uber/A.java"),
+ new ErrorDisplay(
+ "ASSIGN_FIELD_NULLABLE",
+ "assigning @Nullable expression to @NonNull field",
+ "com.uber.A",
+ "baz3",
+ 401,
+ "com/uber/A.java",
+ "FIELD",
+ "com.uber.A",
+ "null",
+ "baz3",
+ "null",
+ "com/uber/A.java"),
+ new ErrorDisplay(
+ "DEREFERENCE_NULLABLE",
+ "dereferenced expression b2 is @Nullable",
+ "com.uber.A",
+ "baz2",
+ 309,
+ "com/uber/A.java"),
+ new ErrorDisplay(
+ "DEREFERENCE_NULLABLE",
+ "dereferenced expression b1.nullableC is @Nullable",
+ "com.uber.A",
+ "baz4",
+ 541,
+ "com/uber/A.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void constructorSerializationTest() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/A.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class A {",
+ " @Nullable Object f;",
+ " A(Object p){",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " Integer hashCode = f.hashCode();",
+ " }",
+ " public void bar(){",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " A a = new A(null);",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "DEREFERENCE_NULLABLE",
+ "dereferenced expression f is @Nullable",
+ "com.uber.A",
+ "A(java.lang.Object)",
+ 194,
+ "com/uber/A.java"),
+ new ErrorDisplay(
+ "PASS_NULLABLE",
+ "passing @Nullable parameter",
+ "com.uber.A",
+ "bar()",
+ 312,
+ "com/uber/A.java",
+ "PARAMETER",
+ "com.uber.A",
+ "A(java.lang.Object)",
+ "p",
+ "0",
+ "com/uber/A.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void fieldRegionComputationWithMemberSelectForInnerClassTest() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/A.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class A {",
+ " Other other1 = new Other();",
+ " @Nullable Other other2 = null;",
+ " public void bar(){",
+ " class Foo {",
+ " // BUG: Diagnostic contains: assigning @Nullable expression to @NonNull field",
+ " Object baz1 = other1.foo;",
+ " // BUG: Diagnostic contains: dereferenced expression other2 is @Nullable",
+ " Object baz2 = other2.foo;",
+ " }",
+ " }",
+ "}",
+ "class Other {",
+ " @Nullable Object foo;",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "ASSIGN_FIELD_NULLABLE",
+ "assigning @Nullable expression to @NonNull field",
+ "com.uber.A$1Foo",
+ "baz1",
+ 272,
+ "com/uber/A.java",
+ "FIELD",
+ "com.uber.A$1Foo",
+ "null",
+ "baz1",
+ "null",
+ "com/uber/A.java"),
+ new ErrorDisplay(
+ "ASSIGN_FIELD_NULLABLE",
+ "assigning @Nullable expression to @NonNull field",
+ "com.uber.A$1Foo",
+ "baz2",
+ 391,
+ "com/uber/A.java",
+ "FIELD",
+ "com.uber.A$1Foo",
+ "null",
+ "baz2",
+ "null",
+ "com/uber/A.java"),
+ new ErrorDisplay(
+ "DEREFERENCE_NULLABLE",
+ "dereferenced expression other2 is @Nullable",
+ "com.uber.A$1Foo",
+ "baz2",
+ 405,
+ "com/uber/A.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void errorSerializationVersion1() {
+ SerializationTestHelper<ErrorDisplayV1> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:SerializeFixMetadataVersion=1",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/Super.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Super {",
+ " Object foo;",
+ " // BUG: Diagnostic contains: initializer method does not guarantee @NonNull field foo",
+ " Super(boolean b) {",
+ " }",
+ " String test(@Nullable Object o) {",
+ " // BUG: Diagnostic contains: assigning @Nullable expression to @NonNull",
+ " foo = null;",
+ " if(o == null) {",
+ " // BUG: Diagnostic contains: dereferenced expression",
+ " return o.toString();",
+ " }",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ " protected void expectNonNull(Object o) {",
+ " System.out.println(o);",
+ " }",
+ "}")
+ .addSourceLines(
+ "com/uber/SubClass.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class SubClass extends Super{",
+ " SubClass(boolean b) {",
+ " super(b);",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " test(null);",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " expectNonNull(null);",
+ " }",
+ " // BUG: Diagnostic contains: method returns @Nullable, but superclass",
+ " @Nullable String test(Object o) {",
+ " return null;",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplayV1(
+ "METHOD_NO_INIT",
+ "initializer method does not guarantee @NonNull field foo",
+ "com.uber.Super",
+ "Super(boolean)"),
+ new ErrorDisplayV1(
+ "ASSIGN_FIELD_NULLABLE",
+ "assigning @Nullable expression to @NonNull field",
+ "com.uber.Super",
+ "test(java.lang.Object)",
+ "FIELD",
+ "com.uber.Super",
+ "null",
+ "foo",
+ "null",
+ "com/uber/Super.java"),
+ new ErrorDisplayV1(
+ "DEREFERENCE_NULLABLE",
+ "dereferenced expression o is @Nullable",
+ "com.uber.Super",
+ "test(java.lang.Object)"),
+ new ErrorDisplayV1(
+ "RETURN_NULLABLE",
+ "returning @Nullable expression from method",
+ "com.uber.Super",
+ "test(java.lang.Object)",
+ "METHOD",
+ "com.uber.Super",
+ "test(java.lang.Object)",
+ "null",
+ "null",
+ "com/uber/Super.java"),
+ new ErrorDisplayV1(
+ "PASS_NULLABLE",
+ "passing @Nullable parameter",
+ "com.uber.SubClass",
+ "SubClass(boolean)",
+ "PARAMETER",
+ "com.uber.SubClass",
+ "test(java.lang.Object)",
+ "o",
+ "0",
+ "com/uber/SubClass.java"),
+ new ErrorDisplayV1(
+ "PASS_NULLABLE",
+ "passing @Nullable parameter",
+ "com.uber.SubClass",
+ "SubClass(boolean)",
+ "PARAMETER",
+ "com.uber.Super",
+ "expectNonNull(java.lang.Object)",
+ "o",
+ "0",
+ "com/uber/Super.java"),
+ new ErrorDisplayV1(
+ "WRONG_OVERRIDE_RETURN",
+ "method returns @Nullable, but superclass",
+ "com.uber.SubClass",
+ "test(java.lang.Object)",
+ "METHOD",
+ "com.uber.Super",
+ "test(java.lang.Object)",
+ "null",
+ "null",
+ "com/uber/Super.java"))
+ .setFactory(ErrorDisplayV1.getFactory())
+ .setOutputFileNameAndHeader(
+ ERROR_FILE_NAME, new SerializationV1Adapter().getErrorsOutputFileHeader())
+ .doTest();
+ }
+
+ @Test
+ public void typeArgumentSerializationForVersion1Test() {
+ SerializationTestHelper<ErrorDisplayV1> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:SerializeFixMetadataVersion=1",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ "com/uber/Foo.java",
+ "package com.uber;",
+ "import java.util.Map;",
+ "public class Foo {",
+ " String test(Map<Object, String> o) {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplayV1(
+ "RETURN_NULLABLE",
+ "returning @Nullable expression",
+ "com.uber.Foo",
+ "test(java.util.Map<java.lang.Object,java.lang.String>)",
+ "METHOD",
+ "com.uber.Foo",
+ "test(java.util.Map<java.lang.Object,java.lang.String>)",
+ "null",
+ "null",
+ "com/uber/Foo.java"))
+ .setFactory(ErrorDisplayV1.getFactory())
+ .setOutputFileNameAndHeader(
+ ERROR_FILE_NAME, new SerializationV1Adapter().getErrorsOutputFileHeader())
+ .doTest();
+ }
+
+ /**
+ * Helper method to verify the correct serialization version number is written in
+ * "serialization_version.txt". Version number can be configured via Error Prone flags by the
+ * user, and {@link com.uber.nullaway.fixserialization.Serializer} should write the exact number
+ * in "serialization_version.txt".
+ *
+ * @param version Version number to pass to NullAway via Error Prone flags and the expected number
+ * to be read from "serialization_version.txt".
+ */
+ public void checkVersionSerialization(int version) {
+ SerializationTestHelper<FixDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:SerializeFixMetadataVersion=" + version,
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ // Just to run serialization features, the serialized fixes are not point of interest in
+ // this test.
+ .addSourceLines(
+ "com/uber/Test.java",
+ "package com.uber;",
+ "public class Test {",
+ " Object run() {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new FixDisplay(
+ "nullable", "run()", "null", "METHOD", "com.uber.Test", "com/uber/Test.java"))
+ .setFactory(fixDisplayFactory)
+ .setOutputFileNameAndHeader(SUGGEST_FIX_FILE_NAME, SUGGEST_FIX_FILE_HEADER)
+ .doTest();
+
+ Path serializationVersionPath = root.resolve("serialization_version.txt");
+ try {
+ List<String> lines = Files.readAllLines(serializationVersionPath);
+ // Check if it contains only one line.
+ assertEquals(lines.size(), 1);
+ // Check the serialized version.
+ assertEquals(Integer.parseInt(lines.get(0)), version);
+ } catch (IOException e) {
+ throw new RuntimeException(
+ "Could not read serialization version at path: " + serializationVersionPath, e);
+ }
+ }
+
+ @Test
+ public void varArgsWithTypeUseAnnotationMethodSerializationTest() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ // toString() call on method symbol will serialize the annotation below which should not
+ // be present in string representation fo a method signature.
+ "com/uber/Custom.java",
+ "package com.uber;",
+ "import java.lang.annotation.Target;",
+ "import static java.lang.annotation.ElementType.TYPE_USE;",
+ "@Target({TYPE_USE})",
+ "public @interface Custom { }")
+ .addSourceLines(
+ "com/uber/Test.java",
+ "package com.uber;",
+ "import java.util.Map;",
+ "import java.util.List;",
+ "public class Test {",
+ " Object m1(@Custom List<Map<String, ?>>[] p2, @Custom Map<? extends String, ?> ... p) {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ " Object m2(@Custom List<?>[] p) {",
+ " // BUG: Diagnostic contains: returning @Nullable expression",
+ " return null;",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "RETURN_NULLABLE",
+ "returning @Nullable expression",
+ "com.uber.Test",
+ "m1(java.util.List[],java.util.Map[])",
+ 245,
+ "com/uber/Test.java",
+ "METHOD",
+ "com.uber.Test",
+ "m1(java.util.List[],java.util.Map[])",
+ "null",
+ "null",
+ "com/uber/Test.java"),
+ new ErrorDisplay(
+ "RETURN_NULLABLE",
+ "returning @Nullable expression",
+ "com.uber.Test",
+ "m2(java.util.List[])",
+ 371,
+ "com/uber/Test.java",
+ "METHOD",
+ "com.uber.Test",
+ "m2(java.util.List[])",
+ "null",
+ "null",
+ "com/uber/Test.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+
+ @Test
+ public void anonymousSubClassConstructorSerializationTest() {
+ SerializationTestHelper<ErrorDisplay> tester = new SerializationTestHelper<>(root);
+ tester
+ .setArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:SerializeFixMetadata=true",
+ "-XepOpt:NullAway:FixSerializationConfigPath=" + configPath))
+ .addSourceLines(
+ // correct serialization of names for constructors invoked while creating anonymous
+ // inner classes, where the name is technically the name of the super-class, not of the
+ // anonymous inner class
+ "com/uber/Foo.java",
+ "package com.uber;",
+ "public class Foo {",
+ " Foo(Object o) {}",
+ " void bar() {",
+ " // BUG: Diagnostic contains: passing @Nullable parameter",
+ " Foo f = new Foo(null) {};",
+ " }",
+ "}")
+ .setExpectedOutputs(
+ new ErrorDisplay(
+ "PASS_NULLABLE",
+ "passing @Nullable parameter",
+ "com.uber.Foo",
+ "bar()",
+ 144,
+ "com/uber/Foo.java",
+ "PARAMETER",
+ "com.uber.Foo",
+ "Foo(java.lang.Object)",
+ "o",
+ "0",
+ "com/uber/Foo.java"))
+ .setFactory(errorDisplayFactory)
+ .setOutputFileNameAndHeader(ERROR_FILE_NAME, ERROR_FILE_HEADER)
+ .doTest();
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayThriftTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayThriftTests.java
index 6ff6858..9753a55 100644
--- a/nullaway/src/test/java/com/uber/nullaway/NullAwayThriftTests.java
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayThriftTests.java
@@ -1,5 +1,6 @@
package com.uber.nullaway;
+import java.util.Arrays;
import org.junit.Test;
public class NullAwayThriftTests extends NullAwayTestsBase {
@@ -141,4 +142,52 @@ public class NullAwayThriftTests extends NullAwayTestsBase {
"}")
.doTest();
}
+
+ @Test
+ public void testThriftAndCastToNonNull() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ // We give the following in Regexp format to test that support
+ "-XepOpt:NullAway:UnannotatedSubPackages=com.uber.nullaway.[a-zA-Z0-9.]+.unannotated",
+ "-XepOpt:NullAway:ExcludedClassAnnotations=com.uber.nullaway.testdata.TestAnnot",
+ "-XepOpt:NullAway:CastToNonNullMethod=com.uber.nullaway.testdata.Util.castToNonNull",
+ "-XepOpt:NullAway:TreatGeneratedAsUnannotated=true",
+ "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true"))
+ .addSourceFile("Util.java")
+ .addSourceLines("TBase.java", "package org.apache.thrift;", "public interface TBase {}")
+ .addSourceLines(
+ "GeneratedClass.java",
+ "package com.uber.lib.unannotated;",
+ "import javax.annotation.Nullable;",
+ "import javax.annotation.Generated;",
+ "@Generated(\"test\")",
+ "public class GeneratedClass implements org.apache.thrift.TBase {",
+ " public @Nullable Object id;",
+ " @Nullable public Object getId() { return this.id; }",
+ " // this is to ensure we don't crash on unions",
+ " public boolean isSet() { return false; }",
+ " public boolean isSetId() { return this.id != null; }",
+ "}")
+ .addSourceLines(
+ "Client.java",
+ "package com.uber;",
+ "import static com.uber.nullaway.testdata.Util.castToNonNull;",
+ "import com.uber.lib.unannotated.GeneratedClass;",
+ "public class Client {",
+ " public void testPos(GeneratedClass g) {",
+ " // g.getId() is @NonNull because it's treated as unannotated code and RestrictiveAnnotationHandler exempts it",
+ " // BUG: Diagnostic contains: passing known @NonNull parameter 'g.getId()' to CastToNonNullMethod",
+ " Object o = castToNonNull(g.getId());",
+ " o.toString();",
+ " }",
+ " public void testNeg(GeneratedClass g) {",
+ " Object o = g.getId();",
+ " o.toString();",
+ " }",
+ "}")
+ .doTest();
+ }
}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayTypeUseAnnotationTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayTypeUseAnnotationTests.java
new file mode 100644
index 0000000..06e13fe
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayTypeUseAnnotationTests.java
@@ -0,0 +1,38 @@
+package com.uber.nullaway;
+
+import org.junit.Test;
+
+public class NullAwayTypeUseAnnotationTests extends NullAwayTestsBase {
+ @Test
+ public void typeParameterAnnotationIsDistinctFromMethodReturnAnnotation() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.jspecify.annotations.Nullable;",
+ "import java.util.function.Supplier;",
+ "public class Test {",
+ " public static <R> @Nullable Supplier<@Nullable R> getNullableSupplierOfNullable() {",
+ " return new Supplier<R>() {",
+ " @Nullable",
+ " public R get() { return null; }",
+ " };",
+ " }",
+ " public static <R> Supplier<@Nullable R> getNonNullSupplierOfNullable() {",
+ " return new Supplier<R>() {",
+ " @Nullable",
+ " public R get() { return null; }",
+ " };",
+ " }",
+ " public static String test1() {",
+ " // BUG: Diagnostic contains: dereferenced expression getNullableSupplierOfNullable() is @Nullable",
+ " return getNullableSupplierOfNullable().toString();",
+ " }",
+ " public static String test2() {",
+ " // The supplier contains null, but isn't itself nullable. Check against a past FP",
+ " return getNonNullSupplierOfNullable().toString();",
+ " }",
+ "}")
+ .doTest();
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayTypeUseAnnotationsTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayTypeUseAnnotationsTests.java
new file mode 100644
index 0000000..7af8b25
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayTypeUseAnnotationsTests.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (c) 2023 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class NullAwayTypeUseAnnotationsTests extends NullAwayTestsBase {
+
+ @Test
+ public void annotationAppliedToTypeParameter() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.List;",
+ "import java.util.ArrayList;",
+ "import org.checkerframework.checker.nullness.qual.Nullable;",
+ "class TypeArgumentAnnotation {",
+ " List<@Nullable String> fSafe = new ArrayList<>();",
+ " @Nullable List<String> fUnsafe = new ArrayList<>();",
+ " void useParamSafe(List<@Nullable String> list) {",
+ " list.hashCode();",
+ " }",
+ " void useParamUnsafe(@Nullable List<String> list) {",
+ " // BUG: Diagnostic contains: dereferenced",
+ " list.hashCode();",
+ " }",
+ " void useFieldSafe() {",
+ " fSafe.hashCode();",
+ " }",
+ " void useFieldUnsafe() {",
+ " // BUG: Diagnostic contains: dereferenced",
+ " fUnsafe.hashCode();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void annotationAppliedToInnerTypeImplicitly() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.checkerframework.checker.nullness.qual.Nullable;",
+ "class Test {",
+ " @Nullable Foo f;", // i.e. Test.@Nullable Foo
+ " class Foo { }",
+ " public void test() {",
+ " // BUG: Diagnostic contains: dereferenced",
+ " f.hashCode();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void annotationAppliedToInnerTypeImplicitlyWithTypeArgs() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.checkerframework.checker.nullness.qual.Nullable;",
+ "class Test {",
+ " @Nullable Foo<String> f1 = null;", // i.e. Test.@Nullable Foo (location [INNER])
+ " // BUG: Diagnostic contains: assigning @Nullable expression to @NonNull field",
+ " Foo<@Nullable String> f2 = null;", // (location [INNER, TYPE_ARG(0)])
+ " @Nullable Foo<@Nullable String> f3 = null;", // two annotations, each with the
+ // locations above
+ " class Foo<T> { }",
+ " public void test() {",
+ " // BUG: Diagnostic contains: dereferenced",
+ " f1.hashCode();",
+ " // safe, because nonnull",
+ " f2.hashCode();",
+ " // BUG: Diagnostic contains: dereferenced",
+ " f3.hashCode();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void annotationAppliedToInnerTypeExplicitly() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.checkerframework.checker.nullness.qual.Nullable;",
+ "class Test {",
+ " Test.@Nullable Foo f1;",
+ " @Nullable Test.Foo f2;",
+ " class Foo { }",
+ " public void test() {",
+ " // BUG: Diagnostic contains: dereferenced",
+ " f1.hashCode();",
+ " // BUG: Diagnostic contains: dereferenced",
+ " f2.hashCode();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void annotationAppliedToInnerTypeExplicitly2() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Bar.java",
+ "package com.uber;",
+ "import org.checkerframework.checker.nullness.qual.Nullable;",
+ "class Bar {",
+ " public class Foo { }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.checkerframework.checker.nullness.qual.Nullable;",
+ "class Test {",
+ " Bar.@Nullable Foo f1;",
+ " @Nullable Bar.Foo f2;",
+ " public void test() {",
+ " // BUG: Diagnostic contains: dereferenced",
+ " f1.hashCode();",
+ " // BUG: Diagnostic contains: dereferenced",
+ " f2.hashCode();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void annotationAppliedToInnerTypeOfTypeArgument() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Set;",
+ "import org.checkerframework.checker.nullness.qual.Nullable;",
+ "class Test {",
+ " // BUG: Diagnostic contains: @NonNull field s not initialized",
+ " Set<@Nullable Foo> s;", // i.e. Set<Test.@Nullable Foo>
+ " class Foo { }",
+ " public void test() {",
+ " // safe because field is @NonNull",
+ " s.hashCode();",
+ " }",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void typeUseAnnotationOnArray() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import org.checkerframework.checker.nullness.qual.Nullable;",
+ "class Test {",
+ " // ok only for backwards compat",
+ " @Nullable Object[] foo1 = null;",
+ " // ok according to spec",
+ " Object @Nullable[] foo2 = null;",
+ " // ok only for backwards compat",
+ " @Nullable Object [][] foo3 = null;",
+ " // ok according to spec",
+ " Object @Nullable [][] foo4 = null;",
+ " // NOT ok; @Nullable applies to first array dimension not the elements or the array ref",
+ " // TODO: Fix this as part of https://github.com/uber/NullAway/issues/708",
+ " Object [] @Nullable [] foo5 = null;",
+ "}")
+ .doTest();
+ }
+
+ @Test
+ public void typeUseAnnotationOnInnerMultiLevel() {
+ defaultCompilationHelper
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.Set;",
+ "import org.checkerframework.checker.nullness.qual.Nullable;",
+ "class A { class B { class C {} } }",
+ "class Test {",
+ " // At some point, we should not treat foo1 or foo2 as @Nullable.",
+ " // For now we do, for ease of compatibility.",
+ " // TODO: Fix this as part of https://github.com/uber/NullAway/issues/708",
+ " @Nullable A.B.C foo1 = null;",
+ " A.@Nullable B.C foo2 = null;",
+ " A.B.@Nullable C foo3 = null;",
+ " // No good reason to support the case below, though!",
+ " // It neither matches were a correct type use annotation for marking foo4 as @Nullable would be,",
+ " // nor the natural position of a declaration annotation at the start of the type!",
+ " // BUG: Diagnostic contains: assigning @Nullable expression to @NonNull field",
+ " A.B.@Nullable C [][] foo4 = null;",
+ "}")
+ .doTest();
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayUnannotatedTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayUnannotatedTests.java
index 55b8330..32e844a 100644
--- a/nullaway/src/test/java/com/uber/nullaway/NullAwayUnannotatedTests.java
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayUnannotatedTests.java
@@ -98,6 +98,43 @@ public class NullAwayUnannotatedTests extends NullAwayTestsBase {
}
@Test
+ public void generatedAsUnannotatedCustomAnnotation() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:CustomGeneratedCodeAnnotations=com.uber.MyGeneratedMarkerAnnotation",
+ "-XepOpt:NullAway:TreatGeneratedAsUnannotated=true"))
+ .addSourceLines(
+ "MyGeneratedMarkerAnnotation.java",
+ "package com.uber;",
+ "import java.lang.annotation.Retention;",
+ "import java.lang.annotation.Target;",
+ "import static java.lang.annotation.ElementType.CONSTRUCTOR;",
+ "import static java.lang.annotation.ElementType.FIELD;",
+ "import static java.lang.annotation.ElementType.TYPE;",
+ "import static java.lang.annotation.ElementType.METHOD;",
+ "import static java.lang.annotation.ElementType.PACKAGE;",
+ "import static java.lang.annotation.RetentionPolicy.SOURCE;",
+ "@Retention(SOURCE)",
+ "@Target({PACKAGE, TYPE, METHOD, CONSTRUCTOR, FIELD})",
+ "public @interface MyGeneratedMarkerAnnotation {}")
+ .addSourceLines(
+ "Generated.java",
+ "package com.uber;",
+ "@MyGeneratedMarkerAnnotation",
+ "public class Generated { public void takeObj(Object o) {} }")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "class Test {",
+ " void foo() { (new Generated()).takeObj(null); }",
+ "}")
+ .doTest();
+ }
+
+ @Test
public void unannotatedClass() {
makeTestHelperWithArgs(
Arrays.asList(
@@ -171,4 +208,30 @@ public class NullAwayUnannotatedTests extends NullAwayTestsBase {
"}")
.doTest();
}
+
+ /**
+ * Ensure we don't crash for a type cast in unannotated code. See
+ * https://github.com/uber/NullAway/issues/711
+ */
+ @Test
+ public void typeCastInUnannotatedCode() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.other"))
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import java.util.function.Consumer;",
+ "import org.junit.Assert;",
+ "class Test {",
+ " private void verifyCountZero() {",
+ " verifyData((count) -> Assert.assertEquals(0, (long) count));",
+ " }",
+ " private void verifyData(Consumer<Long> assertFunction) {",
+ " }",
+ "}")
+ .doTest();
+ }
}
diff --git a/nullaway/src/test/java/com/uber/nullaway/NullAwayVarargsTests.java b/nullaway/src/test/java/com/uber/nullaway/NullAwayVarargsTests.java
index 97ff399..63a6a9d 100644
--- a/nullaway/src/test/java/com/uber/nullaway/NullAwayVarargsTests.java
+++ b/nullaway/src/test/java/com/uber/nullaway/NullAwayVarargsTests.java
@@ -46,13 +46,31 @@ public class NullAwayVarargsTests extends NullAwayTestsBase {
"public class Utilities {",
" public static String takesNullableVarargs(Object o, @Nullable Object... others) {",
" String s = o.toString() + \" \";",
- " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable",
+ " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", // Shouldn't be an error!
" for (Object other : others) {",
" s += (other == null) ? \"(null) \" : other.toString() + \" \";",
" }",
+ " // BUG: Diagnostic contains: enhanced-for expression others is @Nullable", // Shouldn't be an error!
+ " for (Object other : others) {",
+ " s += other.toString();", // SHOULD be an error!
+ " }",
" return s;",
" }",
"}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "public class Test {",
+ " public void testNonNullVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {",
+ " Utilities.takesNullableVarargs(o1, o2, o3, o4);",
+ " Utilities.takesNullableVarargs(o1);", // Empty var args passed
+ " Utilities.takesNullableVarargs(o1, o4);",
+ " Utilities.takesNullableVarargs(o1, (java.lang.Object) null);",
+ // SHOULD be an error!
+ " Utilities.takesNullableVarargs(o1, (java.lang.Object[]) null);",
+ " }",
+ "}")
.doTest();
}
@@ -97,4 +115,66 @@ public class NullAwayVarargsTests extends NullAwayTestsBase {
"}")
.doTest();
}
+
+ // This is required for compatibility with kotlinc generated jars
+ // See https://github.com/uber/NullAway/issues/720
+ @Test
+ public void testSkipJetbrainsNotNullOnVarArgsFromThirdPartyJars() {
+ makeTestHelperWithArgs(
+ Arrays.asList(
+ "-d",
+ temporaryFolder.getRoot().getAbsolutePath(),
+ "-XepOpt:NullAway:AnnotatedPackages=com.uber",
+ "-XepOpt:NullAway:UnannotatedSubPackages=com.uber.nullaway.[a-zA-Z0-9.]+.unannotated",
+ "-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true"))
+ .addSourceLines(
+ "ThirdParty.java",
+ "package com.uber.nullaway.lib.unannotated;",
+ "import org.jetbrains.annotations.NotNull;",
+ "public class ThirdParty {",
+ " public static String takesNullableVarargs(@NotNull Object o, @NotNull Object... others) {",
+ " String s = o.toString() + \" \";",
+ " for (Object other : others) {",
+ " s += (other == null) ? \"(null) \" : other.toString() + \" \";",
+ " }",
+ " return s;",
+ " }",
+ "}")
+ .addSourceLines(
+ "FirstParty.java",
+ "package com.uber;",
+ "import org.jetbrains.annotations.NotNull;",
+ "public class FirstParty {",
+ " public static String takesNonNullVarargs(@NotNull Object o, @NotNull Object... others) {",
+ " String s = o.toString() + \" \";",
+ " for (Object other : others) {",
+ " s += other.toString() + \" \";",
+ " }",
+ " return s;",
+ " }",
+ "}")
+ .addSourceLines(
+ "Test.java",
+ "package com.uber;",
+ "import javax.annotation.Nullable;",
+ "import com.uber.nullaway.lib.unannotated.ThirdParty;",
+ "public class Test {",
+ " public void testNullableVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {",
+ " ThirdParty.takesNullableVarargs(o1, o2, o3, o4);",
+ " ThirdParty.takesNullableVarargs(o1);", // Empty var args passed
+ " ThirdParty.takesNullableVarargs(o1, o4);",
+ " ThirdParty.takesNullableVarargs(o1, (Object) null);",
+ " ThirdParty.takesNullableVarargs(o1, (Object[]) null);", // SHOULD be an error!
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull",
+ " ThirdParty.takesNullableVarargs(o4);", // First arg is not varargs.
+ " }",
+ " public void testNonNullVarargs(Object o1, Object o2, Object o3, @Nullable Object o4) {",
+ " FirstParty.takesNonNullVarargs(o1, o2, o3);",
+ " FirstParty.takesNonNullVarargs(o1);", // Empty var args passed
+ " // BUG: Diagnostic contains: passing @Nullable parameter 'o4' where @NonNull",
+ " FirstParty.takesNonNullVarargs(o1, o4);",
+ " }",
+ "}")
+ .doTest();
+ }
}
diff --git a/nullaway/src/test/java/com/uber/nullaway/handlers/contract/ContractUtilsTest.java b/nullaway/src/test/java/com/uber/nullaway/handlers/contract/ContractUtilsTest.java
new file mode 100644
index 0000000..fd56d03
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/handlers/contract/ContractUtilsTest.java
@@ -0,0 +1,37 @@
+package com.uber.nullaway.handlers.contract;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.mockito.Mockito.RETURNS_MOCKS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+import com.google.errorprone.VisitorState;
+import com.sun.source.tree.Tree;
+import com.sun.tools.javac.code.Symbol;
+import com.uber.nullaway.NullAway;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ContractUtilsTest {
+
+ private Tree tree;
+ private NullAway analysis;
+ private VisitorState state;
+ private Symbol symbol;
+
+ @Before
+ public void setUp() {
+ tree = mock(Tree.class);
+ analysis = mock(NullAway.class, RETURNS_MOCKS);
+ state = mock(VisitorState.class);
+ symbol = mock(Symbol.class);
+ }
+
+ @Test
+ public void getEmptyAntecedent() {
+ String[] antecedent = ContractUtils.getAntecedent("->_", tree, analysis, state, symbol, 0);
+
+ assertArrayEquals(new String[0], antecedent);
+ verifyNoInteractions(tree, state, analysis, symbol);
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/tools/Display.java b/nullaway/src/test/java/com/uber/nullaway/tools/Display.java
new file mode 100644
index 0000000..6b37889
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/tools/Display.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.tools;
+
+/** Marker interface for defining expected output type of {@link SerializationTestHelper}. */
+public interface Display {}
diff --git a/nullaway/src/test/java/com/uber/nullaway/tools/DisplayFactory.java b/nullaway/src/test/java/com/uber/nullaway/tools/DisplayFactory.java
new file mode 100644
index 0000000..043e0ea
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/tools/DisplayFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+package com.uber.nullaway.tools;
+
+/**
+ * Factory class to enable {@link SerializationTestHelper} to create a new instance from values
+ * written in string at each line of output files.
+ */
+public interface DisplayFactory<T extends Display> {
+
+ /**
+ * Creates an instance of {@code T} from values in string.
+ *
+ * @param values values of instance {@code T} in string.
+ * @return instance of T.
+ */
+ T fromValuesInString(String[] values);
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/tools/ErrorDisplay.java b/nullaway/src/test/java/com/uber/nullaway/tools/ErrorDisplay.java
new file mode 100644
index 0000000..0c6d2b7
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/tools/ErrorDisplay.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+package com.uber.nullaway.tools;
+
+import java.util.Objects;
+
+/**
+ * Helper class to represent a {@link com.uber.nullaway.fixserialization.out.ErrorInfo} contents in
+ * a test case's (expected or actual) output.
+ */
+public class ErrorDisplay implements Display {
+ public final String type;
+ public final String message;
+ public final String encMember;
+ public final String encClass;
+ public final int offset;
+ public final String path;
+ public final String kind;
+ public final String clazz;
+ public final String method;
+ public final String variable;
+ public final String index;
+ public final String nonElementPath;
+
+ public ErrorDisplay(
+ String type,
+ String message,
+ String encClass,
+ String encMember,
+ int offset,
+ String path,
+ String kind,
+ String clazz,
+ String method,
+ String variable,
+ String index,
+ String nonElementPath) {
+ this.type = type;
+ this.message = message;
+ this.encMember = encMember;
+ this.encClass = encClass;
+ this.offset = offset;
+ this.path = path;
+ this.kind = kind;
+ this.clazz = clazz;
+ this.method = method;
+ this.variable = variable;
+ this.index = index;
+ this.nonElementPath = nonElementPath;
+ }
+
+ public ErrorDisplay(
+ String type, String message, String encClass, String encMember, int offset, String path) {
+ this(
+ type, message, encClass, encMember, offset, path, "null", "null", "null", "null", "null",
+ "null");
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ErrorDisplay)) {
+ return false;
+ }
+ ErrorDisplay that = (ErrorDisplay) o;
+ return type.equals(that.type)
+ // To increase readability, a shorter version of the actual message might be present in the
+ // expected output of tests.
+ && (message.contains(that.message) || that.message.contains(message))
+ && encMember.equals(that.encMember)
+ && clazz.equals(that.clazz)
+ && encClass.equals(that.encClass)
+ && offset == that.offset
+ && SerializationTestHelper.pathsAreEqual(path, that.path)
+ && kind.equals(that.kind)
+ && method.equals(that.method)
+ && variable.equals(that.variable)
+ && index.equals(that.index)
+ && SerializationTestHelper.pathsAreEqual(nonElementPath, that.nonElementPath);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ type,
+ message,
+ encMember,
+ encClass,
+ offset,
+ path,
+ kind,
+ clazz,
+ method,
+ variable,
+ index,
+ nonElementPath);
+ }
+
+ @Override
+ public String toString() {
+ return "\n ErrorDisplay{"
+ + "\n\ttype='"
+ + type
+ + '\''
+ + "\n\tmessage='"
+ + message
+ + '\''
+ + "\n\tencMember='"
+ + encMember
+ + '\''
+ + "\n\tencClass='"
+ + encClass
+ + '\''
+ + "\n\toffset='"
+ + offset
+ + '\''
+ + "\n\tpath='"
+ + path
+ + '\''
+ + "\n\tkind='"
+ + kind
+ + '\''
+ + "\n\tclazz='"
+ + clazz
+ + '\''
+ + "\n\tmethod='"
+ + method
+ + '\''
+ + "\n\tvariable='"
+ + variable
+ + '\''
+ + "\n\tindex='"
+ + index
+ + '\''
+ + "\n\tnonElementPath='"
+ + nonElementPath
+ + '\''
+ + '}';
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/tools/FieldInitDisplay.java b/nullaway/src/test/java/com/uber/nullaway/tools/FieldInitDisplay.java
new file mode 100644
index 0000000..1f31260
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/tools/FieldInitDisplay.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+package com.uber.nullaway.tools;
+
+import java.util.Objects;
+
+/**
+ * Helper class to represent a {@link
+ * com.uber.nullaway.fixserialization.out.FieldInitializationInfo} contents in a test case's
+ * (expected or actual) output.
+ */
+public class FieldInitDisplay implements Display {
+ public final String method;
+ public final String param;
+ public final String location;
+ public final String className;
+ public final String field;
+ public final String path;
+
+ public FieldInitDisplay(
+ String field, String method, String param, String location, String className, String path) {
+ this.field = field;
+ this.method = method;
+ this.param = param;
+ this.location = location;
+ this.className = className;
+ this.path = path;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof FieldInitDisplay)) {
+ return false;
+ }
+ FieldInitDisplay that = (FieldInitDisplay) o;
+ return Objects.equals(method, that.method)
+ && Objects.equals(param, that.param)
+ && Objects.equals(location, that.location)
+ && Objects.equals(className, that.className)
+ && Objects.equals(field, that.field)
+ && SerializationTestHelper.pathsAreEqual(path, that.path);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(method, param, location, className, field, path);
+ }
+
+ @Override
+ public String toString() {
+ return "\n FieldInitDisplay{"
+ + "\n\tfield='"
+ + field
+ + '\''
+ + ", \n\tmethod='"
+ + method
+ + '\''
+ + ", \n\tparam='"
+ + param
+ + '\''
+ + ", \n\tlocation='"
+ + location
+ + '\''
+ + ", \n\tclassName='"
+ + className
+ + '\''
+ + ", \n\tpath='"
+ + path
+ + '\''
+ + "\n }\n";
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/tools/FixDisplay.java b/nullaway/src/test/java/com/uber/nullaway/tools/FixDisplay.java
new file mode 100644
index 0000000..213d8df
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/tools/FixDisplay.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.tools;
+
+import java.util.Objects;
+
+/**
+ * Helper class to represent a suggested fix contents in a test case's (expected or actual) output.
+ */
+public class FixDisplay implements Display {
+ public final String annotation;
+ public final String method;
+ public final String param;
+ public final String location;
+ public final String className;
+ public final String path;
+
+ public FixDisplay(
+ String annotation,
+ String method,
+ String param,
+ String location,
+ String className,
+ String path) {
+ this.annotation = annotation;
+ this.method = method;
+ this.param = param;
+ this.location = location;
+ this.className = className;
+ this.path = path;
+ }
+
+ @Override
+ public String toString() {
+ return "\n FixDisplay{"
+ + "\n\tannotation='"
+ + annotation
+ + '\''
+ + ", \n\tmethod='"
+ + method
+ + '\''
+ + ", \n\tparam='"
+ + param
+ + '\''
+ + ", \n\tlocation='"
+ + location
+ + '\''
+ + ", \n\tclassName='"
+ + className
+ + '\''
+ + ", \n\tpath='"
+ + path
+ + '\''
+ + "\n }\n";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof FixDisplay)) {
+ return false;
+ }
+ FixDisplay fix = (FixDisplay) o;
+ return Objects.equals(annotation, fix.annotation)
+ && Objects.equals(method, fix.method)
+ && Objects.equals(param, fix.param)
+ && Objects.equals(location, fix.location)
+ && Objects.equals(className, fix.className)
+ && SerializationTestHelper.pathsAreEqual(path, fix.path);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(annotation, method, param, location, className, path);
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/tools/SerializationTestHelper.java b/nullaway/src/test/java/com/uber/nullaway/tools/SerializationTestHelper.java
new file mode 100644
index 0000000..6a01999
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/tools/SerializationTestHelper.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.tools;
+
+import static org.junit.Assert.fail;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.errorprone.CompilationTestHelper;
+import com.uber.nullaway.NullAway;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+public class SerializationTestHelper<T extends Display> {
+
+ private final Path outputDir;
+ private ImmutableList<T> expectedOutputs;
+ private CompilationTestHelper compilationTestHelper;
+ private DisplayFactory<T> factory;
+ private String fileName;
+ private String header;
+
+ public SerializationTestHelper(Path outputDir) {
+ this.outputDir = outputDir;
+ }
+
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ public SerializationTestHelper<T> addSourceLines(String path, String... lines) {
+ compilationTestHelper.addSourceLines(path, lines);
+ return this;
+ }
+
+ @SafeVarargs
+ public final SerializationTestHelper<T> setExpectedOutputs(T... outputs) {
+ this.expectedOutputs = ImmutableList.copyOf(outputs);
+ return this;
+ }
+
+ public SerializationTestHelper<T> expectNoOutput() {
+ this.expectedOutputs = ImmutableList.of();
+ return this;
+ }
+
+ public SerializationTestHelper<T> setArgs(List<String> args) {
+ compilationTestHelper =
+ CompilationTestHelper.newInstance(NullAway.class, getClass()).setArgs(args);
+ return this;
+ }
+
+ public SerializationTestHelper<T> setFactory(DisplayFactory<T> factory) {
+ this.factory = factory;
+ return this;
+ }
+
+ public SerializationTestHelper<T> setOutputFileNameAndHeader(String fileName, String header) {
+ this.fileName = fileName;
+ this.header = header;
+ return this;
+ }
+
+ public void doTest() {
+ Preconditions.checkNotNull(factory, "Factory cannot be null");
+ Preconditions.checkNotNull(fileName, "File name cannot be null");
+ Path outputPath = outputDir.resolve(fileName);
+ try {
+ Files.deleteIfExists(outputPath);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to delete older file at: " + outputPath, e);
+ }
+ compilationTestHelper.doTest();
+ List<T> actualOutputs = readActualOutputs(outputPath);
+ compare(actualOutputs);
+ }
+
+ private void compare(List<T> actualOutput) {
+ List<T> notFound = new ArrayList<>();
+ for (T o : expectedOutputs) {
+ if (!actualOutput.contains(o)) {
+ notFound.add(o);
+ } else {
+ actualOutput.remove(o);
+ }
+ }
+ if (notFound.size() == 0 && actualOutput.size() == 0) {
+ return;
+ }
+ StringBuilder errorMessage = new StringBuilder();
+ if (notFound.size() != 0) {
+ errorMessage
+ .append(notFound.size())
+ .append(" expected outputs were NOT found:")
+ .append("\n")
+ .append(notFound.stream().map(T::toString).collect(Collectors.toList()))
+ .append("\n");
+ }
+ if (actualOutput.size() != 0) {
+ errorMessage
+ .append(actualOutput.size())
+ .append(" unexpected outputs were found:")
+ .append("\n")
+ .append(actualOutput.stream().map(T::toString).collect(Collectors.toList()))
+ .append("\n");
+ }
+ fail(errorMessage.toString());
+ }
+
+ private List<T> readActualOutputs(Path outputPath) {
+ List<T> outputs = new ArrayList<>();
+ BufferedReader reader;
+ try {
+ reader = Files.newBufferedReader(outputPath, Charset.defaultCharset());
+ String actualHeader = reader.readLine();
+ if (!header.equals(actualHeader)) {
+ fail(
+ "Expected header of "
+ + outputPath.getFileName()
+ + " to be: "
+ + header
+ + "\nBut found: "
+ + actualHeader);
+ }
+ String line = reader.readLine();
+ while (line != null) {
+ T output = factory.fromValuesInString(line.split("\\t"));
+ outputs.add(output);
+ line = reader.readLine();
+ }
+ reader.close();
+ } catch (IOException e) {
+ throw new RuntimeException("Error happened in reading the outputs.", e);
+ }
+ return outputs;
+ }
+
+ /**
+ * Checks if given paths are equal. Under different OS environments, identical paths might have a
+ * different string representation. In windows all forward slashes are replaced with backslashes.
+ *
+ * @param expected Expected serialized path.
+ * @param found Serialized path.
+ * @return true, if paths are identical.
+ */
+ public static boolean pathsAreEqual(String expected, String found) {
+ if (found.equals(expected)) {
+ return true;
+ }
+ return found.replaceAll("\\\\", "/").equals(expected);
+ }
+
+ /**
+ * Extracts relative path from the serialized full path.
+ *
+ * @param pathInString Full serialized path.
+ * @return Relative path to "com" from the given path including starting from "com" directory.
+ */
+ public static String getRelativePathFromUnitTestTempDirectory(String pathInString) {
+ if (pathInString.equals("null")) {
+ return "null";
+ }
+ // using atomic refs to use them inside inner class below. This is not due to any concurrent
+ // modifications.
+ AtomicReference<Path> relativePath = new AtomicReference<>(Paths.get("com"));
+ AtomicReference<Boolean> relativePathStarted = new AtomicReference<>(false);
+ Path path = Paths.get(pathInString);
+ path.iterator()
+ .forEachRemaining(
+ remaining -> {
+ if (relativePathStarted.get()) {
+ relativePath.set(relativePath.get().resolve(remaining));
+ }
+ if (remaining.toString().startsWith("com")) {
+ relativePathStarted.set(true);
+ }
+ });
+ return relativePath.get().toString();
+ }
+}
diff --git a/nullaway/src/test/java/com/uber/nullaway/tools/version1/ErrorDisplayV1.java b/nullaway/src/test/java/com/uber/nullaway/tools/version1/ErrorDisplayV1.java
new file mode 100644
index 0000000..445b810
--- /dev/null
+++ b/nullaway/src/test/java/com/uber/nullaway/tools/version1/ErrorDisplayV1.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+package com.uber.nullaway.tools.version1;
+
+import com.google.common.base.Preconditions;
+import com.uber.nullaway.tools.Display;
+import com.uber.nullaway.tools.DisplayFactory;
+import com.uber.nullaway.tools.SerializationTestHelper;
+import java.util.Objects;
+
+/**
+ * Helper class to test backward compatibility of different serialization versions and to represent
+ * a {@link com.uber.nullaway.fixserialization.out.ErrorInfo} contents in a test case's (expected or
+ * actual) output <strong>specifically for serialization version 1.</strong>
+ */
+public class ErrorDisplayV1 implements Display {
+ public final String type;
+ public final String message;
+ public final String encMember;
+ public final String encClass;
+ public final String kind;
+ public final String clazz;
+ public final String method;
+ public final String variable;
+ public final String index;
+ public final String path;
+
+ public ErrorDisplayV1(
+ String type,
+ String message,
+ String encClass,
+ String encMember,
+ String kind,
+ String clazz,
+ String method,
+ String variable,
+ String index,
+ String path) {
+ this.type = type;
+ this.message = message;
+ this.encMember = encMember;
+ this.encClass = encClass;
+ this.kind = kind;
+ this.clazz = clazz;
+ this.method = method;
+ this.variable = variable;
+ this.index = index;
+ this.path = path;
+ }
+
+ public ErrorDisplayV1(String type, String message, String encClass, String encMember) {
+ this(type, message, encClass, encMember, "null", "null", "null", "null", "null", "null");
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ErrorDisplayV1)) {
+ return false;
+ }
+ ErrorDisplayV1 that = (ErrorDisplayV1) o;
+ return type.equals(that.type)
+ // To increase readability, a shorter version of the actual message might be present in the
+ // expected output of tests.
+ && (message.contains(that.message) || that.message.contains(message))
+ && encMember.equals(that.encMember)
+ && clazz.equals(that.clazz)
+ && encClass.equals(that.encClass)
+ && kind.equals(that.kind)
+ && method.equals(that.method)
+ && variable.equals(that.variable)
+ && index.equals(that.index)
+ && SerializationTestHelper.pathsAreEqual(path, that.path);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ type, message, encMember, encClass, kind, clazz, method, variable, index, path);
+ }
+
+ @Override
+ public String toString() {
+ return "\n ErrorDisplay{"
+ + "\n\ttype='"
+ + type
+ + '\''
+ + "\n\tmessage='"
+ + message
+ + '\''
+ + "\n\tencMember='"
+ + encMember
+ + '\''
+ + "\n\tencClass='"
+ + encClass
+ + '\''
+ + "\n\tkind='"
+ + kind
+ + '\''
+ + "\n\tclazz='"
+ + clazz
+ + '\''
+ + "\n\tmethod='"
+ + method
+ + '\''
+ + "\n\tvariable='"
+ + variable
+ + '\''
+ + "\n\tindex='"
+ + index
+ + '\''
+ + "\n\turi='"
+ + path
+ + '\''
+ + '}';
+ }
+
+ /**
+ * Returns the corresponding {@link DisplayFactory} for creating {@link ErrorDisplayV1} objects
+ * from an array of strings.
+ *
+ * @return a {@link DisplayFactory} for {@link ErrorDisplayV1} objects.
+ */
+ public static DisplayFactory<ErrorDisplayV1> getFactory() {
+ return values -> {
+ Preconditions.checkArgument(
+ values.length == 10,
+ "Needs exactly 10 values to create ErrorDisplay for version 1 object but found: "
+ + values.length);
+ return new ErrorDisplayV1(
+ values[0],
+ values[1],
+ values[2],
+ values[3],
+ values[4],
+ values[5],
+ values[6],
+ values[7],
+ values[8],
+ SerializationTestHelper.getRelativePathFromUnitTestTempDirectory(values[9]));
+ };
+ }
+}
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/CheckFieldInitNegativeCases.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/CheckFieldInitNegativeCases.java
index 7b4df6a..9ef64f8 100644
--- a/nullaway/src/test/resources/com/uber/nullaway/testdata/CheckFieldInitNegativeCases.java
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/CheckFieldInitNegativeCases.java
@@ -22,8 +22,8 @@
package com.uber.nullaway.testdata;
-import com.facebook.infer.annotation.Initializer;
import com.google.errorprone.annotations.concurrent.LazyInit;
+import com.uber.nullaway.annotations.Initializer;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.junit.Before;
import org.junit.BeforeClass;
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/CheckFieldInitPositiveCases.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/CheckFieldInitPositiveCases.java
index 15ba2ef..db632ad 100755
--- a/nullaway/src/test/resources/com/uber/nullaway/testdata/CheckFieldInitPositiveCases.java
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/CheckFieldInitPositiveCases.java
@@ -22,7 +22,7 @@
package com.uber.nullaway.testdata;
-import com.facebook.infer.annotation.Initializer;
+import com.uber.nullaway.annotations.Initializer;
import javax.annotation.Nullable;
/** Created by msridhar on 3/7/17. */
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayNativeModels.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayNativeModels.java
index be70a6b..8c337e9 100644
--- a/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayNativeModels.java
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayNativeModels.java
@@ -58,6 +58,10 @@ public class NullAwayNativeModels {
Exception e = new RuntimeException();
// BUG: Diagnostic contains: dereferenced expression
e.getMessage().hashCode();
+ // BUG: Diagnostic contains: dereferenced expression
+ e.getLocalizedMessage().hashCode();
+ // BUG: Diagnostic contains: dereferenced expression
+ e.getCause().toString();
}
// we will add bug annotations when we have full support for maps
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayNegativeCases.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayNegativeCases.java
index 69b3ed3..790a077 100644
--- a/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayNegativeCases.java
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayNegativeCases.java
@@ -437,10 +437,6 @@ public class NullAwayNegativeCases {
return TestAnnot.TEST_STR;
}
- static Void testVoidType() {
- return null;
- }
-
static String stringConcat() {
String x = "hello ";
String y = "world";
@@ -483,6 +479,8 @@ public class NullAwayNegativeCases {
return b | a;
} else if (b == 4) {
return b ^ a;
+ } else if (b == 5) {
+ return ~a;
} else {
return 10;
}
@@ -918,6 +916,7 @@ public class NullAwayNegativeCases {
s += boxAndDeref(n >>= 1);
s += boxAndDeref(m >>>= 4);
s += boxAndDeref(n >>> 3);
+ s += boxAndDeref(~n);
return s;
}
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayPositiveCases.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayPositiveCases.java
index d7316b1..adf7584 100644
--- a/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayPositiveCases.java
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayPositiveCases.java
@@ -333,6 +333,9 @@ public class NullAwayPositiveCases {
Boolean z = null;
// BUG: Diagnostic contains: unboxing
int d = z ? 3 : 4;
+ Integer w = null;
+ // BUG: Diagnostic contains: unboxing
+ int e = ~w;
}
static void unboxingTests2() {
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayStreamSupportNegativeCases.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayStreamSupportNegativeCases.java
index a733304..742a638 100644
--- a/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayStreamSupportNegativeCases.java
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayStreamSupportNegativeCases.java
@@ -22,7 +22,9 @@
package com.uber.nullaway.testdata;
+import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
+import com.uber.nullaway.testdata.unannotated.CustomStream;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.DoubleStream;
@@ -177,6 +179,11 @@ public class NullAwayStreamSupportNegativeCases {
});
}
+ private Stream<String> filterThenMapStreamOfMapsWithGet(
+ Stream<java.util.Map<String, Integer>> stream) {
+ return stream.filter(m -> m.get("hello") != null).map(n -> n.get("hello").toString());
+ }
+
private static class NoOpFilterClass<T> implements Predicate<T> {
public NoOpFilterClass() {}
@@ -281,4 +288,53 @@ public class NullAwayStreamSupportNegativeCases {
private void filterThenForEachOrdered(Stream<NullableContainer<String>> stream) {
stream.filter(s -> s.get() != null).forEachOrdered(s -> System.out.println(s.get().length()));
}
+
+ // CustomStream is modeled in TestLibraryModels
+ private CustomStream<Integer> filterThenMapLambdasCustomStream(CustomStream<String> stream) {
+ return stream.filter(s -> s != null).map(s -> s.length());
+ }
+
+ private CustomStream<Integer> filterThenMapNullableContainerLambdasCustomStream(
+ CustomStream<NullableContainer<String>> stream) {
+ return stream
+ .filter(c -> c.get() != null)
+ .map(c -> c.get().length());
+ }
+
+ private CustomStream<Integer> filterThenMapMethodRefsCustomStream(
+ CustomStream<NullableContainer<String>> stream) {
+ return stream
+ .filter(c -> c.get() != null && perhaps())
+ .map(NullableContainer::get)
+ .map(String::length);
+ }
+
+ private static class CheckFinalBeforeStream<T> {
+ @Nullable private final T ref;
+
+ public CheckFinalBeforeStream(@Nullable T ref) {
+ this.ref = ref;
+ }
+
+ private Stream<T> test1(Stream<T> stream) {
+ Preconditions.checkNotNull(ref);
+ final T asLocal = ref;
+ return stream.filter(s -> asLocal.equals(s));
+ }
+
+ private Stream<T> test2(Stream<T> stream) {
+ Preconditions.checkNotNull(ref);
+ // Safe because ref is final!
+ return stream.filter(s -> ref.equals(s));
+ }
+
+ private Stream<T> test3(Stream<T> stream) {
+ if (ref != null) {
+ // Safe because ref is final!
+ return stream.filter(s -> ref.equals(s));
+ } else {
+ return stream.filter(s -> "CONST".equals(s.toString()));
+ }
+ }
+ }
}
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayStreamSupportPositiveCases.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayStreamSupportPositiveCases.java
index a58d1bb..82a12a0 100644
--- a/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayStreamSupportPositiveCases.java
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/NullAwayStreamSupportPositiveCases.java
@@ -22,6 +22,8 @@
package com.uber.nullaway.testdata;
+import com.google.common.base.Preconditions;
+import com.uber.nullaway.testdata.unannotated.CustomStreamWithoutModel;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.DoubleStream;
@@ -151,4 +153,46 @@ public class NullAwayStreamSupportPositiveCases {
// BUG: Diagnostic contains: dereferenced expression
stream.forEachOrdered(s -> System.out.println(s.get().length()));
}
+
+ // CustomStreamWithoutModel is NOT modeled in TestLibraryModels
+ private CustomStreamWithoutModel<Integer> filterThenMapLambdasCustomStream(CustomStreamWithoutModel<String> stream) {
+ // Safe because generic is String, not @Nullable String
+ return stream.filter(s -> s != null).map(s -> s.length());
+ }
+
+ private CustomStreamWithoutModel<Integer> filterThenMapNullableContainerLambdasCustomStream(
+ CustomStreamWithoutModel<NullableContainer<String>> stream) {
+ return stream
+ .filter(c -> c.get() != null)
+ // BUG: Diagnostic contains: dereferenced expression
+ .map(c -> c.get().length());
+ }
+
+ private CustomStreamWithoutModel<Integer> filterThenMapMethodRefsCustomStream(
+ CustomStreamWithoutModel<NullableContainer<String>> stream) {
+ return stream
+ .filter(c -> c.get() != null && perhaps())
+ .map(NullableContainer::get) // CSWoM<NullableContainer<String>> -> CSWoM<@Nullable String>
+ .map(String::length); // Should be an error with proper generics support!
+ }
+
+ private static class CheckNonfinalBeforeStream<T> {
+ @Nullable private T ref;
+
+ public CheckNonfinalBeforeStream(@Nullable T ref) {
+ this.ref = ref;
+ }
+
+ private Stream<T> test1(Stream<T> stream) {
+ Preconditions.checkNotNull(ref);
+ final T asLocal = ref;
+ return stream.filter(s -> asLocal.equals(s));
+ }
+
+ private Stream<T> test2(Stream<T> stream) {
+ Preconditions.checkNotNull(ref);
+ // BUG: Diagnostic contains: dereferenced expression ref is @Nullable
+ return stream.filter(s -> ref.equals(s));
+ }
+ }
}
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/ReadBeforeInitNegativeCases.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/ReadBeforeInitNegativeCases.java
index f00c95e..c12fe4c 100644
--- a/nullaway/src/test/resources/com/uber/nullaway/testdata/ReadBeforeInitNegativeCases.java
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/ReadBeforeInitNegativeCases.java
@@ -24,7 +24,7 @@ package com.uber.nullaway.testdata;
import static com.uber.nullaway.testdata.Util.castToNonNull;
-import com.facebook.infer.annotation.Initializer;
+import com.uber.nullaway.annotations.Initializer;
import javax.annotation.Nullable;
public class ReadBeforeInitNegativeCases {
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/ReadBeforeInitPositiveCases.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/ReadBeforeInitPositiveCases.java
index 44873c8..2858bab 100644
--- a/nullaway/src/test/resources/com/uber/nullaway/testdata/ReadBeforeInitPositiveCases.java
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/ReadBeforeInitPositiveCases.java
@@ -22,7 +22,7 @@
package com.uber.nullaway.testdata;
-import com.facebook.infer.annotation.Initializer;
+import com.uber.nullaway.annotations.Initializer;
public class ReadBeforeInitPositiveCases {
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/Util.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/Util.java
index db75d29..d2a9bee 100644
--- a/nullaway/src/test/resources/com/uber/nullaway/testdata/Util.java
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/Util.java
@@ -33,6 +33,21 @@ public class Util {
return x;
}
+ public static <T> T castToNonNull(@Nullable T x, String msg) {
+ if (x == null) {
+ throw new RuntimeException(msg);
+ }
+ return x;
+ }
+
+ public static <T> T castToNonNull(String msg, @Nullable T x, int counter) {
+ // counter is needed to distinguish this method from the previous one when T == String
+ if (x == null) {
+ throw new RuntimeException(msg);
+ }
+ return x;
+ }
+
public static <T> T id(T x) {
return x;
}
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/springboot-annotations/MockBean.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/springboot-annotations/MockBean.java
new file mode 100644
index 0000000..9932938
--- /dev/null
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/springboot-annotations/MockBean.java
@@ -0,0 +1,13 @@
+package org.springframework.boot.test.mock.mockito;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE, ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface MockBean {
+}
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/springboot-annotations/SpyBean.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/springboot-annotations/SpyBean.java
new file mode 100644
index 0000000..11a186a
--- /dev/null
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/springboot-annotations/SpyBean.java
@@ -0,0 +1,13 @@
+package org.springframework.boot.test.mock.mockito;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE, ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface SpyBean {
+}
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/unannotated/CustomStream.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/unannotated/CustomStream.java
new file mode 100644
index 0000000..22ff34a
--- /dev/null
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/unannotated/CustomStream.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2023 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.testdata.unannotated;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * A class representing a custom implementation of java.util.stream.Stream to test the ability to
+ * define stream handler specs through library models.
+ */
+public class CustomStream<T> {
+
+ private CustomStream() {}
+
+ public CustomStream<T> filter(Predicate<? super T> predicate) {
+ throw new UnsupportedOperationException();
+ }
+
+ public <R> CustomStream<R> map(Function<? super T, ? extends R> mapper) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/unannotated/CustomStreamWithoutModel.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/unannotated/CustomStreamWithoutModel.java
new file mode 100644
index 0000000..1fd7037
--- /dev/null
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/unannotated/CustomStreamWithoutModel.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2023 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.testdata.unannotated;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * A copy of {@link CustomStream} but for which our test library models have no spec/model, to test that errors still
+ * get reported in the absence of a model.
+ */
+public class CustomStreamWithoutModel<T> {
+
+ private CustomStreamWithoutModel() {}
+
+ public CustomStreamWithoutModel<T> filter(Predicate<? super T> predicate) {
+ throw new UnsupportedOperationException();
+ }
+
+ public <R> CustomStreamWithoutModel<R> map(Function<? super T, ? extends R> mapper) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/nullaway/src/test/resources/com/uber/nullaway/testdata/unannotated/MinimalUnannotatedClass.java b/nullaway/src/test/resources/com/uber/nullaway/testdata/unannotated/MinimalUnannotatedClass.java
new file mode 100644
index 0000000..e78330a
--- /dev/null
+++ b/nullaway/src/test/resources/com/uber/nullaway/testdata/unannotated/MinimalUnannotatedClass.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2022 Uber Technologies, Inc.
+ *
+ * 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.
+ */
+
+package com.uber.nullaway.testdata.unannotated;
+
+/**
+ * A minimal class, used from {@link com.uber.nullaway.NullAwaySerializationTest} to avoid extra
+ * fixes.
+ */
+public class MinimalUnannotatedClass {
+
+ private MinimalUnannotatedClass() {}
+
+ /**
+ * This is an identity method, without Nullability annotations.
+ *
+ * @param x
+ * @return
+ */
+ public static Object foo(Object x) {
+ return x;
+ }
+}
diff --git a/sample-app/build.gradle b/sample-app/build.gradle
index 4b4de17..6c4deb2 100644
--- a/sample-app/build.gradle
+++ b/sample-app/build.gradle
@@ -23,7 +23,6 @@ plugins {
android {
compileSdkVersion deps.build.compileSdkVersion
- buildToolsVersion deps.build.buildToolsVersion
defaultConfig {
applicationId "com.uber.myapplication"
@@ -33,14 +32,14 @@ android {
versionName "1.0"
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_7
- targetCompatibility JavaVersion.VERSION_1_7
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
}
lintOptions {
abortOnError false
}
-
+
DomainObjectSet<BaseVariant> variants = getApplicationVariants() // or getLibraryVariants() in libraries
variants.addAll(getTestVariants())
variants.addAll(getUnitTestVariants())
@@ -52,17 +51,17 @@ android {
}
}
}
-
- // If you want to disable NullAway in justs tests, you can do the below
-// DomainObjectSet<BaseVariant> testVariants = getTestVariants()
-// testVariants.addAll(getUnitTestVariants())
-// testVariants.configureEach { variant ->
-// variant.getJavaCompileProvider().configure {
-// options.errorprone {
-// check("NullAway", CheckSeverity.OFF)
-// }
-// }
-// }
+
+ // If you want to disable NullAway in just tests, you can do the below
+ // DomainObjectSet<BaseVariant> testVariants = getTestVariants()
+ // testVariants.addAll(getUnitTestVariants())
+ // testVariants.configureEach { variant ->
+ // variant.getJavaCompileProvider().configure {
+ // options.errorprone {
+ // check("NullAway", CheckSeverity.OFF)
+ // }
+ // }
+ // }
}
dependencies {
@@ -71,5 +70,10 @@ dependencies {
annotationProcessor project(path: ":sample-library-model")
testImplementation deps.test.junit4
+}
+spotless {
+ java {
+ target 'src/*/java/**/*.java'
+ }
}
diff --git a/sample-app/src/main/java/com/uber/myapplication/MainFragment.java b/sample-app/src/main/java/com/uber/myapplication/MainFragment.java
index 5212a04..f53e880 100644
--- a/sample-app/src/main/java/com/uber/myapplication/MainFragment.java
+++ b/sample-app/src/main/java/com/uber/myapplication/MainFragment.java
@@ -22,7 +22,6 @@ public class MainFragment extends Fragment {
mOnCreateInitialisedField = new Object();
}
- @Nullable
@Override
public View onCreateView(
@NonNull LayoutInflater inflater,
diff --git a/sample-library-model/build.gradle b/sample-library-model/build.gradle
index bd9b794..8feee69 100644
--- a/sample-library-model/build.gradle
+++ b/sample-library-model/build.gradle
@@ -17,12 +17,9 @@
import net.ltgt.gradle.errorprone.CheckSeverity
plugins {
- id "java-library"
+ id "java-library"
}
-sourceCompatibility = "1.8"
-targetCompatibility = "1.8"
-
dependencies {
compileOnly deps.apt.autoService
annotationProcessor deps.apt.autoService
@@ -32,10 +29,10 @@ dependencies {
}
tasks.withType(JavaCompile) {
- if (!name.toLowerCase().contains("test")) {
- options.errorprone {
- check("NullAway", CheckSeverity.ERROR)
- option("NullAway:AnnotatedPackages", "com.uber")
+ if (!name.toLowerCase().contains("test")) {
+ options.errorprone {
+ check("NullAway", CheckSeverity.ERROR)
+ option("NullAway:AnnotatedPackages", "com.uber")
+ }
}
- }
-} \ No newline at end of file
+}
diff --git a/sample-library-model/src/main/java/com/uber/modelexample/ExampleLibraryModels.java b/sample-library-model/src/main/java/com/uber/modelexample/ExampleLibraryModels.java
index ff78dac..a1aa99d 100644
--- a/sample-library-model/src/main/java/com/uber/modelexample/ExampleLibraryModels.java
+++ b/sample-library-model/src/main/java/com/uber/modelexample/ExampleLibraryModels.java
@@ -66,4 +66,9 @@ public class ExampleLibraryModels implements LibraryModels {
public ImmutableSet<MethodRef> nonNullReturns() {
return ImmutableSet.of();
}
+
+ @Override
+ public ImmutableSetMultimap<MethodRef, Integer> castToNonNullMethods() {
+ return ImmutableSetMultimap.of();
+ }
}
diff --git a/sample/build.gradle b/sample/build.gradle
index 2586c8d..58ddd90 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -17,12 +17,9 @@
import net.ltgt.gradle.errorprone.CheckSeverity
plugins {
- id "java-library"
+ id "java-library"
}
-sourceCompatibility = "1.8"
-targetCompatibility = "1.8"
-
dependencies {
annotationProcessor project(":nullaway")
annotationProcessor project(path: ":sample-library-model")
@@ -32,10 +29,10 @@ dependencies {
}
tasks.withType(JavaCompile) {
- if (!name.toLowerCase().contains("test")) {
- options.errorprone {
- check("NullAway", CheckSeverity.ERROR)
- option("NullAway:AnnotatedPackages", "com.uber")
+ if (!name.toLowerCase().contains("test")) {
+ options.errorprone {
+ check("NullAway", CheckSeverity.ERROR)
+ option("NullAway:AnnotatedPackages", "com.uber")
+ }
}
- }
-}
+}
diff --git a/settings.gradle b/settings.gradle
index 2a69fb2..bfafe10 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -4,23 +4,15 @@ pluginManagement {
gradlePluginPortal()
google()
}
- resolutionStrategy {
- eachPlugin {
- if (requested.id.namespace == "com.android") {
- useModule("com.android.tools.build:gradle:${requested.version}")
- }
- }
- }
}
+include ':annotations'
include ':nullaway'
include ':sample-library-model'
include ':sample'
-include ':sample-app'
include ':test-java-lib'
include ':test-java-lib-lombok'
include ':test-library-models'
-include ':compile-bench'
include ':jar-infer:android-jarinfer-models-sdk28'
include ':jar-infer:android-jarinfer-models-sdk29'
include ':jar-infer:android-jarinfer-models-sdk30'
@@ -29,12 +21,9 @@ include ':jar-infer:jar-infer-lib'
include ':jar-infer:jar-infer-cli'
include ':jar-infer:test-java-lib-jarinfer'
include ':jar-infer:nullaway-integration-test'
-include ':jar-infer:test-android-lib-jarinfer'
include ':jmh'
-include ':jdk17-unit-tests'
-
-// On Java 8, the code-coverage-report module fails during Gradle configuration. So, exclude it
-// on pre-JDK-11 JVMs
-if (JavaVersion.current() >= JavaVersion.VERSION_11) {
- include ':code-coverage-report'
-}
+include ':guava-recent-unit-tests'
+include ':jdk-recent-unit-tests'
+include ':code-coverage-report'
+include ':sample-app'
+include ':jar-infer:test-android-lib-jarinfer'
diff --git a/test-java-lib-lombok/build.gradle b/test-java-lib-lombok/build.gradle
index 71b76c2..c70f548 100644
--- a/test-java-lib-lombok/build.gradle
+++ b/test-java-lib-lombok/build.gradle
@@ -17,12 +17,9 @@
import net.ltgt.gradle.errorprone.CheckSeverity
plugins {
- id "java-library"
+ id "java-library"
}
-sourceCompatibility = "1.8"
-targetCompatibility = "1.8"
-
dependencies {
annotationProcessor project(":nullaway")
annotationProcessor deps.test.lombok
@@ -34,18 +31,16 @@ dependencies {
}
tasks.withType(JavaCompile) {
- if (!name.toLowerCase().contains("test")) {
- options.errorprone {
- check("NullAway", CheckSeverity.ERROR)
- check("UnusedVariable", CheckSeverity.OFF) // We are not the only checker that fails on Lombok
- check("MissingBraces", CheckSeverity.OFF) // We are not the only checker that fails on Lombok
- option("NullAway:AnnotatedPackages", "com.uber")
- option("NullAway:UnannotatedSubPackages", "com.uber.lib.unannotated")
+ if (!name.toLowerCase().contains("test")) {
+ options.errorprone {
+ check("NullAway", CheckSeverity.ERROR)
+ option("NullAway:AnnotatedPackages", "com.uber")
+ option("NullAway:UnannotatedSubPackages", "com.uber.lib.unannotated")
+ }
}
- }
- if (JavaVersion.current().java9Compatible) {
// We need to fork on JDK 16+ since Lombok accesses internal compiler APIs
options.fork = true
- options.forkOptions.jvmArgs += ["--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED"]
- }
+ options.forkOptions.jvmArgs += [
+ "--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED"
+ ]
}
diff --git a/test-java-lib-lombok/src/main/java/com/uber/lombok/LombokBuilderInit.java b/test-java-lib-lombok/src/main/java/com/uber/lombok/LombokDTO.java
index 683f0e1..44d6b50 100644
--- a/test-java-lib-lombok/src/main/java/com/uber/lombok/LombokBuilderInit.java
+++ b/test-java-lib-lombok/src/main/java/com/uber/lombok/LombokDTO.java
@@ -28,12 +28,13 @@ import lombok.Data;
@Builder
@Data
-@SuppressWarnings({
- "SameNameButDifferent" /* crashes with EP 2.6.0 */,
- "InlineMeInliner" /* crashes with EP 2.7.1 */
-})
-public class LombokBuilderInit {
+@SuppressWarnings({"MissingBraces" /* false positive warnings */})
+public class LombokDTO {
+ // Note: Lombok generates a constructor directly into this class that initializes this field, so
+ // NullAway does not
+ // issue an uninitialized field warning.
private String field;
@Builder.Default private String fieldWithDefault = "Default";
@Nullable private String nullableField;
+ @Nullable @Builder.Default private String fieldWithNullDefault = null;
}
diff --git a/test-java-lib-lombok/src/main/java/com/uber/lombok/UsesBuilder.java b/test-java-lib-lombok/src/main/java/com/uber/lombok/UsesDTO.java
index 8ed452c..f236a65 100644
--- a/test-java-lib-lombok/src/main/java/com/uber/lombok/UsesBuilder.java
+++ b/test-java-lib-lombok/src/main/java/com/uber/lombok/UsesDTO.java
@@ -22,13 +22,20 @@
package com.uber.lombok;
-class UsesBuilder {
- public static String foo(LombokBuilderInit lbi) {
+import javax.annotation.Nullable;
+
+class UsesDTO {
+
+ public static LombokDTO getDTOInstance(@Nullable String s1, String s2) {
+ return LombokDTO.builder().nullableField(s1).field(s2).build();
+ }
+
+ public static String foo(LombokDTO ldto) {
String s = "";
- s += lbi.getField().toString();
+ s += ldto.getField().toString();
s += " ";
// Removing this nullness check produces a NullAway error
- s += (lbi.getNullableField() == null ? "" : lbi.getNullableField().toString());
+ s += (ldto.getNullableField() == null ? "" : ldto.getNullableField().toString());
return s;
}
}
diff --git a/test-java-lib/build.gradle b/test-java-lib/build.gradle
index 0869fa9..79323d6 100644
--- a/test-java-lib/build.gradle
+++ b/test-java-lib/build.gradle
@@ -17,14 +17,12 @@
import net.ltgt.gradle.errorprone.CheckSeverity
plugins {
- id "java-library"
+ id "java-library"
}
-sourceCompatibility = "1.8"
-targetCompatibility = "1.8"
-
dependencies {
annotationProcessor project(":nullaway")
+ implementation deps.build.jspecify
compileOnly deps.build.jsr305Annotations
compileOnly deps.build.javaxValidation
@@ -32,11 +30,11 @@ dependencies {
}
tasks.withType(JavaCompile) {
- if (!name.toLowerCase().contains("test")) {
- options.errorprone {
- check("NullAway", CheckSeverity.ERROR)
- option("NullAway:AnnotatedPackages", "com.uber")
- option("NullAway:UnannotatedSubPackages", "com.uber.lib.unannotated")
+ if (!name.toLowerCase().contains("test")) {
+ options.errorprone {
+ check("NullAway", CheckSeverity.ERROR)
+ option("NullAway:AnnotatedPackages", "com.uber")
+ option("NullAway:UnannotatedSubPackages", "com.uber.lib.unannotated")
+ }
}
- }
}
diff --git a/test-java-lib/src/main/java/com/example/jspecify/annotatedpackage/Utils.java b/test-java-lib/src/main/java/com/example/jspecify/annotatedpackage/Utils.java
new file mode 100644
index 0000000..43135df
--- /dev/null
+++ b/test-java-lib/src/main/java/com/example/jspecify/annotatedpackage/Utils.java
@@ -0,0 +1,13 @@
+package com.example.jspecify.annotatedpackage;
+
+import org.jspecify.annotations.Nullable;
+
+public class Utils {
+
+ public static String toStringOrDefault(@Nullable Object o1, String s) {
+ if (o1 != null) {
+ return o1.toString();
+ }
+ return s;
+ }
+}
diff --git a/test-java-lib/src/main/java/com/example/jspecify/annotatedpackage/package-info.java b/test-java-lib/src/main/java/com/example/jspecify/annotatedpackage/package-info.java
new file mode 100644
index 0000000..db0fd1c
--- /dev/null
+++ b/test-java-lib/src/main/java/com/example/jspecify/annotatedpackage/package-info.java
@@ -0,0 +1,4 @@
+@NullMarked
+package com.example.jspecify.annotatedpackage;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/test-java-lib/src/main/java/com/example/jspecify/unannotatedpackage/Methods.java b/test-java-lib/src/main/java/com/example/jspecify/unannotatedpackage/Methods.java
new file mode 100644
index 0000000..a8b2411
--- /dev/null
+++ b/test-java-lib/src/main/java/com/example/jspecify/unannotatedpackage/Methods.java
@@ -0,0 +1,30 @@
+package com.example.jspecify.unannotatedpackage;
+
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.NullUnmarked;
+
+public class Methods {
+ @NullMarked
+ public static void foo(Object o) {}
+
+ public static void unchecked(Object o) {}
+
+ public static class ExtendMe {
+ @NullMarked
+ public Object foo(Object o) {
+ return o;
+ }
+
+ public Object unchecked(Object o) {
+ return null;
+ }
+ }
+
+ @NullMarked
+ public static class Marked {
+ public static void foo(Object o) {}
+
+ @NullUnmarked
+ public static void unchecked(Object o) {}
+ }
+}
diff --git a/test-java-lib/src/main/java/com/example/jspecify/unannotatedpackage/Outer.java b/test-java-lib/src/main/java/com/example/jspecify/unannotatedpackage/Outer.java
new file mode 100644
index 0000000..3e2860c
--- /dev/null
+++ b/test-java-lib/src/main/java/com/example/jspecify/unannotatedpackage/Outer.java
@@ -0,0 +1,14 @@
+package com.example.jspecify.unannotatedpackage;
+
+import org.jspecify.annotations.NullMarked;
+
+public class Outer {
+ @NullMarked
+ public static class Inner {
+ public static String foo(String s) {
+ return s;
+ }
+ }
+
+ public static void unchecked(Object o) {}
+}
diff --git a/test-java-lib/src/main/java/com/example/jspecify/unannotatedpackage/TopLevel.java b/test-java-lib/src/main/java/com/example/jspecify/unannotatedpackage/TopLevel.java
new file mode 100644
index 0000000..e7a5545
--- /dev/null
+++ b/test-java-lib/src/main/java/com/example/jspecify/unannotatedpackage/TopLevel.java
@@ -0,0 +1,10 @@
+package com.example.jspecify.unannotatedpackage;
+
+import org.jspecify.annotations.NullMarked;
+
+@NullMarked
+public class TopLevel {
+ public static String foo(String s) {
+ return s;
+ }
+}
diff --git a/test-java-lib/src/main/java/com/uber/lib/CFNullableStuff.java b/test-java-lib/src/main/java/com/uber/lib/CFNullableStuff.java
index d75c943..8a58aad 100644
--- a/test-java-lib/src/main/java/com/uber/lib/CFNullableStuff.java
+++ b/test-java-lib/src/main/java/com/uber/lib/CFNullableStuff.java
@@ -6,8 +6,7 @@ public class CFNullableStuff {
public interface NullableReturn {
- @Nullable
- Object get();
+ @Nullable Object get();
}
public interface NullableParam {
diff --git a/test-java-lib/src/main/java/com/uber/lib/unannotated/UnannotatedWithModels.java b/test-java-lib/src/main/java/com/uber/lib/unannotated/UnannotatedWithModels.java
new file mode 100644
index 0000000..412042d
--- /dev/null
+++ b/test-java-lib/src/main/java/com/uber/lib/unannotated/UnannotatedWithModels.java
@@ -0,0 +1,16 @@
+package com.uber.lib.unannotated;
+
+public class UnannotatedWithModels {
+
+ public Object returnsNullUnannotated() {
+ return null;
+ }
+
+ public Object returnsNullUnannotated2() {
+ return null;
+ }
+
+ public static boolean isNonNull(Object o) {
+ return o != null;
+ }
+}
diff --git a/test-library-models/build.gradle b/test-library-models/build.gradle
index bd9b794..8feee69 100644
--- a/test-library-models/build.gradle
+++ b/test-library-models/build.gradle
@@ -17,12 +17,9 @@
import net.ltgt.gradle.errorprone.CheckSeverity
plugins {
- id "java-library"
+ id "java-library"
}
-sourceCompatibility = "1.8"
-targetCompatibility = "1.8"
-
dependencies {
compileOnly deps.apt.autoService
annotationProcessor deps.apt.autoService
@@ -32,10 +29,10 @@ dependencies {
}
tasks.withType(JavaCompile) {
- if (!name.toLowerCase().contains("test")) {
- options.errorprone {
- check("NullAway", CheckSeverity.ERROR)
- option("NullAway:AnnotatedPackages", "com.uber")
+ if (!name.toLowerCase().contains("test")) {
+ options.errorprone {
+ check("NullAway", CheckSeverity.ERROR)
+ option("NullAway:AnnotatedPackages", "com.uber")
+ }
}
- }
-} \ No newline at end of file
+}
diff --git a/test-library-models/src/main/java/com/uber/nullaway/testlibrarymodels/TestLibraryModels.java b/test-library-models/src/main/java/com/uber/nullaway/testlibrarymodels/TestLibraryModels.java
index 6e9f0a5..c94dfef 100644
--- a/test-library-models/src/main/java/com/uber/nullaway/testlibrarymodels/TestLibraryModels.java
+++ b/test-library-models/src/main/java/com/uber/nullaway/testlibrarymodels/TestLibraryModels.java
@@ -18,9 +18,12 @@ package com.uber.nullaway.testlibrarymodels;
import static com.uber.nullaway.LibraryModels.MethodRef.methodRef;
import com.google.auto.service.AutoService;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.uber.nullaway.LibraryModels;
+import com.uber.nullaway.handlers.stream.StreamModelBuilder;
+import com.uber.nullaway.handlers.stream.StreamTypeRecord;
@AutoService(LibraryModels.class)
public class TestLibraryModels implements LibraryModels {
@@ -53,7 +56,9 @@ public class TestLibraryModels implements LibraryModels {
@Override
public ImmutableSetMultimap<MethodRef, Integer> nullImpliesFalseParameters() {
- return ImmutableSetMultimap.of();
+ return ImmutableSetMultimap.of(
+ methodRef("com.uber.lib.unannotated.UnannotatedWithModels", "isNonNull(java.lang.Object)"),
+ 0);
}
@Override
@@ -63,11 +68,58 @@ public class TestLibraryModels implements LibraryModels {
@Override
public ImmutableSet<MethodRef> nullableReturns() {
- return ImmutableSet.of();
+ return ImmutableSet.of(
+ methodRef("com.uber.AnnotatedWithModels", "returnsNullFromModel()"),
+ methodRef("com.uber.lib.unannotated.UnannotatedWithModels", "returnsNullUnannotated()"),
+ methodRef("com.uber.lib.unannotated.UnannotatedWithModels", "returnsNullUnannotated2()"));
}
@Override
public ImmutableSet<MethodRef> nonNullReturns() {
return ImmutableSet.of();
}
+
+ @Override
+ public ImmutableSetMultimap<MethodRef, Integer> castToNonNullMethods() {
+ return ImmutableSetMultimap.<MethodRef, Integer>builder()
+ .put(
+ methodRef("com.uber.nullaway.testdata.Util", "<T>castToNonNull(T,java.lang.String)"), 0)
+ .put(
+ methodRef(
+ "com.uber.nullaway.testdata.Util", "<T>castToNonNull(java.lang.String,T,int)"),
+ 1)
+ .build();
+ }
+
+ @Override
+ public ImmutableList<StreamTypeRecord> customStreamNullabilitySpecs() {
+ // Identical to the default model for java.util.stream.Stream, but with the original type
+ // renamed
+ return StreamModelBuilder.start()
+ .addStreamTypeFromName("com.uber.nullaway.testdata.unannotated.CustomStream")
+ .withFilterMethodFromSignature("filter(java.util.function.Predicate<? super T>)")
+ .withMapMethodFromSignature(
+ "<R>map(java.util.function.Function<? super T,? extends R>)",
+ "apply",
+ ImmutableSet.of(0))
+ .withMapMethodFromSignature(
+ "mapToInt(java.util.function.ToIntFunction<? super T>)",
+ "applyAsInt",
+ ImmutableSet.of(0))
+ .withMapMethodFromSignature(
+ "mapToLong(java.util.function.ToLongFunction<? super T>)",
+ "applyAsLong",
+ ImmutableSet.of(0))
+ .withMapMethodFromSignature(
+ "mapToDouble(java.util.function.ToDoubleFunction<? super T>)",
+ "applyAsDouble",
+ ImmutableSet.of(0))
+ .withMapMethodFromSignature(
+ "forEach(java.util.function.Consumer<? super T>)", "accept", ImmutableSet.of(0))
+ .withMapMethodFromSignature(
+ "forEachOrdered(java.util.function.Consumer<? super T>)", "accept", ImmutableSet.of(0))
+ .withMapMethodAllFromName("flatMap", "apply", ImmutableSet.of(0))
+ .withPassthroughMethodFromSignature("distinct()")
+ .end();
+ }
}