Skip to content

Commit 7024aab

Browse files
SONARJAVA-4334 Fix FP in S6207, in case where parameter was changed in constructor (#5106)
With the changes in this PR, we more precisely keep track of which component was assigned to what parameter and whether the parameter was changed by the execution of the constructor.
1 parent 17da779 commit 7024aab

2 files changed

Lines changed: 123 additions & 50 deletions

File tree

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.Random;
77

88
public class RedundantRecordMethodsCheckSample {
9+
910
record RedundantConstructorAndGetters(String name, int age) {
1011

1112
static Object variable = null;
@@ -166,4 +167,36 @@ record RecordWithCustomAnnotation(String value) {
166167
this.value = value;
167168
}
168169
}
170+
171+
record AssignmentInConstructor(int arg1, int arg2) {
172+
public AssignmentInConstructor(int arg1, int arg2) { // Compliant
173+
this.arg1 = arg1;
174+
if (arg2 == 5) {
175+
arg2 = 4;
176+
}
177+
this.arg2 = arg2;
178+
}
179+
}
180+
record BranchInConstructor(int arg1, int arg2) {
181+
public BranchInConstructor(int arg1, int arg2) { // Noncompliant
182+
this.arg1 = arg1;
183+
if (arg2 == 5) {
184+
this.arg2 = arg2;
185+
} else {
186+
arg1 = 0;
187+
this.arg2 = arg2;
188+
}
189+
}
190+
}
191+
192+
record BranchInConstructor2(int arg1, int arg2) {
193+
public BranchInConstructor2(int arg1, int arg2) { // Compliant
194+
this.arg1 = arg1;
195+
if (arg2 == 5) {
196+
this.arg2 = arg2;
197+
} else {
198+
this.arg2 = 0;
199+
}
200+
}
201+
}
169202
}

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

Lines changed: 90 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
2828
import org.sonar.plugins.java.api.semantic.Symbol;
2929
import org.sonar.plugins.java.api.tree.AssignmentExpressionTree;
30+
import org.sonar.plugins.java.api.tree.BlockTree;
3031
import org.sonar.plugins.java.api.tree.ClassTree;
3132
import org.sonar.plugins.java.api.tree.ExpressionStatementTree;
3233
import org.sonar.plugins.java.api.tree.ExpressionTree;
3334
import org.sonar.plugins.java.api.tree.IdentifierTree;
35+
import org.sonar.plugins.java.api.tree.IfStatementTree;
3436
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
3537
import org.sonar.plugins.java.api.tree.MethodTree;
3638
import org.sonar.plugins.java.api.tree.ReturnStatementTree;
@@ -114,70 +116,108 @@ public static boolean onlyDoesSimpleAssignments(MethodTree constructor, List<Sym
114116
if (constructor.parameters().size() != components.size()) {
115117
return false;
116118
}
117-
List<Symbol.VariableSymbol> parameters = constructor.parameters().stream()
118-
.map(parameter -> (Symbol.VariableSymbol) parameter.symbol())
119-
.toList();
120-
List<AssignmentExpressionTree> assignments = extractAssignments(constructor.block().body());
121-
Set<Symbol> componentsAssignedInConstructor = new HashSet<>();
122-
for (AssignmentExpressionTree assignment : assignments) {
123-
assignsParameterToComponent(assignment, components, parameters).ifPresent(componentsAssignedInConstructor::add);
124-
}
125-
return componentsAssignedInConstructor.containsAll(components);
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();
126123
}
127124

128125
private static boolean isAnnotated(MethodTree method) {
129126
return !method.modifiers().annotations().isEmpty();
130127
}
131128

132-
private static List<AssignmentExpressionTree> extractAssignments(List<StatementTree> statements) {
133-
return statements.stream()
134-
.map(RedundantRecordMethodsCheck::extractAssignment)
135-
.filter(Optional::isPresent)
136-
.map(Optional::get)
137-
.toList();
138-
}
129+
/**
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.
132+
*/
133+
private static class ConstructorExecutionState {
134+
/**
135+
* Immutable list of the components in the record
136+
*/
137+
final List<String> componentNames;
139138

140-
private static Optional<AssignmentExpressionTree> extractAssignment(StatementTree statement) {
141-
if (!statement.is(Tree.Kind.EXPRESSION_STATEMENT)) {
142-
return Optional.empty();
139+
/**
140+
* Set of names of components of the record that have been assigned to the corresponding parameter so far
141+
*/
142+
Set<String> assignedComponents;
143+
144+
/**
145+
* Set of name of parameters that still hold the value of the argument passed to the constructor
146+
*/
147+
Set<String> unchangedParameters;
148+
149+
private ConstructorExecutionState(List<String> componentNames) {
150+
assignedComponents = new HashSet<>();
151+
this.componentNames = componentNames;
152+
unchangedParameters = new HashSet<>();
153+
unchangedParameters.addAll(componentNames);
143154
}
144-
ExpressionStatementTree initialStatement = (ExpressionStatementTree) statement;
145-
if (!initialStatement.expression().is(Tree.Kind.ASSIGNMENT)) {
146-
return Optional.empty();
155+
156+
private boolean isParameter(Symbol symbol) {
157+
return componentNames.contains(symbol.name());
147158
}
148-
return Optional.of((AssignmentExpressionTree) initialStatement.expression());
149-
}
150159

151-
/**
152-
* Returns the symbol of a component used as the receiving end of the assignment if the matching parameter is used as the value.
153-
* @param assignment The assignment statement
154-
* @param components The components' symbols
155-
* @param parameters The parameters' symbols
156-
* @return Optional of the component's symbol receiving the matching parameter. Optional.empty() otherwise.
157-
*/
158-
private static Optional<Symbol.VariableSymbol> assignsParameterToComponent(
159-
AssignmentExpressionTree assignment,
160-
List<Symbol.VariableSymbol> components,
161-
List<Symbol.VariableSymbol> parameters) {
162-
ExpressionTree leftHandSide = assignment.variable();
163-
if (!leftHandSide.is(Tree.Kind.MEMBER_SELECT)) {
160+
/**
161+
* Return the name of the component if the given expression is of the form `this.name`.
162+
*/
163+
public Optional<String> getComponent(ExpressionTree expression) {
164+
if (expression instanceof MemberSelectExpressionTree memberSelect) {
165+
String name = memberSelect.identifier().name();
166+
if (componentNames.contains(name)) {
167+
return Optional.of(name);
168+
}
169+
}
164170
return Optional.empty();
165171
}
166-
Symbol variableSymbol = ((MemberSelectExpressionTree) leftHandSide).identifier().symbol();
167-
Optional<Symbol.VariableSymbol> component = components.stream()
168-
.filter(variableSymbol::equals)
169-
.findFirst();
170-
if (!component.isPresent()) {
171-
return Optional.empty();
172+
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+
}
172183
}
173-
ExpressionTree rightHandSide = assignment.expression();
174-
if (!rightHandSide.is(Tree.Kind.IDENTIFIER)) {
175-
return Optional.empty();
184+
185+
private ConstructorExecutionState copy() {
186+
var copy = new ConstructorExecutionState(componentNames);
187+
copy.unchangedParameters.clear();
188+
copy.unchangedParameters.addAll(unchangedParameters);
189+
copy.assignedComponents.addAll(assignedComponents);
190+
return copy;
176191
}
177-
Symbol valueSymbol = ((IdentifierTree) rightHandSide).symbol();
178-
if (parameters.stream().anyMatch(valueSymbol::equals) && variableSymbol.name().equals(valueSymbol.name())) {
179-
return component;
192+
193+
/**
194+
* When joining two branches of the execution, a component is considered assigned if it was assigned in both branches.
195+
* A parameter is considered unchanged if it was unchanged on both branches.
196+
*/
197+
private void mergeWith(ConstructorExecutionState other) {
198+
assignedComponents.removeIf(component -> !other.assignedComponents.contains(component));
199+
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();
180221
}
181-
return Optional.empty();
182222
}
183223
}

0 commit comments

Comments
 (0)