Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2890,5 +2890,11 @@
"hasTruePositives": false,
"falseNegatives": 0,
"falsePositives": 0
},
{
"ruleKey": "S8465",
"hasTruePositives": true,
"falseNegatives": 0,
"falsePositives": 0
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ruleKey": "S8465",
"hasTruePositives": true,
"falseNegatives": 0,
"falsePositives": 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package checks;

import java.util.Objects;
import org.apache.commons.lang3.tuple.Pair;

class ScopedValueStableReferenceCheckSample {

private static final ScopedValue<String> 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<String> 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
}

public String readLocalVarInWhere() {
ScopedValue<String> value = ScopedValue.newInstance();
return ScopedValue.where(value, "local value").call(value::get); // Compliant
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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;

@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())) {
invocation = tree;
return;
}
super.visitMethodInvocation(tree);
}

}

}
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<p>A <code>ScopedValue</code> must be accessible within the functional task where it is bound. Creating a new instance directly inside a
<code>ScopedValue.where()</code> call makes the key unreachable, as there is no variable name to reference when calling <code>.get()</code>.</p>
<h2>Why is this an issue?</h2>
<p>The primary purpose of a <code>ScopedValue</code> is to provide a way to share data without passing it as method arguments. To retrieve the value
using <code>.get()</code>, you must have access to the <code>ScopedValue</code> instance (the key).</p>
<p>If a <code>ScopedValue</code> is instantiated anonymously within the <code>.where()</code> method, it is "lost" immediately after the binding is
created. The code inside the <code>Runnable</code> or <code>Callable</code> has no way to reference that specific instance to retrieve the associated
value, rendering the scoped value useless.</p>
<h2>How to fix it</h2>
<p>Assign the <code>ScopedValue</code> instance to a stable reference — typically a <code>static final</code> 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.</p>
<h3>Code examples</h3>
<h4>Noncompliant code example</h4>
<pre data-diff-id="1" data-diff-type="noncompliant">
public class Renderer {

public void process() {
// Noncompliant: The ScopedValue instance is anonymous and unreachable inside the run method
ScopedValue.where(ScopedValue.newInstance(), "DARK").run(() -&gt; {
render();
});
}

void render() {
// There is no way to call .get() here because the key is unknown
}

}
</pre>
<h4>Compliant solution</h4>
<pre data-diff-id="1" data-diff-type="compliant">
public class Renderer {

// Compliant: The ScopedValue is assigned to a constant that can be referenced elsewhere
private static final ScopedValue&lt;String&gt; THEME = ScopedValue.newInstance();

public void process() {
ScopedValue.where(THEME, "DARK").run(() -&gt; {
render();
});
}

void render() {
System.out.println("Theme is: " + THEME.get());
}

}
</pre>
<h2>Resources</h2>
<h3>Documentation</h3>
<ul>
<li><a href="https://openjdk.org/jeps/506">JEP 506: Scoped Values</a></li>
</ul>

Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@
"S8444",
"S8445",
"S8447",
"S8450"
"S8450",
"S8465"
]
}
Loading