From 850fc56a9131a5dc138545eafa40b3c06b93a9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Wed, 5 Aug 2026 17:22:03 +0200 Subject: [PATCH 01/12] Add rule metadata --- .../org/sonar/l10n/java/rules/java/S9142.html | 89 +++++++++++++++++++ .../org/sonar/l10n/java/rules/java/S9142.json | 25 ++++++ .../main/resources/profiles/Sonar_way/S9142 | 0 3 files changed, 114 insertions(+) create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.json create mode 100644 sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9142 diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.html new file mode 100644 index 00000000000..02163e7d004 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.html @@ -0,0 +1,89 @@ +

This is an issue when compilation or preparation methods are called inside loop bodies with constant or loop-invariant arguments. This includes +pattern compilation methods for regular expressions, string methods that accept regular expression patterns (such as match, replace, and split +operations), and database statement preparation methods.

+

In Java, this specifically refers to Pattern.compile(), String regex methods (matches(), replaceAll(), +replaceFirst(), split()), and Connection.prepareStatement().

+

Why is this an issue?

+

Compilation and preparation operations are expensive because they involve parsing, validation, and internal representation building. When these +operations are performed inside loops with constant or loop-invariant arguments, the same work is repeated unnecessarily on every iteration.

+

Regular expression compilation

+

When you call functions that compile regular expressions from strings or use string methods that accept regex patterns, the language runtime +must:

+ +

These steps happen every time, even when the pattern string is identical. For example, calling a string matching method with a pattern like +"\d+" inside a loop that processes 1,000 items means compiling the same pattern 1,000 times.

+

Database prepared statement preparation

+

When you call methods that create prepared statements from SQL strings, the database driver must:

+ +

Prepared statements exist specifically to avoid this overhead by allowing you to compile once and execute many times with different parameters. +Calling statement preparation methods inside a loop with the same SQL string defeats this purpose entirely.

+

The performance cost

+

The repeated compilation/preparation causes:

+ +

What is the potential impact?

+

The application may experience:

+ +

How to fix it

+

For regular expression operations, compile the Pattern once before the loop and reuse it inside the loop. Use the +Pattern.matcher() method to apply the pattern to different input strings.

+

Code examples

+

Noncompliant code example

+
+// Direct Pattern.compile in loop
+for (String input : inputs) {
+    Pattern p = Pattern.compile("[a-z]+");  // Noncompliant
+    Matcher m = p.matcher(input);
+    if (m.find()) {
+        handle(m.group());
+    }
+}
+
+

Compliant solution

+
+// Compile once, reuse Pattern
+Pattern LOWER = Pattern.compile("[a-z]+");
+for (String input : inputs) {
+    Matcher m = LOWER.matcher(input);
+    if (m.find()) {
+        handle(m.group());
+    }
+}
+
+

Resources

