Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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<Tree.Kind> 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*", " ");
}
Comment thread
gitar-bot[bot] marked this conversation as resolved.
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;
}
Comment thread
gitar-bot[bot] marked this conversation as resolved.

private static boolean isAmbiguousFollowUp(char c) {
return c >= '0' && c <= '9';
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<p>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.</p>
<h2>Why is this an issue?</h2>
<p>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.</p>
<p>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:</p>
<ul>
<li>The boundary between the escape sequence and the literal digit is not visually clear</li>
<li>Developers unfamiliar with the specific numeric notation may misinterpret the intended character</li>
<li>These numeric escape sequences are rarely used in modern code, making them unexpected</li>
</ul>
<p>This ambiguity can lead to bugs where the string contains different characters than the developer intended. The issue is particularly problematic
because:</p>
<ul>
<li>Variable-length escapes can use different numbers of digits, so the parsing rules are complex</li>
<li>Once an escape sequence encounters a digit outside its valid range, it terminates, but this isn’t obvious from the code</li>
<li>Modern developers rarely think in alternative number bases, so these sequences are hard to read and verify</li>
</ul>
<p>In Java, these are octal escape sequences consisting of a backslash followed by one to three octal digits (0-7). For example, <code>"\12"</code>
represents a line feed character (decimal 10), and <code>"\128"</code> is actually the octal escape <code>\12</code> (line feed) followed by the
literal character <code>8</code>.</p>
<h3>What is the potential impact?</h3>
<p>This issue affects code maintainability and can lead to subtle bugs. When developers misunderstand what characters are in a string, it can
cause:</p>
<ul>
<li>Incorrect string comparisons or pattern matching</li>
<li>Unexpected behavior when the string is displayed or processed</li>
<li>Difficulty debugging because the string’s actual content differs from its apparent content</li>
<li>Confusion for code reviewers and future maintainers</li>
</ul>
<p>While the impact is typically low severity, it can waste significant debugging time when the actual characters in a string don’t match
expectations.</p>
<h2>How to fix it</h2>
<p>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 <code>\n</code> for newline or <code>\t</code> for tab), prefer using that form. Alternatively, split
the string so the escape sequence and the following literal digit are in separate concatenated strings.</p>
<h3>Code examples</h3>
<h4>Noncompliant code example</h4>
<pre data-diff-id="1" data-diff-type="noncompliant">
String message = "Error code: \128"; // Noncompliant
</pre>
<h4>Compliant solution</h4>
<pre data-diff-id="1" data-diff-type="compliant">
String message = "Error code: \n" + "8";

@nathsou nathsou Aug 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I find that the concatenation adds noise without improving clarity, but if you prefer the current version, feel free to merge of course.

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

</pre>
<h2>Resources</h2>
<h3>Documentation</h3>
<ul>
<li>Oracle Java Documentation - <a href="https://docs.oracle.com/javase/tutorial/java/data/characters.html">Escape Sequences for Character and
String Literals</a></li>
<li>Java Language Specification - <a href="https://docs.oracle.com/javase/specs/jls/se17/html/jls-3.html#jls-3.10.6">3.10.6. Escape Sequences for
Character and String Literals</a></li>
</ul>

Original file line number Diff line number Diff line change
@@ -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"
}
}
Empty file.
Loading