diff --git a/java-checks-test-sources/default/src/main/java/checks/OctalEscapeSequenceFollowedByDigitCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/OctalEscapeSequenceFollowedByDigitCheckSample.java new file mode 100644 index 00000000000..c577c329108 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/OctalEscapeSequenceFollowedByDigitCheckSample.java @@ -0,0 +1,65 @@ +package checks; + +class OctalEscapeSequenceFollowedByDigitCheckSample { + void testNoncompliant() { + String s1 = "\128"; // Noncompliant {{Remove this octal escape sequence or separate it from the following digit.}} + String s2 = "\09"; // Noncompliant + String s3 = "\7778"; // Noncompliant + String s4 = "\1234"; // Noncompliant + String s5 = "\789"; // Noncompliant + String s6 = "\0000"; // Noncompliant + String s7 = "\7777"; // Noncompliant + String s8 = "a\128b"; // Noncompliant + String s9 = "\12\3456"; // Noncompliant + String s10 = "\456"; // Noncompliant + } + + void testCompliant() { + String s1 = "\12"; // Compliant + String s2 = "\12a"; // Compliant + String s3 = "\\128"; // Compliant + String s4 = "128"; // Compliant + String s5 = "\u0041"; // Compliant + String s6 = "\n"; // Compliant + String s7 = "\\08"; // Compliant + String s8 = "\12" + "8"; // Compliant + String s9 = "\1\2"; // Compliant + String s10 = ""; // Compliant - empty string + String s11 = "\377"; // Compliant - max octal at end of string + String s12 = "\377a"; // Compliant - max octal followed by non-digit + String s13 = "\t9"; // Compliant - non-octal escape followed by digit + String s14 = "\n0"; // Compliant - non-octal escape followed by digit + String s15 = "\\\\8"; // Compliant - double escaped backslash followed by digit + String s16 = "\1"; // Compliant - single octal at end + String s17 = "abc"; // Compliant - no escapes + String s18 = "\45"; // Compliant + String s19 = "\45a"; // Compliant + } + + void testNoncompliantTextBlock() { + String tb1 = """ + \128"""; // Noncompliant@-1 + String tb2 = """ + \09"""; // Noncompliant@-1 + } + + void testCompliantTextBlock() { + String tb1 = """ + \12"""; + String tb2 = """ + \12a"""; + String tb3 = """ + \\128"""; + String tb4 = """ + \n0"""; + } + + void testCharacterLiteral() { + char c1 = '\12'; // Compliant + char c2 = '\1'; // Compliant + } + + void testNoncompliantMaxOctal() { + String s1 = "\3778"; // Noncompliant + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/OctalEscapeSequenceFollowedByDigitCheck.java b/java-checks/src/main/java/org/sonar/java/checks/OctalEscapeSequenceFollowedByDigitCheck.java new file mode 100644 index 00000000000..86dd04d7d16 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/OctalEscapeSequenceFollowedByDigitCheck.java @@ -0,0 +1,96 @@ +/* + * 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.Arrays; +import java.util.List; +import org.sonar.check.Rule; +import org.sonar.java.model.LiteralUtils; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.tree.LiteralTree; +import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.Tree.Kind; + +@Rule(key = "S9359") +public class OctalEscapeSequenceFollowedByDigitCheck extends IssuableSubscriptionVisitor { + + @Override + public List nodesToVisit() { + return Arrays.asList(Kind.STRING_LITERAL, Kind.TEXT_BLOCK); + } + + @Override + public void visitNode(Tree node) { + if (LiteralUtils.isEmptyString(node)) { + return; + } + String value = LiteralUtils.trimQuotes(((LiteralTree) node).value()); + if (node.is(Kind.TEXT_BLOCK)) { + value = value.replaceAll("(\\r?\\n|\\r)\\s*", " "); + } + if (containsOctalFollowedByDigit(value)) { + reportIssue(node, "Remove this octal escape sequence or separate it from the following digit."); + } + } + + private static boolean containsOctalFollowedByDigit(String value) { + int i = 0; + while (i < value.length()) { + if (value.charAt(i) != '\\') { + i++; + } else if (i + 1 < value.length() && value.charAt(i + 1) == '\\') { + i += 2; + } else { + i = processBackslash(value, i); + if (i < 0) { + return true; + } + } + } + return false; + } + + private static int processBackslash(String value, int i) { + if (i + 1 < value.length() && isOctalDigit(value.charAt(i + 1))) { + int escapeEnd = findEscapeEnd(value, i); + if (escapeEnd < value.length() && isAmbiguousFollowUp(value.charAt(escapeEnd))) { + return -1; + } + return escapeEnd; + } + return i + 1; + } + + private static boolean isOctalDigit(char c) { + return c >= '0' && c <= '7'; + } + + private static int findEscapeEnd(String value, int start) { + int escapeEnd = start + 2; + int maxEnd = (value.charAt(start + 1) <= '3') ? (start + 4) : (start + 3); + while (escapeEnd < value.length() + && isOctalDigit(value.charAt(escapeEnd)) + && escapeEnd < maxEnd) { + escapeEnd++; + } + return escapeEnd; + } + + private static boolean isAmbiguousFollowUp(char c) { + return c >= '0' && c <= '9'; + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/OctalEscapeSequenceFollowedByDigitCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/OctalEscapeSequenceFollowedByDigitCheckTest.java new file mode 100644 index 00000000000..1cc1a261c89 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/OctalEscapeSequenceFollowedByDigitCheckTest.java @@ -0,0 +1,42 @@ +/* + * 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 OctalEscapeSequenceFollowedByDigitCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/OctalEscapeSequenceFollowedByDigitCheckSample.java")) + .withCheck(new OctalEscapeSequenceFollowedByDigitCheck()) + .verifyIssues(); + } + + @Test + void test_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/OctalEscapeSequenceFollowedByDigitCheckSample.java")) + .withCheck(new OctalEscapeSequenceFollowedByDigitCheck()) + .withoutSemantic() + .verifyIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9359.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9359.html new file mode 100644 index 00000000000..41e6b730168 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9359.html @@ -0,0 +1,57 @@ +

This is an issue when an octal escape sequence in a string literal is immediately followed by another digit, creating ambiguity about which digits +are part of the escape sequence.

+

Why is this an issue?

+

Some programming languages support numeric escape sequences in string and character literals where the escape sequence consists of a backslash +followed by a variable number of digits in a specific number base (such as octal or other bases). For example, an escape sequence might consist of a +backslash followed by one to three digits representing a character code.

+

The problem arises when such a variable-length escape sequence is immediately followed by another digit that could be part of the sequence. +Consider a string containing a backslash, followed by digits that could be interpreted as either a single long escape sequence or a shorter escape +sequence followed by a literal digit character. This creates confusion because:

+ +

This ambiguity can lead to bugs where the string contains different characters than the developer intended. The issue is particularly problematic +because:

+ +

In Java, these are octal escape sequences consisting of a backslash followed by one to three octal digits (0-7). For example, "\12" +represents a line feed character (decimal 10), and "\128" is actually the octal escape \12 (line feed) followed by the +literal character 8.

+

What is the potential impact?

+

This issue affects code maintainability and can lead to subtle bugs. When developers misunderstand what characters are in a string, it can +cause:

+ +

While the impact is typically low severity, it can waste significant debugging time when the actual characters in a string don’t match +expectations.

+

How to fix it

+

Replace the octal escape sequence with the equivalent standard escape sequence or separate the escape from the trailing digit to remove ambiguity. +For characters that have a named escape (such as \n for newline or \t for tab), prefer using that form. Alternatively, split +the string so the escape sequence and the following literal digit are in separate concatenated strings.

+

Code examples

+

Noncompliant code example

+
+String message = "Error code: \128"; // Noncompliant
+
+

Compliant solution

+
+String message = "Error code: \n" + "8";
+
+

Resources

+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9359.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9359.json new file mode 100644 index 00000000000..c895943891a --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9359.json @@ -0,0 +1,24 @@ +{ + "title": "Octal escape sequences should not be followed by digits", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5 min" + }, + "tags": [ + "pitfall", + "confusing" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-9359", + "sqKey": "S9359", + "scope": "All", + "quickfix": "unknown", + "code": { + "impacts": { + "MAINTAINABILITY": "MEDIUM" + }, + "attribute": "CLEAR" + } +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9359 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9359 new file mode 100644 index 00000000000..e69de29bb2d