From 637495468b9f378c436c1ce430389cab51f1b09c Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 28 Sep 2026 10:28:59 +0200 Subject: [PATCH 1/8] SONARJAVA-7070: Implemented rule S9413 Redundant "String.format" should be removed when native formatting is available Reports String.format calls passed directly as the message argument of APIs with native formatting: SLF4J/Log4j 2 loggers, PrintStream/PrintWriter print/println, StringBuilder/StringBuffer append, and Throwable constructors. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...dundantStringFormatCheckLoggingSample.java | 46 +++++ .../RedundantStringFormatCheckSample.java | 86 ++++++++ .../checks/RedundantStringFormatCheck.java | 147 +++++++++++++ .../RedundantStringFormatCheckTest.java | 59 ++++++ .../org/sonar/l10n/java/rules/java/S9413.html | 194 ++++++++++++++++++ .../org/sonar/l10n/java/rules/java/S9413.json | 25 +++ .../main/resources/profiles/Sonar_way/S9413 | 0 7 files changed, 557 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckLoggingSample.java create mode 100644 java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckSample.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/RedundantStringFormatCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/RedundantStringFormatCheckTest.java create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.json create mode 100644 sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9413 diff --git a/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckLoggingSample.java b/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckLoggingSample.java new file mode 100644 index 00000000000..580887a1cad --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckLoggingSample.java @@ -0,0 +1,46 @@ +package checks; + +import java.util.Locale; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.Marker; + +class RedundantStringFormatCheckLoggingSample { + + private static final Logger LOG = LoggerFactory.getLogger(RedundantStringFormatCheckLoggingSample.class); + private static final org.apache.logging.log4j.Logger LOG4J = LogManager.getLogger(); + private static final java.util.logging.Logger JUL = java.util.logging.Logger.getLogger("jul"); + private static final String FORMAT = "User %s"; + + void slf4j(String username, String role, String id, Exception e, Marker marker, int count, double ratio) { + LOG.info(String.format("Activated user '%s' with role '%s'", username, role)); // Noncompliant {{Use the logger's built-in "{}" formatting instead of "String.format".}} +// ^^^^^^^^^^^^^ + LOG.error(String.format("Failed to process %s", id), e); // Noncompliant + LOG.warn(marker, String.format("Retry count %d", count)); // Noncompliant + LOG.debug(String.format("Progress 100%% for %s", id)); // Noncompliant + + LOG.info("Activated user '{}' with role '{}'", username, role); + LOG.info("Ratio: {}", String.format("%.2f", ratio)); + LOG.info(String.format("Ratio %.2f", ratio)); + LOG.info(String.format(Locale.FRANCE, "User %s", username)); + LOG.info(String.format(FORMAT, username)); + LOG.info(String.format("User %s", username).trim()); + String message = String.format("User %s", username); + LOG.info(message); + } + + void log4j(String id, int count) { + LOG4J.debug(String.format("Loaded %s", id)); // Noncompliant + LOG4J.fatal(String.format("Crashed after %d attempts", count)); // Noncompliant + LOG4J.log(Level.INFO, String.format("Loaded %s", id)); // Noncompliant + + LOG4J.debug("Loaded {}", id); + LOG4J.info(String.format("Id %x", count)); + } + + void jul(String id) { + JUL.info(String.format("Loaded %s", id)); + } +} diff --git a/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckSample.java new file mode 100644 index 00000000000..01b87754a2e --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckSample.java @@ -0,0 +1,86 @@ +package checks; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +class RedundantStringFormatCheckSample { + + private static final String ROW_FORMAT = "%s: %d"; + + void printing(int a, int b, int sum, double ratio, long count, PrintWriter writer, int pageNum, String title) { + System.out.println(String.format("Result: %d + %d = %d", a, b, sum)); // Noncompliant {{Use "printf" instead of "String.format".}} +// ^^^^^^^^^^^^^ + System.err.print(String.format("%.2f", ratio)); // Noncompliant + System.out.println(String.format(Locale.US, "%,d", count)); // Noncompliant + writer.println(String.format("Page %d: %s", pageNum, title)); // Noncompliant + writer.print(String.format(ROW_FORMAT, title, pageNum)); // Noncompliant + System.out.println((String.format("Total: %d", sum))); // Noncompliant + + System.out.printf("Result: %d + %d = %d%n", a, b, sum); + System.out.println("Result: " + sum); + System.out.println(String.format("Result: %d", sum).trim()); + System.out.println("Total " + String.format("%.2f", ratio)); + } + + String json(String name, int age, int hash) { + StringBuilder sb = new StringBuilder("{"); + sb.append(String.format("\"name\":\"%s\",", name)); // Noncompliant {{Use chained "append" calls instead of "String.format".}} +// ^^^^^^^^^^^^^ + sb.append(String.format("\"age\":%d", age)).append("}"); // Noncompliant + StringBuffer buffer = new StringBuffer(); + buffer.append(String.format("Progress 100%% for %s", name)); // Noncompliant + + sb.append("\"age\":").append(age); + sb.append(String.format("%08X", hash)); + sb.append(String.format("%.2f", age / 3.0)); + sb.append(String.format(Locale.ROOT, "%d", age)); + sb.append(String.format(ROW_FORMAT, name, age)); + sb.append(String.format("%n")); + sb.append(String.format("%1$s-%1$s", name)); + sb.append(String.format("100%", name)); + sb.append(String.format(""" + %s""", name)); + return sb.toString(); + } + + void validate(int age, String name, Exception cause) throws ValidationException { + if (age < 0) { + throw new IllegalArgumentException(String.format("Age cannot be negative: %d", age)); // Noncompliant {{Use string concatenation instead of "String.format".}} +// ^^^^^^^^^^^^^ + } + if (name == null) { + throw new RuntimeException(String.format("Missing name for age %d", age), cause); // Noncompliant + } + if (name.isEmpty()) { + throw new ValidationException(String.format("Empty name %s", name)); // Noncompliant + } + if (name.length() > 10) { + throw new IllegalStateException(String.format("%-10s|", name)); + } + if (age > 150) { + throw new IllegalArgumentException("Age too high: " + age); + } + } + + List unrelatedTargets(String name, int age) { + List lines = new ArrayList<>(); + lines.add(String.format("name=%s", name)); + Object[] values = new Object[]{String.format("age=%d", age)}; + String message = String.format("name=%s", name); + System.out.println(message); + new Wrapper(String.format("name=%s", name)); + lines.add("name=%s".formatted(name)); + return lines; + } + + static class ValidationException extends Exception { + ValidationException(String message) { + super(String.format("Validation failed: %s", message)); + } + } + + record Wrapper(String value) { + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/RedundantStringFormatCheck.java b/java-checks/src/main/java/org/sonar/java/checks/RedundantStringFormatCheck.java new file mode 100644 index 00000000000..32380e71728 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/RedundantStringFormatCheck.java @@ -0,0 +1,147 @@ +/* + * 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.List; +import javax.annotation.Nullable; +import org.sonar.check.Rule; +import org.sonar.java.checks.helpers.LoggingMatchers; +import org.sonar.java.model.LiteralUtils; +import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; +import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.semantic.Symbol; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.LiteralTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.NewClassTree; +import org.sonar.plugins.java.api.tree.Tree; + +@Rule(key = "S9413") +public class RedundantStringFormatCheck extends IssuableSubscriptionVisitor { + + private static final String STRING = "java.lang.String"; + + private static final MethodMatchers STRING_FORMAT = MethodMatchers.create() + .ofTypes(STRING) + .names("format") + .withAnyParameters() + .build(); + + private static final MethodMatchers PRINT_METHODS = MethodMatchers.create() + .ofSubTypes("java.io.PrintStream", "java.io.PrintWriter") + .names("print", "println") + .addParametersMatcher(STRING) + .build(); + + private static final MethodMatchers APPEND_METHODS = MethodMatchers.create() + .ofTypes("java.lang.StringBuilder", "java.lang.StringBuffer") + .names("append") + .addParametersMatcher(STRING) + .build(); + + private static final MethodMatchers LOG_METHODS = MethodMatchers.or(LoggingMatchers.SLF4J_LOG_METHODS, LoggingMatchers.LOG4J_LOG_METHODS); + + private static final String PRINTF_MESSAGE = "Use \"printf\" instead of \"String.format\"."; + private static final String LOGGING_MESSAGE = "Use the logger's built-in \"{}\" formatting instead of \"String.format\"."; + private static final String APPEND_MESSAGE = "Use chained \"append\" calls instead of \"String.format\"."; + private static final String EXCEPTION_MESSAGE = "Use string concatenation instead of \"String.format\"."; + + @Override + public List nodesToVisit() { + return List.of(Tree.Kind.METHOD_INVOCATION); + } + + @Override + public void visitNode(Tree tree) { + MethodInvocationTree formatCall = (MethodInvocationTree) tree; + if (!STRING_FORMAT.matches(formatCall)) { + return; + } + Tree argument = formatCall; + while (argument.parent().is(Tree.Kind.PARENTHESIZED_EXPRESSION)) { + argument = argument.parent(); + } + Tree arguments = argument.parent(); + if (!arguments.is(Tree.Kind.ARGUMENTS)) { + return; + } + String message = targetMessage(arguments.parent(), argument); + if (message != null && (PRINTF_MESSAGE.equals(message) || hasSimpleLiteralFormat(formatCall))) { + reportIssue(formatCall.methodSelect(), message); + } + } + + @Nullable + private static String targetMessage(Tree call, Tree argument) { + if (call instanceof MethodInvocationTree mit) { + if (!isMessageArgument(mit.methodSymbol(), mit.arguments().indexOf(argument))) { + return null; + } + if (PRINT_METHODS.matches(mit)) { + return PRINTF_MESSAGE; + } + if (LOG_METHODS.matches(mit)) { + return LOGGING_MESSAGE; + } + if (APPEND_METHODS.matches(mit)) { + return APPEND_MESSAGE; + } + } else if (call instanceof NewClassTree newClass + && newClass.symbolType().isSubtypeOf("java.lang.Throwable") + && isMessageArgument(newClass.methodSymbol(), newClass.arguments().indexOf(argument))) { + return EXCEPTION_MESSAGE; + } + return null; + } + + /** + * An argument is the message argument when its declared parameter is the first parameter of type {@code String}. + */ + private static boolean isMessageArgument(Symbol.MethodSymbol symbol, int index) { + if (symbol.isUnknown()) { + return false; + } + List parameterTypes = symbol.parameterTypes(); + if (index < 0 || index >= parameterTypes.size() || !parameterTypes.get(index).is(STRING)) { + return false; + } + return parameterTypes.subList(0, index).stream().noneMatch(type -> type.is(STRING)); + } + + private static boolean hasSimpleLiteralFormat(MethodInvocationTree formatCall) { + List arguments = formatCall.arguments(); + if (arguments.isEmpty() || !arguments.get(0).is(Tree.Kind.STRING_LITERAL)) { + return false; + } + return hasOnlySimpleConversions(LiteralUtils.trimQuotes(((LiteralTree) arguments.get(0)).value())); + } + + /** + * Returns true when every {@code %} sequence of the format string is {@code %s}, {@code %d} or {@code %%}. + */ + private static boolean hasOnlySimpleConversions(String format) { + int index = format.indexOf('%'); + while (index >= 0) { + if (index + 1 >= format.length() || "sd%".indexOf(format.charAt(index + 1)) < 0) { + return false; + } + index = format.indexOf('%', index + 2); + } + return true; + } +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/RedundantStringFormatCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/RedundantStringFormatCheckTest.java new file mode 100644 index 00000000000..a8bc55f5e7d --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/RedundantStringFormatCheckTest.java @@ -0,0 +1,59 @@ +/* + * 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 RedundantStringFormatCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/RedundantStringFormatCheckSample.java")) + .withCheck(new RedundantStringFormatCheck()) + .verifyIssues(); + } + + @Test + void test_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/RedundantStringFormatCheckSample.java")) + .withCheck(new RedundantStringFormatCheck()) + .withoutSemantic() + .verifyIssues(); + } + + @Test + void test_logging() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/RedundantStringFormatCheckLoggingSample.java")) + .withCheck(new RedundantStringFormatCheck()) + .verifyIssues(); + } + + @Test + void test_logging_without_semantic() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/RedundantStringFormatCheckLoggingSample.java")) + .withCheck(new RedundantStringFormatCheck()) + .withoutSemantic() + .verifyNoIssues(); + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html new file mode 100644 index 00000000000..a2188a1c779 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html @@ -0,0 +1,194 @@ +

An issue is raised when string formatting functions that substitute placeholders with values are used as arguments to methods that already support +formatting or parameterized messages natively.

+

In Java, this specifically refers to String.format.

+

Why is this an issue?

+

Many APIs provide built-in formatting capabilities. When you wrap generic string formatting functions around these APIs, you’re adding an +unnecessary layer that hurts both performance and readability.

+

Performance concerns

+

Generic string formatting functions are typically eagerly evaluated. This means the formatted string is created immediately, even if it’s never +used. This is particularly problematic in logging scenarios:

+
+LOG.debug(format("Processing %d items", items.size())); // Always formats, even if DEBUG is disabled
+
+

When the debug level is disabled, the application still pays the cost of:

+
    +
  • Creating the format string
  • +
  • Boxing primitive values into objects
  • +
  • Parsing the format specifiers
  • +
  • Building the final string
  • +
+

Logging frameworks provide parameterized logging that defers string construction until the message is actually needed:

+
+LOG.debug("Processing {} items", items.size()); // Only formats if DEBUG is enabled
+
+

Readability and idioms

+

Each API has its own idiomatic way of formatting. Using generic string formatting makes the code less recognizable to developers familiar with +these frameworks:

+
    +
  • Logging frameworks use their own placeholder syntax
  • +
  • Output stream APIs have their own formatting methods
  • +
  • String builder APIs are designed for efficient concatenation
  • +
+

Using the native approach makes your intent clearer and your code more maintainable.

+

When generic formatting defeats the purpose

+

Some APIs are specifically designed for performance. Using generic string formatting with them undermines their benefits:

+
+StringBuilder sb = new StringBuilder();
+sb.append(format("User: %s, Age: %d", name, age)); // Creates intermediate String object
+
+

The string builder API is meant to avoid creating intermediate string objects, but generic formatting functions create one anyway, negating the +performance benefit.

+

In Java, this specifically refers to String.format(). Common APIs with their own formatting include SLF4J (using {} +placeholders), PrintStream and PrintWriter (with printf methods), and StringBuilder (designed for +efficient string concatenation).

+

What is the potential impact?

+

Using redundant manual string formatting can lead to:

+
    +
  • Performance degradation: In high-throughput applications, especially those with extensive logging, the unnecessary string + formatting can waste significant CPU cycles and create memory pressure from temporary objects.
  • +
  • Maintainability issues: Code that doesn’t follow framework conventions is harder for other developers to understand and + maintain, increasing the likelihood of bugs during refactoring.
  • +
  • Inconsistent codebase: Mixing manual string formatting with native API calls creates inconsistency, making the code harder to + review and understand.
  • +
+

In Java, this specifically refers to redundant String.format calls.

+

How to fix it in SLF4J

+

SLF4J and similar logging frameworks (Log4j2, Logback) support parameterized logging with {} placeholders. This approach defers string +construction until the log level is enabled.

+

Code examples

+

Noncompliant code example

+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class UserService {
+  private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
+
+  void activateUser(String username, String role) {
+    LOG.info(String.format("Activated user '%s' with role '%s'", username, role)); // Noncompliant
+  }
+}
+
+

Compliant solution

+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class UserService {
+  private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
+
+  void activateUser(String username, String role) {
+    LOG.info("Activated user '{}' with role '{}'", username, role);
+  }
+}
+
+

How to fix it in System.out/System.err

+

For console output, use printf instead of println with String.format. The printf method +supports the same format specifiers as String.format.

+

Code examples

+

Noncompliant code example

+
+class Calculator {
+  void displayResult(int a, int b, int sum) {
+    System.out.println(String.format("Result: %d + %d = %d", a, b, sum)); // Noncompliant
+  }
+}
+
+

Compliant solution

+
+class Calculator {
+  void displayResult(int a, int b, int sum) {
+    System.out.printf("Result: %d + %d = %d%n", a, b, sum);
+  }
+}
+
+

How to fix it in PrintStream/PrintWriter

+

Both PrintStream and PrintWriter provide printf and format methods. Use these instead of +wrapping String.format in print or println.

+

Code examples

+

Noncompliant code example

+
+import java.io.PrintWriter;
+
+class ReportGenerator {
+  void writeHeader(PrintWriter writer, String title, int pageNum) {
+    writer.println(String.format("Page %d: %s", pageNum, title)); // Noncompliant
+  }
+}
+
+

Compliant solution

+
+import java.io.PrintWriter;
+
+class ReportGenerator {
+  void writeHeader(PrintWriter writer, String title, int pageNum) {
+    writer.printf("Page %d: %s%n", pageNum, title);
+  }
+}
+
+

How to fix it in StringBuilder

+

StringBuilder is designed for efficient string concatenation. Using String.format with it creates an unnecessary +intermediate String object, defeating the purpose. Use chained append calls instead.

+

Code examples

+

Noncompliant code example

+
+class JsonBuilder {
+  String buildUserJson(String name, int age, String email) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("{");
+    sb.append(String.format(""name":"%s",", name)); // Noncompliant
+    sb.append(String.format(""age":%d,", age)); // Noncompliant
+    sb.append(String.format(""email":"%s"", email)); // Noncompliant
+    sb.append("}");
+    return sb.toString();
+  }
+}
+
+

Compliant solution

+
+class JsonBuilder {
+  String buildUserJson(String name, int age, String email) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("{");
+    sb.append(""name":"").append(name).append("",");
+    sb.append(""age":").append(age).append(",");
+    sb.append(""email":"").append(email).append(""");
+    sb.append("}");
+    return sb.toString();
+  }
+}
+
+

