Skip to content

Commit b3407bb

Browse files
SONARJAVA-5985 Improve S6207's logic for constructors (#5512)
1 parent f5247f5 commit b3407bb

3 files changed

Lines changed: 142 additions & 64 deletions

File tree

java-checks-test-sources/default/src/main/java/checks/RedundantRecordMethodsCheckSample.java

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@ record RedundantConstructorAndGetters(String name, int age) {
1414

1515
RedundantConstructorAndGetters(String name, int age) { // Noncompliant {{Remove this redundant constructor which is the same as a default one.}}
1616
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
17-
System.out.println("Just printing something...");
1817
this.name = name;
19-
int x = 42;
20-
variable = new Object();
21-
this.someOtherVariable = new Object();
2218
this.age = age;
2319
}
2420

@@ -50,6 +46,28 @@ public int age() { // Noncompliant {{Remove this redundant method which is the s
5046
}
5147
}
5248

49+
record ConstructorWithSideEffects(String name, int age) {
50+
ConstructorWithSideEffects(String name, int age) { // Noncompliant {{Consider using a compact constructor here.}}
51+
sideEffect();
52+
this.name = name;
53+
this.age = age;
54+
}
55+
56+
public void sideEffect() {
57+
System.out.println("Here's a side effect!");
58+
}
59+
}
60+
61+
record ThrowingConstructor(String name, int age) {
62+
ThrowingConstructor(String name, int age) { // Noncompliant {{Consider using a compact constructor here.}}
63+
if (age < 0) {
64+
throw new IllegalArgumentException("Negative age");
65+
}
66+
this.name = name;
67+
this.age = age;
68+
}
69+
}
70+
5371
record ParameterMismatch(String name, String address) {
5472
ParameterMismatch(String name, String address) { // Compliant
5573
this.name = address;
@@ -85,7 +103,7 @@ record ConstructorAssignsStaticValue(String name, int age) {
85103
}
86104

87105
record EmptyConstructorAndRedundantGetter(String name, int age) {
88-
EmptyConstructorAndRedundantGetter { // Noncompliant {{Remove this redundant constructor which is the same as a default one.}}
106+
EmptyConstructorAndRedundantGetter { // Noncompliant {{Remove this useless empty compact constructor.}}
89107
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
90108
}
91109

@@ -105,6 +123,17 @@ record CompliantConstructorWithAddedValue(String name, int age) {
105123
}
106124
}
107125

126+
record CompliantConstructorWithSideEffect(String name, int age) {
127+
CompliantConstructorWithSideEffect(String name, int age) { // Compliant: the constructor includes side effects after assignments
128+
if (age < 0) {
129+
throw new IllegalArgumentException("Negative age");
130+
}
131+
this.name = name.toLowerCase(Locale.ROOT);
132+
this.age = age;
133+
System.out.println("Hello");
134+
}
135+
}
136+
108137
record CompliantConstructorComplementAndTransformativeGetter(String name, int age) {
109138
CompliantConstructorComplementAndTransformativeGetter { // Compliant
110139
if (age < 0) {

java-checks/src/main/java/org/sonar/java/checks/RedundantRecordMethodsCheck.java

Lines changed: 98 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,35 @@ public void visitNode(Tree tree) {
6262

6363
for (Tree member : targetRecord.members()) {
6464
if (member.is(Tree.Kind.CONSTRUCTOR)) {
65-
checkConstructor((MethodTree) member, components);
65+
checkConstructor((MethodTree) member, componentNames);
6666
} else if (member.is(Tree.Kind.METHOD)) {
6767
checkMethod((MethodTree) member, components, componentNames);
6868
}
6969
}
7070
}
7171

72-
private void checkConstructor(MethodTree constructor, List<Symbol.VariableSymbol> components) {
73-
if(isAnnotated(constructor)) {
72+
private void checkConstructor(MethodTree constructor, Set<String> componentNames) {
73+
if (isAnnotated(constructor)) {
7474
return;
7575
}
76-
if (constructor.block().body().isEmpty() || onlyDoesSimpleAssignments(constructor, components)) {
76+
if (constructor.parameters().isEmpty() && constructor.block().body().isEmpty()) {
77+
reportIssue(constructor.simpleName(), "Remove this useless empty compact constructor.");
78+
return;
79+
}
80+
if (constructor.parameters().size() != componentNames.size()
81+
|| constructor.parameters().stream().anyMatch(parameter -> !componentNames.contains(parameter.symbol().name()))) {
82+
return;
83+
}
84+
85+
ConstructorExecutionState executionState = new ConstructorExecutionState(componentNames);
86+
constructor.block().body().forEach(executionState::applyStatement);
87+
88+
if (!executionState.componentsAreFullyAssigned() || executionState.logicAfterAssignments) {
89+
return;
90+
}
91+
if (executionState.logicBeforeAssignments) {
92+
reportIssue(constructor.simpleName(), "Consider using a compact constructor here.");
93+
} else {
7794
reportIssue(constructor.simpleName(), "Remove this redundant constructor which is the same as a default one.");
7895
}
7996
}
@@ -112,55 +129,101 @@ private static Optional<ReturnStatementTree> getFirstReturnStatement(MethodTree
112129
.findFirst();
113130
}
114131

115-
public static boolean onlyDoesSimpleAssignments(MethodTree constructor, List<Symbol.VariableSymbol> components) {
116-
if (constructor.parameters().size() != components.size()) {
117-
return false;
118-
}
119-
List<String> componentNames = components.stream().map(Symbol.VariableSymbol::name).toList();
120-
ConstructorExecutionState executionState = new ConstructorExecutionState(componentNames);
121-
constructor.block().body().forEach(executionState::applyStatement);
122-
return executionState.componentsAreFullyAssigned();
123-
}
124-
125132
private static boolean isAnnotated(MethodTree method) {
126133
return !method.modifiers().annotations().isEmpty();
127134
}
128135

129136
/**
130-
* Class to perform a simple symbolic execution of a record constructor. The state keeps track of which components have been
131-
* assigned to the corresponding parameters and which parameters have been changed from their initial values.
137+
* Class to perform a simple symbolic execution of a record constructor. The state keeps track of which components have been assigned to the corresponding parameters and which
138+
* parameters have been changed from their initial values, as well as if the constructor contains logic before or after assignments to components.
132139
*/
133140
private static class ConstructorExecutionState {
134141
/**
135-
* Immutable list of the components in the record
142+
* Immutable set of the names of components in the record.
143+
*/
144+
private final Set<String> componentNames;
145+
146+
/**
147+
* Set of names of components of the record that have been assigned to their corresponding parameter so far.
136148
*/
137-
final List<String> componentNames;
149+
private final Set<String> assignedComponents = new HashSet<>();
138150

139151
/**
140-
* Set of names of components of the record that have been assigned to the corresponding parameter so far
152+
* Set of names of parameters that still hold the value of the argument passed to the constructor.
141153
*/
142-
Set<String> assignedComponents;
154+
private final Set<String> unchangedParameters = new HashSet<>();
143155

144156
/**
145-
* Set of name of parameters that still hold the value of the argument passed to the constructor
157+
* Flag indicating whether the constructor has logic before any of the component assignments.
146158
*/
147-
Set<String> unchangedParameters;
159+
boolean logicBeforeAssignments = false;
148160

149-
private ConstructorExecutionState(List<String> componentNames) {
150-
assignedComponents = new HashSet<>();
161+
/**
162+
* Flag indicating whether the constructor has logic after any of the component assignments.
163+
*/
164+
boolean logicAfterAssignments = false;
165+
166+
private ConstructorExecutionState(Set<String> componentNames) {
151167
this.componentNames = componentNames;
152-
unchangedParameters = new HashSet<>();
153168
unchangedParameters.addAll(componentNames);
154169
}
155170

156-
private boolean isParameter(Symbol symbol) {
157-
return componentNames.contains(symbol.name());
171+
public boolean componentsAreFullyAssigned() {
172+
return assignedComponents.size() == componentNames.size();
173+
}
174+
175+
public void applyStatement(StatementTree statement) {
176+
if (statement instanceof ExpressionStatementTree expression
177+
&& expression.expression() instanceof AssignmentExpressionTree assignment) {
178+
applyAssignment(assignment.variable(), assignment.expression());
179+
} else if (statement instanceof IfStatementTree ifStatement) {
180+
applyIf(ifStatement);
181+
} else if (statement instanceof BlockTree block) {
182+
block.body().forEach(this::applyStatement);
183+
} else if (this.assignedComponents.isEmpty()) {
184+
applyLogic();
185+
}
186+
}
187+
188+
private void applyAssignment(ExpressionTree lhs, ExpressionTree rhs) {
189+
if (rhs instanceof IdentifierTree identifier
190+
&& isParameter(identifier.symbol())
191+
&& unchangedParameters.contains(identifier.name())) {
192+
Optional<String> component = getComponent(lhs).filter(name -> name.equals(identifier.name()));
193+
if (component.isPresent()) {
194+
assignedComponents.add(component.get());
195+
} else {
196+
applyLogic();
197+
}
198+
} else if (lhs instanceof IdentifierTree identifier && isParameter(identifier.symbol())) {
199+
unchangedParameters.remove(identifier.name());
200+
} else {
201+
applyLogic();
202+
}
203+
}
204+
205+
private void applyIf(IfStatementTree ifStatement) {
206+
ConstructorExecutionState stateCopy = copy();
207+
applyStatement(ifStatement.thenStatement());
208+
StatementTree elseStatement = ifStatement.elseStatement();
209+
if (elseStatement != null) {
210+
stateCopy.applyStatement(elseStatement);
211+
}
212+
mergeWith(stateCopy);
213+
}
214+
215+
private void applyLogic() {
216+
if (this.assignedComponents.isEmpty()) {
217+
logicBeforeAssignments = true;
218+
} else {
219+
logicAfterAssignments = true;
220+
}
158221
}
159222

160223
/**
161224
* Return the name of the component if the given expression is of the form `this.name`.
162225
*/
163-
public Optional<String> getComponent(ExpressionTree expression) {
226+
private Optional<String> getComponent(ExpressionTree expression) {
164227
if (expression instanceof MemberSelectExpressionTree memberSelect) {
165228
String name = memberSelect.identifier().name();
166229
if (componentNames.contains(name)) {
@@ -170,54 +233,30 @@ public Optional<String> getComponent(ExpressionTree expression) {
170233
return Optional.empty();
171234
}
172235

173-
private void applyAssignment(ExpressionTree lhs, ExpressionTree rhs) {
174-
if (rhs instanceof IdentifierTree identifier
175-
&& isParameter(identifier.symbol())
176-
&& unchangedParameters.contains(identifier.name())) {
177-
getComponent(lhs)
178-
.filter(name -> name.equals(identifier.name()))
179-
.ifPresent(assignedComponents::add);
180-
} else if (lhs instanceof IdentifierTree identifier && isParameter(identifier.symbol())) {
181-
unchangedParameters.remove(identifier.name());
182-
}
236+
private boolean isParameter(Symbol symbol) {
237+
return componentNames.contains(symbol.name());
183238
}
184239

185240
private ConstructorExecutionState copy() {
186241
var copy = new ConstructorExecutionState(componentNames);
187242
copy.unchangedParameters.clear();
188243
copy.unchangedParameters.addAll(unchangedParameters);
189244
copy.assignedComponents.addAll(assignedComponents);
245+
copy.logicBeforeAssignments = logicBeforeAssignments;
246+
copy.logicAfterAssignments = logicAfterAssignments;
190247
return copy;
191248
}
192249

193250
/**
194251
* When joining two branches of the execution, a component is considered assigned if it was assigned in both branches.
195252
* A parameter is considered unchanged if it was unchanged on both branches.
253+
* Logic before or after assignments is considered present if either branch has them.
196254
*/
197255
private void mergeWith(ConstructorExecutionState other) {
198256
assignedComponents.removeIf(component -> !other.assignedComponents.contains(component));
199257
unchangedParameters.removeIf(component -> !other.unchangedParameters.contains(component));
200-
}
201-
202-
public void applyStatement(StatementTree statement) {
203-
if (statement instanceof ExpressionStatementTree expression
204-
&& expression.expression() instanceof AssignmentExpressionTree assignment) {
205-
applyAssignment(assignment.variable(), assignment.expression());
206-
} else if (statement instanceof IfStatementTree ifStatement) {
207-
ConstructorExecutionState stateCopy = copy();
208-
applyStatement(ifStatement.thenStatement());
209-
StatementTree elseStatement = ifStatement.elseStatement();
210-
if (elseStatement != null) {
211-
stateCopy.applyStatement(elseStatement);
212-
}
213-
mergeWith(stateCopy);
214-
} else if (statement instanceof BlockTree block) {
215-
block.body().forEach(this::applyStatement);
216-
}
217-
}
218-
219-
public boolean componentsAreFullyAssigned() {
220-
return assignedComponents.size() == componentNames.size();
258+
logicBeforeAssignments = logicBeforeAssignments || other.logicBeforeAssignments;
259+
logicAfterAssignments = logicAfterAssignments || other.logicAfterAssignments;
221260
}
222261
}
223262
}

sonar-java-plugin/src/main/resources/org/sonar/l10n/java/rules/java/S6207.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ <h3>Noncompliant code example</h3>
1919
return name;
2020
}
2121
}
22+
23+
record Person(String name, int age) {
24+
Person(String name, int age) { // Noncompliant, this can be implemented with a compact constructor
25+
if (age &lt; 0) {
26+
throw new IllegalArgumentException("Negative age");
27+
}
28+
this.name = name;
29+
this.age = age;
30+
}
31+
}
2232
</pre>
2333
<h3>Compliant solution</h3>
2434
<pre>

0 commit comments

Comments
 (0)