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 1/4] 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 2/4] 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 3/4] 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 4/4] 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 +] +}