Skip to content

Commit 002039f

Browse files
SONARJAVA-5483 implement S7475: RemoveTypeFromUnusedPatternCheck (#5093)
1 parent 2247e49 commit 002039f

8 files changed

Lines changed: 327 additions & 1 deletion

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"ruleKey": "S7475",
3+
"hasTruePositives": true,
4+
"falseNegatives": 0,
5+
"falsePositives": 0
6+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package checks;
2+
3+
import java.util.List;
4+
5+
public class RemoveTypeFromUnusedPatternCheckSample {
6+
record Guest(String name, String email, String phoneNumber) {
7+
}
8+
9+
record Bar(String a, int b){}
10+
record Foo(String a, Bar b){}
11+
12+
void nonCompliantExamples(Object o) {
13+
if (o instanceof Guest(String name, String _, String phoneNumber)) { // Noncompliant
14+
}
15+
16+
if(o instanceof Foo(String _, Bar b)){ // Noncompliant {{Remove unused type from unnamed pattern}}
17+
// ^^^^^^^^
18+
// fix@qf1 {{Remove unused type}}
19+
// edit@qf1 [[sc=25;ec=31]] {{}}
20+
if(b instanceof Bar(String a, int _)){} // Noncompliant {{Remove unused type from unnamed pattern}}
21+
}
22+
23+
if(o instanceof Foo(String s, Bar(String _, int b))) { // Noncompliant
24+
}
25+
26+
String s1 = switch (o) {
27+
case Guest(String name, String _, String phoneNumber) -> "Hello " + name + "!"; // Noncompliant
28+
default -> "Hello!";
29+
};
30+
31+
String s2 = switch (o) {
32+
case Bar(String s, int _) when s.isEmpty() -> ""; // Noncompliant
33+
// ^^^^^
34+
// fix@qf1 {{Remove unused type}}
35+
// edit@qf1 [[sc=26;ec=29]] {{}}
36+
default -> "Hello!";
37+
};
38+
39+
String s3 = switch (o) {
40+
case Foo(String s, Bar b) when b instanceof Bar(String v, int _) -> ""; // Noncompliant
41+
default -> "Hello!";
42+
};
43+
44+
45+
46+
String s4 = switch (o) {
47+
case Foo(String s, Bar(String _, int b)) -> ""; // Noncompliant
48+
default -> "Hello!";
49+
};
50+
}
51+
52+
record Primitives(double a, float b, int c, long d, float e, boolean g){}
53+
record ClassTypes(Object simple, List<Integer> parametrize, int[] array, List<List<Integer>> nested, List<?> wildcard){}
54+
record ParametrizeClass<A>(List<A> a){}
55+
56+
void nonCompliantDifferentTypes(Object o){
57+
if(o instanceof Primitives(double _, float a, int b, long c, float d, boolean f)){} // Noncompliant
58+
// ^^^^^^^^
59+
if(o instanceof Primitives(double a, float _, int b, long c, float d, boolean f)){} // Noncompliant
60+
// ^^^^^^^
61+
if(o instanceof Primitives(double a, float b, int _, long c, float d, boolean f)){} // Noncompliant
62+
// ^^^^^
63+
if(o instanceof Primitives(double a, float b, int c, long _, float d, boolean f)){} // Noncompliant
64+
// ^^^^^^
65+
if(o instanceof Primitives(double a, float b, int c, long d, float _, boolean f)){} // Noncompliant
66+
// ^^^^^^^
67+
if(o instanceof Primitives(double a, float b, int c, long d, float e, boolean _)){} // Noncompliant
68+
// ^^^^^^^^^
69+
if(o instanceof ClassTypes(Object _, List<Integer> parametrize, int[] array, List<List<Integer>> nested, List<?> wildcard)){} // Noncompliant
70+
// ^^^^^^^^
71+
if(o instanceof ClassTypes(Object simple, List<Integer> _, int[] array, List<List<Integer>> nested, List<?> wildcard)){} // Noncompliant
72+
// ^^^^^^^^^^^^^^^
73+
if(o instanceof ClassTypes(Object simple, List<Integer> parametrize, int[] _, List<List<Integer>> nested, List<?> wildcard)){} // Noncompliant
74+
// ^^^^^^^
75+
if(o instanceof ClassTypes(Object simple, List<Integer> parametrize, int[] array, List<List<Integer>> _, List<?> wildcard)){} // Noncompliant
76+
// ^^^^^^^^^^^^^^^^^^^^^
77+
if(o instanceof ClassTypes(Object simple, List<Integer> parametrize, int[] array, List<List<Integer>> nested, List<?> _)){} // Noncompliant
78+
// ^^^^^^^^^
79+
if(o instanceof ParametrizeClass(var _)){} // Noncompliant
80+
// ^^^^^
81+
}
82+
83+
void compliantExamples(Object o) {
84+
if (o instanceof Guest(String name, _, _)) { // compliant
85+
}
86+
87+
if(o instanceof Foo(_, Bar b)){ // compliant
88+
if(b instanceof Bar(String a, _)){} // compliant
89+
}
90+
91+
if(o instanceof Foo(String s, Bar(_, int b))) { // compliant
92+
}
93+
94+
95+
String s1 = switch (o) {
96+
case Guest(String name, _, _) -> "Hello " + name + "!"; // compliant
97+
default -> "Hello!";
98+
};
99+
100+
String s2 = switch (o) {
101+
case Foo(String s, Bar(_, int b)) -> ""; // compliant
102+
default -> "Hello!";
103+
};
104+
}
105+
106+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package checks;
2+
3+
public class RemoveTypeFromUnusedPatternCheck_Java21 {
4+
record Guest(String name, String email, String phoneNumber) {
5+
}
6+
public void foo(Object o){
7+
if(o instanceof Guest(String name, String email, String phoneNumber)){} // compliant not java 22
8+
}
9+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource SA
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.helpers.QuickFixHelper;
22+
import org.sonar.java.reporting.JavaQuickFix;
23+
import org.sonar.java.reporting.JavaTextEdit;
24+
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
25+
import org.sonar.plugins.java.api.JavaVersion;
26+
import org.sonar.plugins.java.api.JavaVersionAwareVisitor;
27+
import org.sonar.plugins.java.api.tree.InferedTypeTree;
28+
import org.sonar.plugins.java.api.tree.PatternTree;
29+
import org.sonar.plugins.java.api.tree.RecordPatternTree;
30+
import org.sonar.plugins.java.api.tree.Tree;
31+
import org.sonar.plugins.java.api.tree.TypePatternTree;
32+
import org.sonar.plugins.java.api.tree.TypeTree;
33+
import org.sonar.plugins.java.api.tree.VariableTree;
34+
35+
@Rule(key = "S7475")
36+
public class RemoveTypeFromUnusedPatternCheck extends IssuableSubscriptionVisitor implements JavaVersionAwareVisitor {
37+
38+
@Override
39+
public List<Tree.Kind> nodesToVisit() {
40+
return List.of(Tree.Kind.RECORD_PATTERN);
41+
}
42+
43+
@Override
44+
public void visitNode(Tree tree) {
45+
RecordPatternTree pattern = (RecordPatternTree) tree;
46+
for (PatternTree patternTree : pattern.patterns()) {
47+
if (patternTree instanceof TypePatternTree pat
48+
&& pat.patternVariable().simpleName().isUnnamedVariable()
49+
&& !typeIsMissing(pat.patternVariable())) {
50+
QuickFixHelper.newIssue(context)
51+
.forRule(this)
52+
.onTree(pat.patternVariable())
53+
.withMessage("Remove unused type from unnamed pattern")
54+
.withQuickFix(() -> getQuickFix(pat.patternVariable().type()))
55+
.report();
56+
}
57+
}
58+
59+
}
60+
61+
@Override
62+
public boolean isCompatibleWithJavaVersion(JavaVersion version) {
63+
return version.isJava22Compatible();
64+
}
65+
66+
private static JavaQuickFix getQuickFix(TypeTree tree) {
67+
return JavaQuickFix.newQuickFix("Remove unused type")
68+
.addTextEdit(JavaTextEdit.removeTree(tree))
69+
.build();
70+
}
71+
72+
private static boolean typeIsMissing(VariableTree v) {
73+
return v.type() instanceof InferedTypeTree;
74+
}
75+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* SonarQube Java
3+
* Copyright (C) 2012-2025 SonarSource SA
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 RemoveTypeFromUnusedPatternCheckTest {
25+
26+
@Test
27+
void test() {
28+
CheckVerifier.newVerifier()
29+
.onFile(mainCodeSourcesPath("checks/RemoveTypeFromUnusedPatternCheckSample.java"))
30+
.withCheck(new RemoveTypeFromUnusedPatternCheck())
31+
.withJavaVersion(22)
32+
.verifyIssues();
33+
}
34+
35+
@Test
36+
void test_java21() {
37+
CheckVerifier.newVerifier()
38+
.onFile(mainCodeSourcesPath("checks/RemoveTypeFromUnusedPatternCheck_Java21.java"))
39+
.withCheck(new RemoveTypeFromUnusedPatternCheck())
40+
.withJavaVersion(21)
41+
.verifyNoIssues();
42+
}
43+
44+
@Test
45+
void test_without_semantic() {
46+
CheckVerifier.newVerifier()
47+
.onFile(mainCodeSourcesPath("checks/RemoveTypeFromUnusedPatternCheckSample.java"))
48+
.withCheck(new RemoveTypeFromUnusedPatternCheck())
49+
.withJavaVersion(22)
50+
.withoutSemantic()
51+
.verifyIssues();
52+
}
53+
54+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<h2>Why is this an issue?</h2>
2+
<p>When using pattern matching on records, matching is done against the canonical constructor of the record. This implies listing all the components
3+
in the canonical constructor even if some are unused. To make the intent of not using the component clear, Java 22 introduced the unnamed variable
4+
pattern <code>_</code>.</p>
5+
<p>Because we can only pattern match against the canonical constructor, there is no need to disambiguate by specifying the types of its parameters.
6+
Therefore, the type of unused variables in pattern matching should be omitted, as it does not bring additional value.</p>
7+
<h2>How to fix it</h2>
8+
<p>Remove the type of the unused component.</p>
9+
<h3>Code examples</h3>
10+
<h4>Noncompliant code example</h4>
11+
<pre data-diff-id="1" data-diff-type="noncompliant">
12+
record Guest(String name, String email, String phoneNumber) {}
13+
14+
String greet(Object o) {
15+
if (o instanceof Guest(String name, String _, String _)) { // Noncompliant
16+
return "Hello " + name + "!";
17+
}
18+
return "Hello!";
19+
}
20+
21+
String switchToGreet(Object o) {
22+
return switch (o) {
23+
case Guest(String name, String _, String _) -&gt; "Hello " + name + "!"; // Noncompliant
24+
default -&gt; "Hello!";
25+
};
26+
}
27+
</pre>
28+
<h4>Compliant solution</h4>
29+
<pre data-diff-id="1" data-diff-type="compliant">
30+
record Guest(String name, String email, String phoneNumber) {}
31+
32+
String greet(Object o) {
33+
if (o instanceof Guest(String name, _, _)) {
34+
return "Hello " + name + "!";
35+
}
36+
return "Hello!";
37+
}
38+
39+
String switchToGreet(Object o) {
40+
return switch (o) {
41+
case Guest(String name, _, _) -&gt; "Hello " + name + "!";
42+
default -&gt; "Hello!";
43+
};
44+
}
45+
</pre>
46+
<h2>Resources</h2>
47+
<h3>Documentation</h3>
48+
<ul>
49+
<li> <a href="https://openjdk.org/jeps/456">JEP 456: Unnamed Variables &amp; Patterns</a> </li>
50+
</ul>
51+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"title": "Types of unused record components should be removed from pattern matching",
3+
"type": "CODE_SMELL",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [
10+
"java22",
11+
"unused"
12+
],
13+
"defaultSeverity": "Info",
14+
"ruleSpecification": "RSPEC-7475",
15+
"sqKey": "S7475",
16+
"scope": "All",
17+
"quickfix": "targeted",
18+
"code": {
19+
"impacts": {
20+
"MAINTAINABILITY": "INFO"
21+
},
22+
"attribute": "CLEAR"
23+
}
24+
}

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
@@ -505,6 +505,7 @@
505505
"S7409",
506506
"S7435",
507507
"S7466",
508-
"S7467"
508+
"S7467",
509+
"S7475"
509510
]
510511
}

0 commit comments

Comments
 (0)