From 3b4841815c72810b176ccab24ef8d0bd54295fd6 Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 11:00:16 +0200 Subject: [PATCH 1/5] Implement S9353 --- .../java/checks/BareDotRegexpCheckSample.java | 43 +++++++++ .../sonar/java/checks/BareDotRegexpCheck.java | 91 +++++++++++++++++++ .../java/checks/BareDotRegexpCheckTest.java | 43 +++++++++ .../org/sonar/l10n/java/rules/java/S9353.html | 69 ++++++++++++++ .../org/sonar/l10n/java/rules/java/S9353.json | 23 +++++ .../main/resources/profiles/Sonar_way/S9353 | 0 6 files changed, 269 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/java/checks/BareDotRegexpCheckSample.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/BareDotRegexpCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/BareDotRegexpCheckTest.java create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.json create mode 100644 sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9353 diff --git a/java-checks-test-sources/default/src/main/java/checks/BareDotRegexpCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/BareDotRegexpCheckSample.java new file mode 100644 index 00000000000..c436cb7e709 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/BareDotRegexpCheckSample.java @@ -0,0 +1,43 @@ +package checks; + +import java.util.regex.Pattern; + +class BareDotRegexpCheckSample { + + private static final String DOT = "."; + private static final String ESCAPED_DOT = "\\."; + + void noncompliant(String filename) { + filename.split("."); // Noncompliant {{This regex "." matches any character, not a literal dot; escape it as "\\." if a period was intended.}} +// ^^^ + filename.split(".", 2); // Noncompliant + filename.split(DOT); // Noncompliant + filename.replaceAll(".", "_"); // Noncompliant + filename.replaceFirst(".", "_"); // Noncompliant + filename.matches("."); // Noncompliant + Pattern.compile("."); // Noncompliant + Pattern.compile(DOT); // Noncompliant + Pattern.compile(".", Pattern.DOTALL); // Noncompliant + Pattern.matches(".", filename); // Noncompliant + } + + void compliant(String filename, String regex, int flags) { + filename.split("\\."); + filename.split(ESCAPED_DOT); + filename.split(Pattern.quote(".")); + filename.split("[.]"); + filename.split(".*"); + filename.split(regex); + filename.replace(".", "_"); + filename.replaceAll("\\.", "_"); + filename.replaceFirst("\\.", "_"); + filename.matches("\\."); + Pattern.compile("\\."); + Pattern.compile(".", Pattern.LITERAL); + Pattern.compile(".", Pattern.LITERAL | Pattern.CASE_INSENSITIVE); + Pattern.compile(regex); + Pattern.compile(".", flags); + Pattern.matches("\\.", filename); + } + +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/BareDotRegexpCheck.java b/java-checks/src/main/java/org/sonar/java/checks/BareDotRegexpCheck.java new file mode 100644 index 00000000000..7b9a9b3b8c0 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/BareDotRegexpCheck.java @@ -0,0 +1,91 @@ +/* + * 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.regex.Pattern; +import org.sonar.check.Rule; +import org.sonar.java.checks.methods.AbstractMethodDetection; +import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; + +@Rule(key = "S9353") +public class BareDotRegexpCheck extends AbstractMethodDetection { + + private static final String MESSAGE = "This regex \".\" matches any character, not a literal dot; escape it as \"\\\\.\" if a period was intended."; + private static final String JAVA_LANG_STRING = "java.lang.String"; + private static final String JAVA_UTIL_REGEX_PATTERN = "java.util.regex.Pattern"; + + private static final MethodMatchers REGEX_METHODS = MethodMatchers.or( + MethodMatchers.create() + .ofTypes(JAVA_LANG_STRING) + .names("split", "matches") + .addParametersMatcher(JAVA_LANG_STRING) + .build(), + MethodMatchers.create() + .ofTypes(JAVA_LANG_STRING) + .names("split") + .addParametersMatcher(JAVA_LANG_STRING, "int") + .build(), + MethodMatchers.create() + .ofTypes(JAVA_LANG_STRING) + .names("replaceAll", "replaceFirst") + .addParametersMatcher(JAVA_LANG_STRING, JAVA_LANG_STRING) + .build(), + MethodMatchers.create() + .ofTypes(JAVA_UTIL_REGEX_PATTERN) + .names("matches") + .addParametersMatcher(JAVA_LANG_STRING, "java.lang.CharSequence") + .build(), + MethodMatchers.create() + .ofTypes(JAVA_UTIL_REGEX_PATTERN) + .names("compile") + .addParametersMatcher(JAVA_LANG_STRING) + .build(), + MethodMatchers.create() + .ofTypes(JAVA_UTIL_REGEX_PATTERN) + .names("compile") + .addParametersMatcher(JAVA_LANG_STRING, "int") + .build()); + + @Override + protected MethodMatchers getMethodInvocationMatchers() { + return REGEX_METHODS; + } + + @Override + protected void onMethodInvocationFound(MethodInvocationTree mit) { + ExpressionTree regexArgument = mit.arguments().get(0); + if (isBareDot(regexArgument) && !isLiteralPattern(mit)) { + reportIssue(regexArgument, MESSAGE); + } + } + + private static boolean isBareDot(ExpressionTree regexArgument) { + return regexArgument.asConstant(String.class).filter("."::equals).isPresent(); + } + + private static boolean isLiteralPattern(MethodInvocationTree mit) { + if (mit.arguments().size() < 2 || !"compile".equals(mit.methodSymbol().name())) { + return false; + } + return mit.arguments().get(1).asConstant(Integer.class) + .map(flags -> (flags & Pattern.LITERAL) != 0) + .orElse(true); + } + +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/BareDotRegexpCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/BareDotRegexpCheckTest.java new file mode 100644 index 00000000000..400c6bed0f8 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/BareDotRegexpCheckTest.java @@ -0,0 +1,43 @@ +/* + * 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 BareDotRegexpCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/BareDotRegexpCheckSample.java")) + .withCheck(new BareDotRegexpCheck()) + .verifyIssues(); + } + + @Test + void test_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/BareDotRegexpCheckSample.java")) + .withCheck(new BareDotRegexpCheck()) + .withoutSemantic() + .verifyIssues(); + } + +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.html new file mode 100644 index 00000000000..037ce1a1ae4 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.html @@ -0,0 +1,69 @@ +

In Java regular expressions, . is a metacharacter that matches any single character, not a literal period.

+

Passing "." to regex APIs such as String.split, String.replaceAll, String.replaceFirst, +String.matches, Pattern.compile, or Pattern.matches therefore does not treat the argument as a dot separator. +Developers almost always intended a literal '.'.

+

Why is this an issue?

+

String.split and Pattern.compile interpret their pattern argument as a regular expression. A pattern that is exactly +"." matches every character.

+

This is especially surprising with String.split: "archive.tar.gz".split(".") matches each character as a delimiter. +Trailing empty strings are discarded, so the result is an empty array rather than ["archive", "tar", "gz"].

+

The same pattern is just as wrong with replacement APIs. "report.2026.pdf".replaceAll(".", "x") replaces every character with +x, instead of replacing only the dots.

+

To match a literal period, escape the metacharacter ("\\."), quote it (Pattern.quote(".")), or use a non-regex API such +as String.replace.

+

Exceptions

+

This rule does not raise an issue when Pattern.compile is called with the Pattern.LITERAL flag, because that flag treats +the pattern as a literal string.

+

Code examples

+

Noncompliant code example

+
+String filename = "archive.tar.gz";
+String[] parts = filename.split("."); // Noncompliant: matches every character instead of a literal '.'
+
+

Compliant solution

+
+String filename = "archive.tar.gz";
+String[] parts = filename.split("\\.");
+
+

Noncompliant code example

+
+String filename = "report.2026.pdf";
+String sanitized = filename.replaceAll(".", "_"); // Noncompliant: replaces every character
+String first = filename.replaceFirst(".", "_"); // Noncompliant
+
+

Compliant solution

+
+String filename = "report.2026.pdf";
+String sanitized = filename.replace(".", "_");
+String first = filename.replaceFirst("\\.", "_");
+
+

Noncompliant code example

+
+Pattern dots = Pattern.compile("."); // Noncompliant: compiles a match-any-character pattern
+boolean dotted = Pattern.matches(".", "a"); // Noncompliant
+boolean oneChar = "a".matches("."); // Noncompliant
+
+

Compliant solution

+
+Pattern dots = Pattern.compile("\\.");
+boolean dotted = Pattern.matches("\\.", "a");
+boolean oneChar = "a".matches("\\.");
+
+

Resources

+

Documentation

+ +

Related rules

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.json new file mode 100644 index 00000000000..a431c57a68d --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.json @@ -0,0 +1,23 @@ +{ + "title": "A bare \".\" should not be used as a regular expression", + "type": "BUG", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [ + "regex" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-9353", + "sqKey": "S9353", + "scope": "All", + "quickfix": "targeted", + "code": { + "impacts": { + "RELIABILITY": "MEDIUM" + }, + "attribute": "LOGICAL" + } +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9353 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9353 new file mode 100644 index 00000000000..e69de29bb2d From cc107922a4b52ebc3d425393ad70c8250a75903f Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 11:09:03 +0200 Subject: [PATCH 2/5] fix-ci: add S9353 ruling expected issues --- .../src/test/resources/regex-examples/java-S9353.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 its/ruling/src/test/resources/regex-examples/java-S9353.json diff --git a/its/ruling/src/test/resources/regex-examples/java-S9353.json b/its/ruling/src/test/resources/regex-examples/java-S9353.json new file mode 100644 index 00000000000..e446c2ebc1c --- /dev/null +++ b/its/ruling/src/test/resources/regex-examples/java-S9353.json @@ -0,0 +1,8 @@ +{ +"org.regex-examples:regex-examples:src/main/java/org/regex/examples/RegexDatabase2.java": [ +457 +], +"org.regex-examples:regex-examples:src/main/java/org/regex/examples/RegexDatabase8.java": [ +1129 +] +} From 755681ee11c7480534b6ca90904ea7acd1540d33 Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 11:12:42 +0200 Subject: [PATCH 3/5] Document explicit alternatives for intentional "." regexes --- .../java/checks/BareDotRegexpCheckSample.java | 4 +++ .../org/sonar/l10n/java/rules/java/S9353.html | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/java-checks-test-sources/default/src/main/java/checks/BareDotRegexpCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/BareDotRegexpCheckSample.java index c436cb7e709..e12e6a7e94e 100644 --- a/java-checks-test-sources/default/src/main/java/checks/BareDotRegexpCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/BareDotRegexpCheckSample.java @@ -38,6 +38,10 @@ void compliant(String filename, String regex, int flags) { Pattern.compile(regex); Pattern.compile(".", flags); Pattern.matches("\\.", filename); + filename.matches("[\\s\\S]"); + Pattern.compile("[\\s\\S]"); + boolean oneChar = filename.length() == 1; + String masked = "*".repeat(filename.length()); } } diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.html index 037ce1a1ae4..34cbaa61a2e 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.html +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.html @@ -14,6 +14,10 @@

Why is this an issue?

Exceptions

This rule does not raise an issue when Pattern.compile is called with the Pattern.LITERAL flag, because that flag treats the pattern as a literal string.

+

If matching any character is intentional, prefer a more explicit alternative: input.length() == 1 instead of +input.matches("."), "*".repeat(input.length()) instead of input.replaceAll(".", "*"), or the character class +"[\\s\\S]" when a regular expression is required. If you must keep ".", add an inline // NOSONAR: "." intentionally +matches any character comment so the intent is explicit.

Code examples

Noncompliant code example

@@ -49,6 +53,28 @@ 

Compliant solution

boolean dotted = Pattern.matches("\\.", "a"); boolean oneChar = "a".matches("\\.");
+

Noncompliant code example

+
+boolean oneChar = input.matches("."); // Noncompliant: "." matches any character, not a literal period
+String masked = secret.replaceAll(".", "*"); // Noncompliant
+Pattern anyChar = Pattern.compile("."); // Noncompliant
+
+

Compliant solution

+
+boolean oneChar = input.length() == 1;
+String masked = "*".repeat(secret.length());
+Pattern anyChar = Pattern.compile("[\\s\\S]");
+
+

Noncompliant code example

+
+Pattern anyChar = Pattern.compile("."); // Noncompliant
+
+

If keeping "." is intentional, add an inline // NOSONAR: "." intentionally matches any character comment so the intent is +explicit.

+

Compliant solution

+
+Pattern anyChar = Pattern.compile("."); // NOSONAR: "." intentionally matches any character
+

Resources

Documentation

    From 9ebd36bfbc7b1033a044a840899dd75ae7de9eec Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 13:46:59 +0200 Subject: [PATCH 4/5] Fix duplicate compile literal in S9353 check Use a shared method-name constant to satisfy S1192 without changing rule behavior. --- .../java/org/sonar/java/checks/BareDotRegexpCheck.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/BareDotRegexpCheck.java b/java-checks/src/main/java/org/sonar/java/checks/BareDotRegexpCheck.java index 7b9a9b3b8c0..58aedc4577e 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/BareDotRegexpCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/BareDotRegexpCheck.java @@ -29,6 +29,7 @@ public class BareDotRegexpCheck extends AbstractMethodDetection { private static final String MESSAGE = "This regex \".\" matches any character, not a literal dot; escape it as \"\\\\.\" if a period was intended."; private static final String JAVA_LANG_STRING = "java.lang.String"; private static final String JAVA_UTIL_REGEX_PATTERN = "java.util.regex.Pattern"; + private static final String COMPILE_METHOD_NAME = "compile"; private static final MethodMatchers REGEX_METHODS = MethodMatchers.or( MethodMatchers.create() @@ -53,12 +54,12 @@ public class BareDotRegexpCheck extends AbstractMethodDetection { .build(), MethodMatchers.create() .ofTypes(JAVA_UTIL_REGEX_PATTERN) - .names("compile") + .names(COMPILE_METHOD_NAME) .addParametersMatcher(JAVA_LANG_STRING) .build(), MethodMatchers.create() .ofTypes(JAVA_UTIL_REGEX_PATTERN) - .names("compile") + .names(COMPILE_METHOD_NAME) .addParametersMatcher(JAVA_LANG_STRING, "int") .build()); @@ -80,7 +81,7 @@ private static boolean isBareDot(ExpressionTree regexArgument) { } private static boolean isLiteralPattern(MethodInvocationTree mit) { - if (mit.arguments().size() < 2 || !"compile".equals(mit.methodSymbol().name())) { + if (mit.arguments().size() < 2 || !COMPILE_METHOD_NAME.equals(mit.methodSymbol().name())) { return false; } return mit.arguments().get(1).asConstant(Integer.class) From c46968ac1d23a44c199252e86d6b2603903f8b58 Mon Sep 17 00:00:00 2001 From: nathsou Date: Wed, 19 Aug 2026 15:47:45 +0200 Subject: [PATCH 5/5] Drop inexact any-character alternatives from S9353 docs Keep NOSONAR as the only documented suppression for intentional "." regexes. --- .../java/checks/BareDotRegexpCheckSample.java | 4 ---- .../org/sonar/l10n/java/rules/java/S9353.html | 20 ++----------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/BareDotRegexpCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/BareDotRegexpCheckSample.java index e12e6a7e94e..c436cb7e709 100644 --- a/java-checks-test-sources/default/src/main/java/checks/BareDotRegexpCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/BareDotRegexpCheckSample.java @@ -38,10 +38,6 @@ void compliant(String filename, String regex, int flags) { Pattern.compile(regex); Pattern.compile(".", flags); Pattern.matches("\\.", filename); - filename.matches("[\\s\\S]"); - Pattern.compile("[\\s\\S]"); - boolean oneChar = filename.length() == 1; - String masked = "*".repeat(filename.length()); } } diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.html index 34cbaa61a2e..5cd1143f448 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.html +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9353.html @@ -14,10 +14,8 @@

    Why is this an issue?

    Exceptions

    This rule does not raise an issue when Pattern.compile is called with the Pattern.LITERAL flag, because that flag treats the pattern as a literal string.

    -

    If matching any character is intentional, prefer a more explicit alternative: input.length() == 1 instead of -input.matches("."), "*".repeat(input.length()) instead of input.replaceAll(".", "*"), or the character class -"[\\s\\S]" when a regular expression is required. If you must keep ".", add an inline // NOSONAR: "." intentionally -matches any character comment so the intent is explicit.

    +

    If matching any character is intentional, add an inline // NOSONAR: "." intentionally matches any character comment so the intent is +explicit.

    Code examples

    Noncompliant code example

    @@ -55,24 +53,10 @@ 

    Compliant solution

    Noncompliant code example

    -boolean oneChar = input.matches("."); // Noncompliant: "." matches any character, not a literal period
    -String masked = secret.replaceAll(".", "*"); // Noncompliant
     Pattern anyChar = Pattern.compile("."); // Noncompliant
     

    Compliant solution

    -boolean oneChar = input.length() == 1;
    -String masked = "*".repeat(secret.length());
    -Pattern anyChar = Pattern.compile("[\\s\\S]");
    -
    -

    Noncompliant code example

    -
    -Pattern anyChar = Pattern.compile("."); // Noncompliant
    -
    -

    If keeping "." is intentional, add an inline // NOSONAR: "." intentionally matches any character comment so the intent is -explicit.

    -

    Compliant solution

    -
     Pattern anyChar = Pattern.compile("."); // NOSONAR: "." intentionally matches any character
     

    Resources