@@ -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}
0 commit comments