1 /*
2 * SPDX-FileCopyrightText: Copyright (c) 2011-2026 Yegor Bugayenko
3 * SPDX-License-Identifier: MIT
4 */
5 package com.qulice.checkstyle;
6
7 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
8 import com.puppycrawl.tools.checkstyle.api.DetailAST;
9 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
10
11 /**
12 * Checks that constructors do not contain any method calls.
13 *
14 * <p>A constructor must only assign fields from constructor parameters
15 * or from newly created objects, and may delegate to another constructor
16 * via {@code this(...)} or {@code super(...)}. Calling any method
17 * (static or instance) from inside a constructor is forbidden,
18 * including as the right-hand side of a field assignment
19 * (e.g. {@code this.bar = Foo.createBar()}), as an argument to a
20 * delegating constructor call, or as a nested argument to a {@code new}
21 * expression.
22 *
23 * <p>Method calls nested inside lambda bodies or anonymous class bodies
24 * are not considered constructor code, because they are not executed
25 * at construction time: only the lambda object or the anonymous class
26 * instance is created. Such subtrees are skipped.
27 *
28 * <p>Defensive array copy idioms — {@code Arrays.copyOf(...)} and
29 * {@code <expr>.clone()} — are also tolerated, since there is no
30 * method-call-free way to defensively copy an array field at
31 * construction time (Effective Java item 50).
32 *
33 * @since 0.24
34 */
35 public final class ConstructorsCodeFreeCheck extends AbstractCheck {
36
37 @Override
38 public int[] getDefaultTokens() {
39 return new int[] {TokenTypes.CTOR_DEF};
40 }
41
42 @Override
43 public int[] getAcceptableTokens() {
44 return this.getDefaultTokens();
45 }
46
47 @Override
48 public int[] getRequiredTokens() {
49 return this.getDefaultTokens();
50 }
51
52 @Override
53 public void visitToken(final DetailAST ast) {
54 final DetailAST body = ast.findFirstToken(TokenTypes.SLIST);
55 if (body != null) {
56 this.reportCalls(body);
57 }
58 }
59
60 /**
61 * Reports every method call found anywhere in the given subtree,
62 * except those nested inside lambda bodies or anonymous class bodies,
63 * and except sanctioned defensive-copy idioms.
64 * @param node Root of the subtree to scan
65 */
66 private void reportCalls(final DetailAST node) {
67 for (DetailAST child = node.getFirstChild();
68 child != null; child = child.getNextSibling()) {
69 final int type = child.getType();
70 if (type == TokenTypes.LAMBDA || type == TokenTypes.OBJBLOCK) {
71 continue;
72 }
73 if (type == TokenTypes.METHOD_CALL
74 && !ConstructorsCodeFreeCheck.isDefensiveCopy(child)) {
75 this.log(
76 child.getLineNo(),
77 "Constructor must not contain method calls"
78 );
79 }
80 this.reportCalls(child);
81 }
82 }
83
84 /**
85 * Is this method call a defensive array-copy idiom?
86 *
87 * <p>Recognizes {@code Arrays.copyOf(...)} (static, any qualifier
88 * ending in {@code Arrays.copyOf}) and any zero-argument
89 * {@code <expr>.clone()} call.
90 *
91 * @param call A {@code METHOD_CALL} AST node
92 * @return True if the call is a sanctioned defensive copy
93 */
94 private static boolean isDefensiveCopy(final DetailAST call) {
95 final DetailAST dot = call.getFirstChild();
96 final boolean defensive;
97 if (dot == null || dot.getType() != TokenTypes.DOT) {
98 defensive = false;
99 } else {
100 final DetailAST method = dot.getLastChild();
101 defensive = method != null
102 && method.getType() == TokenTypes.IDENT
103 && (
104 ConstructorsCodeFreeCheck.isArraysCopyOf(dot, method)
105 || ConstructorsCodeFreeCheck.isArrayClone(call, method)
106 );
107 }
108 return defensive;
109 }
110
111 /**
112 * Is this an {@code Arrays.copyOf(...)} call?
113 * @param dot The {@code DOT} node that is first child of {@code METHOD_CALL}
114 * @param method The {@code IDENT} that is the called method's name
115 * @return True if it matches the {@code Arrays.copyOf} idiom
116 */
117 private static boolean isArraysCopyOf(
118 final DetailAST dot, final DetailAST method
119 ) {
120 final DetailAST qualifier = dot.getFirstChild();
121 return "copyOf".equals(method.getText())
122 && qualifier != null
123 && ConstructorsCodeFreeCheck.endsWith(qualifier, "Arrays");
124 }
125
126 /**
127 * Is this a no-argument {@code <expr>.clone()} call?
128 * @param call The {@code METHOD_CALL} AST node
129 * @param method The {@code IDENT} that is the called method's name
130 * @return True if it matches the array-clone idiom
131 */
132 private static boolean isArrayClone(
133 final DetailAST call, final DetailAST method
134 ) {
135 final DetailAST elist = call.findFirstToken(TokenTypes.ELIST);
136 return "clone".equals(method.getText())
137 && elist != null
138 && elist.getFirstChild() == null;
139 }
140
141 /**
142 * Does the qualifier expression end in the given identifier?
143 *
144 * <p>Handles a bare {@code IDENT} (e.g. {@code Arrays}) and a
145 * dotted chain (e.g. {@code java.util.Arrays}), where the final
146 * segment of the chain is the identifier we look for.
147 *
148 * @param node The qualifier AST node
149 * @param name The expected last identifier
150 * @return True if the qualifier's last segment matches
151 */
152 private static boolean endsWith(final DetailAST node, final String name) {
153 final boolean match;
154 if (node.getType() == TokenTypes.IDENT) {
155 match = name.equals(node.getText());
156 } else if (node.getType() == TokenTypes.DOT) {
157 final DetailAST last = node.getLastChild();
158 match = last != null
159 && last.getType() == TokenTypes.IDENT
160 && name.equals(last.getText());
161 } else {
162 match = false;
163 }
164 return match;
165 }
166 }