From 9e3a8eefe199056dd810c45dd26fca3d357e6464 Mon Sep 17 00:00:00 2001 From: Andrea Guarino Date: Thu, 23 Oct 2025 13:00:13 +0200 Subject: [PATCH 1/3] SONARJAVA-5803 JSpecify @NullUnmarked should be treated as unknown --- .../ChangeMethodContractCheckNullMarked.java | 2 +- ...ldNotBeUsedWithOptionalCheckNullMarked_guava.java | 10 ++++------ ...ouldNotBeUsedWithOptionalCheckNullMarked_jdk.java | 10 ++++------ .../RedundantNullabilityAnnotationsCheckSample.java | 12 ++++++------ .../nullmarked/ChangeMethodContractCheck.java | 2 +- .../NullShouldNotBeUsedWithOptionalCheck_guava.java | 10 ++++------ .../NullShouldNotBeUsedWithOptionalCheck_jdk.java | 10 ++++------ .../nullmarked/PrimitivesMarkedNullableCheck.java | 12 +++--------- .../java/checks/ChangeMethodContractCheckTest.java | 4 ++-- .../checks/PrimitivesMarkedNullableCheckTest.java | 4 ++-- .../java/model/JSymbolMetadataNullabilityHelper.java | 2 +- 11 files changed, 32 insertions(+), 46 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/jspecify/ChangeMethodContractCheckNullMarked.java b/java-checks-test-sources/default/src/main/java/checks/jspecify/ChangeMethodContractCheckNullMarked.java index 730323970ba..38709953dac 100644 --- a/java-checks-test-sources/default/src/main/java/checks/jspecify/ChangeMethodContractCheckNullMarked.java +++ b/java-checks-test-sources/default/src/main/java/checks/jspecify/ChangeMethodContractCheckNullMarked.java @@ -16,7 +16,7 @@ class ChangeMethodContractCheck_B extends ChangeMethodContractCheck { @NullMarked @Override - String annotatedUnmarked(Object a) { return null; } // Noncompliant {{Fix the incompatibility of the annotation @NullMarked to honor @NullUnmarked of the overridden method.}} + String annotatedUnmarked(Object a) { return null; } // Compliant - NullUnmarked doesn't add any information about nullability } diff --git a/java-checks-test-sources/default/src/main/java/checks/jspecify/NullShouldNotBeUsedWithOptionalCheckNullMarked_guava.java b/java-checks-test-sources/default/src/main/java/checks/jspecify/NullShouldNotBeUsedWithOptionalCheckNullMarked_guava.java index 5ba2a8e138c..9a11c06f8b4 100644 --- a/java-checks-test-sources/default/src/main/java/checks/jspecify/NullShouldNotBeUsedWithOptionalCheckNullMarked_guava.java +++ b/java-checks-test-sources/default/src/main/java/checks/jspecify/NullShouldNotBeUsedWithOptionalCheckNullMarked_guava.java @@ -10,9 +10,8 @@ @NullMarked interface NullShouldNotBeUsedWithOptionalCheck_guava { - @NullUnmarked // Noncompliant {{Methods with an "Optional" return type should not be "@NullUnmarked".}} -//^^^^^^^^^^^^^ - public Optional getOptionalKo(); + @NullUnmarked // Compliant - NullUnmarked doesn't say anything about nullability + public Optional getOptional(); } @@ -22,9 +21,8 @@ class NullShouldNotBeUsedWithOptionalCheck_guavaClassA { public NullShouldNotBeUsedWithOptionalCheck_guavaClassA() { } - @NullUnmarked // Noncompliant {{Methods with an "Optional" return type should not be "@NullUnmarked".}} -//^^^^^^^^^^^^^ - public Optional getOptionalKo() { + @NullUnmarked // Compliant - NullUnmarked doesn't say anything about nullability + public Optional getOptional() { return null; // Noncompliant {{Methods with an "Optional" return type should never return null.}} // ^^^^ } diff --git a/java-checks-test-sources/default/src/main/java/checks/jspecify/NullShouldNotBeUsedWithOptionalCheckNullMarked_jdk.java b/java-checks-test-sources/default/src/main/java/checks/jspecify/NullShouldNotBeUsedWithOptionalCheckNullMarked_jdk.java index 4f3ba39af7c..095257d98b7 100644 --- a/java-checks-test-sources/default/src/main/java/checks/jspecify/NullShouldNotBeUsedWithOptionalCheckNullMarked_jdk.java +++ b/java-checks-test-sources/default/src/main/java/checks/jspecify/NullShouldNotBeUsedWithOptionalCheckNullMarked_jdk.java @@ -11,9 +11,8 @@ @NullMarked interface NullShouldNotBeUsedWithOptionalCheck_jdk { - @NullUnmarked // Noncompliant {{Methods with an "Optional" return type should not be "@NullUnmarked".}} -//^^^^^^^^^^^^^ - public Optional getOptionalKo(); + @NullUnmarked // Compliant - NullUnmarked doesn't say anything about nullability + public Optional getOptional(); } @@ -23,9 +22,8 @@ class NullShouldNotBeUsedWithOptionalCheck_jdkClassA { public NullShouldNotBeUsedWithOptionalCheck_jdkClassA() { } - @NullUnmarked // Noncompliant {{Methods with an "Optional" return type should not be "@NullUnmarked".}} -//^^^^^^^^^^^^^ - public Optional getOptionalKo() { + @NullUnmarked // Compliant - NullUnmarked doesn't say anything about nullability + public Optional getOptional() { return null; // Noncompliant {{Methods with an "Optional" return type should never return null.}} // ^^^^ } diff --git a/java-checks-test-sources/default/src/main/java/checks/jspecify/RedundantNullabilityAnnotationsCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/jspecify/RedundantNullabilityAnnotationsCheckSample.java index c292437cd03..54a5fafc3ee 100644 --- a/java-checks-test-sources/default/src/main/java/checks/jspecify/RedundantNullabilityAnnotationsCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/jspecify/RedundantNullabilityAnnotationsCheckSample.java @@ -262,7 +262,7 @@ class InnerClassTests { class InnerNullmarked { @NullMarked // Compliant class Inner { - @NonNull Object o; // Noncompliant {{Remove redundant annotation @NonNull as inside scope annotation @NullMarked at class level.}} + @NonNull Object o; // Compliant @Nullable Object o2; // Compliant } } @@ -272,7 +272,7 @@ class InnerNullUnmarked { @NullUnmarked class Inner { @NonNull Object o; // Compliant - public void methodNonNullParamTyped(List<@Nullable Object> o) { // Noncompliant {{Remove redundant annotation @Nullable as inside scope annotation @NullUnmarked at class level.}} + public void methodNonNullParamTyped(List<@Nullable Object> o) { // Compliant // .. } } @@ -282,7 +282,7 @@ public void methodNonNullParamTyped(List<@Nullable Object> o) { // Noncompliant class InnerRedundantNullMarked { @NullMarked // Compliant class Inner { - @NullMarked // Noncompliant {{Remove redundant annotation @NullMarked at class level as inside scope annotation @NullMarked at class level.}} + @NullMarked // Compliant class InnerInner {} } } @@ -291,7 +291,7 @@ class InnerInner {} class InnerRedundantNullUnmarked { @NullUnmarked // Compliant class Inner { - @NullUnmarked // Noncompliant {{Remove redundant annotation @NullUnmarked at class level as inside scope annotation @NullUnmarked at class level.}} + @NullUnmarked // Compliant class InnerInner {} } } @@ -299,11 +299,11 @@ class InnerInner {} @NullUnmarked class InnerClassTestsTwo { - @NullUnmarked // Noncompliant {{Remove redundant annotation @NullUnmarked at class level as inside scope annotation @NullUnmarked at class level.}} + @NullUnmarked // Compliant class InnerNullmarked { @NullMarked // Compliant class Inner { - @NonNull Object o; // Noncompliant {{Remove redundant annotation @NonNull as inside scope annotation @NullMarked at class level.}} + @NonNull Object o; // Compliant @Nullable Object o2; // Compliant } } diff --git a/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/ChangeMethodContractCheck.java b/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/ChangeMethodContractCheck.java index adf2f0c749b..6a6e6df18e8 100644 --- a/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/ChangeMethodContractCheck.java +++ b/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/ChangeMethodContractCheck.java @@ -16,7 +16,7 @@ class ChangeMethodContractCheck_B extends ChangeMethodContractCheck { @NullMarked @Override - String annotatedUnmarked(Object a) { return null; } // Noncompliant {{Fix the incompatibility of the annotation @NullMarked to honor @NullUnmarked of the overridden method.}} + String annotatedUnmarked(Object a) { return null; } // Compliant - NullUnmarked doesn't add any information about nullability } diff --git a/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/NullShouldNotBeUsedWithOptionalCheck_guava.java b/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/NullShouldNotBeUsedWithOptionalCheck_guava.java index c1809d2652d..542c40d942e 100644 --- a/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/NullShouldNotBeUsedWithOptionalCheck_guava.java +++ b/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/NullShouldNotBeUsedWithOptionalCheck_guava.java @@ -9,9 +9,8 @@ // NullMarked at the package level interface NullShouldNotBeUsedWithOptionalCheck_guava { - @NullUnmarked // Noncompliant {{Methods with an "Optional" return type should not be "@NullUnmarked".}} -//^^^^^^^^^^^^^ - public Optional getOptionalKo(); + @NullUnmarked // Compliant - NullUnmarked doesn't say anything about nullability + public Optional getOptional(); } @@ -21,9 +20,8 @@ class NullShouldNotBeUsedWithOptionalCheck_guavaClassA { public NullShouldNotBeUsedWithOptionalCheck_guavaClassA() { } - @NullUnmarked // Noncompliant {{Methods with an "Optional" return type should not be "@NullUnmarked".}} -//^^^^^^^^^^^^^ - public Optional getOptionalKo() { + @NullUnmarked // Compliant - NullUnmarked doesn't say anything about nullability + public Optional getOptional() { return null; // Noncompliant {{Methods with an "Optional" return type should never return null.}} // ^^^^ } diff --git a/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/NullShouldNotBeUsedWithOptionalCheck_jdk.java b/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/NullShouldNotBeUsedWithOptionalCheck_jdk.java index 6ec4a775988..1abc60eff38 100644 --- a/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/NullShouldNotBeUsedWithOptionalCheck_jdk.java +++ b/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/NullShouldNotBeUsedWithOptionalCheck_jdk.java @@ -10,9 +10,8 @@ // NullMarked at the package level interface NullShouldNotBeUsedWithOptionalCheck_jdk { - @NullUnmarked // Noncompliant {{Methods with an "Optional" return type should not be "@NullUnmarked".}} -//^^^^^^^^^^^^^ - public Optional getOptionalKo(); + @NullUnmarked // Compliant - NullUnmarked doesn't say anything about nullability + public Optional getOptional(); } @@ -22,9 +21,8 @@ class NullShouldNotBeUsedWithOptionalCheck_jdkClassA { public NullShouldNotBeUsedWithOptionalCheck_jdkClassA() { } - @NullUnmarked // Noncompliant {{Methods with an "Optional" return type should not be "@NullUnmarked".}} -//^^^^^^^^^^^^^ - public Optional getOptionalKo() { + @NullUnmarked // Compliant - NullUnmarked doesn't say anything about nullability + public Optional getOptional() { return null; // Noncompliant {{Methods with an "Optional" return type should never return null.}} // ^^^^ } diff --git a/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/PrimitivesMarkedNullableCheck.java b/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/PrimitivesMarkedNullableCheck.java index 40d3d32e724..5a659ddc3e8 100644 --- a/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/PrimitivesMarkedNullableCheck.java +++ b/java-checks-test-sources/default/src/main/java/checks/jspecify/nullmarked/PrimitivesMarkedNullableCheck.java @@ -19,13 +19,10 @@ abstract class PrimitivesMarkedNullableCheckSample { abstract int getInt2(); @NullUnmarked - protected abstract boolean isBool(); // Noncompliant {{"@NullUnmarked" annotation should not be used on primitive types}} [[quickfixes=qf1]] -// ^^^^^^^ - // fix@qf1 {{Remove "@NullUnmarked"}} - // edit@qf1 [[sl=-1;el=+0;sc=3;ec=3]] {{}} + protected abstract boolean isBool(); // Compliant - NullUnmarked doesn't add any information about nullability @NullUnmarked - public double getDouble1() { return 0.0; } // Noncompliant {{"@NullUnmarked" annotation should not be used on primitive types}} + public double getDouble1() { return 0.0; } // Compliant - NullUnmarked doesn't add any information about nullability public double getDouble2() { return 0.0; } @@ -49,10 +46,7 @@ abstract class PrimitivesMarkedNullableCheckSample { public Object getObj2() { return null; } - protected abstract @NullUnmarked boolean isBool2(); // Noncompliant {{"@NullUnmarked" annotation should not be used on primitive types}} [[quickfixes=qf3]] -// ^^^^^^^ - // fix@qf3 {{Remove "@NullUnmarked"}} - // edit@qf3 [[sc=22;ec=36]] {{}} + protected abstract @NullUnmarked boolean isBool2(); // Compliant - NullUnmarked doesn't add any information about nullability @NullUnmarked Object containsAnonymousClass() { diff --git a/java-checks/src/test/java/org/sonar/java/checks/ChangeMethodContractCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/ChangeMethodContractCheckTest.java index fb615a2b54e..5f74a0f4c3c 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/ChangeMethodContractCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/ChangeMethodContractCheckTest.java @@ -53,11 +53,11 @@ void test_jspecify_null_marked() { CheckVerifier.newVerifier() .onFile(mainCodeSourcesPath("checks/jspecify/ChangeMethodContractCheckNullMarked.java")) .withCheck(new ChangeMethodContractCheck()) - .verifyIssues(); + .verifyNoIssues(); CheckVerifier.newVerifier() .onFile(mainCodeSourcesPath("checks/jspecify/nullmarked/ChangeMethodContractCheck.java")) .withCheck(new ChangeMethodContractCheck()) - .verifyIssues(); + .verifyNoIssues(); } @Test diff --git a/java-checks/src/test/java/org/sonar/java/checks/PrimitivesMarkedNullableCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/PrimitivesMarkedNullableCheckTest.java index 8b50c8050a2..e0f7b18fee2 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/PrimitivesMarkedNullableCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/PrimitivesMarkedNullableCheckTest.java @@ -45,11 +45,11 @@ void test_jspecify_null_marked() { CheckVerifier.newVerifier() .onFile(mainCodeSourcesPath("checks/jspecify/PrimitivesMarkedNullableCheckNullMarked.java")) .withCheck(new PrimitivesMarkedNullableCheck()) - .verifyIssues(); + .verifyNoIssues(); CheckVerifier.newVerifier() .onFile(mainCodeSourcesPath("checks/jspecify/nullmarked/PrimitivesMarkedNullableCheck.java")) .withCheck(new PrimitivesMarkedNullableCheck()) - .verifyIssues(); + .verifyNoIssues(); } } diff --git a/java-frontend/src/main/java/org/sonar/java/model/JSymbolMetadataNullabilityHelper.java b/java-frontend/src/main/java/org/sonar/java/model/JSymbolMetadataNullabilityHelper.java index ace41a9c5b3..05d6af67a6b 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JSymbolMetadataNullabilityHelper.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JSymbolMetadataNullabilityHelper.java @@ -224,7 +224,7 @@ private JSymbolMetadataNullabilityHelper() { configureAnnotation(ORG_JSPECIFY_ANNOTATIONS_NULL_MARKED, NON_NULL, Arrays.asList(NullabilityTarget.CLASS, FIELD, METHOD, PARAMETER), Arrays.asList(NullabilityLevel.METHOD, CLASS, PACKAGE)); - configureAnnotation(ORG_JSPECIFY_ANNOTATIONS_NULL_UNMARKED, WEAK_NULLABLE, + configureAnnotation(ORG_JSPECIFY_ANNOTATIONS_NULL_UNMARKED, UNKNOWN, Arrays.asList(NullabilityTarget.CLASS, FIELD, METHOD, PARAMETER), Arrays.asList(NullabilityLevel.METHOD, CLASS, PACKAGE)); From c69c346618eb4fc530e83ecaf6e02befa0ea182c Mon Sep 17 00:00:00 2001 From: Andrea Guarino Date: Thu, 23 Oct 2025 14:29:42 +0200 Subject: [PATCH 2/3] Update autoscan expectations --- its/autoscan/src/test/resources/autoscan/diffs/diff_S2638.json | 2 +- its/autoscan/src/test/resources/autoscan/diffs/diff_S2789.json | 2 +- its/autoscan/src/test/resources/autoscan/diffs/diff_S4682.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2638.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2638.json index 6f053b24114..c7948e28b68 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2638.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2638.json @@ -1,6 +1,6 @@ { "ruleKey": "S2638", "hasTruePositives": true, - "falseNegatives": 22, + "falseNegatives": 20, "falsePositives": 0 } diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2789.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2789.json index 94076c3803f..b8c1de06538 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S2789.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S2789.json @@ -1,6 +1,6 @@ { "ruleKey": "S2789", "hasTruePositives": true, - "falseNegatives": 43, + "falseNegatives": 35, "falsePositives": 0 } diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S4682.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S4682.json index 1a9c6a09fe0..262b982dbdb 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S4682.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S4682.json @@ -1,6 +1,6 @@ { "ruleKey": "S4682", "hasTruePositives": true, - "falseNegatives": 10, + "falseNegatives": 2, "falsePositives": 0 } From 87a44fba94518cfc7929a5e43c5214b7ce611783 Mon Sep 17 00:00:00 2001 From: Andrea Guarino Date: Mon, 27 Oct 2025 13:53:04 +0100 Subject: [PATCH 3/3] Fixes after review --- .../PrimitivesMarkedNullableCheckNullMarked.java | 12 +++--------- .../RedundantNullabilityAnnotationsCheckSample.java | 10 +++++----- .../RedundantNullabilityAnnotationsCheck.java | 13 ++++++++++++- .../checks/PrimitivesMarkedNullableCheckTest.java | 1 - 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/jspecify/PrimitivesMarkedNullableCheckNullMarked.java b/java-checks-test-sources/default/src/main/java/checks/jspecify/PrimitivesMarkedNullableCheckNullMarked.java index 6f9db4265c3..5a0fb65402a 100644 --- a/java-checks-test-sources/default/src/main/java/checks/jspecify/PrimitivesMarkedNullableCheckNullMarked.java +++ b/java-checks-test-sources/default/src/main/java/checks/jspecify/PrimitivesMarkedNullableCheckNullMarked.java @@ -19,13 +19,10 @@ abstract class PrimitivesMarkedNullableCheckSample { abstract int getInt2(); @NullUnmarked - protected abstract boolean isBool(); // Noncompliant {{"@NullUnmarked" annotation should not be used on primitive types}} [[quickfixes=qf1]] -// ^^^^^^^ - // fix@qf1 {{Remove "@NullUnmarked"}} - // edit@qf1 [[sl=-1;el=+0;sc=3;ec=3]] {{}} + protected abstract boolean isBool(); // Compliant @NullUnmarked - public double getDouble1() { return 0.0; } // Noncompliant {{"@NullUnmarked" annotation should not be used on primitive types}} + public double getDouble1() { return 0.0; } // Compliant public double getDouble2() { return 0.0; } @@ -49,10 +46,7 @@ abstract class PrimitivesMarkedNullableCheckSample { public Object getObj2() { return null; } - protected abstract @NullUnmarked boolean isBool2(); // Noncompliant {{"@NullUnmarked" annotation should not be used on primitive types}} [[quickfixes=qf3]] -// ^^^^^^^ - // fix@qf3 {{Remove "@NullUnmarked"}} - // edit@qf3 [[sc=22;ec=36]] {{}} + protected abstract @NullUnmarked boolean isBool2(); // Compliant @NullUnmarked Object containsAnonymousClass() { diff --git a/java-checks-test-sources/default/src/main/java/checks/jspecify/RedundantNullabilityAnnotationsCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/jspecify/RedundantNullabilityAnnotationsCheckSample.java index 54a5fafc3ee..0bf545a82cb 100644 --- a/java-checks-test-sources/default/src/main/java/checks/jspecify/RedundantNullabilityAnnotationsCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/jspecify/RedundantNullabilityAnnotationsCheckSample.java @@ -262,7 +262,7 @@ class InnerClassTests { class InnerNullmarked { @NullMarked // Compliant class Inner { - @NonNull Object o; // Compliant + @NonNull Object o; // Noncompliant {{Remove redundant annotation @NonNull as inside scope annotation @NullMarked at class level.}} @Nullable Object o2; // Compliant } } @@ -282,7 +282,7 @@ public void methodNonNullParamTyped(List<@Nullable Object> o) { // Compliant class InnerRedundantNullMarked { @NullMarked // Compliant class Inner { - @NullMarked // Compliant + @NullMarked // Noncompliant {{Remove redundant annotation @NullMarked at class level as inside scope annotation @NullMarked at class level.}} class InnerInner {} } } @@ -291,7 +291,7 @@ class InnerInner {} class InnerRedundantNullUnmarked { @NullUnmarked // Compliant class Inner { - @NullUnmarked // Compliant + @NullUnmarked // FN class InnerInner {} } } @@ -299,11 +299,11 @@ class InnerInner {} @NullUnmarked class InnerClassTestsTwo { - @NullUnmarked // Compliant + @NullUnmarked // FN class InnerNullmarked { @NullMarked // Compliant class Inner { - @NonNull Object o; // Compliant + @NonNull Object o; // Noncompliant {{Remove redundant annotation @NonNull as inside scope annotation @NullMarked at class level.}} @Nullable Object o2; // Compliant } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/RedundantNullabilityAnnotationsCheck.java b/java-checks/src/main/java/org/sonar/java/checks/RedundantNullabilityAnnotationsCheck.java index 2ffc29414a5..6536ee9202d 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/RedundantNullabilityAnnotationsCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/RedundantNullabilityAnnotationsCheck.java @@ -60,6 +60,10 @@ public void visitNode(Tree tree) { // if nullable, either directly or inherited from higher scope // then check my members are not directly annotated with non-null checkMembers(classNullabilityData, classTree, NULLABILITY_SCOPE.NULLABLE); + } else if (classNullabilityData.type() == SymbolMetadata.NullabilityType.UNKNOWN) { + // Handle classes with UNKNOWN nullability (e.g., @NullUnmarked) + // Still need to check their members in case they have nested @NullMarked/@NullUnmarked + checkMembers(classNullabilityData, classTree, NULLABILITY_SCOPE.UNSPECIFIED); } } } @@ -99,6 +103,12 @@ private void checkInnerClass(SymbolMetadata.NullabilityData classNullabilityData } // now recurse to check class members checkMembers(innerNullabilityData, tree, NULLABILITY_SCOPE.NULLABLE); + } else { + // Handle classes with UNKNOWN nullability (e.g., @NullUnmarked) + // Still need to check their members in case they have nested @NullMarked/@NullUnmarked + if (innerNullabilityData.type() == SymbolMetadata.NullabilityType.UNKNOWN) { + checkMembers(innerNullabilityData, tree, NULLABILITY_SCOPE.UNSPECIFIED); + } } } @@ -148,7 +158,8 @@ private void reportIssue(Tree reportLocation, // track class scope nullability state during recursion private enum NULLABILITY_SCOPE { NULLABLE, - NON_NULLABLE + NON_NULLABLE, + UNSPECIFIED } } diff --git a/java-checks/src/test/java/org/sonar/java/checks/PrimitivesMarkedNullableCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/PrimitivesMarkedNullableCheckTest.java index e0f7b18fee2..aa0253e6a7c 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/PrimitivesMarkedNullableCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/PrimitivesMarkedNullableCheckTest.java @@ -51,5 +51,4 @@ void test_jspecify_null_marked() { .withCheck(new PrimitivesMarkedNullableCheck()) .verifyNoIssues(); } - }