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()}) or as a nested argument
20   * to a {@code new} expression.</p>
21   *
22   * <p>A constructor whose only statement is a delegating
23   * {@code this(...)} or {@code super(...)} call is exempt: such a
24   * constructor performs no work of its own, and the method-call
25   * arguments to the delegate (e.g. {@code this(System.currentTimeMillis())})
26   * are accepted as a sanctioned factory-style entry point.</p>
27   *
28   * <p>Method calls nested inside lambda bodies or anonymous class bodies
29   * are not considered constructor code, because they are not executed
30   * at construction time: only the lambda object or the anonymous class
31   * instance is created. Such subtrees are skipped.</p>
32   *
33   * <p>Defensive array copy idioms — {@code Arrays.copyOf(...)} and
34   * {@code <expr>.clone()} — are also tolerated, since there is no
35   * method-call-free way to defensively copy an array field at
36   * construction time (Effective Java item 50).</p>
37   *
38   * @since 0.24
39   */
40  public final class ConstructorsCodeFreeCheck extends AbstractCheck {
41  
42      /**
43       * Default constructor.
44       */
45      public ConstructorsCodeFreeCheck() {
46          // nothing to initialize
47      }
48  
49      @Override
50      public int[] getDefaultTokens() {
51          return new int[] {TokenTypes.CTOR_DEF};
52      }
53  
54      @Override
55      public int[] getAcceptableTokens() {
56          return this.getDefaultTokens();
57      }
58  
59      @Override
60      public int[] getRequiredTokens() {
61          return this.getDefaultTokens();
62      }
63  
64      @Override
65      public void visitToken(final DetailAST ast) {
66          final DetailAST body = ast.findFirstToken(TokenTypes.SLIST);
67          if (body != null && !ConstructorsCodeFreeCheck.isOnlyDelegate(body)) {
68              this.reportCalls(body);
69          }
70      }
71  
72      private static boolean isOnlyDelegate(final DetailAST body) {
73          final DetailAST first = body.getFirstChild();
74          final boolean delegate;
75          if (ConstructorsCodeFreeCheck.isDelegate(first)) {
76              final DetailAST next = first.getNextSibling();
77              delegate = next == null || next.getType() == TokenTypes.RCURLY;
78          } else {
79              delegate = false;
80          }
81          return delegate;
82      }
83  
84      private static boolean isDelegate(final DetailAST node) {
85          final boolean delegate;
86          if (node == null) {
87              delegate = false;
88          } else {
89              delegate = node.getType() == TokenTypes.CTOR_CALL
90                  || node.getType() == TokenTypes.SUPER_CTOR_CALL;
91          }
92          return delegate;
93      }
94  
95      private void reportCalls(final DetailAST node) {
96          for (DetailAST child = node.getFirstChild();
97              child != null; child = child.getNextSibling()) {
98              final int type = child.getType();
99              if (type == TokenTypes.LAMBDA || type == TokenTypes.OBJBLOCK) {
100                 continue;
101             }
102             if (type == TokenTypes.METHOD_CALL
103                 && !ConstructorsCodeFreeCheck.isDefensiveCopy(child)) {
104                 this.log(
105                     child.getLineNo(),
106                     "Constructor must not contain method calls"
107                 );
108             }
109             this.reportCalls(child);
110         }
111     }
112 
113     private static boolean isDefensiveCopy(final DetailAST call) {
114         final DetailAST dot = call.getFirstChild();
115         final boolean defensive;
116         if (dot == null || dot.getType() != TokenTypes.DOT) {
117             defensive = false;
118         } else {
119             final DetailAST method = dot.getLastChild();
120             defensive = method != null
121                 && method.getType() == TokenTypes.IDENT
122                 && (
123                     ConstructorsCodeFreeCheck.isArraysCopyOf(dot, method)
124                         || ConstructorsCodeFreeCheck.isArrayClone(call, method)
125                 );
126         }
127         return defensive;
128     }
129 
130     private static boolean isArraysCopyOf(
131         final DetailAST dot, final DetailAST method
132     ) {
133         final DetailAST qualifier = dot.getFirstChild();
134         return "copyOf".equals(method.getText())
135             && qualifier != null
136             && ConstructorsCodeFreeCheck.endsWith(qualifier, "Arrays");
137     }
138 
139     private static boolean isArrayClone(
140         final DetailAST call, final DetailAST method
141     ) {
142         final DetailAST elist = call.findFirstToken(TokenTypes.ELIST);
143         return "clone".equals(method.getText())
144             && elist != null
145             && elist.getFirstChild() == null;
146     }
147 
148     private static boolean endsWith(final DetailAST node, final String name) {
149         final boolean match;
150         if (node.getType() == TokenTypes.IDENT) {
151             match = name.equals(node.getText());
152         } else if (node.getType() == TokenTypes.DOT) {
153             final DetailAST last = node.getLastChild();
154             match = last != null
155                 && last.getType() == TokenTypes.IDENT
156                 && name.equals(last.getText());
157         } else {
158             match = false;
159         }
160         return match;
161     }
162 }