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 @@ -2896,5 +2896,11 @@
"hasTruePositives": false,
"falseNegatives": 0,
"falsePositives": 0
},
{
"ruleKey": "S8471",
"hasTruePositives": true,
"falseNegatives": 0,
"falsePositives": 0
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ruleKey": "S8471",
"hasTruePositives": true,
"falseNegatives": 0,
"falsePositives": 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package checks;

public class MinimizeBoundScopedValuesCheckSample {

private static final ScopedValue<String> USER_NAME = ScopedValue.newInstance();
private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
private static final ScopedValue<String> USER_EMAIL = ScopedValue.newInstance();
private static final ScopedValue<String> USER_ROLE = ScopedValue.newInstance();

public void processFour() {
ScopedValue.where(USER_NAME, "John") // Noncompliant {{Consider grouping the 4 scoped values bound in this chain of method calls into a record class to maintain good performance.}}
.where(USER_ID, "123")
.where(USER_EMAIL, "john@example.com")
.where(USER_ROLE, "admin")
.run(this::doWork);
}

public void processThree() {
ScopedValue.where(USER_NAME, "John") // Noncompliant {{Consider grouping the 3 scoped values bound in this chain of method calls into a record class to maintain good performance.}}
.where(USER_ID, "123")
.where(USER_EMAIL, "john@example.com")
.run(this::doWork2);
}

public void processThreeSeparated() {
ScopedValue<ScopedValue.Carrier> carrier = ScopedValue.newInstance();
ScopedValue.where(carrier, ScopedValue.where(USER_NAME, "John")).run(() ->
ScopedValue.where(USER_ID, "123") // Noncompliant {{Consider grouping the 3 scoped values bound in this chain of method calls into a record class to maintain good performance.}}
.call(carrier::get)
.where(USER_EMAIL, "john@example.com")
.where(USER_ROLE, "admin")
.run(this::doWork));
}

public void processThreeFromCarrier() {
ScopedValue<ScopedValue.Carrier> carrier = ScopedValue.newInstance();
ScopedValue.where(carrier, ScopedValue.where(USER_NAME, "John")).run(() ->
carrier.get() // Noncompliant {{Consider grouping the 3 scoped values bound in this chain of method calls into a record class to maintain good performance.}}
.where(USER_ID, "123")
.where(USER_NAME, "John")
.where(USER_EMAIL, "john@mail.com")
.run(this::doWork)
);

var c = carrier.get();
c.where(USER_ID, "123") // Noncompliant {{Consider grouping the 3 scoped values bound in this chain of method calls into a record class to maintain good performance.}}
.where(USER_NAME, "John")
.where(USER_EMAIL, "john@mail.com")
.run(this::doWork);
}

public void processTwoOrLess() {
ScopedValue.where(USER_NAME, "John") // Compliant
.where(USER_ID, "123")
.run(this::doWork2);

ScopedValue.where(USER_EMAIL, "john@email.org").run(() -> IO.print(USER_EMAIL.get())); // Compliant
}

private void doWork() {
String importantMessage = String.format("User %s, ID %s, email %s, role %s", USER_NAME.get(), USER_ID.get(), USER_EMAIL.get(), USER_ROLE.get());
IO.println(importantMessage);
}

private void doWork2() {
String importantMessage = String.format("User %s, ID %s", USER_NAME.get(), USER_ID.get());
IO.println(importantMessage);
}

}
Original file line number Diff line number Diff line change
@@ -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 java.util.HashSet;
import java.util.Set;
import org.apache.commons.lang3.tuple.Pair;
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.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.Tree;

@Rule(key = "S8471")
public class MinimizeBoundScopedValuesCheck extends AbstractMethodDetection implements JavaVersionAwareVisitor {

private static final int THRESHOLD = 3;
private static final String MESSAGE = "Consider grouping the %d scoped values bound in this chain of method calls into a record class to maintain good performance.";

private final Set<Tree> visitedMethodInvocations = new HashSet<>();

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 methodInvocation) {
Pair<Integer, Tree> whereCountAndChainStart = countWhereChain(methodInvocation);
int whereCount = whereCountAndChainStart.getLeft();
Tree chainStart = whereCountAndChainStart.getRight();
if (whereCount >= THRESHOLD) {
reportIssue(chainStart, methodInvocation, String.format(MESSAGE, whereCount));
}
}

private Pair<Integer, Tree> countWhereChain(MethodInvocationTree methodInvocation) {
int count = 0;
while (visitedMethodInvocations.add(methodInvocation)) {
if (WHERE_MATCHER.matches(methodInvocation)) {
count++;
}
if (methodInvocation.methodSelect() instanceof MemberSelectExpressionTree memberSelect &&
memberSelect.expression() instanceof MethodInvocationTree previousInvocation) {
methodInvocation = previousInvocation;
}
}
return Pair.of(count, methodInvocation);
}

}
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 MinimizeBoundScopedValuesCheckTest {

@Test
void test() {
CheckVerifier.newVerifier()
.onFile(mainCodeSourcesPath("checks/MinimizeBoundScopedValuesCheckSample.java"))
.withCheck(new MinimizeBoundScopedValuesCheck())
.withJavaVersion(25)
.verifyIssues();
}

@Test
void test_24() {
CheckVerifier.newVerifier()
.onFile(mainCodeSourcesPath("checks/MinimizeBoundScopedValuesCheckSample.java"))
.withCheck(new MinimizeBoundScopedValuesCheck())
.withJavaVersion(24)
.verifyNoIssues();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<p>Java 25 introduced scoped values as a lightweight alternative to thread-local variables. They provide a way to safely and efficiently pass data to
faraway methods without using method parameters. To maintain good performance, however, the number of scoped values bound at the same time should
remain low.</p>
<h2>Why is this an issue?</h2>
<p>Invocations of <code>ScopedValue.get()</code> typically need to search through enclosing scopes to find the innermost binding and then cache the
result of this search for subsequent accesses. In the presence of too many scoped values, this cache will have a low hit rate, which will cause poor
performance.</p>
<h2>How to fix it</h2>
<p>To maintain good performance, minimize the number of bound scoped values in use. When multiple values need to be passed, create a record class to
hold those values and bind a single <code>ScopedValue</code> to an instance of that record, rather than binding multiple separate instances.</p>
<h3>Code examples</h3>
<h4>Noncompliant code example</h4>
<pre data-diff-id="1" data-diff-type="noncompliant">
private static final ScopedValue&lt;String&gt; USER_NAME = ScopedValue.newInstance();
private static final ScopedValue&lt;String&gt; USER_ID = ScopedValue.newInstance();
private static final ScopedValue&lt;String&gt; USER_EMAIL = ScopedValue.newInstance();
private static final ScopedValue&lt;String&gt; USER_ROLE = ScopedValue.newInstance();

public void process() {
ScopedValue.where(USER_NAME, "John")
.where(USER_ID, "123")
.where(USER_EMAIL, "john@example.com")
.where(USER_ROLE, "admin")
.run(() -&gt; doWork());
}
</pre>
<h4>Compliant solution</h4>
<pre data-diff-id="1" data-diff-type="compliant">
record UserContext(String name, String id, String email, String role) {}

private static final ScopedValue&lt;UserContext&gt; USER_CONTEXT = ScopedValue.newInstance();

public void process() {
UserContext context = new UserContext("John", "123", "john@example.com", "admin");
ScopedValue.where(USER_CONTEXT, context).run(() -&gt; doWork());
}
</pre>
<h2>Resources</h2>
<h3>Documentation</h3>
<ul>
<li><a href="https://docs.oracle.com/en/java/javase/25/docs//api/java.base/java/lang/ScopedValue.html">Oracle Docs - Class
ScopedValue&lt;T&gt;</a></li>
</ul>

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"title": "Minimize the number of bound ScopedValues for performance",
"type": "CODE_SMELL",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "15min"
},
"tags": [
"java25"
],
"defaultSeverity": "Minor",
"ruleSpecification": "RSPEC-8471",
"sqKey": "S8471",
"scope": "All",
"quickfix": "unknown",
"code": {
"impacts": {
"MAINTAINABILITY": "LOW",
"RELIABILITY": "LOW"
},
"attribute": "EFFICIENT"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@
"S8445",
"S8446",
"S8447",
"S8450"
"S8450",
"S8471"
]
}