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"