+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.json new file mode 100644 index 00000000000..5325495596e --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.json @@ -0,0 +1,25 @@ +{ + "title": "Expensive compilation or preparation operations should not be performed inside loops", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5 min" + }, + "tags": [ + "performance", + "regex", + "sql" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-9142", + "sqKey": "S9142", + "scope": "All", + "quickfix": "unknown", + "code": { + "impacts": { + "MAINTAINABILITY": "MEDIUM" + }, + "attribute": "EFFICIENT" + } +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9142 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9142 new file mode 100644 index 00000000000..e69de29bb2d From f5ef064353caa71cb2ca6db1acc8a0a10803f3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Thu, 6 Aug 2026 08:59:43 +0200 Subject: [PATCH 02/12] Add failing reproducer --- ...ilationOrPreparationInLoopCheckSample.java | 75 +++++++++++++++++++ ...mpilationOrPreparationInLoopCheckTest.java | 34 +++++++++ 2 files changed, 109 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheckTest.java diff --git a/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java new file mode 100644 index 00000000000..559fec0dcff --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java @@ -0,0 +1,75 @@ +package checks; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import java.util.regex.Pattern; + +class CompilationOrPreparationInLoopCheckSample { + + private static final String CONSTANT_PATTERN = "[a-z]+"; + + void patternCompileNoncompliant(List inputs) { + for (String input : inputs) { + Pattern.compile("[a-z]+").matcher(input).find(); // Noncompliant {{Move this "compile" call outside the loop.}} + //^^^^^^^^^^^^^^^^^^^^^^^^^ + } + + int i = 0; + while (i++ < inputs.size()) { + Pattern.compile("[a-z]+"); // Noncompliant + } + + for (String input : inputs) { + Pattern.compile(CONSTANT_PATTERN).matcher(input).find(); // Noncompliant + } + + String invariantPattern = "[a-z]+"; + for (String input : inputs) { + Pattern.compile(invariantPattern).matcher(input).find(); // Noncompliant + } + } + + void stringMethodsNoncompliant(List inputs) { + for (String input : inputs) { + input.matches("[a-z]+"); // Noncompliant + input.replaceAll("[a-z]+", "X"); // Noncompliant + input.replaceFirst("[a-z]+", "X"); // Noncompliant + input.split("[,;]"); // Noncompliant + } + } + + void prepareStatementNoncompliant(Connection conn, List ids) throws SQLException { + for (int id : ids) { + PreparedStatement ps = conn.prepareStatement("SELECT * FROM t WHERE id = ?"); // Noncompliant + ps.setInt(1, id); + ps.execute(); + ps.close(); + } + } + + void compliant(List inputs, Connection conn, List ids) throws SQLException { + Pattern p = Pattern.compile("[a-z]+"); + for (String input : inputs) { + p.matcher(input).find(); + } + + for (String input : inputs) { + input.toLowerCase(); // not a regex method + } + + PreparedStatement ps = conn.prepareStatement("SELECT * FROM t WHERE id = ?"); + for (int id : ids) { + ps.setInt(1, id); + ps.execute(); + } + } + + void patternVariesPerIteration(List patterns, List inputs) { + for (int i = 0; i < inputs.size(); i++) { + String pattern = patterns.get(i); + Pattern.compile(pattern).matcher(inputs.get(i)).find(); // Compliant - pattern changes per iteration + } + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheckTest.java new file mode 100644 index 00000000000..9a5c7e8b243 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheckTest.java @@ -0,0 +1,34 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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 CompilationOrPreparationInLoopCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/CompilationOrPreparationInLoopCheckSample.java")) + .withCheck(new CompilationOrPreparationInLoopCheck()) + .verifyIssues(); + } + +} From cee8cea9ffb04ef1af95a7f84ae5908dbbb2f4b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Thu, 6 Aug 2026 09:01:34 +0200 Subject: [PATCH 03/12] Implement rule --- .../CompilationOrPreparationInLoopCheck.java | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java diff --git a/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java b/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java new file mode 100644 index 00000000000..258e91614f2 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java @@ -0,0 +1,135 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * 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.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.sonar.check.Rule; +import org.sonar.java.checks.helpers.TreeHelper; +import org.sonar.java.model.ExpressionUtils; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.tree.AssignmentExpressionTree; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.ForEachStatement; +import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.UnaryExpressionTree; +import org.sonar.plugins.java.api.tree.VariableTree; + +@Rule(key = "S9142") +public class CompilationOrPreparationInLoopCheck extends IssuableSubscriptionVisitor { + + private static final Set LOOP_KINDS = EnumSet.of( + Tree.Kind.FOR_STATEMENT, Tree.Kind.FOR_EACH_STATEMENT, + Tree.Kind.WHILE_STATEMENT, Tree.Kind.DO_STATEMENT + ); + + private static final MethodMatchers MATCHERS = MethodMatchers.or( + MethodMatchers.create() + .ofTypes("java.util.regex.Pattern") + .names("compile") + .withAnyParameters() + .build(), + MethodMatchers.create() + .ofTypes("java.lang.String") + .names("matches", "replaceAll", "replaceFirst", "split") + .withAnyParameters() + .build(), + MethodMatchers.create() + .ofSubTypes("java.sql.Connection") + .names("prepareStatement") + .withAnyParameters() + .build() + ); + + @Override + public List nodesToVisit() { + return Collections.singletonList(Tree.Kind.METHOD_INVOCATION); + } + + @Override + public void visitNode(Tree tree) { + MethodInvocationTree mit = (MethodInvocationTree) tree; + if (!MATCHERS.matches(mit) || mit.arguments().isEmpty()) { + return; + } + Tree loop = TreeHelper.findClosestParentOfKind(mit, LOOP_KINDS); + if (loop == null) { + return; + } + ExpressionTree patternArg = mit.arguments().get(0); + if (isLoopInvariant(patternArg, loop)) { + reportIssue(mit, String.format( + "Move this \"%s\" call outside the loop.", ExpressionUtils.methodName(mit).name())); + } + } + + private static boolean isLoopInvariant(ExpressionTree arg, Tree loop) { + if (arg.is(Tree.Kind.IDENTIFIER)) { + var collector = new DeclaredOrAssignedLocalsCollector(); + loop.accept(collector); + return !collector.names.contains(((IdentifierTree) arg).name()); + } + return ExpressionUtils.resolveAsConstant(arg) != null; + } + + private static class DeclaredOrAssignedLocalsCollector extends BaseTreeVisitor { + + final Set names = new HashSet<>(); + + @Override + public void visitVariable(VariableTree tree) { + super.visitVariable(tree); + names.add(tree.simpleName().name()); + } + + @Override + public void visitAssignmentExpression(AssignmentExpressionTree tree) { + super.visitAssignmentExpression(tree); + if (tree.variable().is(Tree.Kind.IDENTIFIER)) { + names.add(((IdentifierTree) tree.variable()).name()); + } + } + + @Override + public void visitUnaryExpression(UnaryExpressionTree tree) { + super.visitUnaryExpression(tree); + switch (tree.kind()) { + case POSTFIX_INCREMENT, POSTFIX_DECREMENT, PREFIX_INCREMENT, PREFIX_DECREMENT -> { + if (tree.expression().is(Tree.Kind.IDENTIFIER)) { + names.add(((IdentifierTree) tree.expression()).name()); + } + } + default -> { + // not a mutation + } + } + } + + @Override + public void visitForEachStatement(ForEachStatement tree) { + super.visitForEachStatement(tree); + names.add(tree.variable().simpleName().name()); + } + } +} From 15b7b160c2863847d01031b22eac79805c99dd59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Thu, 6 Aug 2026 11:38:42 +0200 Subject: [PATCH 04/12] Update ruling expectations --- .../resources/autoscan/diffs/diff_S9142.json | 6 ++++++ .../test/resources/eclipse-jetty/java-S9142.json | 16 ++++++++++++++++ .../test/resources/sonar-server/java-S9142.json | 7 +++++++ 3 files changed, 29 insertions(+) create mode 100644 its/autoscan/src/test/resources/autoscan/diffs/diff_S9142.json create mode 100644 its/ruling/src/test/resources/eclipse-jetty/java-S9142.json create mode 100644 its/ruling/src/test/resources/sonar-server/java-S9142.json diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S9142.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S9142.json new file mode 100644 index 00000000000..c7d0a63969c --- /dev/null +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S9142.json @@ -0,0 +1,6 @@ +{ + "ruleKey": "S9142", + "hasTruePositives": true, + "falseNegatives": 0, + "falsePositives": 0 +} diff --git a/its/ruling/src/test/resources/eclipse-jetty/java-S9142.json b/its/ruling/src/test/resources/eclipse-jetty/java-S9142.json new file mode 100644 index 00000000000..e9ade210966 --- /dev/null +++ b/its/ruling/src/test/resources/eclipse-jetty/java-S9142.json @@ -0,0 +1,16 @@ +{ +"org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/MultiPartCaptureTest.java": [ +220 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetAccessHandlerTest.java": [ +91, +98, +105, +112, +121 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java": [ +806, +808 +] +} diff --git a/its/ruling/src/test/resources/sonar-server/java-S9142.json b/its/ruling/src/test/resources/sonar-server/java-S9142.json new file mode 100644 index 00000000000..f42c8789bd2 --- /dev/null +++ b/its/ruling/src/test/resources/sonar-server/java-S9142.json @@ -0,0 +1,7 @@ +{ +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/source/DecorationDataHolder.java": [ +48, +60, +73 +] +} From b82ea91d00d32b43dc524cf028eae6c4e456df93 Mon Sep 17 00:00:00 2001 From: nathsou Date: Fri, 14 Aug 2026 08:36:48 +0200 Subject: [PATCH 05/12] Fix S9142 messages, split fast path, and loop-invariant false positives. Use a String-specific issue message, skip OpenJDK split fast-path and for-initializers, and require all Pattern.compile arguments to be loop-invariant. --- ...ilationOrPreparationInLoopCheckSample.java | 19 +++- .../CompilationOrPreparationInLoopCheck.java | 95 +++++++++++++++---- .../org/sonar/l10n/java/rules/java/S9142.html | 75 ++++++++++++--- .../org/sonar/l10n/java/rules/java/S9142.json | 2 +- 4 files changed, 156 insertions(+), 35 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java index 559fec0dcff..0ffd5afc5bb 100644 --- a/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java @@ -29,14 +29,22 @@ void patternCompileNoncompliant(List inputs) { for (String input : inputs) { Pattern.compile(invariantPattern).matcher(input).find(); // Noncompliant } + + for (String input : inputs) { + Pattern.compile("[a-z]+", Pattern.CASE_INSENSITIVE); // Noncompliant + int flags = input.isEmpty() ? 0 : Pattern.CASE_INSENSITIVE; + Pattern.compile("[a-z]+", flags); // Compliant: flags vary + } } void stringMethodsNoncompliant(List inputs) { for (String input : inputs) { - input.matches("[a-z]+"); // Noncompliant + input.matches("[a-z]+"); // Noncompliant {{Extract this regular expression to a Pattern compiled outside the loop.}} input.replaceAll("[a-z]+", "X"); // Noncompliant input.replaceFirst("[a-z]+", "X"); // Noncompliant input.split("[,;]"); // Noncompliant + input.split(","); // Compliant: split fast path + input.split("."); // Noncompliant } } @@ -49,6 +57,15 @@ void prepareStatementNoncompliant(Connection conn, List ids) throws SQL } } + void forInitializer(String s) { + for (Pattern p = Pattern.compile("[a-z]+"); p.matcher(s).find(); ) { // Compliant: initializer runs once + break; + } + for (; Pattern.compile("[a-z]+").matcher(s).find(); ) { // Noncompliant + break; + } + } + void compliant(List inputs, Connection conn, List ids) throws SQLException { Pattern p = Pattern.compile("[a-z]+"); for (String input : inputs) { diff --git a/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java b/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java index 258e91614f2..89173d41855 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java @@ -21,6 +21,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import org.apache.commons.lang3.StringEscapeUtils; import org.sonar.check.Rule; import org.sonar.java.checks.helpers.TreeHelper; import org.sonar.java.model.ExpressionUtils; @@ -30,6 +31,7 @@ import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.ForEachStatement; +import org.sonar.plugins.java.api.tree.ForStatementTree; import org.sonar.plugins.java.api.tree.IdentifierTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; import org.sonar.plugins.java.api.tree.Tree; @@ -39,22 +41,35 @@ @Rule(key = "S9142") public class CompilationOrPreparationInLoopCheck extends IssuableSubscriptionVisitor { + private static final String STRING_REGEX_MESSAGE = + "Extract this regular expression to a Pattern compiled outside the loop."; + private static final Set LOOP_KINDS = EnumSet.of( Tree.Kind.FOR_STATEMENT, Tree.Kind.FOR_EACH_STATEMENT, Tree.Kind.WHILE_STATEMENT, Tree.Kind.DO_STATEMENT ); + private static final MethodMatchers PATTERN_COMPILE = MethodMatchers.create() + .ofTypes("java.util.regex.Pattern") + .names("compile") + .withAnyParameters() + .build(); + + private static final MethodMatchers STRING_REGEX_METHODS = MethodMatchers.create() + .ofTypes("java.lang.String") + .names("matches", "replaceAll", "replaceFirst", "split") + .withAnyParameters() + .build(); + + private static final MethodMatchers SPLIT = MethodMatchers.create() + .ofTypes("java.lang.String") + .names("split") + .withAnyParameters() + .build(); + private static final MethodMatchers MATCHERS = MethodMatchers.or( - MethodMatchers.create() - .ofTypes("java.util.regex.Pattern") - .names("compile") - .withAnyParameters() - .build(), - MethodMatchers.create() - .ofTypes("java.lang.String") - .names("matches", "replaceAll", "replaceFirst", "split") - .withAnyParameters() - .build(), + PATTERN_COMPILE, + STRING_REGEX_METHODS, MethodMatchers.create() .ofSubTypes("java.sql.Connection") .names("prepareStatement") @@ -74,23 +89,67 @@ public void visitNode(Tree tree) { return; } Tree loop = TreeHelper.findClosestParentOfKind(mit, LOOP_KINDS); - if (loop == null) { + if (loop == null || isInForInitializer(mit, loop)) { return; } - ExpressionTree patternArg = mit.arguments().get(0); - if (isLoopInvariant(patternArg, loop)) { - reportIssue(mit, String.format( - "Move this \"%s\" call outside the loop.", ExpressionUtils.methodName(mit).name())); + if (SPLIT.matches(mit) && isSplitFastPath(mit.arguments().get(0))) { + return; + } + List argsToCheck = PATTERN_COMPILE.matches(mit) ? mit.arguments() : List.of(mit.arguments().get(0)); + if (argsToCheck.stream().allMatch(arg -> isLoopInvariant(arg, loop))) { + reportIssue(mit, message(mit)); + } + } + + private static String message(MethodInvocationTree mit) { + if (STRING_REGEX_METHODS.matches(mit)) { + return STRING_REGEX_MESSAGE; + } + return String.format("Move this \"%s\" call outside the loop.", ExpressionUtils.methodName(mit).name()); + } + + private static boolean isInForInitializer(Tree tree, Tree loop) { + if (!loop.is(Tree.Kind.FOR_STATEMENT)) { + return false; } + Tree initializer = ((ForStatementTree) loop).initializer(); + for (Tree current = tree; current != null && current != loop; current = current.parent()) { + if (current == initializer) { + return true; + } + } + return false; + } + + private static boolean isSplitFastPath(ExpressionTree arg) { + return ExpressionUtils.skipParentheses(arg).asConstant(String.class) + .filter(CompilationOrPreparationInLoopCheck::exceptionSplitMethod) + .isPresent(); + } + + /** + * Copy of {@link java.lang.String#split(String, int)} fast-path, matching {@link RegexPatternsNeedlesslyCheck}. + */ + private static boolean exceptionSplitMethod(String argValue) { + String regex = StringEscapeUtils.unescapeJava(argValue); + char ch; + return ((regex.length() == 1 && ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) || + (regex.length() == 2 && + regex.charAt(0) == '\\' && + (((ch = regex.charAt(1)) - '0') | ('9' - ch)) < 0 && + ((ch - 'a') | ('z' - ch)) < 0 && + ((ch - 'A') | ('Z' - ch)) < 0)) && + (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE); } private static boolean isLoopInvariant(ExpressionTree arg, Tree loop) { - if (arg.is(Tree.Kind.IDENTIFIER)) { + ExpressionTree expression = ExpressionUtils.skipParentheses(arg); + if (expression.is(Tree.Kind.IDENTIFIER)) { var collector = new DeclaredOrAssignedLocalsCollector(); loop.accept(collector); - return !collector.names.contains(((IdentifierTree) arg).name()); + return !collector.names.contains(((IdentifierTree) expression).name()); } - return ExpressionUtils.resolveAsConstant(arg) != null; + return ExpressionUtils.resolveAsConstant(expression) != null; } private static class DeclaredOrAssignedLocalsCollector extends BaseTreeVisitor { diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.html index 02163e7d004..1197214413b 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.html +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.html @@ -6,7 +6,7 @@

Why is this an issue?

Compilation and preparation operations are expensive because they involve parsing, validation, and internal representation building. When these operations are performed inside loops with constant or loop-invariant arguments, the same work is repeated unnecessarily on every iteration.

-

Regular expression compilation

+

Regular expression compilation

When you call functions that compile regular expressions from strings or use string methods that accept regex patterns, the language runtime must:

    @@ -17,7 +17,7 @@

    Regular expression compilation

These steps happen every time, even when the pattern string is identical. For example, calling a string matching method with a pattern like "\d+" inside a loop that processes 1,000 items means compiling the same pattern 1,000 times.

-

Database prepared statement preparation

+

Database prepared statement preparation

When you call methods that create prepared statements from SQL strings, the database driver must:

  • Send the SQL string to the database server
  • @@ -28,7 +28,7 @@

    Database prepared statement preparation

Prepared statements exist specifically to avoid this overhead by allowing you to compile once and execute many times with different parameters. Calling statement preparation methods inside a loop with the same SQL string defeats this purpose entirely.

-

The performance cost

+

The performance cost

The repeated compilation/preparation causes:

  • CPU waste: Parsing and compilation happen repeatedly instead of once
  • @@ -48,13 +48,21 @@

    What is the potential impact?

  • Increased costs: Higher CPU usage and longer execution times can lead to increased infrastructure costs in cloud environments
+

Exceptions

+

String.split does not compile a regular expression when the argument meets either of these conditions:

+
    +
  • It is a one-char String and this character is not one of the regex metacharacters ".$|()[{^?*+\"
  • +
  • It is a two-char String and the first char is the backslash and the second is not an ASCII digit or letter.
  • +
+

In these cases, no issue is raised.

How to fix it

-

For regular expression operations, compile the Pattern once before the loop and reuse it inside the loop. Use the -Pattern.matcher() method to apply the pattern to different input strings.

+

For regular expression operations, compile the Pattern once before the loop and reuse it inside the loop. Do not move +String.matches(), replaceAll(), replaceFirst(), or split() outside the loop: the receiver still +changes on each iteration. Replace those calls with Pattern.matcher() or Pattern.split() applied to each input.

+

For database operations, prepare the statement once before the loop and reuse it with different parameters.

Code examples

Noncompliant code example

-// Direct Pattern.compile in loop
 for (String input : inputs) {
     Pattern p = Pattern.compile("[a-z]+");  // Noncompliant
     Matcher m = p.matcher(input);
@@ -65,7 +73,6 @@ 

Noncompliant code example

Compliant solution

-// Compile once, reuse Pattern
 Pattern LOWER = Pattern.compile("[a-z]+");
 for (String input : inputs) {
     Matcher m = LOWER.matcher(input);
@@ -74,16 +81,54 @@ 

Compliant solution

} }
+

Noncompliant code example

+
+for (String input : inputs) {
+    if (input.matches("[a-z]+")) {  // Noncompliant
+        handle(input);
+    }
+}
+
+

Compliant solution

+
+Pattern LOWER = Pattern.compile("[a-z]+");
+for (String input : inputs) {
+    if (LOWER.matcher(input).matches()) {
+        handle(input);
+    }
+}
+
+

Noncompliant code example

+
+for (int id : ids) {
+    PreparedStatement ps = connection.prepareStatement("SELECT * FROM t WHERE id = ?");  // Noncompliant
+    ps.setInt(1, id);
+    ps.execute();
+    ps.close();
+}
+
+

Compliant solution

+
+PreparedStatement ps = connection.prepareStatement("SELECT * FROM t WHERE id = ?");
+for (int id : ids) {
+    ps.setInt(1, id);
+    ps.execute();
+}
+

Resources

Documentation

+

Articles & blog posts

+ - +

Related rules

+
    +
  • {rule:java:S4248} - Regex patterns should not be created needlessly
  • +
  • {rule:java:S6909} - Constant parameters in a "PreparedStatement" should not be set more than once
  • +
diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.json index 5325495596e..344210924c4 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9142.json @@ -4,7 +4,7 @@ "status": "ready", "remediation": { "func": "Constant\/Issue", - "constantCost": "5 min" + "constantCost": "5min" }, "tags": [ "performance", From 8771e5c9f30984607ecd6387b85891da42b2952b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Fri, 14 Aug 2026 12:00:48 +0200 Subject: [PATCH 06/12] Address review comments and fix sonarqube issues --- ...ilationOrPreparationInLoopCheckSample.java | 7 +++++ .../CompilationOrPreparationInLoopCheck.java | 26 ++++++++++++++----- ...mpilationOrPreparationInLoopCheckTest.java | 9 +++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java index 0ffd5afc5bb..e10a42e980a 100644 --- a/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java @@ -9,6 +9,7 @@ class CompilationOrPreparationInLoopCheckSample { private static final String CONSTANT_PATTERN = "[a-z]+"; + private String mutablePattern = "[a-z]+"; void patternCompileNoncompliant(List inputs) { for (String input : inputs) { @@ -89,4 +90,10 @@ void patternVariesPerIteration(List patterns, List inputs) { Pattern.compile(pattern).matcher(inputs.get(i)).find(); // Compliant - pattern changes per iteration } } + + void mutableFieldPattern(List inputs) { + for (String input : inputs) { + Pattern.compile(mutablePattern).matcher(input).find(); // Compliant - non-final field may be mutated via member select or method call + } + } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java b/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java index 89173d41855..a997dd8776c 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java @@ -27,6 +27,7 @@ import org.sonar.java.model.ExpressionUtils; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.tree.AssignmentExpressionTree; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.ExpressionTree; @@ -133,18 +134,31 @@ private static boolean isSplitFastPath(ExpressionTree arg) { private static boolean exceptionSplitMethod(String argValue) { String regex = StringEscapeUtils.unescapeJava(argValue); char ch; - return ((regex.length() == 1 && ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) || - (regex.length() == 2 && - regex.charAt(0) == '\\' && - (((ch = regex.charAt(1)) - '0') | ('9' - ch)) < 0 && + if (regex.length() == 1) { + ch = regex.charAt(0); + return ".$|()[{^?*+\\".indexOf(ch) == -1 && + (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE); + } + if (regex.length() == 2 && regex.charAt(0) == '\\') { + ch = regex.charAt(1); + return (((ch - '0') | ('9' - ch)) < 0 && ((ch - 'a') | ('z' - ch)) < 0 && - ((ch - 'A') | ('Z' - ch)) < 0)) && - (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE); + ((ch - 'A') | ('Z' - ch)) < 0) && + (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE); + } + return false; } private static boolean isLoopInvariant(ExpressionTree arg, Tree loop) { ExpressionTree expression = ExpressionUtils.skipParentheses(arg); if (expression.is(Tree.Kind.IDENTIFIER)) { + Symbol symbol = ((IdentifierTree) expression).symbol(); + if (!symbol.isVariableSymbol()) { + return false; + } + if (symbol.owner().isTypeSymbol()) { + return symbol.isFinal(); + } var collector = new DeclaredOrAssignedLocalsCollector(); loop.accept(collector); return !collector.names.contains(((IdentifierTree) expression).name()); diff --git a/java-checks/src/test/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheckTest.java index 9a5c7e8b243..09e4c9a6435 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheckTest.java @@ -31,4 +31,13 @@ void test() { .verifyIssues(); } + @Test + void testWithoutSemantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/CompilationOrPreparationInLoopCheckSample.java")) + .withCheck(new CompilationOrPreparationInLoopCheck()) + .withoutSemantic() + .verifyIssues(); + } + } From c22a79d6ba0ddab56e968a1861e7eed074439008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Fri, 14 Aug 2026 13:32:31 +0200 Subject: [PATCH 07/12] Update ruling expectations --- .../resources/eclipse-jetty/java-S9142.json | 17 +---------------- .../test/resources/sonar-server/java-S9142.json | 8 +------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/its/ruling/src/test/resources/eclipse-jetty/java-S9142.json b/its/ruling/src/test/resources/eclipse-jetty/java-S9142.json index e9ade210966..9e26dfeeb6e 100644 --- a/its/ruling/src/test/resources/eclipse-jetty/java-S9142.json +++ b/its/ruling/src/test/resources/eclipse-jetty/java-S9142.json @@ -1,16 +1 @@ -{ -"org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/MultiPartCaptureTest.java": [ -220 -], -"org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/handler/InetAccessHandlerTest.java": [ -91, -98, -105, -112, -121 -], -"org.eclipse.jetty:jetty-project:jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java": [ -806, -808 -] -} +{} \ No newline at end of file diff --git a/its/ruling/src/test/resources/sonar-server/java-S9142.json b/its/ruling/src/test/resources/sonar-server/java-S9142.json index f42c8789bd2..9e26dfeeb6e 100644 --- a/its/ruling/src/test/resources/sonar-server/java-S9142.json +++ b/its/ruling/src/test/resources/sonar-server/java-S9142.json @@ -1,7 +1 @@ -{ -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/source/DecorationDataHolder.java": [ -48, -60, -73 -] -} +{} \ No newline at end of file From 9024910da78956a6e1c35b58e2262236f3087275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Fri, 14 Aug 2026 13:42:16 +0200 Subject: [PATCH 08/12] Improve test coverage --- ...ilationOrPreparationInLoopCheckSample.java | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java index e10a42e980a..4cf994ddf50 100644 --- a/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java @@ -44,8 +44,10 @@ void stringMethodsNoncompliant(List inputs) { input.replaceAll("[a-z]+", "X"); // Noncompliant input.replaceFirst("[a-z]+", "X"); // Noncompliant input.split("[,;]"); // Noncompliant - input.split(","); // Compliant: split fast path input.split("."); // Noncompliant + input.split("\\a"); // Noncompliant + input.split(","); // Compliant: single non-metacharacter fast path + input.split("\\."); // Compliant: escaped non-alphanumeric fast path } } @@ -96,4 +98,34 @@ void mutableFieldPattern(List inputs) { Pattern.compile(mutablePattern).matcher(input).find(); // Compliant - non-final field may be mutated via member select or method call } } + + void localReassignedInLoop(List inputs) { + String pattern = "[a-z]+"; + for (String input : inputs) { + pattern = input; // reassigned each iteration + Pattern.compile(pattern).matcher(input).find(); // Compliant - pattern changes per iteration + } + } + + void doWhileLoop(List inputs) { + int i = 0; + do { + Pattern.compile("[a-z]+").matcher(inputs.get(i)).find(); // Noncompliant + } while (i++ < inputs.size()); + } + + void nonConstantArg(List inputs) { + for (String input : inputs) { + Pattern.compile(input.trim()).matcher(input).find(); // Compliant - method call result is not a constant + } + } + + void nonIncrementUnaryInLoop(List inputs) { + boolean inverse = false; + for (String input : inputs) { + if (!inverse) { // non-increment unary expression + Pattern.compile("[a-z]+").matcher(input).find(); // Noncompliant + } + } + } } From ba917e19eb08059c53dae59d32a79f7e0c32801a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Fri, 14 Aug 2026 13:56:18 +0200 Subject: [PATCH 09/12] Improve test coverage again --- .../CompilationOrPreparationInLoopCheckSample.java | 9 +++++++++ .../checks/CompilationOrPreparationInLoopCheckTest.java | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java index 4cf994ddf50..05fc87c75ca 100644 --- a/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java @@ -128,4 +128,13 @@ void nonIncrementUnaryInLoop(List inputs) { } } } + + void nonIdentifierMutationsInLoop(List inputs) { + int[] counters = new int[2]; + for (String input : inputs) { + counters[0] = input.length(); // assignment to non-identifier target + counters[1]++; // increment on non-identifier target + Pattern.compile("[a-z]+").matcher(input).find(); // Noncompliant + } + } } diff --git a/java-checks/src/test/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheckTest.java index 09e4c9a6435..6067d2eca1d 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheckTest.java @@ -20,6 +20,7 @@ import org.sonar.java.checks.verifier.CheckVerifier; import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath; +import static org.sonar.java.checks.verifier.TestUtils.nonCompilingTestSourcesPath; class CompilationOrPreparationInLoopCheckTest { @@ -31,6 +32,14 @@ void test() { .verifyIssues(); } + @Test + void testNonCompiling() { + CheckVerifier.newVerifier() + .onFile(nonCompilingTestSourcesPath("checks/CompilationOrPreparationInLoopCheckSample.java")) + .withCheck(new CompilationOrPreparationInLoopCheck()) + .verifyNoIssues(); + } + @Test void testWithoutSemantic() { CheckVerifier.newVerifier() From a3a26114770a2c037e896162622d6118a96647a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Fri, 14 Aug 2026 14:33:49 +0200 Subject: [PATCH 10/12] Improve test coverage --- .../CompilationOrPreparationInLoopCheckSample.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/files/non-compiling/checks/CompilationOrPreparationInLoopCheckSample.java diff --git a/java-checks-test-sources/default/src/main/files/non-compiling/checks/CompilationOrPreparationInLoopCheckSample.java b/java-checks-test-sources/default/src/main/files/non-compiling/checks/CompilationOrPreparationInLoopCheckSample.java new file mode 100644 index 00000000000..4696432851c --- /dev/null +++ b/java-checks-test-sources/default/src/main/files/non-compiling/checks/CompilationOrPreparationInLoopCheckSample.java @@ -0,0 +1,12 @@ +package checks; + +import java.util.List; +import java.util.regex.Pattern; + +class CompilationOrPreparationInLoopCheckSampleNonCompiling { + void test(List inputs) { + for (String input : inputs) { + Pattern.compile(unknownVar).matcher(input).find(); // Compliant - unresolved symbol is not a variable symbol + } + } +} From 6a18a32c6dc05ff820116381caed2afff02d18a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Mon, 17 Aug 2026 08:58:20 +0200 Subject: [PATCH 11/12] Address review comments --- ...ompilationOrPreparationInLoopCheckSample.java | 16 ++++++++++++++++ .../CompilationOrPreparationInLoopCheck.java | 15 ++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java index 05fc87c75ca..ffbdbedeac5 100644 --- a/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java @@ -137,4 +137,20 @@ void nonIdentifierMutationsInLoop(List inputs) { Pattern.compile("[a-z]+").matcher(input).find(); // Noncompliant } } + + void expressionInEnhancedForLoopWithSplit(String text) { + for (String s : text.split(";")) { // Compliant; text.split(";") evaluated only once + if (s.isEmpty()) { + return; + } + } + } + + void expressionInEnhancedForLoopWithCompile(String text) { + for (String s : Pattern.compile(";").split(text)) { // Compliant; Pattern.compile(";").split(text) evaluated only once + if (s.isEmpty()) { + return; + } + } + } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java b/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java index a997dd8776c..29fcde141b0 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java @@ -90,7 +90,7 @@ public void visitNode(Tree tree) { return; } Tree loop = TreeHelper.findClosestParentOfKind(mit, LOOP_KINDS); - if (loop == null || isInForInitializer(mit, loop)) { + if (loop == null || isExecutedOncePerLoop(mit, loop)) { return; } if (SPLIT.matches(mit) && isSplitFastPath(mit.arguments().get(0))) { @@ -109,13 +109,18 @@ private static String message(MethodInvocationTree mit) { return String.format("Move this \"%s\" call outside the loop.", ExpressionUtils.methodName(mit).name()); } - private static boolean isInForInitializer(Tree tree, Tree loop) { - if (!loop.is(Tree.Kind.FOR_STATEMENT)) { + private static boolean isExecutedOncePerLoop(Tree tree, Tree loop) { + Tree anchor = null; + if (loop instanceof ForStatementTree forStatementTree) { + anchor = forStatementTree.initializer(); + } else if (loop instanceof ForEachStatement forEachStatement) { + anchor = forEachStatement.expression(); + } + if (anchor == null) { return false; } - Tree initializer = ((ForStatementTree) loop).initializer(); for (Tree current = tree; current != null && current != loop; current = current.parent()) { - if (current == initializer) { + if (current == anchor) { return true; } } From 1fb4dd2e0cae386c60d2313f7654fbe8f3e83325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mie=20B=C3=A9nard?= Date: Mon, 17 Aug 2026 11:43:35 +0200 Subject: [PATCH 12/12] Fix false negative for once-per-outer-loop calls --- .../CompilationOrPreparationInLoopCheckSample.java | 10 ++++++++++ .../checks/CompilationOrPreparationInLoopCheck.java | 11 +++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java index ffbdbedeac5..9d12916ba19 100644 --- a/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/CompilationOrPreparationInLoopCheckSample.java @@ -153,4 +153,14 @@ void expressionInEnhancedForLoopWithCompile(String text) { } } } + + void forEachIterableInsideOuterLoop(List texts) { + for (String text : texts) { + for (String s : Pattern.compile(";").split(text)) { // Noncompliant + if (s.isEmpty()) { + return; + } + } + } + } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java b/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java index 29fcde141b0..8dfa122b5fd 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/CompilationOrPreparationInLoopCheck.java @@ -90,14 +90,21 @@ public void visitNode(Tree tree) { return; } Tree loop = TreeHelper.findClosestParentOfKind(mit, LOOP_KINDS); - if (loop == null || isExecutedOncePerLoop(mit, loop)) { + if (loop == null) { return; } + if (isExecutedOncePerLoop(mit, loop)) { + loop = TreeHelper.findClosestParentOfKind(loop.parent(), LOOP_KINDS); + if (loop == null) { + return; + } + } if (SPLIT.matches(mit) && isSplitFastPath(mit.arguments().get(0))) { return; } + Tree effectiveLoop = loop; List argsToCheck = PATTERN_COMPILE.matches(mit) ? mit.arguments() : List.of(mit.arguments().get(0)); - if (argsToCheck.stream().allMatch(arg -> isLoopInvariant(arg, loop))) { + if (argsToCheck.stream().allMatch(arg -> isLoopInvariant(arg, effectiveLoop))) { reportIssue(mit, message(mit)); } }