Skip to content

Commit 675fcaf

Browse files
authored
SONARJAVA-6100 : Implement rule S8450 - Use IO.readln() for console input instead of BufferedReader boilerplate (#5460)
1 parent 024502c commit 675fcaf

9 files changed

Lines changed: 267 additions & 2 deletions

File tree

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(17);
202+
softly.assertThat(rulesNotReporting).hasSize(18);
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
@@ -2878,5 +2878,11 @@
28782878
"hasTruePositives": false,
28792879
"falseNegatives": 0,
28802880
"falsePositives": 0
2881+
},
2882+
{
2883+
"ruleKey": "S8450",
2884+
"hasTruePositives": false,
2885+
"falseNegatives": 0,
2886+
"falsePositives": 0
28812887
}
28822888
]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ruleKey": "S8450",
3+
"hasTruePositives": false,
4+
"falseNegatives": 0,
5+
"falsePositives": 0
6+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package checks;
2+
3+
import java.io.BufferedReader;
4+
import java.io.IO;
5+
import java.io.InputStreamReader;
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import java.io.Reader;
9+
10+
class BufferedReaderBoilerplateCheckSample {
11+
void noncompliantBasic() throws IOException {
12+
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); // Noncompliant {{Use "IO.readln()" instead of this "BufferedReader" boilerplate.}}
13+
String line = reader.readLine();
14+
}
15+
16+
void compliantBasic() throws IOException {
17+
String line = IO.readln();
18+
}
19+
20+
void noncompliantWithPrompt() throws IOException {
21+
System.out.print("Enter text: ");
22+
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); // Noncompliant
23+
String line = reader.readLine();
24+
}
25+
26+
void noncompliantInTryCatch() {
27+
try {
28+
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); // Noncompliant
29+
String line = reader.readLine();
30+
} catch (IOException ioe) {
31+
ioe.printStackTrace();
32+
}
33+
}
34+
35+
void noncompliantInTryWithResources() throws IOException {
36+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) { // Noncompliant
37+
String line = reader.readLine();
38+
}
39+
}
40+
41+
void noncompliantInlineUsage() throws IOException {
42+
String line = new BufferedReader(new InputStreamReader(System.in)).readLine(); // Noncompliant
43+
}
44+
45+
void compliantWithFileStream(InputStream fileStream) throws IOException {
46+
BufferedReader reader = new BufferedReader(new InputStreamReader(fileStream)); // Compliant - not wrapping System.in
47+
String line = reader.readLine();
48+
}
49+
50+
void compliantWithExistingReader(Reader existingReader) throws IOException {
51+
BufferedReader reader = new BufferedReader(existingReader); // Compliant - not wrapping System.in via InputStreamReader
52+
String line = reader.readLine();
53+
}
54+
55+
void compliantNoSystemIn() throws IOException {
56+
InputStream other = new java.io.ByteArrayInputStream(new byte[0]);
57+
BufferedReader reader = new BufferedReader(new InputStreamReader(other)); // Compliant - not System.in
58+
String line = reader.readLine();
59+
}
60+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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.ExpressionTree;
26+
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
27+
import org.sonar.plugins.java.api.tree.NewClassTree;
28+
import org.sonar.plugins.java.api.tree.Tree;
29+
30+
@Rule(key = "S8450")
31+
public class BufferedReaderBoilerplateCheck extends AbstractMethodDetection implements JavaVersionAwareVisitor {
32+
33+
private static final MethodMatchers BUFFERED_READER_CONSTRUCTOR = MethodMatchers.create()
34+
.ofTypes("java.io.BufferedReader")
35+
.constructor()
36+
.addParametersMatcher("java.io.Reader")
37+
.build();
38+
39+
private static final MethodMatchers INPUT_STREAM_READER_CONSTRUCTOR = MethodMatchers.create()
40+
.ofTypes("java.io.InputStreamReader")
41+
.constructor()
42+
.addParametersMatcher("java.io.InputStream")
43+
.build();
44+
45+
@Override
46+
public boolean isCompatibleWithJavaVersion(JavaVersion version) {
47+
return version.isJava25Compatible();
48+
}
49+
50+
@Override
51+
public List<Tree.Kind> nodesToVisit() {
52+
return List.of(Tree.Kind.NEW_CLASS);
53+
}
54+
55+
@Override
56+
protected MethodMatchers getMethodInvocationMatchers() {
57+
return BUFFERED_READER_CONSTRUCTOR;
58+
}
59+
60+
@Override
61+
protected void onConstructorFound(NewClassTree newClassTree) {
62+
if (newClassTree.arguments().get(0) instanceof NewClassTree innerNewClass
63+
&& INPUT_STREAM_READER_CONSTRUCTOR.matches(innerNewClass)
64+
&& isSystemIn(innerNewClass.arguments().get(0))) {
65+
reportIssue(newClassTree, "Use \"IO.readln()\" instead of this \"BufferedReader\" boilerplate.");
66+
}
67+
}
68+
69+
private static boolean isSystemIn(ExpressionTree expression) {
70+
return expression instanceof MemberSelectExpressionTree memberSelect
71+
&&"in".equals(memberSelect.identifier().name())
72+
&& memberSelect.expression().symbolType().is("java.lang.System");
73+
}
74+
}
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.nonCompilingTestSourcesPath;
23+
24+
class BufferedReaderBoilerplateCheckTest {
25+
26+
@Test
27+
void test() {
28+
CheckVerifier.newVerifier()
29+
.onFile(nonCompilingTestSourcesPath("checks/BufferedReaderBoilerplateCheckSample.java"))
30+
.withCheck(new BufferedReaderBoilerplateCheck())
31+
.withJavaVersion(25)
32+
.verifyIssues();
33+
}
34+
35+
@Test
36+
void test_jav24() {
37+
CheckVerifier.newVerifier()
38+
.onFile(nonCompilingTestSourcesPath("checks/BufferedReaderBoilerplateCheckSample.java"))
39+
.withCheck(new BufferedReaderBoilerplateCheck())
40+
.withJavaVersion(24)
41+
.verifyNoIssues();
42+
}
43+
44+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<p>The <code>java.io.IO</code> class provides simple methods for interactive console I/O. For standard console input, <code>IO.readln()</code> should
2+
be preferred over the traditional, more verbose <code>BufferedReader</code>.</p>
3+
<h2>Why is this an issue?</h2>
4+
<p>Traditionally, reading a line from the console required significant boilerplate code, involving <code>InputStreamReader</code>,
5+
<code>BufferedReader</code>, and explicit <code>IOException</code> handling. This pattern is not only verbose but also error-prone and harder to
6+
read.</p>
7+
<p>The modern <code>IO.readln()</code> method:</p>
8+
<ul>
9+
<li> Reduces boilerplate by providing a single-method call for reading lines. </li>
10+
<li> Eliminates the need for explicit checked exception handling for standard console input. </li>
11+
<li> Allows for an optional prompt string, combining the "print" and "read" steps into one cohesive operation. </li>
12+
<li> Avoids the resource management overhead (like <code>try-with-resources</code>) required for manual stream wrappers. </li>
13+
</ul>
14+
<h2>How to fix it</h2>
15+
<p>Replace manual stream wrapping of <code>System.in</code> with the static <code>IO.readln()</code> method. If you are using a version of Java where
16+
<code>IO</code> is automatically imported (such as in JShell or modern entry points), you can call <code>readln()</code> directly; otherwise, use
17+
<code>import static java.io.IO.readln;</code>.</p>
18+
<h3>Code examples</h3>
19+
<h4>Noncompliant code example</h4>
20+
<pre data-diff-id="1" data-diff-type="noncompliant">
21+
import java.io.BufferedReader;
22+
import java.io.InputStreamReader;
23+
import java.io.IOException;
24+
25+
void main() {
26+
try {
27+
System.out.print("Enter text: ");
28+
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
29+
String line = reader.readLine(); // Noncompliant: verbose boilerplate
30+
System.out.println(line);
31+
} catch (IOException ioe) {
32+
ioe.printStackTrace();
33+
}
34+
}
35+
</pre>
36+
<h4>Compliant solution</h4>
37+
<pre data-diff-id="1" data-diff-type="compliant">
38+
import static java.io.IO.readln;
39+
40+
void main() {
41+
// Compliant: concise and readable console input
42+
String line = readln("Enter text: ");
43+
System.out.println(line);
44+
}
45+
</pre>
46+
<h2>Resources</h2>
47+
<h3>Documentation</h3>
48+
<ul>
49+
<li> <a href="https://openjdk.org/jeps/512">JEP 512: Compact Source Files and Instance Main Methods</a> </li>
50+
</ul>
51+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"title": "Prefer \"IO.readln()\" over \"BufferedReader\" boilerplate",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [
10+
"java25"
11+
],
12+
"defaultSeverity": "Minor",
13+
"ruleSpecification": "RSPEC-8450",
14+
"sqKey": "S8450",
15+
"scope": "All",
16+
"quickfix": "unknown",
17+
"code": {
18+
"impacts": {
19+
"MAINTAINABILITY": "LOW"
20+
},
21+
"attribute": "CONVENTIONAL"
22+
}
23+
}

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
@@ -522,6 +522,7 @@
522522
"S8432",
523523
"S8433",
524524
"S8444",
525-
"S8445"
525+
"S8445",
526+
"S8450"
526527
]
527528
}

0 commit comments

Comments
 (0)