From 30fe1fe3c1cf6905e4ca6beda7b215fdd6af8c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Mon, 23 Feb 2026 14:32:24 +0100 Subject: [PATCH 1/7] Implement rule S8465 --- ...ScopedValueStableReferenceCheckSample.java | 31 ++++++++++ .../ScopedValueStableReferenceCheck.java | 58 +++++++++++++++++++ .../ScopedValueStableReferenceCheckTest.java | 28 +++++++++ 3 files changed, 117 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/ScopedValueStableReferenceCheckTest.java diff --git a/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java new file mode 100644 index 00000000000..70902c3677f --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java @@ -0,0 +1,31 @@ +package checks; + +class ScopedValueStableReferenceCheckSample { + + private static final ScopedValue VALUE = ScopedValue.newInstance(); + + public void whereNC() { + ScopedValue.where(ScopedValue.newInstance(), "inaccessible").run(() -> { // Noncompliant {{Consider using a stable reference for ScopedValue instances.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^ + // Cannot reference the scoped value here, as it has no name. + }); + } + + public String readFieldInWhere() { + return ScopedValue.where(VALUE, "field value").call(VALUE::get); // Compliant + } + + public String readLocalVarInWhere() { + ScopedValue value = ScopedValue.newInstance(); + return ScopedValue.where(value, "local value").call(value::get); // Compliant + } + + public String directRead() { + return VALUE.get(); // Compliant + } + + public String readNewInstance() { + return ScopedValue.newInstance().get(); // Compliant + } + +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java b/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java new file mode 100644 index 00000000000..d4c7b10d632 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java @@ -0,0 +1,58 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks; + +import org.sonar.check.Rule; +import org.sonar.java.checks.methods.AbstractMethodDetection; +import org.sonar.plugins.java.api.JavaVersion; +import org.sonar.plugins.java.api.JavaVersionAwareVisitor; +import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.Tree; + +@Rule(key = "S8465") +public class ScopedValueStableReferenceCheck extends AbstractMethodDetection implements JavaVersionAwareVisitor { + + @Override + public boolean isCompatibleWithJavaVersion(JavaVersion version) { + return version.isJava25Compatible(); + } + + @Override + protected MethodMatchers getMethodInvocationMatchers() { + return MethodMatchers.create() + .ofTypes("java.lang.ScopedValue") + .names("where") + .withAnyParameters() + .build(); + } + + @Override + protected void onMethodInvocationFound(MethodInvocationTree mit) { + ExpressionTree scopedValue = mit.arguments().get(0); + if (!scopedValue.is(Tree.Kind.METHOD_INVOCATION)) { + return; + } + MethodInvocationTree methodInvocation = (MethodInvocationTree) scopedValue; + if (methodInvocation.methodSymbol().owner().type().fullyQualifiedName().equals("java.lang.ScopedValue") && + methodInvocation.methodSymbol().name().equals("newInstance")) { + reportIssue(methodInvocation, "Consider using a stable reference for ScopedValue instances."); + } + } + +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/ScopedValueStableReferenceCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/ScopedValueStableReferenceCheckTest.java new file mode 100644 index 00000000000..2f282002f72 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/ScopedValueStableReferenceCheckTest.java @@ -0,0 +1,28 @@ +package org.sonar.java.checks; + +import org.junit.jupiter.api.Test; +import org.sonar.java.checks.verifier.CheckVerifier; + +import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; + +class ScopedValueStableReferenceCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/ScopedValueStableReferenceCheckSample.java")) + .withCheck(new ScopedValueStableReferenceCheck()) + .withJavaVersion(25) + .verifyIssues(); + } + + @Test + void test_java_24() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/ScopedValueStableReferenceCheckSample.java")) + .withCheck(new ScopedValueStableReferenceCheck()) + .withJavaVersion(24) + .verifyNoIssues(); + } + +} From d1d776a24f9100da3f2b655f1174faa9d0c8d748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Mon, 23 Feb 2026 14:45:47 +0100 Subject: [PATCH 2/7] Change the rule's logic to detect more cases --- ...ScopedValueStableReferenceCheckSample.java | 16 ++++++++++++---- .../ScopedValueStableReferenceCheck.java | 19 ++++++++----------- .../ScopedValueStableReferenceCheckTest.java | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java index 70902c3677f..6af886e5723 100644 --- a/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java @@ -11,6 +11,18 @@ public void whereNC() { }); } + public String readNewInstance() { + // A new instance of a scoped value can never be bound without passing it as an argument to `where`. + return ScopedValue.newInstance().get(); // Noncompliant {{Consider using a stable reference for ScopedValue instances.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + } + + public boolean newInstanceIsBound() { + // A new instance of a scoped value can never be bound without passing it as an argument to `where`. + return ScopedValue.newInstance().isBound(); // Noncompliant {{Consider using a stable reference for ScopedValue instances.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^ + } + public String readFieldInWhere() { return ScopedValue.where(VALUE, "field value").call(VALUE::get); // Compliant } @@ -24,8 +36,4 @@ public String directRead() { return VALUE.get(); // Compliant } - public String readNewInstance() { - return ScopedValue.newInstance().get(); // Compliant - } - } diff --git a/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java b/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java index d4c7b10d632..927d0c40856 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java @@ -16,18 +16,20 @@ */ package org.sonar.java.checks; +import java.util.List; import org.sonar.check.Rule; import org.sonar.java.checks.methods.AbstractMethodDetection; import org.sonar.plugins.java.api.JavaVersion; import org.sonar.plugins.java.api.JavaVersionAwareVisitor; import org.sonar.plugins.java.api.semantic.MethodMatchers; -import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; import org.sonar.plugins.java.api.tree.Tree; @Rule(key = "S8465") public class ScopedValueStableReferenceCheck extends AbstractMethodDetection implements JavaVersionAwareVisitor { + private static final List VALID_PARENTS = List.of(Tree.Kind.ASSIGNMENT, Tree.Kind.VARIABLE); + @Override public boolean isCompatibleWithJavaVersion(JavaVersion version) { return version.isJava25Compatible(); @@ -37,21 +39,16 @@ public boolean isCompatibleWithJavaVersion(JavaVersion version) { protected MethodMatchers getMethodInvocationMatchers() { return MethodMatchers.create() .ofTypes("java.lang.ScopedValue") - .names("where") - .withAnyParameters() + .names("newInstance") + .addWithoutParametersMatcher() .build(); } @Override protected void onMethodInvocationFound(MethodInvocationTree mit) { - ExpressionTree scopedValue = mit.arguments().get(0); - if (!scopedValue.is(Tree.Kind.METHOD_INVOCATION)) { - return; - } - MethodInvocationTree methodInvocation = (MethodInvocationTree) scopedValue; - if (methodInvocation.methodSymbol().owner().type().fullyQualifiedName().equals("java.lang.ScopedValue") && - methodInvocation.methodSymbol().name().equals("newInstance")) { - reportIssue(methodInvocation, "Consider using a stable reference for ScopedValue instances."); + // It only makes sense to assign a new instance of a ScopedValue to a variable or a field. + if (!VALID_PARENTS.contains(mit.parent().kind())) { + reportIssue(mit, "Consider using a stable reference for ScopedValue instances."); } } diff --git a/java-checks/src/test/java/org/sonar/java/checks/ScopedValueStableReferenceCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/ScopedValueStableReferenceCheckTest.java index 2f282002f72..6530327cbfc 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/ScopedValueStableReferenceCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/ScopedValueStableReferenceCheckTest.java @@ -1,3 +1,19 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ package org.sonar.java.checks; import org.junit.jupiter.api.Test; From f709012a09337bc6983096511f7b0e399c81659e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Mon, 23 Feb 2026 16:20:55 +0100 Subject: [PATCH 3/7] Add rule S8465 description and metadata --- .../org/sonar/l10n/java/rules/java/S8465.html | 54 +++++++++++++++++++ .../org/sonar/l10n/java/rules/java/S8465.json | 22 ++++++++ .../java/rules/java/Sonar_way_profile.json | 3 +- 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8465.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8465.json diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8465.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8465.html new file mode 100644 index 00000000000..54c1c172966 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8465.html @@ -0,0 +1,54 @@ +

A ScopedValue must be accessible within the functional task where it is bound. Creating a new instance directly inside a +ScopedValue.where() call makes the key unreachable, as there is no variable name to reference when calling .get().

+

Why is this an issue?

+

The primary purpose of a ScopedValue is to provide a way to share data without passing it as method arguments. To retrieve the value +using .get(), you must have access to the ScopedValue instance (the key).

+

If a ScopedValue is instantiated anonymously within the .where() method, it is "lost" immediately after the binding is +created. The code inside the Runnable or Callable has no way to reference that specific instance to retrieve the associated +value, rendering the scoped value useless.

+

How to fix it

+

Assign the ScopedValue instance to a stable reference — typically a static final field — before using it in a binding. +This allows different parts of the code to refer to the same key to set and retrieve values.

+

Code examples

+

Noncompliant code example

+
+public class Renderer {
+
+  public void process() {
+    // Noncompliant: The ScopedValue instance is anonymous and unreachable inside the run method
+    ScopedValue.where(ScopedValue.newInstance(), "DARK").run(() -> {
+        render();
+    });
+  }
+
+  void render() {
+    // There is no way to call .get() here because the key is unknown
+  }
+
+}
+
+

Compliant solution

+
+public class Renderer {
+
+  // Compliant: The ScopedValue is assigned to a constant that can be referenced elsewhere
+  private static final ScopedValue<String> THEME = ScopedValue.newInstance();
+
+  public void process() {
+    ScopedValue.where(THEME, "DARK").run(() -> {
+        render();
+    });
+  }
+
+  void render() {
+    System.out.println("Theme is: " + THEME.get());
+  }
+
+}
+
+

Resources

+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8465.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8465.json new file mode 100644 index 00000000000..fd9450a28d9 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8465.json @@ -0,0 +1,22 @@ +{ + "title": "ScopedValue instances should be assigned to a stable reference", + "type": "BUG", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-8465", + "sqKey": "S8465", + "scope": "All", + "quickfix": "unknown", + "code": { + "impacts": { + "MAINTAINABILITY": "HIGH", + "RELIABILITY": "MEDIUM" + }, + "attribute": "CONVENTIONAL" + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json index 6945b8dab69..c5de2fe78c2 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json @@ -524,6 +524,7 @@ "S8444", "S8445", "S8447", - "S8450" + "S8450", + "S8465" ] } From 70418ac495d5db51ec972a320bd28dc5ee1c4313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Tue, 24 Feb 2026 08:25:57 +0100 Subject: [PATCH 4/7] Fix the autoscan tests --- .../src/test/resources/autoscan/autoscan-diff-by-rules.json | 6 ++++++ .../src/test/resources/autoscan/diffs/diff_S8465.json | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 its/autoscan/src/test/resources/autoscan/diffs/diff_S8465.json diff --git a/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json b/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json index 1c996af0ac8..0a01d7b2e59 100644 --- a/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json +++ b/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json @@ -2890,5 +2890,11 @@ "hasTruePositives": false, "falseNegatives": 0, "falsePositives": 0 + }, + { + "ruleKey": "S8465", + "hasTruePositives": true, + "falseNegatives": 0, + "falsePositives": 0 } ] diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8465.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8465.json new file mode 100644 index 00000000000..a531f4c3680 --- /dev/null +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8465.json @@ -0,0 +1,6 @@ +{ + "ruleKey": "S8465", + "hasTruePositives": true, + "falseNegatives": 0, + "falsePositives": 0 +} From a25609503656e7d9e4fde59dd5a5028cb45ccd8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Wed, 25 Feb 2026 14:59:23 +0100 Subject: [PATCH 5/7] Revert to more conservative logic for the rule --- ...ScopedValueStableReferenceCheckSample.java | 12 ------- .../ScopedValueStableReferenceCheck.java | 31 +++++++++++++------ 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java index 6af886e5723..510ad3f4853 100644 --- a/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java @@ -11,18 +11,6 @@ public void whereNC() { }); } - public String readNewInstance() { - // A new instance of a scoped value can never be bound without passing it as an argument to `where`. - return ScopedValue.newInstance().get(); // Noncompliant {{Consider using a stable reference for ScopedValue instances.}} -// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - } - - public boolean newInstanceIsBound() { - // A new instance of a scoped value can never be bound without passing it as an argument to `where`. - return ScopedValue.newInstance().isBound(); // Noncompliant {{Consider using a stable reference for ScopedValue instances.}} -// ^^^^^^^^^^^^^^^^^^^^^^^^^ - } - public String readFieldInWhere() { return ScopedValue.where(VALUE, "field value").call(VALUE::get); // Compliant } diff --git a/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java b/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java index 927d0c40856..643fb951ecc 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java @@ -16,19 +16,29 @@ */ package org.sonar.java.checks; -import java.util.List; import org.sonar.check.Rule; import org.sonar.java.checks.methods.AbstractMethodDetection; import org.sonar.plugins.java.api.JavaVersion; import org.sonar.plugins.java.api.JavaVersionAwareVisitor; import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; import org.sonar.plugins.java.api.tree.Tree; @Rule(key = "S8465") public class ScopedValueStableReferenceCheck extends AbstractMethodDetection implements JavaVersionAwareVisitor { - private static final List VALID_PARENTS = List.of(Tree.Kind.ASSIGNMENT, Tree.Kind.VARIABLE); + private static final MethodMatchers WHERE_MATCHER = MethodMatchers.create() + .ofTypes("java.lang.ScopedValue") + .names("where") + .withAnyParameters() + .build(); + + private static final MethodMatchers NEW_INSTANCE_MATCHER = MethodMatchers.create() + .ofTypes("java.lang.ScopedValue") + .names("newInstance") + .addWithoutParametersMatcher() + .build(); @Override public boolean isCompatibleWithJavaVersion(JavaVersion version) { @@ -37,18 +47,19 @@ public boolean isCompatibleWithJavaVersion(JavaVersion version) { @Override protected MethodMatchers getMethodInvocationMatchers() { - return MethodMatchers.create() - .ofTypes("java.lang.ScopedValue") - .names("newInstance") - .addWithoutParametersMatcher() - .build(); + return WHERE_MATCHER; } @Override protected void onMethodInvocationFound(MethodInvocationTree mit) { - // It only makes sense to assign a new instance of a ScopedValue to a variable or a field. - if (!VALID_PARENTS.contains(mit.parent().kind())) { - reportIssue(mit, "Consider using a stable reference for ScopedValue instances."); + ExpressionTree scopedValue = mit.arguments().get(0); + if (!scopedValue.is(Tree.Kind.METHOD_INVOCATION)) { + return; + } + + MethodInvocationTree methodInvocation = (MethodInvocationTree) scopedValue; + if (NEW_INSTANCE_MATCHER.matches(methodInvocation.methodSymbol())) { + reportIssue(methodInvocation, "Consider using a stable reference for ScopedValue instances."); } } From 4fcb1670d5c279fb7b0798108725eea3b812e736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Wed, 25 Feb 2026 16:51:50 +0100 Subject: [PATCH 6/7] Add support for chained where and nested calls to ScopedValue.newInstance() --- ...ScopedValueStableReferenceCheckSample.java | 25 +++++++++--- .../ScopedValueStableReferenceCheck.java | 40 ++++++++++++------- 2 files changed, 46 insertions(+), 19 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java index 510ad3f4853..3fa9cf7853f 100644 --- a/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java @@ -1,16 +1,35 @@ package checks; +import java.util.Objects; +import org.apache.commons.lang3.tuple.Pair; + class ScopedValueStableReferenceCheckSample { private static final ScopedValue VALUE = ScopedValue.newInstance(); - public void whereNC() { + public void where() { ScopedValue.where(ScopedValue.newInstance(), "inaccessible").run(() -> { // Noncompliant {{Consider using a stable reference for ScopedValue instances.}} // ^^^^^^^^^^^^^^^^^^^^^^^^^ // Cannot reference the scoped value here, as it has no name. }); } + public void chainedWhere() { + ScopedValue scopedValue = ScopedValue.newInstance(); + ScopedValue.where(scopedValue, "accessible").where(ScopedValue.newInstance(), "inaccessible").run(() -> { // Noncompliant {{Consider using a stable reference for ScopedValue instances.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^ + }); + } + + public void nestedArgument() { + ScopedValue.where(Objects.requireNonNull(ScopedValue.newInstance()), "scopedValue").run(() -> {}); // Noncompliant {{Consider using a stable reference for ScopedValue instances.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^ + ScopedValue.where((ScopedValue.newInstance()), "scopedValue").run(() -> {}); // Noncompliant {{Consider using a stable reference for ScopedValue instances.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^ + ScopedValue.where(Pair.of(ScopedValue.newInstance(), 0).getLeft(), "scopedValue").run(() -> {}); // Noncompliant {{Consider using a stable reference for ScopedValue instances.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^ + } + public String readFieldInWhere() { return ScopedValue.where(VALUE, "field value").call(VALUE::get); // Compliant } @@ -20,8 +39,4 @@ public String readLocalVarInWhere() { return ScopedValue.where(value, "local value").call(value::get); // Compliant } - public String directRead() { - return VALUE.get(); // Compliant - } - } diff --git a/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java b/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java index 643fb951ecc..68ffa2d31ce 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java @@ -21,25 +21,19 @@ import org.sonar.plugins.java.api.JavaVersion; import org.sonar.plugins.java.api.JavaVersionAwareVisitor; import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; -import org.sonar.plugins.java.api.tree.Tree; @Rule(key = "S8465") public class ScopedValueStableReferenceCheck extends AbstractMethodDetection implements JavaVersionAwareVisitor { private static final MethodMatchers WHERE_MATCHER = MethodMatchers.create() - .ofTypes("java.lang.ScopedValue") + .ofTypes("java.lang.ScopedValue", "java.lang.ScopedValue$Carrier") .names("where") .withAnyParameters() .build(); - private static final MethodMatchers NEW_INSTANCE_MATCHER = MethodMatchers.create() - .ofTypes("java.lang.ScopedValue") - .names("newInstance") - .addWithoutParametersMatcher() - .build(); - @Override public boolean isCompatibleWithJavaVersion(JavaVersion version) { return version.isJava25Compatible(); @@ -52,15 +46,33 @@ protected MethodMatchers getMethodInvocationMatchers() { @Override protected void onMethodInvocationFound(MethodInvocationTree mit) { - ExpressionTree scopedValue = mit.arguments().get(0); - if (!scopedValue.is(Tree.Kind.METHOD_INVOCATION)) { - return; + ExpressionTree firstArgument = mit.arguments().get(0); + var finder = new NewInstanceInvocationFinder(); + firstArgument.accept(finder); + if (finder.invocation != null) { + reportIssue(finder.invocation, "Consider using a stable reference for ScopedValue instances."); } + } + + private static class NewInstanceInvocationFinder extends BaseTreeVisitor { - MethodInvocationTree methodInvocation = (MethodInvocationTree) scopedValue; - if (NEW_INSTANCE_MATCHER.matches(methodInvocation.methodSymbol())) { - reportIssue(methodInvocation, "Consider using a stable reference for ScopedValue instances."); + private static final MethodMatchers NEW_INSTANCE_MATCHER = MethodMatchers.create() + .ofTypes("java.lang.ScopedValue") + .names("newInstance") + .addWithoutParametersMatcher() + .build(); + + public MethodInvocationTree invocation = null; + + @Override + public void visitMethodInvocation(MethodInvocationTree tree) { + if (NEW_INSTANCE_MATCHER.matches(tree.methodSymbol())) { + invocation = tree; + return; + } + super.visitMethodInvocation(tree); } + } } From 3ef64d8f5433d7f9940771e90d3b3d2bba776a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Wed, 25 Feb 2026 17:07:07 +0100 Subject: [PATCH 7/7] Fix the quality gate --- .../org/sonar/java/checks/ScopedValueStableReferenceCheck.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java b/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java index 68ffa2d31ce..5355130e7fd 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java @@ -62,7 +62,7 @@ private static class NewInstanceInvocationFinder extends BaseTreeVisitor { .addWithoutParametersMatcher() .build(); - public MethodInvocationTree invocation = null; + private MethodInvocationTree invocation = null; @Override public void visitMethodInvocation(MethodInvocationTree tree) {