From 5f80c4fc787c43f9753d8f145040d15747536c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Wed, 25 Feb 2026 13:48:26 +0100 Subject: [PATCH 1/9] Implement rule S8469 --- .../checks/ReadlnWithPromptCheckSample.java | 36 ++++++++ .../java/checks/ReadlnWithPromptCheck.java | 87 +++++++++++++++++++ .../checks/ReadlnWithPromptCheckTest.java | 44 ++++++++++ 3 files changed, 167 insertions(+) create mode 100644 java-checks-test-sources/default/src/main/java/checks/ReadlnWithPromptCheckSample.java create mode 100644 java-checks/src/main/java/org/sonar/java/checks/ReadlnWithPromptCheck.java create mode 100644 java-checks/src/test/java/org/sonar/java/checks/ReadlnWithPromptCheckTest.java diff --git a/java-checks-test-sources/default/src/main/java/checks/ReadlnWithPromptCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/ReadlnWithPromptCheckSample.java new file mode 100644 index 00000000000..7bd51043471 --- /dev/null +++ b/java-checks-test-sources/default/src/main/java/checks/ReadlnWithPromptCheckSample.java @@ -0,0 +1,36 @@ +package checks; + +public class ReadlnWithPromptCheckSample { + + void nonCompliant() { + IO.print("Enter your name: "); + String name = IO.readln(); // Noncompliant {{Use "IO.readln(prompt)" instead of separate "IO.print(prompt)" and "IO.readln()" calls.}} +// ^^^^^^^^^^^ + IO.print(name); + + IO.println("Enter your age: "); + String age = IO.readln(); // Noncompliant {{Use "IO.readln(prompt)" instead of separate "IO.println(prompt)" and "IO.readln()" calls.}} +// ^^^^^^^^^^^ + IO.print(age); + + IO.print("Enter city: "); + IO.readln(); // Noncompliant {{Use "IO.readln(prompt)" instead of separate "IO.print(prompt)" and "IO.readln()" calls.}} +// ^^^^^^^^^^^ + } + + void compliant() { + IO.readln(); // Compliant + + String name = IO.readln("Enter your name: "); // Compliant + + IO.println("Welcome!"); + String input = IO.readln("Please state your name:"); // Compliant + + IO.println(); + IO.readln(); // Compliant + + IO.print("Enter email: "); + int x = 5; + String email = IO.readln(); // Compliant + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/ReadlnWithPromptCheck.java b/java-checks/src/main/java/org/sonar/java/checks/ReadlnWithPromptCheck.java new file mode 100644 index 00000000000..c7b626c775d --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/ReadlnWithPromptCheck.java @@ -0,0 +1,87 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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 org.sonar.check.Rule; +import org.sonar.java.checks.methods.AbstractMethodDetection; +import org.sonar.plugins.java.api.JavaVersion; +import org.sonar.plugins.java.api.JavaVersionAwareVisitor; +import org.sonar.plugins.java.api.semantic.MethodMatchers; +import org.sonar.plugins.java.api.tree.BlockTree; +import org.sonar.plugins.java.api.tree.ExpressionStatementTree; +import org.sonar.plugins.java.api.tree.MethodInvocationTree; +import org.sonar.plugins.java.api.tree.StatementTree; +import org.sonar.plugins.java.api.tree.Tree; + +@Rule(key = "S8469") +public class ReadlnWithPromptCheck extends AbstractMethodDetection implements JavaVersionAwareVisitor { + + private static final String MESSAGE = "Use \"IO.readln(prompt)\" instead of separate \"IO.%s(prompt)\" and \"IO.readln()\" calls."; + + private static final MethodMatchers PRINT_MATCHERS = MethodMatchers.create() + .ofTypes("java.lang.IO") + .names("print", "println") + .addParametersMatcher("java.lang.Object") + .build(); + + private static final MethodMatchers READLN_MATCHER = MethodMatchers.create() + .ofTypes("java.lang.IO") + .names("readln") + .addWithoutParametersMatcher() + .build(); + + @Override + public boolean isCompatibleWithJavaVersion(JavaVersion version) { + return version.isJava25Compatible(); + } + + @Override + protected MethodMatchers getMethodInvocationMatchers() { + return READLN_MATCHER; + } + + @Override + protected void onMethodInvocationFound(MethodInvocationTree mit) { + Tree parent = mit.parent(); + if (parent == null) { + return; + } + + Tree grandParent = parent.parent(); + if (grandParent == null || !grandParent.is(Tree.Kind.BLOCK)) { + return; + } + + BlockTree block = (BlockTree) grandParent; + List statements = block.body(); + + int currentIndex = statements.indexOf(parent); + if (currentIndex <= 0) { + return; + } + + StatementTree previousStatement = statements.get(currentIndex - 1); + if (previousStatement instanceof ExpressionStatementTree exprStmt + && exprStmt.expression() instanceof MethodInvocationTree previousCall + && PRINT_MATCHERS.matches(previousCall)) { + String message = String.format(MESSAGE, previousCall.methodSymbol().name()); + reportIssue(mit, message); + } + } + +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/ReadlnWithPromptCheckTest.java b/java-checks/src/test/java/org/sonar/java/checks/ReadlnWithPromptCheckTest.java new file mode 100644 index 00000000000..b3a13ad0df6 --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/ReadlnWithPromptCheckTest.java @@ -0,0 +1,44 @@ +/* + * SonarQube Java + * Copyright (C) 2012-2025 SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA. + * + * 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 ReadlnWithPromptCheckTest { + + @Test + void test() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/ReadlnWithPromptCheckSample.java")) + .withCheck(new ReadlnWithPromptCheck()) + .withJavaVersion(25) + .verifyIssues(); + } + + @Test + void test_java24() { + CheckVerifier.newVerifier() + .onFile(mainCodeSourcesPath("checks/ReadlnWithPromptCheckSample.java")) + .withCheck(new ReadlnWithPromptCheck()) + .withJavaVersion(24) + .verifyNoIssues(); + } + +} From 57d514edb71981ce0523e0530318a1b1f10be7e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Wed, 25 Feb 2026 14:06:58 +0100 Subject: [PATCH 2/9] Add the rule's description and metadata --- .../org/sonar/l10n/java/rules/java/S8469.html | 36 +++++++++++++++++++ .../org/sonar/l10n/java/rules/java/S8469.json | 21 +++++++++++ .../java/rules/java/Sonar_way_profile.json | 3 +- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8469.html create mode 100644 sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8469.json diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8469.html b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8469.html new file mode 100644 index 00000000000..b06d1e77499 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8469.html @@ -0,0 +1,36 @@ +

The java.io.IO class in Java 25+ (introduced via JEP 512) provides a simplified interface for console interaction. It is common to see +code that prints a message to the console using IO.print(Object obj) or IO.println(Object obj) and then immediately calls +IO.readln() to wait for user input. This sequence should be replaced with the single call IO.readln(String prompt).

+

Why is this an issue?

+

Using the prompt-aware overload is more idiomatic, reduces boilerplate, and explicitly links the prompt text to the input request in a single +operation.

+

How to fix it

+

Remove calls to IO.print(Object obj) or IO.println(Object obj) directly followed IO.readln() with a single +call to IO.readln(String prompt).

+

Code examples

+

Noncompliant code example

+
+// Compact source file
+void main() {
+    // Non-compliant: manual prompt followed by readln() with no arguments
+    IO.print("Please enter your username: ");
+    String user = IO.readln();
+    IO.println("Welcome, " + user);
+}
+
+

Compliant solution

+
+// Compact source file
+void main() {
+    // Compliant: The prompt is directly passed to the readln method
+    String user = IO.readln("Please enter your username: ");
+    IO.println("Welcome, " + user);
+}
+
+

Resources

+

Documentation

+ + diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8469.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8469.json new file mode 100644 index 00000000000..7d9e612bc46 --- /dev/null +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S8469.json @@ -0,0 +1,21 @@ +{ + "title": "Prefer \"IO.readln(String prompt)\" rather than \"IO.print(Object obj)\" or \"IO.println(Object obj)\" followed by \"IO.readln()\"", + "type": "CODE_SMELL", + "status": "ready", + "remediation": { + "func": "Constant\/Issue", + "constantCost": "5min" + }, + "tags": [], + "defaultSeverity": "Minor", + "ruleSpecification": "RSPEC-8469", + "sqKey": "S8469", + "scope": "All", + "quickfix": "unknown", + "code": { + "impacts": { + "MAINTAINABILITY": "LOW" + }, + "attribute": "CONVENTIONAL" + } +} diff --git a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json index 4feda171218..f83818b22df 100644 --- a/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json +++ b/sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json @@ -525,6 +525,7 @@ "S8445", "S8446", "S8447", - "S8450" + "S8450", + "S8469" ] } From c2e9a57328349a673261145ba707b1e4b9dc5a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Thu, 26 Feb 2026 10:07:27 +0100 Subject: [PATCH 3/9] Add another example to test samples to increase coverage --- .../src/main/java/checks/ReadlnWithPromptCheckSample.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/java-checks-test-sources/default/src/main/java/checks/ReadlnWithPromptCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/ReadlnWithPromptCheckSample.java index 7bd51043471..adb1e9dcfa2 100644 --- a/java-checks-test-sources/default/src/main/java/checks/ReadlnWithPromptCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/ReadlnWithPromptCheckSample.java @@ -2,6 +2,8 @@ public class ReadlnWithPromptCheckSample { + private static final String text = IO.readln("Enter your name: "); // Compliant + void nonCompliant() { IO.print("Enter your name: "); String name = IO.readln(); // Noncompliant {{Use "IO.readln(prompt)" instead of separate "IO.print(prompt)" and "IO.readln()" calls.}} From 4a703190570d0b9b9ac48c73a73495a74b3ecde7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Thu, 26 Feb 2026 10:53:08 +0100 Subject: [PATCH 4/9] Fix the autoscan tests --- .../src/test/resources/autoscan/autoscan-diff-by-rules.json | 6 ++++++ .../src/test/resources/autoscan/diffs/diff_S8469.json | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 its/autoscan/src/test/resources/autoscan/diffs/diff_S8469.json diff --git a/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json b/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json index 5ffa4920834..274b1f773b4 100644 --- a/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json +++ b/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json @@ -2896,5 +2896,11 @@ "hasTruePositives": false, "falseNegatives": 0, "falsePositives": 0 + }, + { + "ruleKey": "S8469", + "hasTruePositives": true, + "falseNegatives": 0, + "falsePositives": 0 } ] diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8469.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8469.json new file mode 100644 index 00000000000..a4ca5beb9c3 --- /dev/null +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8469.json @@ -0,0 +1,6 @@ +{ + "ruleKey": "S8469", + "hasTruePositives": true, + "falseNegatives": 0, + "falsePositives": 0 +} From b592787cec0a36ecabe2003e0270295f873ec20b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Thu, 26 Feb 2026 11:13:44 +0100 Subject: [PATCH 5/9] Try again to fix the autoscan tests --- .../src/test/resources/autoscan/autoscan-diff-by-rules.json | 2 +- its/autoscan/src/test/resources/autoscan/diffs/diff_S8469.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json b/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json index 274b1f773b4..8f14f0934dc 100644 --- a/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json +++ b/its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json @@ -2899,7 +2899,7 @@ }, { "ruleKey": "S8469", - "hasTruePositives": true, + "hasTruePositives": false, "falseNegatives": 0, "falsePositives": 0 } diff --git a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8469.json b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8469.json index a4ca5beb9c3..8124008dadc 100644 --- a/its/autoscan/src/test/resources/autoscan/diffs/diff_S8469.json +++ b/its/autoscan/src/test/resources/autoscan/diffs/diff_S8469.json @@ -1,6 +1,6 @@ { "ruleKey": "S8469", - "hasTruePositives": true, + "hasTruePositives": false, "falseNegatives": 0, "falsePositives": 0 } From 275b23c695b065cedf92599cbc19a406e9d9b88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Thu, 26 Feb 2026 11:29:37 +0100 Subject: [PATCH 6/9] Fix the number of rules not reporting in autoscan tests --- its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java b/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java index 230f0cc399f..307045af9c4 100644 --- a/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java +++ b/its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java @@ -199,7 +199,7 @@ public void javaCheckTestSources() throws Exception { softly.assertThat(newDiffs).containsExactlyInAnyOrderElementsOf(knownDiffs.values()); softly.assertThat(newTotal).isEqualTo(knownTotal); softly.assertThat(rulesCausingFPs).hasSize(10); - softly.assertThat(rulesNotReporting).hasSize(19); + softly.assertThat(rulesNotReporting).hasSize(20); /** * 4. Check total number of differences (FPs + FNs) From 0c967f68fd9ff7e7765f85fd156afb685f80e463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Thu, 26 Feb 2026 12:07:03 +0100 Subject: [PATCH 7/9] Remove useless null check --- .../java/org/sonar/java/checks/ReadlnWithPromptCheck.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/ReadlnWithPromptCheck.java b/java-checks/src/main/java/org/sonar/java/checks/ReadlnWithPromptCheck.java index c7b626c775d..f7d9f6827e6 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/ReadlnWithPromptCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/ReadlnWithPromptCheck.java @@ -58,10 +58,6 @@ protected MethodMatchers getMethodInvocationMatchers() { @Override protected void onMethodInvocationFound(MethodInvocationTree mit) { Tree parent = mit.parent(); - if (parent == null) { - return; - } - Tree grandParent = parent.parent(); if (grandParent == null || !grandParent.is(Tree.Kind.BLOCK)) { return; From f80c430885b8d411d8b98f8b2306f5202bc8b238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Fri, 27 Feb 2026 11:45:31 +0100 Subject: [PATCH 8/9] Change the error message to match method signatures in the doc --- .../main/java/org/sonar/java/checks/ReadlnWithPromptCheck.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-checks/src/main/java/org/sonar/java/checks/ReadlnWithPromptCheck.java b/java-checks/src/main/java/org/sonar/java/checks/ReadlnWithPromptCheck.java index f7d9f6827e6..6473a686ab7 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/ReadlnWithPromptCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/ReadlnWithPromptCheck.java @@ -31,7 +31,7 @@ @Rule(key = "S8469") public class ReadlnWithPromptCheck extends AbstractMethodDetection implements JavaVersionAwareVisitor { - private static final String MESSAGE = "Use \"IO.readln(prompt)\" instead of separate \"IO.%s(prompt)\" and \"IO.readln()\" calls."; + private static final String MESSAGE = "Use \"IO.readln(String prompt)\" instead of separate \"IO.%s(Object obj)\" and \"IO.readln()\" calls."; private static final MethodMatchers PRINT_MATCHERS = MethodMatchers.create() .ofTypes("java.lang.IO") From b1017075ca454b99cb2e4c02ad6f48a7df7529f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Coet?= Date: Fri, 27 Feb 2026 13:29:01 +0100 Subject: [PATCH 9/9] Fix the error messages in the tests --- .../src/main/java/checks/ReadlnWithPromptCheckSample.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/java-checks-test-sources/default/src/main/java/checks/ReadlnWithPromptCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/ReadlnWithPromptCheckSample.java index adb1e9dcfa2..9aaa4408949 100644 --- a/java-checks-test-sources/default/src/main/java/checks/ReadlnWithPromptCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/ReadlnWithPromptCheckSample.java @@ -6,17 +6,17 @@ public class ReadlnWithPromptCheckSample { void nonCompliant() { IO.print("Enter your name: "); - String name = IO.readln(); // Noncompliant {{Use "IO.readln(prompt)" instead of separate "IO.print(prompt)" and "IO.readln()" calls.}} + String name = IO.readln(); // Noncompliant {{Use "IO.readln(String prompt)" instead of separate "IO.print(Object obj)" and "IO.readln()" calls.}} // ^^^^^^^^^^^ IO.print(name); IO.println("Enter your age: "); - String age = IO.readln(); // Noncompliant {{Use "IO.readln(prompt)" instead of separate "IO.println(prompt)" and "IO.readln()" calls.}} + String age = IO.readln(); // Noncompliant {{Use "IO.readln(String prompt)" instead of separate "IO.println(Object obj)" and "IO.readln()" calls.}} // ^^^^^^^^^^^ IO.print(age); IO.print("Enter city: "); - IO.readln(); // Noncompliant {{Use "IO.readln(prompt)" instead of separate "IO.print(prompt)" and "IO.readln()" calls.}} + IO.readln(); // Noncompliant {{Use "IO.readln(String prompt)" instead of separate "IO.print(Object obj)" and "IO.readln()" calls.}} // ^^^^^^^^^^^ }