View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks;
21  
22  import java.util.Arrays;
23  
24  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
25  import com.puppycrawl.tools.checkstyle.PropertyType;
26  import com.puppycrawl.tools.checkstyle.XdocsPropertyType;
27  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28  import com.puppycrawl.tools.checkstyle.api.DetailAST;
29  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
30  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
31  
32  /**
33   * <div>
34   * Checks for restricted tokens beneath other tokens.
35   * </div>
36   *
37   * <p>
38   * WARNING: This is a very powerful and flexible check, but, at the same time,
39   * it is low-level and very implementation-dependent because its results depend
40   * on the grammar we use to build abstract syntax trees. Thus, we recommend using
41   * other checks when they provide the desired functionality. Essentially, this
42   * check just works on the level of an abstract syntax tree and knows nothing
43   * about language structures.
44   * </p>
45   *
46   * @since 3.2
47   */
48  @FileStatefulCheck
49  public class DescendantTokenCheck extends AbstractCheck {
50  
51      /**
52       * A key is pointing to the warning message text in "messages.properties"
53       * file.
54       */
55      public static final String MSG_KEY_MIN = "descendant.token.min";
56  
57      /**
58       * A key is pointing to the warning message text in "messages.properties"
59       * file.
60       */
61      public static final String MSG_KEY_MAX = "descendant.token.max";
62  
63      /**
64       * A key is pointing to the warning message text in "messages.properties"
65       * file.
66       */
67      public static final String MSG_KEY_SUM_MIN = "descendant.token.sum.min";
68  
69      /**
70       * A key is pointing to the warning message text in "messages.properties"
71       * file.
72       */
73      public static final String MSG_KEY_SUM_MAX = "descendant.token.sum.max";
74  
75      /** Specify the minimum depth for descendant counts. */
76      private int minimumDepth;
77      /** Specify the maximum depth for descendant counts. */
78      private int maximumDepth = Integer.MAX_VALUE;
79      /** Specify a minimum count for descendants. */
80      private int minimumNumber;
81      /** Specify a maximum count for descendants. */
82      private int maximumNumber = Integer.MAX_VALUE;
83      /**
84       * Control whether the number of tokens found should be calculated from
85       * the sum of the individual token counts.
86       */
87      private boolean sumTokenCounts;
88      /** Specify set of tokens with limited occurrences as descendants. */
89      @XdocsPropertyType(PropertyType.TOKEN_ARRAY)
90      private int[] limitedTokens = CommonUtil.EMPTY_INT_ARRAY;
91      /** Define the violation message when the minimum count is not reached. */
92      private String minimumMessage;
93      /** Define the violation message when the maximum count is exceeded. */
94      private String maximumMessage;
95  
96      /**
97       * Counts of descendant tokens.
98       * Indexed by (token ID - 1) for performance.
99       */
100     private int[] counts = CommonUtil.EMPTY_INT_ARRAY;
101 
102     /**
103      * Creates a new {@code DescendantTokenCheck} instance.
104      */
105     public DescendantTokenCheck() {
106         // no code by default
107     }
108 
109     @Override
110     public int[] getAcceptableTokens() {
111         return TokenUtil.getAllTokenIds();
112     }
113 
114     @Override
115     public int[] getDefaultTokens() {
116         return getRequiredTokens();
117     }
118 
119     @Override
120     public int[] getRequiredTokens() {
121         return CommonUtil.EMPTY_INT_ARRAY;
122     }
123 
124     @Override
125     public void visitToken(DetailAST ast) {
126         // reset counts
127         Arrays.fill(counts, 0);
128         countTokens(ast, 0);
129 
130         if (sumTokenCounts) {
131             logAsTotal(ast);
132         }
133         else {
134             logAsSeparated(ast);
135         }
136     }
137 
138     /**
139      * Log violations for each Token.
140      *
141      * @param ast token
142      */
143     private void logAsSeparated(DetailAST ast) {
144         // name of this token
145         final String name = TokenUtil.getTokenName(ast.getType());
146 
147         for (int element : limitedTokens) {
148             final int tokenCount = counts[element - 1];
149             if (tokenCount < minimumNumber) {
150                 final String descendantName = TokenUtil.getTokenName(element);
151 
152                 if (minimumMessage == null) {
153                     minimumMessage = MSG_KEY_MIN;
154                 }
155                 log(ast,
156                         minimumMessage,
157                         String.valueOf(tokenCount),
158                         String.valueOf(minimumNumber),
159                         name,
160                         descendantName);
161             }
162             if (tokenCount > maximumNumber) {
163                 final String descendantName = TokenUtil.getTokenName(element);
164 
165                 if (maximumMessage == null) {
166                     maximumMessage = MSG_KEY_MAX;
167                 }
168                 log(ast,
169                         maximumMessage,
170                         String.valueOf(tokenCount),
171                         String.valueOf(maximumNumber),
172                         name,
173                         descendantName);
174             }
175         }
176     }
177 
178     /**
179      * Log validation as one violation.
180      *
181      * @param ast current token
182      */
183     private void logAsTotal(DetailAST ast) {
184         // name of this token
185         final String name = TokenUtil.getTokenName(ast.getType());
186 
187         int total = 0;
188         for (int element : limitedTokens) {
189             total += counts[element - 1];
190         }
191         if (total < minimumNumber) {
192             if (minimumMessage == null) {
193                 minimumMessage = MSG_KEY_SUM_MIN;
194             }
195             log(ast,
196                     minimumMessage,
197                     String.valueOf(total),
198                     String.valueOf(minimumNumber), name);
199         }
200         if (total > maximumNumber) {
201             if (maximumMessage == null) {
202                 maximumMessage = MSG_KEY_SUM_MAX;
203             }
204             log(ast,
205                     maximumMessage,
206                     String.valueOf(total),
207                     String.valueOf(maximumNumber), name);
208         }
209     }
210 
211     /**
212      * Counts the number of occurrences of descendant tokens.
213      *
214      * @param ast the root token for descendants.
215      * @param depth the maximum depth of the counted descendants.
216      */
217     private void countTokens(DetailAST ast, int depth) {
218         if (depth <= maximumDepth) {
219             // update count
220             if (depth >= minimumDepth) {
221                 final int type = ast.getType();
222                 if (type <= counts.length) {
223                     counts[type - 1]++;
224                 }
225             }
226             DetailAST child = ast.getFirstChild();
227             final int nextDepth = depth + 1;
228             while (child != null) {
229                 countTokens(child, nextDepth);
230                 child = child.getNextSibling();
231             }
232         }
233     }
234 
235     /**
236      * Setter to specify set of tokens with limited occurrences as descendants.
237      *
238      * @param limitedTokensParam tokens to ignore.
239      * @since 3.2
240      */
241     public void setLimitedTokens(String... limitedTokensParam) {
242         limitedTokens = new int[limitedTokensParam.length];
243 
244         int maxToken = 0;
245         for (int i = 0; i < limitedTokensParam.length; i++) {
246             limitedTokens[i] = TokenUtil.getTokenId(limitedTokensParam[i]);
247             if (limitedTokens[i] >= maxToken + 1) {
248                 maxToken = limitedTokens[i];
249             }
250         }
251         counts = new int[maxToken];
252     }
253 
254     /**
255      * Setter to specify the minimum depth for descendant counts.
256      *
257      * @param minimumDepth the minimum depth for descendant counts.
258      * @since 3.2
259      */
260     public void setMinimumDepth(int minimumDepth) {
261         this.minimumDepth = minimumDepth;
262     }
263 
264     /**
265      * Setter to specify the maximum depth for descendant counts.
266      *
267      * @param maximumDepth the maximum depth for descendant counts.
268      * @since 3.2
269      */
270     public void setMaximumDepth(int maximumDepth) {
271         this.maximumDepth = maximumDepth;
272     }
273 
274     /**
275      * Setter to specify a minimum count for descendants.
276      *
277      * @param minimumNumber the minimum count for descendants.
278      * @since 3.2
279      */
280     public void setMinimumNumber(int minimumNumber) {
281         this.minimumNumber = minimumNumber;
282     }
283 
284     /**
285      * Setter to specify a maximum count for descendants.
286      *
287      * @param maximumNumber the maximum count for descendants.
288      * @since 3.2
289      */
290     public void setMaximumNumber(int maximumNumber) {
291         this.maximumNumber = maximumNumber;
292     }
293 
294     /**
295      * Setter to define the violation message when the minimum count is not reached.
296      *
297      * @param message the violation message for minimum count not reached.
298      *     Used as a {@code MessageFormat} pattern with arguments
299      *     <ul>
300      *     <li>{0} - token count</li>
301      *     <li>{1} - minimum number</li>
302      *     <li>{2} - name of token</li>
303      *     <li>{3} - name of limited token</li>
304      *     </ul>
305      * @since 3.2
306      */
307     public void setMinimumMessage(String message) {
308         minimumMessage = message;
309     }
310 
311     /**
312      * Setter to define the violation message when the maximum count is exceeded.
313      *
314      * @param message the violation message for maximum count exceeded.
315      *     Used as a {@code MessageFormat} pattern with arguments
316      *     <ul>
317      *     <li>{0} - token count</li>
318      *     <li>{1} - maximum number</li>
319      *     <li>{2} - name of token</li>
320      *     <li>{3} - name of limited token</li>
321      *     </ul>
322      * @since 3.2
323      */
324 
325     public void setMaximumMessage(String message) {
326         maximumMessage = message;
327     }
328 
329     /**
330      * Setter to control whether the number of tokens found should be calculated
331      * from the sum of the individual token counts.
332      *
333      * @param sum whether to use the sum.
334      * @since 5.0
335      */
336     public void setSumTokenCounts(boolean sum) {
337         sumTokenCounts = sum;
338     }
339 
340 }