How to fix it in Exception constructors

+

For simple exception messages, string concatenation is clearer and more efficient than String.format. The + operator is +optimized by the Java compiler and doesn’t require parsing format specifiers.

+

Code examples

+

Noncompliant code example

+
+class Validator {
+  void validateAge(int age) {
+    if (age < 0) {
+      throw new IllegalArgumentException(String.format("Age cannot be negative: %d", age)); // Noncompliant
+    }
+  }
+}
+
+

Compliant solution

+
+class Validator {
+  void validateAge(int age) {
+    if (age < 0) {
+      throw new IllegalArgumentException("Age cannot be negative: " + age);
+    }
+  }
+}
+
+

Resources

+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.json new file mode 100644 index 00000000000..feaa55ba8ef --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.json @@ -0,0 +1,25 @@ +{ + "title": "Redundant \"String.format\" should be removed when native formatting is available", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5 min" + }, + "tags": [ + "slf4j", + "performance", + "logging" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-9413", + "sqKey": "S9413", + "scope": "All", + "quickfix": "unknown", + "code": { + "impacts": { + "MAINTAINABILITY": "MEDIUM" + }, + "attribute": "CLEAR" + } +} diff --git a/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9413 b/sonar-java-plugin/src/main/resources/profiles/Sonar_way/S9413 new file mode 100644 index 00000000000..e69de29bb2d From 18a826c90933787d1aa13d1199dfbd5c17a84613 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 28 Sep 2026 08:45:59 +0000 Subject: [PATCH 2/8] Update ruling results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with GitHub Actions --- .../expected/eclipse-jetty/java-S9413.json | 36 ++ .../resources/expected/guava/java-S9413.json | 5 + .../expected/sonar-server/java-S9413.json | 455 ++++++++++++++++++ 3 files changed, 496 insertions(+) create mode 100644 its/ruling/src/test/resources/expected/eclipse-jetty/java-S9413.json create mode 100644 its/ruling/src/test/resources/expected/guava/java-S9413.json create mode 100644 its/ruling/src/test/resources/expected/sonar-server/java-S9413.json diff --git a/its/ruling/src/test/resources/expected/eclipse-jetty/java-S9413.json b/its/ruling/src/test/resources/expected/eclipse-jetty/java-S9413.json new file mode 100644 index 00000000000..29fb34e79dc --- /dev/null +++ b/its/ruling/src/test/resources/expected/eclipse-jetty/java-S9413.json @@ -0,0 +1,36 @@ +{ +"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java": [ +613 +], +"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java": [ +1974 +], +"org.eclipse.jetty:jetty-project:jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java": [ +368 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java": [ +1345 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/MultiPartParser.java": [ +714 +], +"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/Server.java": [ +420, +440, +480 +], +"org.eclipse.jetty:jetty-project:jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java": [ +289, +295, +309 +], +"org.eclipse.jetty:jetty-project:jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java": [ +639 +], +"org.eclipse.jetty:jetty-project:jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPoolBudget.java": [ +145 +], +"org.eclipse.jetty:jetty-project:jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java": [ +396 +] +} diff --git a/its/ruling/src/test/resources/expected/guava/java-S9413.json b/its/ruling/src/test/resources/expected/guava/java-S9413.json new file mode 100644 index 00000000000..b3dcfd6ff18 --- /dev/null +++ b/its/ruling/src/test/resources/expected/guava/java-S9413.json @@ -0,0 +1,5 @@ +{ +"com.google.guava:guava:src/com/google/common/io/ByteStreams.java": [ +394 +] +} diff --git a/its/ruling/src/test/resources/expected/sonar-server/java-S9413.json b/its/ruling/src/test/resources/expected/sonar-server/java-S9413.json new file mode 100644 index 00000000000..942a5dc2f29 --- /dev/null +++ b/its/ruling/src/test/resources/expected/sonar-server/java-S9413.json @@ -0,0 +1,455 @@ +{ +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/ce/http/CeHttpClient.java": [ +112 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/app/ProcessCommandWrapperImpl.java": [ +89, +93 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/app/TomcatConnectors.java": [ +51 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/app/TomcatContexts.java": [ +85 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/authentication/AuthenticationError.java": [ +69, +81 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/authentication/IdentityProviderRepository.java": [ +54 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java": [ +106 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/batch/IssuesAction.java": [ +130 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/component/ws/FilterParser.java": [ +69, +198 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactory.java": [ +129, +151 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/component/ws/SuggestionCategory.java": [ +51 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/queue/ReportSubmitter.java": [ +98 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/api/measurecomputer/MeasureComputerContextImpl.java": [ +177 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/api/measurecomputer/MeasureImpl.java": [ +80 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/ConditionToCondition.java": [ +72, +89 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java": [ +162 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/component/ComponentRootBuilder.java": [ +105 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/component/PathAwareCrawler.java": [ +117, +138 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/component/PathAwareVisitorWrapper.java": [ +116 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/component/VisitorsCrawler.java": [ +141 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/formula/AverageFormula.java": [ +157 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/issue/EffortAggregator.java": [ +122 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IntegrateIssuesVisitor.java": [ +83 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/issue/NewEffortAggregator.java": [ +150 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/issue/commonrule/CommentDensityRule.java": [ +81 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/issue/commonrule/CommonRule.java": [ +86, +90 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/measure/MapBasedRawMeasureRepository.java": [ +79, +95 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/measure/Measure.java": [ +308 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/metric/MetricRepositoryImpl.java": [ +72, +83 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/Condition.java": [ +72 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/ConditionEvaluator.java": [ +62, +90, +108, +138 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingGrid.java": [ +77 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingSettings.java": [ +85 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/source/HighlightingLineReader.java": [ +128 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/AbstractComputationSteps.java": [ +45 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/ComputeMeasureVariationsStep.java": [ +146 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/CustomMeasuresCopyStep.java": [ +106 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStep.java": [ +94 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/PersistComponentsStep.java": [ +128 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/PersistEventsStep.java": [ +137 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/PersistFileSourcesStep.java": [ +124 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/PersistProjectLinksStep.java": [ +114, +145 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/QualityGateMeasuresStep.java": [ +135, +150 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/UnitTestMeasuresStep.java": [ +97 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java": [ +107, +113 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/BaseDoc.java": [ +57 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyBulkRequestBuilder.java": [ +50 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyClearCacheRequestBuilder.java": [ +44, +72, +76 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyClusterHealthRequestBuilder.java": [ +44, +72 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyClusterStateRequestBuilder.java": [ +44, +72 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyClusterStatsRequestBuilder.java": [ +44, +72 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyCreateIndexRequestBuilder.java": [ +46 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyDeleteRequestBuilder.java": [ +43 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyFlushRequestBuilder.java": [ +44, +72 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyGetRequestBuilder.java": [ +43, +69, +70, +71 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyIndexRequestBuilder.java": [ +43, +69, +70, +71 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyIndicesExistsRequestBuilder.java": [ +44, +72 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyIndicesStatsRequestBuilder.java": [ +44, +72 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyMultiGetRequestBuilder.java": [ +44, +72, +73, +76 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyNodesStatsRequestBuilder.java": [ +44, +72 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyPutMappingRequestBuilder.java": [ +44, +72, +76, +80 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyRefreshRequestBuilder.java": [ +44, +72 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxySearchRequestBuilder.java": [ +48, +74, +76, +79 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxySearchScrollRequestBuilder.java": [ +43 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/issue/IssueFinder.java": [ +43 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/issue/index/IssueIndex.java": [ +665 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java": [ +149 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/issue/ws/AssignAction.java": [ +149, +151, +153 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/issue/ws/DeleteCommentAction.java": [ +113 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/issue/ws/EditCommentAction.java": [ +136 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/measure/custom/ws/CustomMeasureValueDescription.java": [ +35 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/measure/ws/ComponentAction.java": [ +195 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/measure/ws/ComponentTreeDataLoader.java": [ +170 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/measure/ws/MeasureValueFormatter.java": [ +84 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/measure/ws/SearchHistoryAction.java": [ +178 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/organization/OrganizationCreationImpl.java": [ +95 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/organization/ws/UpdateAction.java": [ +120 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/permission/PermissionTemplateService.java": [ +212, +223 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/permission/ws/AddUserAction.java": [ +99 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/permission/ws/PermissionWsSupport.java": [ +110 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/platform/web/RedirectFilter.java": [ +67 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java": [ +88, +101 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/plugins/ServerPluginJarExploder.java": [ +62 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/plugins/ServerPluginRepository.java": [ +206, +309, +363 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/plugins/ws/InstallAction.java": [ +95 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/plugins/ws/UninstallAction.java": [ +72 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/plugins/ws/UpdateAction.java": [ +98 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/project/ws/UpdateVisibilityAction.java": [ +103 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/projectanalysis/ws/CreateEventAction.java": [ +133, +197, +201 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/projectanalysis/ws/DeleteAction.java": [ +102 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/projectanalysis/ws/DeleteEventAction.java": [ +92, +98 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/projectanalysis/ws/UpdateEventAction.java": [ +127, +142, +151 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java": [ +99, +107 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualitygate/QualityGateFinder.java": [ +72 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java": [ +174, +184 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualityprofile/BuiltInQualityProfilesNotificationTemplate.java": [ +99 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualityprofile/QProfileCopier.java": [ +82 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualityprofile/ws/ChangeParentAction.java": [ +94 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualityprofile/ws/DeleteAction.java": [ +106 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualityprofile/ws/ProjectsAction.java": [ +129 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualityprofile/ws/SearchAction.java": [ +178 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualityprofile/ws/SetDefaultAction.java": [ +72 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/root/ws/SetRootAction.java": [ +70 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/root/ws/UnsetRootAction.java": [ +71 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/DefaultRuleFinder.java": [ +81 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoader.java": [ +136 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/RuleCreator.java": [ +76, +78, +206, +213 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/RuleUpdater.java": [ +219 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/index/RuleQuery.java": [ +224 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/ws/CreateAction.java": [ +167 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/ws/RuleMapper.java": [ +250 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/ws/ShowAction.java": [ +109 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/ws/UpdateAction.java": [ +230, +275 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/setting/ws/ValuesAction.java": [ +173 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/source/ws/HashAction.java": [ +104 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/startup/RegisterMetrics.java": [ +123, +127 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/startup/RegisterPermissionTemplates.java": [ +105, +107 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/test/index/TestIndex.java": [ +87 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/user/SecurityRealmFactory.java": [ +51, +58 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/usergroups/DefaultGroupFinder.java": [ +40 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/usergroups/ws/AddUserAction.java": [ +102 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/util/MetricKeyValidator.java": [ +55 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/view/index/ViewIndexer.java": [ +128 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/ws/RemovedWebServiceHandler.java": [ +40 +], +"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/ws/RequestVerifier.java": [ +42 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/component/ReportComponent.java": [ +74, +82 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/measure/MeasureRepositoryRule.java": [ +226, +238 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/source/SourceLinesRepositoryRule.java": [ +47 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/rule/RegisterRulesMediumTest.java": [ +94 +], +"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/ws/WsTester.java": [ +334, +339 +] +} From c509c9e6a581648af35eaf86632b869681962e79 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Mon, 28 Sep 2026 11:19:43 +0200 Subject: [PATCH 3/8] SONARJAVA-7070: Fix StringBuilder code examples in S9413 description Co-Authored-By: Claude Opus 5.5 (1M context) --- .../org/sonar/l10n/java/rules/java/S9413.html | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html index a2188a1c779..081d54d304d 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html @@ -132,28 +132,24 @@

How to fix it in StringBuilder

Code examples

Noncompliant code example

-class JsonBuilder {
-  String buildUserJson(String name, int age, String email) {
+class ReportBuilder {
+  String buildUserSummary(String name, int age, String email) {
     StringBuilder sb = new StringBuilder();
-    sb.append("{");
-    sb.append(String.format(""name":"%s",", name)); // Noncompliant
-    sb.append(String.format(""age":%d,", age)); // Noncompliant
-    sb.append(String.format(""email":"%s"", email)); // Noncompliant
-    sb.append("}");
+    sb.append(String.format("User: %s, ", name)); // Noncompliant
+    sb.append(String.format("age: %d, ", age)); // Noncompliant
+    sb.append(String.format("email: %s", email)); // Noncompliant
     return sb.toString();
   }
 }
 

Compliant solution

-class JsonBuilder {
-  String buildUserJson(String name, int age, String email) {
+class ReportBuilder {
+  String buildUserSummary(String name, int age, String email) {
     StringBuilder sb = new StringBuilder();
-    sb.append("{");
-    sb.append(""name":"").append(name).append("",");
-    sb.append(""age":").append(age).append(",");
-    sb.append(""email":"").append(email).append(""");
-    sb.append("}");
+    sb.append("User: ").append(name).append(", ");
+    sb.append("age: ").append(age).append(", ");
+    sb.append("email: ").append(email);
     return sb.toString();
   }
 }

From 5741a3f4b55defd26f29efb6ef5e6172697e9791 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" 
Date: Mon, 28 Sep 2026 09:37:31 +0000
Subject: [PATCH 4/8] Update ruling results
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

🤖 Generated with GitHub Actions
---
 .../java-S9413.json                           | 22 +++++++++++++++++++
 1 file changed, 22 insertions(+)
 create mode 100644 its/ruling/src/test/resources/expected/eclipse-jetty-similar-to-main/java-S9413.json

diff --git a/its/ruling/src/test/resources/expected/eclipse-jetty-similar-to-main/java-S9413.json b/its/ruling/src/test/resources/expected/eclipse-jetty-similar-to-main/java-S9413.json
new file mode 100644
index 00000000000..4d9a38debfb
--- /dev/null
+++ b/its/ruling/src/test/resources/expected/eclipse-jetty-similar-to-main/java-S9413.json
@@ -0,0 +1,22 @@
+{
+"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java": [
+613
+],
+"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java": [
+1974
+],
+"org.eclipse.jetty:jetty-project:jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java": [
+368
+],
+"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java": [
+1345
+],
+"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/MultiPartParser.java": [
+714
+],
+"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/Server.java": [
+420,
+440,
+480
+]
+}

From db7dc27766ba66dc1037e9d5e7c0a11c1f217b23 Mon Sep 17 00:00:00 2001
From: Romain Brenguier 
Date: Tue, 29 Sep 2026 15:49:42 +0200
Subject: [PATCH 5/8] SONARJAVA-7070: Drop exception constructor case from
 S9413

String concatenation instead of String.format is already covered by S9397.

Co-Authored-By: Claude Opus 5.5 (1M context) 
---
 .../RedundantStringFormatCheckSample.java     |  7 ++---
 .../checks/RedundantStringFormatCheck.java    | 30 +++++++------------
 .../org/sonar/l10n/java/rules/java/S9413.html | 24 ---------------
 3 files changed, 14 insertions(+), 47 deletions(-)

diff --git a/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckSample.java
index 01b87754a2e..da70de6261c 100644
--- a/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckSample.java
+++ b/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckSample.java
@@ -47,14 +47,13 @@ String json(String name, int age, int hash) {
 
   void validate(int age, String name, Exception cause) throws ValidationException {
     if (age < 0) {
-      throw new IllegalArgumentException(String.format("Age cannot be negative: %d", age)); // Noncompliant {{Use string concatenation instead of "String.format".}}
-//                                       ^^^^^^^^^^^^^
+      throw new IllegalArgumentException(String.format("Age cannot be negative: %d", age));
     }
     if (name == null) {
-      throw new RuntimeException(String.format("Missing name for age %d", age), cause); // Noncompliant
+      throw new RuntimeException(String.format("Missing name for age %d", age), cause);
     }
     if (name.isEmpty()) {
-      throw new ValidationException(String.format("Empty name %s", name)); // Noncompliant
+      throw new ValidationException(String.format("Empty name %s", name));
     }
     if (name.length() > 10) {
       throw new IllegalStateException(String.format("%-10s|", name));
diff --git a/java-checks/src/main/java/org/sonar/java/checks/RedundantStringFormatCheck.java b/java-checks/src/main/java/org/sonar/java/checks/RedundantStringFormatCheck.java
index 32380e71728..ab45f34517d 100644
--- a/java-checks/src/main/java/org/sonar/java/checks/RedundantStringFormatCheck.java
+++ b/java-checks/src/main/java/org/sonar/java/checks/RedundantStringFormatCheck.java
@@ -28,7 +28,6 @@
 import org.sonar.plugins.java.api.tree.ExpressionTree;
 import org.sonar.plugins.java.api.tree.LiteralTree;
 import org.sonar.plugins.java.api.tree.MethodInvocationTree;
-import org.sonar.plugins.java.api.tree.NewClassTree;
 import org.sonar.plugins.java.api.tree.Tree;
 
 @Rule(key = "S9413")
@@ -59,7 +58,6 @@ public class RedundantStringFormatCheck extends IssuableSubscriptionVisitor {
   private static final String PRINTF_MESSAGE = "Use \"printf\" instead of \"String.format\".";
   private static final String LOGGING_MESSAGE = "Use the logger's built-in \"{}\" formatting instead of \"String.format\".";
   private static final String APPEND_MESSAGE = "Use chained \"append\" calls instead of \"String.format\".";
-  private static final String EXCEPTION_MESSAGE = "Use string concatenation instead of \"String.format\".";
 
   @Override
   public List nodesToVisit() {
@@ -88,23 +86,17 @@ public void visitNode(Tree tree) {
 
   @Nullable
   private static String targetMessage(Tree call, Tree argument) {
-    if (call instanceof MethodInvocationTree mit) {
-      if (!isMessageArgument(mit.methodSymbol(), mit.arguments().indexOf(argument))) {
-        return null;
-      }
-      if (PRINT_METHODS.matches(mit)) {
-        return PRINTF_MESSAGE;
-      }
-      if (LOG_METHODS.matches(mit)) {
-        return LOGGING_MESSAGE;
-      }
-      if (APPEND_METHODS.matches(mit)) {
-        return APPEND_MESSAGE;
-      }
-    } else if (call instanceof NewClassTree newClass
-      && newClass.symbolType().isSubtypeOf("java.lang.Throwable")
-      && isMessageArgument(newClass.methodSymbol(), newClass.arguments().indexOf(argument))) {
-      return EXCEPTION_MESSAGE;
+    if (!(call instanceof MethodInvocationTree mit) || !isMessageArgument(mit.methodSymbol(), mit.arguments().indexOf(argument))) {
+      return null;
+    }
+    if (PRINT_METHODS.matches(mit)) {
+      return PRINTF_MESSAGE;
+    }
+    if (LOG_METHODS.matches(mit)) {
+      return LOGGING_MESSAGE;
+    }
+    if (APPEND_METHODS.matches(mit)) {
+      return APPEND_MESSAGE;
     }
     return null;
   }
diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html
index 081d54d304d..9224297ff51 100644
--- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html
+++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html
@@ -154,30 +154,6 @@ 

Compliant solution

} }
-

How to fix it in Exception constructors

-

For simple exception messages, string concatenation is clearer and more efficient than String.format. The + operator is -optimized by the Java compiler and doesn’t require parsing format specifiers.

-

Code examples

-

Noncompliant code example

-
-class Validator {
-  void validateAge(int age) {
-    if (age < 0) {
-      throw new IllegalArgumentException(String.format("Age cannot be negative: %d", age)); // Noncompliant
-    }
-  }
-}
-
-

Compliant solution

-
-class Validator {
-  void validateAge(int age) {
-    if (age < 0) {
-      throw new IllegalArgumentException("Age cannot be negative: " + age);
-    }
-  }
-}
-

Resources

Documentation

    From 33395fa1dfd5b83bf16c21244a7e930c9203f693 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 08:51:36 +0200 Subject: [PATCH 6/8] Update ruling results for PR #6246 (#6269) Co-authored-by: github-actions[bot] --- .../expected/eclipse-jetty/java-S9413.json | 20 - .../expected/sonar-server/java-S9413.json | 394 ------------------ 2 files changed, 414 deletions(-) diff --git a/its/ruling/src/test/resources/expected/eclipse-jetty/java-S9413.json b/its/ruling/src/test/resources/expected/eclipse-jetty/java-S9413.json index 29fb34e79dc..f5bd1e6ee14 100644 --- a/its/ruling/src/test/resources/expected/eclipse-jetty/java-S9413.json +++ b/its/ruling/src/test/resources/expected/eclipse-jetty/java-S9413.json @@ -1,16 +1,10 @@ { -"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java": [ -613 -], "org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java": [ 1974 ], "org.eclipse.jetty:jetty-project:jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java": [ 368 ], -"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java": [ -1345 -], "org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/MultiPartParser.java": [ 714 ], @@ -18,19 +12,5 @@ 420, 440, 480 -], -"org.eclipse.jetty:jetty-project:jetty-util/src/main/java/org/eclipse/jetty/util/IteratingCallback.java": [ -289, -295, -309 -], -"org.eclipse.jetty:jetty-project:jetty-util/src/main/java/org/eclipse/jetty/util/UrlEncoded.java": [ -639 -], -"org.eclipse.jetty:jetty-project:jetty-util/src/main/java/org/eclipse/jetty/util/thread/ThreadPoolBudget.java": [ -145 -], -"org.eclipse.jetty:jetty-project:jetty-xml/src/main/java/org/eclipse/jetty/xml/XmlConfiguration.java": [ -396 ] } diff --git a/its/ruling/src/test/resources/expected/sonar-server/java-S9413.json b/its/ruling/src/test/resources/expected/sonar-server/java-S9413.json index 942a5dc2f29..2c4a75cc50f 100644 --- a/its/ruling/src/test/resources/expected/sonar-server/java-S9413.json +++ b/its/ruling/src/test/resources/expected/sonar-server/java-S9413.json @@ -1,455 +1,61 @@ { -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/ce/http/CeHttpClient.java": [ -112 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/app/ProcessCommandWrapperImpl.java": [ -89, -93 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/app/TomcatConnectors.java": [ -51 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/app/TomcatContexts.java": [ -85 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/authentication/AuthenticationError.java": [ -69, -81 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/authentication/IdentityProviderRepository.java": [ -54 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/authentication/OAuth2ContextFactory.java": [ -106 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/batch/IssuesAction.java": [ -130 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/component/ws/FilterParser.java": [ -69, -198 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactory.java": [ -129, -151 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/component/ws/SuggestionCategory.java": [ -51 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/queue/ReportSubmitter.java": [ -98 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/api/measurecomputer/MeasureComputerContextImpl.java": [ -177 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/api/measurecomputer/MeasureImpl.java": [ -80 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/ConditionToCondition.java": [ -72, -89 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java": [ -162 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/component/ComponentRootBuilder.java": [ -105 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/component/PathAwareCrawler.java": [ -117, -138 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/component/PathAwareVisitorWrapper.java": [ -116 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/component/VisitorsCrawler.java": [ -141 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/formula/AverageFormula.java": [ -157 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/issue/EffortAggregator.java": [ -122 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IntegrateIssuesVisitor.java": [ -83 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/issue/NewEffortAggregator.java": [ -150 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/issue/commonrule/CommentDensityRule.java": [ -81 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/issue/commonrule/CommonRule.java": [ -86, -90 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/measure/MapBasedRawMeasureRepository.java": [ -79, -95 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/measure/Measure.java": [ -308 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/metric/MetricRepositoryImpl.java": [ -72, -83 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/Condition.java": [ -72 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/ConditionEvaluator.java": [ -62, -90, -108, -138 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingGrid.java": [ -77 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingSettings.java": [ -85 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/source/HighlightingLineReader.java": [ -128 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/AbstractComputationSteps.java": [ -45 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/ComputeMeasureVariationsStep.java": [ -146 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/CustomMeasuresCopyStep.java": [ -106 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStep.java": [ -94 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/PersistComponentsStep.java": [ -128 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/PersistEventsStep.java": [ -137 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/PersistFileSourcesStep.java": [ -124 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/PersistProjectLinksStep.java": [ -114, -145 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/QualityGateMeasuresStep.java": [ -135, -150 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/step/UnitTestMeasuresStep.java": [ -97 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java": [ -107, -113 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/BaseDoc.java": [ -57 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyBulkRequestBuilder.java": [ -50 -], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyClearCacheRequestBuilder.java": [ -44, 72, 76 ], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyClusterHealthRequestBuilder.java": [ -44, 72 ], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyClusterStateRequestBuilder.java": [ -44, 72 ], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyClusterStatsRequestBuilder.java": [ -44, 72 ], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyCreateIndexRequestBuilder.java": [ -46 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyDeleteRequestBuilder.java": [ -43 -], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyFlushRequestBuilder.java": [ -44, 72 ], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyGetRequestBuilder.java": [ -43, 69, 70, 71 ], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyIndexRequestBuilder.java": [ -43, 69, 70, 71 ], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyIndicesExistsRequestBuilder.java": [ -44, 72 ], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyIndicesStatsRequestBuilder.java": [ -44, 72 ], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyMultiGetRequestBuilder.java": [ -44, 72, 73, 76 ], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyNodesStatsRequestBuilder.java": [ -44, 72 ], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyPutMappingRequestBuilder.java": [ -44, 72, 76, 80 ], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyRefreshRequestBuilder.java": [ -44, 72 ], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxySearchRequestBuilder.java": [ -48, 74, 76, 79 ], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxySearchScrollRequestBuilder.java": [ -43 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/issue/IssueFinder.java": [ -43 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/issue/index/IssueIndex.java": [ -665 -], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java": [ 149 ], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/issue/ws/AssignAction.java": [ -149, -151, -153 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/issue/ws/DeleteCommentAction.java": [ -113 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/issue/ws/EditCommentAction.java": [ -136 -], "org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/measure/custom/ws/CustomMeasureValueDescription.java": [ 35 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/measure/ws/ComponentAction.java": [ -195 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/measure/ws/ComponentTreeDataLoader.java": [ -170 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/measure/ws/MeasureValueFormatter.java": [ -84 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/measure/ws/SearchHistoryAction.java": [ -178 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/organization/OrganizationCreationImpl.java": [ -95 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/organization/ws/UpdateAction.java": [ -120 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/permission/PermissionTemplateService.java": [ -212, -223 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/permission/ws/AddUserAction.java": [ -99 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/permission/ws/PermissionWsSupport.java": [ -110 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/platform/web/RedirectFilter.java": [ -67 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/plugins/ServerExtensionInstaller.java": [ -88, -101 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/plugins/ServerPluginJarExploder.java": [ -62 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/plugins/ServerPluginRepository.java": [ -206, -309, -363 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/plugins/ws/InstallAction.java": [ -95 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/plugins/ws/UninstallAction.java": [ -72 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/plugins/ws/UpdateAction.java": [ -98 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/project/ws/UpdateVisibilityAction.java": [ -103 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/projectanalysis/ws/CreateEventAction.java": [ -133, -197, -201 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/projectanalysis/ws/DeleteAction.java": [ -102 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/projectanalysis/ws/DeleteEventAction.java": [ -92, -98 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/projectanalysis/ws/UpdateEventAction.java": [ -127, -142, -151 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java": [ -99, -107 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualitygate/QualityGateFinder.java": [ -72 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java": [ -174, -184 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualityprofile/BuiltInQualityProfilesNotificationTemplate.java": [ -99 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualityprofile/QProfileCopier.java": [ -82 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualityprofile/ws/ChangeParentAction.java": [ -94 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualityprofile/ws/DeleteAction.java": [ -106 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualityprofile/ws/ProjectsAction.java": [ -129 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualityprofile/ws/SearchAction.java": [ -178 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/qualityprofile/ws/SetDefaultAction.java": [ -72 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/root/ws/SetRootAction.java": [ -70 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/root/ws/UnsetRootAction.java": [ -71 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/DefaultRuleFinder.java": [ -81 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/DeprecatedRulesDefinitionLoader.java": [ -136 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/RuleCreator.java": [ -76, -78, -206, -213 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/RuleUpdater.java": [ -219 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/index/RuleQuery.java": [ -224 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/ws/CreateAction.java": [ -167 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/ws/RuleMapper.java": [ -250 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/ws/ShowAction.java": [ -109 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/rule/ws/UpdateAction.java": [ -230, -275 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/setting/ws/ValuesAction.java": [ -173 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/source/ws/HashAction.java": [ -104 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/startup/RegisterMetrics.java": [ -123, -127 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/startup/RegisterPermissionTemplates.java": [ -105, -107 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/test/index/TestIndex.java": [ -87 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/user/SecurityRealmFactory.java": [ -51, -58 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/usergroups/DefaultGroupFinder.java": [ -40 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/usergroups/ws/AddUserAction.java": [ -102 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/util/MetricKeyValidator.java": [ -55 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/view/index/ViewIndexer.java": [ -128 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/ws/RemovedWebServiceHandler.java": [ -40 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/ws/RequestVerifier.java": [ -42 -], -"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/component/ReportComponent.java": [ -74, -82 -], -"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/measure/MeasureRepositoryRule.java": [ -226, -238 -], -"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/computation/task/projectanalysis/source/SourceLinesRepositoryRule.java": [ -47 -], -"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/rule/RegisterRulesMediumTest.java": [ -94 -], -"org.sonarsource.sonarqube:sonar-server:src/test/java/org/sonar/server/ws/WsTester.java": [ -334, -339 ] } From e4fe00a131d57da2d12ee938c817588258c9e5c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:02:20 +0200 Subject: [PATCH 7/8] Update ruling results for PR #6246 (#6273) Co-authored-by: github-actions[bot] --- .../expected/eclipse-jetty-similar-to-main/java-S9413.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/its/ruling/src/test/resources/expected/eclipse-jetty-similar-to-main/java-S9413.json b/its/ruling/src/test/resources/expected/eclipse-jetty-similar-to-main/java-S9413.json index 4d9a38debfb..f5bd1e6ee14 100644 --- a/its/ruling/src/test/resources/expected/eclipse-jetty-similar-to-main/java-S9413.json +++ b/its/ruling/src/test/resources/expected/eclipse-jetty-similar-to-main/java-S9413.json @@ -1,16 +1,10 @@ { -"org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/HttpGenerator.java": [ -613 -], "org.eclipse.jetty:jetty-project:jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java": [ 1974 ], "org.eclipse.jetty:jetty-project:jetty-io/src/main/java/org/eclipse/jetty/io/SelectorManager.java": [ 368 ], -"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/HttpOutput.java": [ -1345 -], "org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/MultiPartParser.java": [ 714 ], From 6fa88a758202b35ae23d496399736ffae7f05aa1 Mon Sep 17 00:00:00 2001 From: Romain Brenguier Date: Wed, 30 Sep 2026 14:23:26 +0200 Subject: [PATCH 8/8] SONARJAVA-7070: Drop StringBuilder.append case from S9413 and address review - Stop reporting String.format passed to StringBuilder/StringBuffer.append. - Skip logging cases when a format argument implements java.util.Formattable, since %s calls formatTo while "{}" placeholders call toString. - Add a quick fix turning print/println(String.format(...)) into printf(...), appending %n for println when the format is a string literal. - Remove stale ruling expectations for dropped cases. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../resources/expected/guava/java-S9413.json | 5 -- .../expected/sonar-server/java-S9413.json | 61 -------------- ...dundantStringFormatCheckLoggingSample.java | 16 ++++ .../RedundantStringFormatCheckSample.java | 68 +++++++++++----- .../checks/RedundantStringFormatCheck.java | 80 ++++++++++++------- .../org/sonar/l10n/java/rules/java/S9413.html | 42 +--------- .../org/sonar/l10n/java/rules/java/S9413.json | 2 +- 7 files changed, 118 insertions(+), 156 deletions(-) delete mode 100644 its/ruling/src/test/resources/expected/guava/java-S9413.json delete mode 100644 its/ruling/src/test/resources/expected/sonar-server/java-S9413.json diff --git a/its/ruling/src/test/resources/expected/guava/java-S9413.json b/its/ruling/src/test/resources/expected/guava/java-S9413.json deleted file mode 100644 index b3dcfd6ff18..00000000000 --- a/its/ruling/src/test/resources/expected/guava/java-S9413.json +++ /dev/null @@ -1,5 +0,0 @@ -{ -"com.google.guava:guava:src/com/google/common/io/ByteStreams.java": [ -394 -] -} diff --git a/its/ruling/src/test/resources/expected/sonar-server/java-S9413.json b/its/ruling/src/test/resources/expected/sonar-server/java-S9413.json deleted file mode 100644 index 2c4a75cc50f..00000000000 --- a/its/ruling/src/test/resources/expected/sonar-server/java-S9413.json +++ /dev/null @@ -1,61 +0,0 @@ -{ -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyClearCacheRequestBuilder.java": [ -72, -76 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyClusterHealthRequestBuilder.java": [ -72 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyClusterStateRequestBuilder.java": [ -72 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyClusterStatsRequestBuilder.java": [ -72 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyFlushRequestBuilder.java": [ -72 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyGetRequestBuilder.java": [ -69, -70, -71 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyIndexRequestBuilder.java": [ -69, -70, -71 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyIndicesExistsRequestBuilder.java": [ -72 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyIndicesStatsRequestBuilder.java": [ -72 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyMultiGetRequestBuilder.java": [ -72, -73, -76 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyNodesStatsRequestBuilder.java": [ -72 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyPutMappingRequestBuilder.java": [ -72, -76, -80 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxyRefreshRequestBuilder.java": [ -72 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/es/request/ProxySearchRequestBuilder.java": [ -74, -76, -79 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/issue/notification/AbstractNewIssuesEmailTemplate.java": [ -149 -], -"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/measure/custom/ws/CustomMeasureValueDescription.java": [ -35 -] -} diff --git a/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckLoggingSample.java b/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckLoggingSample.java index 580887a1cad..366b3269787 100644 --- a/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckLoggingSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckLoggingSample.java @@ -1,5 +1,7 @@ package checks; +import java.util.Formattable; +import java.util.Formatter; import java.util.Locale; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; @@ -31,6 +33,20 @@ void slf4j(String username, String role, String id, Exception e, Marker marker, LOG.info(message); } + void formattable(FormattableId id, Formattable formattable, Object value) { + LOG.info(String.format("Value: %s", id)); + LOG.info(String.format("Value: %s", formattable)); + LOG4J.info(String.format("Values: %s %s", value, id)); + LOG.info(String.format("Value: %s", value)); // Noncompliant + } + + static class FormattableId implements Formattable { + @Override + public void formatTo(Formatter formatter, int flags, int width, int precision) { + formatter.format("ID"); + } + } + void log4j(String id, int count) { LOG4J.debug(String.format("Loaded %s", id)); // Noncompliant LOG4J.fatal(String.format("Crashed after %d attempts", count)); // Noncompliant diff --git a/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckSample.java index da70de6261c..9a24c00117a 100644 --- a/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/RedundantStringFormatCheckSample.java @@ -2,6 +2,8 @@ import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Formattable; +import java.util.Formatter; import java.util.List; import java.util.Locale; @@ -10,13 +12,41 @@ class RedundantStringFormatCheckSample { private static final String ROW_FORMAT = "%s: %d"; void printing(int a, int b, int sum, double ratio, long count, PrintWriter writer, int pageNum, String title) { - System.out.println(String.format("Result: %d + %d = %d", a, b, sum)); // Noncompliant {{Use "printf" instead of "String.format".}} + System.out.println(String.format("Result: %d + %d = %d", a, b, sum)); // Noncompliant {{Use "printf" instead of "String.format".}} [[quickfixes=qf1]] // ^^^^^^^^^^^^^ - System.err.print(String.format("%.2f", ratio)); // Noncompliant - System.out.println(String.format(Locale.US, "%,d", count)); // Noncompliant + // fix@qf1 {{Replace with "printf"}} + // edit@qf1 [[sc=59;ec=59]] {{%n}} + // edit@qf1 [[sc=16;ec=23]] {{printf}} + // edit@qf1 [[sc=24;ec=38]] {{}} + // edit@qf1 [[sc=71;ec=72]] {{}} + System.err.print(String.format("%.2f", ratio)); // Noncompliant [[quickfixes=qf2]] + // fix@qf2 {{Replace with "printf"}} + // edit@qf2 [[sc=16;ec=21]] {{printf}} + // edit@qf2 [[sc=22;ec=36]] {{}} + // edit@qf2 [[sc=49;ec=50]] {{}} + System.out.println(String.format(Locale.US, "%,d", count)); // Noncompliant [[quickfixes=qf3]] + // fix@qf3 {{Replace with "printf"}} + // edit@qf3 [[sc=53;ec=53]] {{%n}} + // edit@qf3 [[sc=16;ec=23]] {{printf}} + // edit@qf3 [[sc=24;ec=38]] {{}} + // edit@qf3 [[sc=61;ec=62]] {{}} writer.println(String.format("Page %d: %s", pageNum, title)); // Noncompliant - writer.print(String.format(ROW_FORMAT, title, pageNum)); // Noncompliant - System.out.println((String.format("Total: %d", sum))); // Noncompliant + writer.print(String.format(ROW_FORMAT, title, pageNum)); // Noncompliant [[quickfixes=qf4]] + // fix@qf4 {{Replace with "printf"}} + // edit@qf4 [[sc=12;ec=17]] {{printf}} + // edit@qf4 [[sc=18;ec=32]] {{}} + // edit@qf4 [[sc=58;ec=59]] {{}} + System.out.println((String.format("Total: %d", sum))); // Noncompliant [[quickfixes=qf5]] + // fix@qf5 {{Replace with "printf"}} + // edit@qf5 [[sc=49;ec=49]] {{%n}} + // edit@qf5 [[sc=16;ec=23]] {{printf}} + // edit@qf5 [[sc=24;ec=39]] {{}} + // edit@qf5 [[sc=55;ec=57]] {{}} + System.out.println(String.format(ROW_FORMAT, title, pageNum)); // Noncompliant [[quickfixes=!]] + // Noncompliant@+1 [[quickfixes=!]] + writer.println(String.format(""" + %s""", title)); + System.out.println(String.format("Id %s", new FormattableId())); // Noncompliant System.out.printf("Result: %d + %d = %d%n", a, b, sum); System.out.println("Result: " + sum); @@ -24,24 +54,11 @@ void printing(int a, int b, int sum, double ratio, long count, PrintWriter write System.out.println("Total " + String.format("%.2f", ratio)); } - String json(String name, int age, int hash) { + String builder(String name, int age) { StringBuilder sb = new StringBuilder("{"); - sb.append(String.format("\"name\":\"%s\",", name)); // Noncompliant {{Use chained "append" calls instead of "String.format".}} -// ^^^^^^^^^^^^^ - sb.append(String.format("\"age\":%d", age)).append("}"); // Noncompliant - StringBuffer buffer = new StringBuffer(); - buffer.append(String.format("Progress 100%% for %s", name)); // Noncompliant - - sb.append("\"age\":").append(age); - sb.append(String.format("%08X", hash)); - sb.append(String.format("%.2f", age / 3.0)); - sb.append(String.format(Locale.ROOT, "%d", age)); - sb.append(String.format(ROW_FORMAT, name, age)); - sb.append(String.format("%n")); - sb.append(String.format("%1$s-%1$s", name)); - sb.append(String.format("100%", name)); - sb.append(String.format(""" - %s""", name)); + sb.append(String.format("name: %s, ", name)); + new StringBuffer().append(String.format("age: %d", age)); + sb.append("name: ").append(name); return sb.toString(); } @@ -82,4 +99,11 @@ static class ValidationException extends Exception { record Wrapper(String value) { } + + static class FormattableId implements Formattable { + @Override + public void formatTo(Formatter formatter, int flags, int width, int precision) { + formatter.format("ID"); + } + } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/RedundantStringFormatCheck.java b/java-checks/src/main/java/org/sonar/java/checks/RedundantStringFormatCheck.java index ab45f34517d..1f74b523a59 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/RedundantStringFormatCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/RedundantStringFormatCheck.java @@ -17,10 +17,14 @@ package org.sonar.java.checks; import java.util.List; -import javax.annotation.Nullable; import org.sonar.check.Rule; import org.sonar.java.checks.helpers.LoggingMatchers; +import org.sonar.java.checks.helpers.QuickFixHelper; +import org.sonar.java.model.ExpressionUtils; import org.sonar.java.model.LiteralUtils; +import org.sonar.java.reporting.AnalyzerMessage; +import org.sonar.java.reporting.JavaQuickFix; +import org.sonar.java.reporting.JavaTextEdit; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.semantic.MethodMatchers; import org.sonar.plugins.java.api.semantic.Symbol; @@ -47,17 +51,10 @@ public class RedundantStringFormatCheck extends IssuableSubscriptionVisitor { .addParametersMatcher(STRING) .build(); - private static final MethodMatchers APPEND_METHODS = MethodMatchers.create() - .ofTypes("java.lang.StringBuilder", "java.lang.StringBuffer") - .names("append") - .addParametersMatcher(STRING) - .build(); - private static final MethodMatchers LOG_METHODS = MethodMatchers.or(LoggingMatchers.SLF4J_LOG_METHODS, LoggingMatchers.LOG4J_LOG_METHODS); private static final String PRINTF_MESSAGE = "Use \"printf\" instead of \"String.format\"."; private static final String LOGGING_MESSAGE = "Use the logger's built-in \"{}\" formatting instead of \"String.format\"."; - private static final String APPEND_MESSAGE = "Use chained \"append\" calls instead of \"String.format\"."; @Override public List nodesToVisit() { @@ -70,35 +67,55 @@ public void visitNode(Tree tree) { if (!STRING_FORMAT.matches(formatCall)) { return; } - Tree argument = formatCall; - while (argument.parent().is(Tree.Kind.PARENTHESIZED_EXPRESSION)) { - argument = argument.parent(); - } + Tree argument = outermostParentheses(formatCall); Tree arguments = argument.parent(); if (!arguments.is(Tree.Kind.ARGUMENTS)) { return; } - String message = targetMessage(arguments.parent(), argument); - if (message != null && (PRINTF_MESSAGE.equals(message) || hasSimpleLiteralFormat(formatCall))) { - reportIssue(formatCall.methodSelect(), message); + if (!(arguments.parent() instanceof MethodInvocationTree call) || !isMessageArgument(call.methodSymbol(), call.arguments().indexOf(argument))) { + return; + } + if (PRINT_METHODS.matches(call)) { + QuickFixHelper.newIssue(context) + .forRule(this) + .onTree(formatCall.methodSelect()) + .withMessage(PRINTF_MESSAGE) + .withQuickFixes(() -> printfQuickFix(call, argument, formatCall)) + .report(); + } else if (LOG_METHODS.matches(call) && hasSimpleLiteralFormat(formatCall) && hasNoFormattableArgument(formatCall)) { + reportIssue(formatCall.methodSelect(), LOGGING_MESSAGE); } } - @Nullable - private static String targetMessage(Tree call, Tree argument) { - if (!(call instanceof MethodInvocationTree mit) || !isMessageArgument(mit.methodSymbol(), mit.arguments().indexOf(argument))) { - return null; - } - if (PRINT_METHODS.matches(mit)) { - return PRINTF_MESSAGE; + private static Tree outermostParentheses(Tree tree) { + Tree result = tree; + while (result.parent().is(Tree.Kind.PARENTHESIZED_EXPRESSION)) { + result = result.parent(); } - if (LOG_METHODS.matches(mit)) { - return LOGGING_MESSAGE; - } - if (APPEND_METHODS.matches(mit)) { - return APPEND_MESSAGE; + return result; + } + + /** + * Builds a quick fix turning {@code print(String.format(args))} into {@code printf(args)}. + * For {@code println}, a {@code %n} is appended to the format, which is only possible when the format is a string literal. + */ + private static List printfQuickFix(MethodInvocationTree printCall, Tree argument, MethodInvocationTree formatCall) { + List formatArguments = formatCall.arguments(); + boolean isPrintln = "println".equals(printCall.methodSymbol().name()); + JavaQuickFix.Builder builder = JavaQuickFix.newQuickFix("Replace with \"printf\""); + if (isPrintln) { + ExpressionTree format = formatArguments.get(formatArguments.get(0).symbolType().is("java.util.Locale") ? 1 : 0); + if (!format.is(Tree.Kind.STRING_LITERAL)) { + return List.of(); + } + AnalyzerMessage.TextSpan formatSpan = AnalyzerMessage.textSpanFor(format); + builder.addTextEdit(JavaTextEdit.insertAtPosition(formatSpan.endLine, formatSpan.endCharacter - 1, "%n")); } - return null; + return List.of(builder + .addTextEdit(JavaTextEdit.replaceTree(ExpressionUtils.methodName(printCall), "printf")) + .addTextEdit(JavaTextEdit.removeTextSpan(AnalyzerMessage.textSpanBetween(argument, true, formatArguments.get(0), false))) + .addTextEdit(JavaTextEdit.removeTextSpan(AnalyzerMessage.textSpanBetween(formatArguments.get(formatArguments.size() - 1), false, argument, true))) + .build()); } /** @@ -123,6 +140,13 @@ private static boolean hasSimpleLiteralFormat(MethodInvocationTree formatCall) { return hasOnlySimpleConversions(LiteralUtils.trimQuotes(((LiteralTree) arguments.get(0)).value())); } + /** + * {@code %s} calls {@code formatTo} on {@link java.util.Formattable} values instead of {@code toString}, which "{}" placeholders would not preserve. + */ + private static boolean hasNoFormattableArgument(MethodInvocationTree formatCall) { + return formatCall.arguments().stream().noneMatch(arg -> arg.symbolType().isSubtypeOf("java.util.Formattable")); + } + /** * Returns true when every {@code %} sequence of the format string is {@code %s}, {@code %d} or {@code %%}. */ diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html index 9224297ff51..007806b4765 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.html @@ -27,20 +27,10 @@

    Readability and idioms

    • Logging frameworks use their own placeholder syntax
    • Output stream APIs have their own formatting methods
    • -
    • String builder APIs are designed for efficient concatenation

    Using the native approach makes your intent clearer and your code more maintainable.

    -

    When generic formatting defeats the purpose

    -

    Some APIs are specifically designed for performance. Using generic string formatting with them undermines their benefits:

    -
    -StringBuilder sb = new StringBuilder();
    -sb.append(format("User: %s, Age: %d", name, age)); // Creates intermediate String object
    -
    -

    The string builder API is meant to avoid creating intermediate string objects, but generic formatting functions create one anyway, negating the -performance benefit.

    In Java, this specifically refers to String.format(). Common APIs with their own formatting include SLF4J (using {} -placeholders), PrintStream and PrintWriter (with printf methods), and StringBuilder (designed for -efficient string concatenation).

    +placeholders), and PrintStream and PrintWriter (with printf methods).

    What is the potential impact?

    Using redundant manual string formatting can lead to:

      @@ -55,6 +45,8 @@

      What is the potential impact?

      How to fix it in SLF4J

      SLF4J and similar logging frameworks (Log4j2, Logback) support parameterized logging with {} placeholders. This approach defers string construction until the log level is enabled.

      +

      Arguments implementing java.util.Formattable are an exception: String.format formats them with +formatTo instead of toString, so replacing it with {} placeholders could change the message.

      Code examples

      Noncompliant code example

      @@ -126,34 +118,6 @@ 

      Compliant solution

      } }
      -

      How to fix it in StringBuilder

      -

      StringBuilder is designed for efficient string concatenation. Using String.format with it creates an unnecessary -intermediate String object, defeating the purpose. Use chained append calls instead.

      -

      Code examples

      -

      Noncompliant code example

      -
      -class ReportBuilder {
      -  String buildUserSummary(String name, int age, String email) {
      -    StringBuilder sb = new StringBuilder();
      -    sb.append(String.format("User: %s, ", name)); // Noncompliant
      -    sb.append(String.format("age: %d, ", age)); // Noncompliant
      -    sb.append(String.format("email: %s", email)); // Noncompliant
      -    return sb.toString();
      -  }
      -}
      -
      -

      Compliant solution

      -
      -class ReportBuilder {
      -  String buildUserSummary(String name, int age, String email) {
      -    StringBuilder sb = new StringBuilder();
      -    sb.append("User: ").append(name).append(", ");
      -    sb.append("age: ").append(age).append(", ");
      -    sb.append("email: ").append(email);
      -    return sb.toString();
      -  }
      -}
      -

      Resources

      Documentation

        diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.json index feaa55ba8ef..3965c92ae70 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S9413.json @@ -15,7 +15,7 @@ "ruleSpecification": "RSPEC-9413", "sqKey": "S9413", "scope": "All", - "quickfix": "unknown", + "quickfix": "partial", "code": { "impacts": { "MAINTAINABILITY": "MEDIUM"