View Javadoc
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 }