-
Notifications
You must be signed in to change notification settings - Fork 724
SONARJAVA-6826: Implemented rule S9359 (Octal escape sequences should not be followed by digits) #6008
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
SONARJAVA-6826: Implemented rule S9359 (Octal escape sequences should not be followed by digits) #6008
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
8f1cc61
SONARJAVA-6826: Implemented rule S9359 (Octal escape sequences should…
romainbrenguier 3165e2b
SONARJAVA-6826: Fix S9359 false positives and CI failures
romainbrenguier b630e14
SONARJAVA-6826: Refactor S9359 to fix SonarQube quality gate
romainbrenguier 80fffa9
SONARJAVA-6826: Fix S9359 false negative for octal escapes starting w…
romainbrenguier f984604
SONARJAVA-6826: Add explicit parentheses to fix S864 quality gate issue
romainbrenguier 16d87f5
SONARJAVA-6826: Update S9359 rule metadata and documentation
romainbrenguier File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
65 changes: 65 additions & 0 deletions
65
...t-sources/default/src/main/java/checks/OctalEscapeSequenceFollowedByDigitCheckSample.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
| } |
96 changes: 96 additions & 0 deletions
96
java-checks/src/main/java/org/sonar/java/checks/OctalEscapeSequenceFollowedByDigitCheck.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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*", " "); | ||
| } | ||
| 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; | ||
| } | ||
|
gitar-bot[bot] marked this conversation as resolved.
|
||
|
|
||
| private static boolean isAmbiguousFollowUp(char c) { | ||
| return c >= '0' && c <= '9'; | ||
| } | ||
| } | ||
42 changes: 42 additions & 0 deletions
42
...ecks/src/test/java/org/sonar/java/checks/OctalEscapeSequenceFollowedByDigitCheckTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| } | ||
| } |
57 changes: 57 additions & 0 deletions
57
sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9359.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||
| </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> | ||||||
|
|
||||||
24 changes: 24 additions & 0 deletions
24
sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9359.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.