Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2896,5 +2896,11 @@
"hasTruePositives": false,
"falseNegatives": 0,
"falsePositives": 0
},
{
"ruleKey": "S8469",
"hasTruePositives": false,
"falseNegatives": 0,
"falsePositives": 0
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ruleKey": "S8469",
"hasTruePositives": false,
"falseNegatives": 0,
"falsePositives": 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package checks;

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(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(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(String prompt)" instead of separate "IO.print(Object obj)" 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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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(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")
.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();
Tree grandParent = parent.parent();
if (grandParent == null || !grandParent.is(Tree.Kind.BLOCK)) {
return;
}

BlockTree block = (BlockTree) grandParent;
List<StatementTree> 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);
}
}

}
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<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
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
<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>
<h2>Why is this an issue?</h2>
<p>Using the prompt-aware overload is more idiomatic, reduces boilerplate, and explicitly links the prompt text to the input request in a single
operation.</p>
<h2>How to fix it</h2>
<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
call to <code>IO.readln(String prompt)</code>.</p>
<h3>Code examples</h3>
<h4>Noncompliant code example</h4>
<pre data-diff-id="1" data-diff-type="noncompliant">
// 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);
}
</pre>
<h4>Compliant solution</h4>
<pre data-diff-id="1" data-diff-type="compliant">
// 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);
}
</pre>
<h2>Resources</h2>
<h3>Documentation</h3>
<ul>
<li><a href="https://openjdk.org/jeps/512">OpenJDK - JEP 512</a></li>
<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>
</ul>

Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,7 @@
"S8445",
"S8446",
"S8447",
"S8450"
"S8450",
"S8469"
]
}
Loading