diff --git a/java-checks/src/main/java/org/sonar/java/checks/OctalValuesCheck.java b/java-checks/src/main/java/org/sonar/java/checks/OctalValuesCheck.java index de2d8458fb0..c1fe4846f10 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/OctalValuesCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/OctalValuesCheck.java @@ -16,6 +16,7 @@ */ package org.sonar.java.checks; +import java.util.regex.Pattern; import org.sonar.check.Rule; import org.sonar.plugins.java.api.JavaFileScanner; import org.sonar.plugins.java.api.JavaFileScannerContext; @@ -26,6 +27,15 @@ @Rule(key = "S1314") public class OctalValuesCheck extends BaseTreeVisitor implements JavaFileScanner { + /** + * Matches Unix file permission modes such as {@code 0644} or {@code 0755}: + * owner, group and others digits, where owner has at least read permission + * and each digit is one of the common values 0, 4, 5, 6 or 7. + */ + private static final Pattern PERMISSION_PATTERN = Pattern.compile("0[4-7][04-7]{2}"); + + private static final String EXPLICIT_PREFIX_MESSAGE = "If an octal value is intended, use the more explicit \"0_\" prefix."; + private JavaFileScannerContext context; @Override @@ -36,15 +46,48 @@ public void scanFile(final JavaFileScannerContext context) { @Override public void visitLiteral(LiteralTree tree) { - if (tree.is(Tree.Kind.INT_LITERAL) && isOctal(tree.value())) { - context.reportIssue(this, tree, "Use decimal values instead of octal ones."); + String value = tree.value(); + if (!tree.is(Tree.Kind.INT_LITERAL) || !isOctal(value) || value.length() <= 2 || hasExplicitOctalPrefix(value)) { + return; + } + if (isPermission(value)) { + String message = String.format( + "If this is a file permission mode, use the symbolic permission string \"%s\"; otherwise use the decimal value. %s", + toSymbolicPermissions(value), + EXPLICIT_PREFIX_MESSAGE); + context.reportIssue(this, tree, message); + } else { + context.reportIssue(this, tree, "Use decimal values instead of octal ones. " + EXPLICIT_PREFIX_MESSAGE); + } + } + + /** + * Converts an octal permission mode such as {@code 0755} into its symbolic representation such as {@code rwxr-xr-x}, + * as accepted by {@code java.nio.file.attribute.PosixFilePermissions.fromString}. + */ + private static String toSymbolicPermissions(String value) { + StringBuilder sb = new StringBuilder(); + for (char digit : value.substring(1).toCharArray()) { + int bits = digit - '0'; + sb.append((bits & 4) != 0 ? 'r' : '-') + .append((bits & 2) != 0 ? 'w' : '-') + .append((bits & 1) != 0 ? 'x' : '-'); } + return sb.toString(); } private static boolean isOctal(String value) { return value.charAt(0) == '0' && !"0".equals(value) && !isHexadecimal(value) && !isBinary(value); } + private static boolean hasExplicitOctalPrefix(String value) { + return value.startsWith("0_"); + } + + private static boolean isPermission(String value) { + return PERMISSION_PATTERN.matcher(value).matches(); + } + private static boolean isHexadecimal(String value) { return value.startsWith("0x") || value.startsWith("0X"); } diff --git a/java-checks/src/test/files/checks/OctalValuesCheck.java b/java-checks/src/test/files/checks/OctalValuesCheck.java deleted file mode 100644 index f96c08f5bdb..00000000000 --- a/java-checks/src/test/files/checks/OctalValuesCheck.java +++ /dev/null @@ -1,13 +0,0 @@ -class A { - int a = 0; - int b = 1; - int c = 42; - int d = 010; // Noncompliant {{Use decimal values instead of octal ones.}} -// ^^^ - int e = 00; // Noncompliant - int f = 0.; - int g = 0x00; - int h = 0X00; - int j = 0b0101; - int k = 0B0101; -} diff --git a/java-checks/src/test/files/checks/OctalValuesCheckSample.java b/java-checks/src/test/files/checks/OctalValuesCheckSample.java new file mode 100644 index 00000000000..a24a411323b --- /dev/null +++ b/java-checks/src/test/files/checks/OctalValuesCheckSample.java @@ -0,0 +1,51 @@ +class A { + int a = 0; + int b = 1; + int c = 42; + int d = 010; // Noncompliant {{Use decimal values instead of octal ones. If an octal value is intended, use the more explicit "0_" prefix.}} +// ^^^ + int e = 00; + int f = 0.; + int g = 0x00; + int h = 0X00; + int j = 0b0101; + int k = 0B0101; + int l = 01; + int m = 02; + int n = 07; + int o = 012; // Noncompliant + int p = 0123; // Noncompliant + int q = 0012; // Noncompliant + int r = 0311; // Noncompliant + int s = 01234; // Noncompliant + + int perm1 = 0400; // Noncompliant {{If this is a file permission mode, use the symbolic permission string "r--------"; otherwise use the decimal value. If an octal value is intended, use the more explicit "0_" prefix.}} +// ^^^^ + int perm2 = 0444; // Noncompliant {{If this is a file permission mode, use the symbolic permission string "r--r--r--"; otherwise use the decimal value. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm3 = 0600; // Noncompliant {{If this is a file permission mode, use the symbolic permission string "rw-------"; otherwise use the decimal value. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm4 = 0640; // Noncompliant {{If this is a file permission mode, use the symbolic permission string "rw-r-----"; otherwise use the decimal value. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm5 = 0644; // Noncompliant {{If this is a file permission mode, use the symbolic permission string "rw-r--r--"; otherwise use the decimal value. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm6 = 0660; // Noncompliant {{If this is a file permission mode, use the symbolic permission string "rw-rw----"; otherwise use the decimal value. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm7 = 0664; // Noncompliant {{If this is a file permission mode, use the symbolic permission string "rw-rw-r--"; otherwise use the decimal value. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm8 = 0666; // Noncompliant {{If this is a file permission mode, use the symbolic permission string "rw-rw-rw-"; otherwise use the decimal value. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm9 = 0700; // Noncompliant {{If this is a file permission mode, use the symbolic permission string "rwx------"; otherwise use the decimal value. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm10 = 0750; // Noncompliant {{If this is a file permission mode, use the symbolic permission string "rwxr-x---"; otherwise use the decimal value. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm11 = 0755; // Noncompliant {{If this is a file permission mode, use the symbolic permission string "rwxr-xr-x"; otherwise use the decimal value. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm12 = 0770; // Noncompliant {{If this is a file permission mode, use the symbolic permission string "rwxrwx---"; otherwise use the decimal value. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm13 = 0775; // Noncompliant {{If this is a file permission mode, use the symbolic permission string "rwxrwxr-x"; otherwise use the decimal value. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm14 = 0777; // Noncompliant {{If this is a file permission mode, use the symbolic permission string "rwxrwxrwx"; otherwise use the decimal value. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm15 = 01777; // Noncompliant {{Use decimal values instead of octal ones. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm16 = 02755; // Noncompliant {{Use decimal values instead of octal ones. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm17 = 04755; // Noncompliant {{Use decimal values instead of octal ones. If an octal value is intended, use the more explicit "0_" prefix.}} + int perm18 = 0_755; + int perm19 = 0_644; + + int explicit1 = 0_7; + int explicit2 = 0_13; + int explicit3 = 0_123; + int explicit4 = 0_311; + int explicit5 = 0_1234; + int explicit6 = 0_12_34; + int underscore1 = 01_234; // Noncompliant + int underscore2 = 00_13; // Noncompliant +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/OctalValuesCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/OctalValuesCheckTest.java index 0e07be63abf..9aa16168091 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/OctalValuesCheckTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/OctalValuesCheckTest.java @@ -24,7 +24,7 @@ class OctalValuesCheckTest { @Test void test() { CheckVerifier.newVerifier() - .onFile("src/test/files/checks/OctalValuesCheck.java") + .onFile("src/test/files/checks/OctalValuesCheckSample.java") .withCheck(new OctalValuesCheck()) .verifyIssues(); } @@ -32,7 +32,7 @@ void test() { @Test void test_without_semantic() { CheckVerifier.newVerifier() - .onFile("src/test/files/checks/OctalValuesCheck.java") + .onFile("src/test/files/checks/OctalValuesCheckSample.java") .withCheck(new OctalValuesCheck()) .withoutSemantic() .verifyIssues(); diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S1314.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S1314.html index adf30b1fdf0..d306ada71f2 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S1314.html +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S1314.html @@ -1,15 +1,80 @@ +
This rule raises an issue when an integer literal starts with a zero followed by other digits, such as 010 or 060. Java
+interprets such a literal as a base-8 octal value rather than a base-10 decimal value.
Integer literals starting with a zero are octal rather than decimal values. While using octal values is fully supported, most developers do not -have experience with them. They may not recognize octal values as such, mistaking them instead for decimal values.
-In Java, integer literals that begin with a leading zero are interpreted as octal numbers, not decimal numbers. This is a legacy feature inherited +from C.
+For example:
+010 represents 8 in decimal, computed as 1×8 + 0060 represents 48 in decimal, computed as 6×8 + 001000 represents 512 in decimal, computed as 1×512 + 0×64 + 0×8 + 0This syntax is counterintuitive: most developers expect a literal such as 060 to mean sixty, with the leading zero being mere padding.
+There is no visual cue that octal interpretation is happening, and code reviewers can easily miss the leading zero.
Octal literals are rarely needed in modern code. When a value is better expressed in another base, Java provides explicit prefixes: 0x
+for hexadecimal and 0b for binary.
A common legitimate use of octal literals is Unix file permission modes, such as 0644 or 0755. Even there, the symbolic
+notation accepted by java.nio.file.attribute.PosixFilePermissions.fromString, such as "rw-r—r--" or
+"rwxr-xr-x", is easier to read and harder to get wrong.
For an integer literal whose complete source text matches 0[4-7][04-7]{2}, the rule message suggests the equivalent symbolic
+permission string. This means exactly four characters: a leading zero, an owner digit from 4 to 7, and group and others digits each equal to 0, 4, 5,
+6 or 7. Literals such as 04755, 0311 or 06_44 do not match this criterion and receive the general octal message
+instead. Permission-mode literals are still reported, not exempted. The dedicated message also mentions the decimal value as an alternative, since
+such a literal may be a zero-padded decimal like a time of day 0700.
This rule does not raise an issue for:
+00 or 07. Their octal and decimal values are identical, so there is no
+ risk of confusion.0_ prefix, such as 0_7, 0_13 or 0_755, which makes the
+ intent explicit.-int myNumber = 010; // Noncompliant. myNumber will hold 8, not 10 - was this really expected? +int zero = 00; // Compliant: same value in octal and decimal +int seven = 07; // Compliant: same value in octal and decimal +int explicitOctal = 0_13; // Compliant: explicit octal intent +int mode = 0_755; // Compliant: explicit octal intent ++
Using an octal literal when a decimal value is intended leads to incorrect calculations and logic errors. For example, setting a timeout to
+060 seconds results in a 48-second timeout instead of the expected 60 seconds.
Such bugs are difficult to diagnose because the code compiles without any warning, and the discrepancy between the expected and actual values may +only show up under specific conditions.
+Use the decimal value instead. For file permission modes, prefer the symbolic permission string. If an octal value is really intended, use the
+0_ prefix, which makes it clear to readers that the literal is octal.
+int myNumber = 010; // Noncompliant: myNumber holds 8, not 10 +int timeout = 060; // Noncompliant: timeout holds 48, not 60 +int count = 01000; // Noncompliant: count holds 512, not 1000-
+int myNumber = 10; +int timeout = 60; +int count = 1000; ++
+int mode = 0640; // Noncompliant: if this is a permission mode, use "rw-r-----" instead ++
+Set<PosixFilePermission> mode = PosixFilePermissions.fromString("rw-r-----");
+
+Or, if an octal value is intended, such as a file permission mode passed to a native API:
-int myNumber = 8; +int mode = 0_640;