|
27 | 27 | import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; |
28 | 28 | import org.sonar.plugins.java.api.semantic.Symbol; |
29 | 29 | import org.sonar.plugins.java.api.tree.AssignmentExpressionTree; |
| 30 | +import org.sonar.plugins.java.api.tree.BlockTree; |
30 | 31 | import org.sonar.plugins.java.api.tree.ClassTree; |
31 | 32 | import org.sonar.plugins.java.api.tree.ExpressionStatementTree; |
32 | 33 | import org.sonar.plugins.java.api.tree.ExpressionTree; |
33 | 34 | import org.sonar.plugins.java.api.tree.IdentifierTree; |
| 35 | +import org.sonar.plugins.java.api.tree.IfStatementTree; |
34 | 36 | import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; |
35 | 37 | import org.sonar.plugins.java.api.tree.MethodTree; |
36 | 38 | import org.sonar.plugins.java.api.tree.ReturnStatementTree; |
@@ -114,70 +116,108 @@ public static boolean onlyDoesSimpleAssignments(MethodTree constructor, List<Sym |
114 | 116 | if (constructor.parameters().size() != components.size()) { |
115 | 117 | return false; |
116 | 118 | } |
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(); |
126 | 123 | } |
127 | 124 |
|
128 | 125 | private static boolean isAnnotated(MethodTree method) { |
129 | 126 | return !method.modifiers().annotations().isEmpty(); |
130 | 127 | } |
131 | 128 |
|
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; |
139 | 138 |
|
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); |
143 | 154 | } |
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()); |
147 | 158 | } |
148 | | - return Optional.of((AssignmentExpressionTree) initialStatement.expression()); |
149 | | - } |
150 | 159 |
|
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 | + } |
164 | 170 | return Optional.empty(); |
165 | 171 | } |
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 | + } |
172 | 183 | } |
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; |
176 | 191 | } |
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(); |
180 | 221 | } |
181 | | - return Optional.empty(); |
182 | 222 | } |
183 | 223 | } |
0 commit comments