Skip to content

Commit 53fc4bb

Browse files
SONARJAVA-6104 S8469 Use IO.readln(String prompt) instead of IO.print(Object obj) followed by IO.readln() (#5491)
1 parent 49377ab commit 53fc4bb

File tree

9 files changed

+237
-2
lines changed

9 files changed

+237
-2
lines changed

its/autoscan/src/test/java/org/sonar/java/it/AutoScanTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ public void javaCheckTestSources() throws Exception {
199199
softly.assertThat(newDiffs).containsExactlyInAnyOrderElementsOf(knownDiffs.values());
200200
softly.assertThat(newTotal).isEqualTo(knownTotal);
201201
softly.assertThat(rulesCausingFPs).hasSize(10);
202-
softly.assertThat(rulesNotReporting).hasSize(19);
202+
softly.assertThat(rulesNotReporting).hasSize(20);
203203

204204
/**
205205
* 4. Check total number of differences (FPs + FNs)

its/autoscan/src/test/resources/autoscan/autoscan-diff-by-rules.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2902,5 +2902,11 @@
29022902
"hasTruePositives": true,
29032903
"falseNegatives": 0,
29042904
"falsePositives": 0
2905+
},
2906+
{
2907+
"ruleKey": "S8469",
2908+
"hasTruePositives": false,
2909+
"falseNegatives": 0,
2910+
"falsePositives": 0
29052911
}
29062912
]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ruleKey": "S8469",
3+
"hasTruePositives": false,
4+
"falseNegatives": 0,
5+
"falsePositives": 0
6+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package checks;
2+
3+
public class ReadlnWithPromptCheckSample {
4+
5+
private static final String text = IO.readln("Enter your name: "); // Compliant
6+
7+
void nonCompliant() {
8+
IO.print("Enter your name: ");
9+
String name = IO.readln(); // Noncompliant {{Use "IO.readln(String prompt)" instead of separate "IO.print(Object obj)" and "IO.readln()" calls.}}
10+
// ^^^^^^^^^^^
11+
IO.print(name);
12+
13+
IO.println("Enter your age: ");
14+
String age = IO.readln(); // Noncompliant {{Use "IO.readln(String prompt)" instead of separate "IO.println(Object obj)" and "IO.readln()" calls.}}
15+
// ^^^^^^^^^^^
16+
IO.print(age);
17+
18+
IO.print("Enter city: ");
19+
IO.readln(); // Noncompliant {{Use "IO.readln(String prompt)" instead of separate "IO.print(Object obj)" and "IO.readln()" calls.}}
20+
// ^^^^^^^^^^^
21+
}
22+
23+
void compliant() {
24+
IO.readln(); // Compliant
25+
26+
String name = IO.readln("Enter your name: "); // Compliant
27+
28+
IO.println("Welcome!");
29+
String input = IO.readln("Please state your name:"); // Compliant
30+
31+
IO.println();
32+
IO.readln(); // Compliant
33+
34+
IO.print("Enter email: ");
35+
int x = 5;
36+
String email = IO.readln(); // Compliant
37+
}
38+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java.checks;
18+
19+
import java.util.List;
20+
import org.sonar.check.Rule;
21+
import org.sonar.java.checks.methods.AbstractMethodDetection;
22+
import org.sonar.plugins.java.api.JavaVersion;
23+
import org.sonar.plugins.java.api.JavaVersionAwareVisitor;
24+
import org.sonar.plugins.java.api.semantic.MethodMatchers;
25+
import org.sonar.plugins.java.api.tree.BlockTree;
26+
import org.sonar.plugins.java.api.tree.ExpressionStatementTree;
27+
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
28+
import org.sonar.plugins.java.api.tree.StatementTree;
29+
import org.sonar.plugins.java.api.tree.Tree;
30+
31+
@Rule(key = "S8469")
32+
public class ReadlnWithPromptCheck extends AbstractMethodDetection implements JavaVersionAwareVisitor {
33+
34+
private static final String MESSAGE = "Use \"IO.readln(String prompt)\" instead of separate \"IO.%s(Object obj)\" and \"IO.readln()\" calls.";
35+
36+
private static final MethodMatchers PRINT_MATCHERS = MethodMatchers.create()
37+
.ofTypes("java.lang.IO")
38+
.names("print", "println")
39+
.addParametersMatcher("java.lang.Object")
40+
.build();
41+
42+
private static final MethodMatchers READLN_MATCHER = MethodMatchers.create()
43+
.ofTypes("java.lang.IO")
44+
.names("readln")
45+
.addWithoutParametersMatcher()
46+
.build();
47+
48+
@Override
49+
public boolean isCompatibleWithJavaVersion(JavaVersion version) {
50+
return version.isJava25Compatible();
51+
}
52+
53+
@Override
54+
protected MethodMatchers getMethodInvocationMatchers() {
55+
return READLN_MATCHER;
56+
}
57+
58+
@Override
59+
protected void onMethodInvocationFound(MethodInvocationTree mit) {
60+
Tree parent = mit.parent();
61+
Tree grandParent = parent.parent();
62+
if (grandParent == null || !grandParent.is(Tree.Kind.BLOCK)) {
63+
return;
64+
}
65+
66+
BlockTree block = (BlockTree) grandParent;
67+
List<StatementTree> statements = block.body();
68+
69+
int currentIndex = statements.indexOf(parent);
70+
if (currentIndex <= 0) {
71+
return;
72+
}
73+
74+
StatementTree previousStatement = statements.get(currentIndex - 1);
75+
if (previousStatement instanceof ExpressionStatementTree exprStmt
76+
&& exprStmt.expression() instanceof MethodInvocationTree previousCall
77+
&& PRINT_MATCHERS.matches(previousCall)) {
78+
String message = String.format(MESSAGE, previousCall.methodSymbol().name());
79+
reportIssue(mit, message);
80+
}
81+
}
82+
83+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.java.checks;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.sonar.java.checks.verifier.CheckVerifier;
21+
22+
import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath;
23+
24+
class ReadlnWithPromptCheckTest {
25+
26+
@Test
27+
void test() {
28+
CheckVerifier.newVerifier()
29+
.onFile(mainCodeSourcesPath("checks/ReadlnWithPromptCheckSample.java"))
30+
.withCheck(new ReadlnWithPromptCheck())
31+
.withJavaVersion(25)
32+
.verifyIssues();
33+
}
34+
35+
@Test
36+
void test_java24() {
37+
CheckVerifier.newVerifier()
38+
.onFile(mainCodeSourcesPath("checks/ReadlnWithPromptCheckSample.java"))
39+
.withCheck(new ReadlnWithPromptCheck())
40+
.withJavaVersion(24)
41+
.verifyNoIssues();
42+
}
43+
44+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<p>The <code>java.io.IO</code> class in Java 25+ (introduced via JEP 512) provides a simplified interface for console interaction. It is common to see
2+
code that prints a message to the console using <code>IO.print(Object obj)</code> or <code>IO.println(Object obj)</code> and then immediately calls
3+
<code>IO.readln()</code> to wait for user input. This sequence should be replaced with the single call <code>IO.readln(String prompt)</code>.</p>
4+
<h2>Why is this an issue?</h2>
5+
<p>Using the prompt-aware overload is more idiomatic, reduces boilerplate, and explicitly links the prompt text to the input request in a single
6+
operation.</p>
7+
<h2>How to fix it</h2>
8+
<p>Remove calls to <code>IO.print(Object obj)</code> or <code>IO.println(Object obj)</code> directly followed <code>IO.readln()</code> with a single
9+
call to <code>IO.readln(String prompt)</code>.</p>
10+
<h3>Code examples</h3>
11+
<h4>Noncompliant code example</h4>
12+
<pre data-diff-id="1" data-diff-type="noncompliant">
13+
// Compact source file
14+
void main() {
15+
// Non-compliant: manual prompt followed by readln() with no arguments
16+
IO.print("Please enter your username: ");
17+
String user = IO.readln();
18+
IO.println("Welcome, " + user);
19+
}
20+
</pre>
21+
<h4>Compliant solution</h4>
22+
<pre data-diff-id="1" data-diff-type="compliant">
23+
// Compact source file
24+
void main() {
25+
// Compliant: The prompt is directly passed to the readln method
26+
String user = IO.readln("Please enter your username: ");
27+
IO.println("Welcome, " + user);
28+
}
29+
</pre>
30+
<h2>Resources</h2>
31+
<h3>Documentation</h3>
32+
<ul>
33+
<li><a href="https://openjdk.org/jeps/512">OpenJDK - JEP 512</a></li>
34+
<li><a href="https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/IO.html">Java 25 API Documentation - java.io.IO</a></li>
35+
</ul>
36+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"title": "Prefer \"IO.readln(String prompt)\" rather than \"IO.print(Object obj)\" or \"IO.println(Object obj)\" followed by \"IO.readln()\"",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [],
10+
"defaultSeverity": "Minor",
11+
"ruleSpecification": "RSPEC-8469",
12+
"sqKey": "S8469",
13+
"scope": "All",
14+
"quickfix": "unknown",
15+
"code": {
16+
"impacts": {
17+
"MAINTAINABILITY": "LOW"
18+
},
19+
"attribute": "CONVENTIONAL"
20+
}
21+
}

sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/Sonar_way_profile.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@
526526
"S8446",
527527
"S8447",
528528
"S8450",
529-
"S8465"
529+
"S8465",
530+
"S8469"
530531
]
531532
}

0 commit comments

Comments
 (0)