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 +} 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..1fea746a282 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/ScopedValueStableReferenceCheckSample.java @@ -0,0 +1,50 @@ +package checks; + +import java.util.Objects; +import org.apache.commons.lang3.tuple.Pair; + +class ScopedValueStableReferenceCheckSample { + + private static final ScopedValue VALUE = ScopedValue.newInstance(); + + 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.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^ + ScopedValue scopedValue; + ScopedValue.where(Pair.of((scopedValue = ScopedValue.newInstance()), ScopedValue.newInstance()).getLeft(), "scopedValue").run(scopedValue::get); // Noncompliant {{Consider using a stable reference for ScopedValue instances.}} +// ^^^^^^^^^^^^^^^^^^^^^^^^^ + } + + 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 assignLocalVarInWhere() { + ScopedValue value; + return ScopedValue.where((value = ScopedValue.newInstance()), "local 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 new file mode 100644 index 00000000000..a4509035291 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/ScopedValueStableReferenceCheck.java @@ -0,0 +1,79 @@ +/* + * 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.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", "java.lang.ScopedValue$Carrier") + .names("where") + .withAnyParameters() + .build(); + + @Override + public boolean isCompatibleWithJavaVersion(JavaVersion version) { + return version.isJava25Compatible(); + } + + @Override + protected MethodMatchers getMethodInvocationMatchers() { + return WHERE_MATCHER; + } + + @Override + protected void onMethodInvocationFound(MethodInvocationTree mit) { + 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 { + + private static final MethodMatchers NEW_INSTANCE_MATCHER = MethodMatchers.create() + .ofTypes("java.lang.ScopedValue") + .names("newInstance") + .addWithoutParametersMatcher() + .build(); + + private MethodInvocationTree invocation = null; + + @Override + public void visitMethodInvocation(MethodInvocationTree tree) { + if (NEW_INSTANCE_MATCHER.matches(tree.methodSymbol()) && !tree.parent().is(Tree.Kind.ASSIGNMENT)) { + invocation = tree; + return; + } + super.visitMethodInvocation(tree); + } + + } + +} 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..6530327cbfc --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/ScopedValueStableReferenceCheckTest.java @@ -0,0 +1,44 @@ +/* + * 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; +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(); + } + +} 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" ] }